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
- Reads the source snapshot from
input.source_snapshot. - Normalizes
include_pathsfrominput.data["include_paths"]; setsscope_modeto"selected_paths"or"full_repository". - Emits
plan_source_snapshot_readwith{source_snapshot, source_scope, file_count}. - Renders the planning prompt (
stages/plan/prompts/default.txt), injecting the source file summary (up to 200 sample files), include scope, plugin prompt sections, andplan_max_parallel_modulesmetadata (default 8, capped 1–32). - Runs an agent task (
purpose="plan") via the registeredAgentRunner. - Checks for cancellation; raises
StageCancelledif signalled. - Parses the agent output into
PlanModuleandPlanComponentobjects, filtering components to those with at least one in-scope path. - Builds
audit_priority(ordered module list) andcomponent_manifest(flat component list) and raisesempty_plan_manifestif no components survived. - Emits
plan_component_manifest_readywith{module_count, component_count}. - Writes the
vega.planJSON artifact includingplanning_fingerprint. - Emits
plan_artifact_createdwith{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_manifest— flat list of all components across all modules. Each entry hasmodule_nameto link it back to its module.source_scope.scope_mode—"selected_paths"wheninclude_pathswas provided,"full_repository"otherwise.- There is no top-level
moduleskey or nestedcomponentslist; 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.