Skip to content

Plan Stage

PlanStage reads a source snapshot and produces a durable vega.plan artifact that describes the repository's component structure. This artifact is the foundation for all later scans — it tells the audit stage what components exist and how work can be divided.

Location

src/stages/plan/stage.py

When it runs

Planning runs via plan_repo(PlanRequest). It is a separate lifecycle step from scanning: typically triggered once when a repository is created or refreshed, not on every scan. The resulting plan artifact is stored by vega-backend and passed back into run_scan requests.

Stage declaration

class PlanStage(BaseStage[StageInput, StageOutput]):
    name = StageName.PLAN
    required_artifacts = ("source_snapshot",)
    produced_artifacts = ("plan",)

What it does

  1. Reads the source snapshot from input.source_snapshot.
  2. Normalizes include_paths from input.data["include_paths"]; sets scope_mode to "selected_paths" or "full_repository".
  3. Emits plan_source_snapshot_read with {source_snapshot, source_scope, file_count}.
  4. Renders the planning prompt (stages/plan/prompts/default.txt), injecting the source file summary (up to 200 sample files), include scope, plugin prompt sections, and plan_max_parallel_modules metadata (default 8, capped 1–32).
  5. Runs an agent task (purpose="plan") via the registered AgentRunner.
  6. Checks for cancellation; raises StageCancelled if signalled.
  7. Parses the agent output into PlanModule and PlanComponent objects, filtering components to those with at least one in-scope path.
  8. Builds audit_priority (ordered module list) and component_manifest (flat component list) and raises empty_plan_manifest if no components survived.
  9. Emits plan_component_manifest_ready with {module_count, component_count}.
  10. Writes the vega.plan JSON artifact including planning_fingerprint.
  11. Emits plan_artifact_created with {plan_artifact, module_count, component_count}.

Plan artifact structure

The vega.plan artifact is a JSON document. Its top-level fields are:

{
  "format_version": 1,
  "artifact_kind": "vega.plan",
  "repo_id": "...",
  "run_id": "...",
  "source_snapshot": {"uri": "...", "kind": "source_snapshot", ...},
  "source_scope": {
    "include_paths": ["src/auth/"],
    "scope_mode": "selected_paths"
  },
  "planning_fingerprint": {
    "planner": "agent-plan-v1",
    "plugin_id": "default",
    "plugin_version": "1",
    "model": "o3",
    "agent_provider": "codex",
    "agent_metadata": {...},
    "prompt_template": "default.txt"
  },
  "source_snapshot_summary": {
    "file_count": 342,
    "sample_files": ["src/auth/login.py", "..."],
    "repo_url": "...",
    "commit_sha": "..."
  },
  "audit_priority": [
    {"module_name": "auth", "module_paths": ["src/auth/"]}
  ],
  "component_manifest": [
    {
      "component_name": "login_handler",
      "module_name": "auth",
      "scope": ["src/auth/login.py", "src/auth/session.py"],
      "status": "pending",
      "metadata": null
    }
  ],
  "summary": "Plan for repo: 12 components across 3 modules"
}

Key structural notes:

  • audit_priority — ordered list of {module_name, module_paths} entries, one per module. The order represents agent-determined priority.
  • component_manifestflat list of all components across all modules. Each entry has module_name to link it back to its module.
  • source_scope.scope_mode"selected_paths" when include_paths was provided, "full_repository" otherwise.
  • There is no top-level modules key or nested components list; the manifest is always flat.

Stage models

# src/stages/plan/models.py

@dataclass(frozen=True)
class PlanComponent:
    module_name: str
    component_name: str
    scope: list[str]        # file paths included in this component
    status: str = "pending"
    metadata: dict | None = None

    @property
    def component_id(self) -> str: ...   # "{module_name}/{component_name}"
    def to_manifest_entry(self) -> dict: ...

@dataclass(frozen=True)
class PlanModule:
    module_name: str
    module_paths: list[str]
    components: list[PlanComponent]

    def to_audit_priority_entry(self) -> dict: ...

Domain events

Event kind When emitted
plan_source_snapshot_read After source tree is read
plan_component_manifest_ready After agent output is parsed
plan_artifact_created After artifact is written to store

Cancellation

PlanStage calls context.checkpoint(StageName.PLAN) before the agent call and after. If cancellation is requested, StageCancelled is raised and a stage_cancelled lifecycle event is emitted.

Error handling

Condition Error code Retryable
Agent fails or times out plan_agent_failed Yes
Agent produces zero components empty_plan_manifest Yes
Agent output format is invalid invalid_plan_output Yes
No AgentRunner.run() available agent_runner_unavailable No

All errors are wrapped in PlanStageError and mapped through map_error() to a StageError. Planning errors that are retryable=True indicate a transient agent failure; the backend can surface the error and allow the user to retry.

A component without any in-scope paths raises invalid_plan_output, preventing a plan that references out-of-scope files from proceeding to audit.

Artifact resume

Planning does not support resume. If an existing vega.plan artifact is already current (same source snapshot fingerprint), backend should skip planning and reuse the existing artifact.