Skip to content

Plan schema

A plan is a structured artifact validated against a published JSON Schema. The current version is standard_v1: a summary, a file scope, an ordered approach, a verification strategy, risks and assumptions, and a rollback plan.

The canonical schema and its reference are docs/spec/plan-standard-v1.schema.json and docs/spec/plan-standard-v1.md.

Generated from the canonical sources by scripts/gen-site-reference; do not edit between the markers. Description-only edits to a source are not diffed — the delta tables below compare shape (type, requiredness, enum members, default).

See also: Driving a run — the operator loop these reference surfaces serve. This page is the field reference, not a restatement of that guide.

Field Type Required Constraints Description
plan_version string required enum: standard_v1 Schema version the document was authored against.
ticket_reference ticket-reference required
generated_by generated-by required
summary string required minLength: 1 Human-readable description of the change. Rendered as the lead paragraph in the issue comment.
scope scope required
approach array of approach-step required minItems: 1 Ordered steps the agent intends to take.
verification verification required
predicted_runtime_minutes integer required min: 1 Agent’s estimate of how long the implement stage will take, in minutes. Used to surface scope problems early and to populate decomposition.sub_plans when the estimate exceeds the implement-stage budget.
predicted_runtime_confidence string required enum: low, medium, high Agent’s confidence in predicted_runtime_minutes. low = rough guess; medium = reasonably grounded; high = well-understood scope.
risks_and_assumptions array of string optional Optional. Things the agent flagged as uncertain or assumed; surfaces in the review UI.
raw_predicted_runtime_minutes integer optional min: 1 Optional (#2862). The planner’s PRE-calibration runtime estimate in minutes — the number it arrived at BEFORE multiplying by the fleet calibration factor the plan-stage calibration hint supplied. predicted_runtime_minutes keeps its existing meaning (the CALIBRATED value, so the dynamic implement kill cap and the runtime_observed calibration series are unperturbed); this field carries the raw one alongside it. The implement-budget gate evaluates max(predicted_runtime_minutes, raw_predicted_runtime_minutes), so a sub-1.0 calibration factor can never pull a plan under the budget and dissolve a required decomposition, while a factor above 1.0 can still push one over — calibration applies only in the structure-ADDING direction. Populate it whenever the prompt rendered a calibration hint; omitting it is legal (additive-optional within standard_v1) and leaves the gate reading predicted_runtime_minutes exactly as before, but a hint-bearing plan that omits it draws a plan_warnings advisory because the gate then cannot verify that calibration did not clear the budget. Equal to predicted_runtime_minutes is legitimate (factor 1.0, or no hint rendered).
decomposition object optional Optional. Populated when predicted_runtime_minutes exceeds the implement-stage budget. Stored in the audit log but not acted upon until D3/D4.
model_recommendation model-recommendation optional
surface_sweep_exemptions array of surface-sweep-exemption optional Optional (#1544). Machine-readable declarations that a surface-sweep lockstep pattern’s named sibling correctly needs no change in this plan — the structured form of the prose ‘justify why a sibling needs no change’ escape hatch. The plan-gate surface sweep honors a matching (pattern, sibling) entry to suppress the would-be missing-sibling finding, and records the applied exemption in the audit payload + plan-review evidence so a bogus reason stays reviewer-challengeable (never silent). A non-matching or non-firing entry is a harmless no-op.
scope_removals array of scope-removal optional Optional (#2516). Machine-readable declarations that a path present in the REVISION BASE plan is deliberately dropped by this revision — the structured form of ‘this file is genuinely obsoleted by the constraint’. The plan-gate scope-regression sweep REFUSES a revision that narrows scope without declaring the drop (bounded in-run re-dispatch); a matching entry suppresses the refusal for exactly that path, with the reason surfaced to reviewers as a challengeable justification (never silent). An entry naming a path that was NOT dropped is a harmless no-op.
over_cap boolean optional Optional (#2053). A planner SELF-DECLARATION HINT that scope.files exceeds the resolved implement-stage max_files_changed cap. Advisory only: the plan-gate over-cap advisory is derived SERVER-SIDE from len(scope.files) vs the resolved cap and does NOT depend on this flag — no enforcement or detection path may branch on over_cap to decide whether a plan is over-cap. Set it true as a courtesy when you must emit a single plan whose scope genuinely exceeds the cap; omitting it (or setting it false) never suppresses the server’s count-derived advisory.
split_proposal object optional Optional (#2055, E50.3). The ordered-phase split a plan carries when scope.files exceeds the resolved implement-stage max_files_changed cap BY COUNT. A plan over the cap by count MUST carry split_proposal or it is REJECTED server-side (plan_review_failed / category-B) REGARDLESS of the over_cap hint — the AUTHORITATIVE over-cap signal is the server-derived len(scope.files) vs the resolved cap, never over_cap (which remains an advisory courtesy). Each phase carries its own scope.files (intended at or under the cap) and depends_on edges, shaped expand->migrate->contract for compile-atomic changes. Additive-optional within standard_v1 (no major bump).
irreducible object optional Optional (#2412). The structured way for a planner facing a COMPILE-ATOMIC change to DECLINE proposing a split_proposal instead of fabricating one it knows is invalid — PRESENCE of this object (with a non-empty rationale) is the declaration, the schema-idiomatic form of the issue’s irreducible: true. Three load-bearing semantics: (a) it is the honest DECLINE of a split for a change that cannot be phased into independently-landable at-or-under-cap commits (e.g. a Go method whose receiver base type must live in the method’s own package); (b) UNLIKE over_cap (hint-only — no enforcement path may read it) this field IS read by the plan gate: a well-formed irreducible with NO split_proposal converts the count-derived over-cap HARD REJECT into a surfaced, operator-decidable advisory, and it NEVER suppresses the count-derived over-cap advisory itself (that advisory still fires; a SECOND advisory surfaces this rationale as a challengeable claim); (c) it does NOT make the change landable — the implement stage re-checks max_files_changed against the REAL diff, so an approved irreducible over-cap plan still needs a governed spec raise. Mutually exclusive with split_proposal: a plan both declining and proposing a split is contradictory and is rejected by plan.Parse.
new_architectural_decision new-architectural-decision optional Optional (E78.4 / #3748). The planner’s declaration that this plan sets NEW ARCHITECTURAL DIRECTION — PRESENCE of this object is the declaration, the same idiom as irreducible. Declare it when the plan introduces a new component boundary, a new persistence shape, a new protocol or wire contract, a new trust boundary, or departs from a cited ADR. Do NOT declare it for routine work inside existing decisions, which is most plans. It is surfaced to the captain as one plan_warnings advisory naming the decision summary, the rationale and the related ADRs, and the captain decides whether the direction needs an ADR. It gates nothing: unlike irreducible, no enforcement path reads it. Additive-optional within standard_v1 (no major bump).
delivery string optional enum: full, partial Optional (E83.52 / #4085). Absent means full. partial declares that the approved plan deliberately delivers only a slice of the triggering issue’s scope (part of its done-means is left for a later run). For a partial plan the PR references the issue with Refs #N instead of Closes #N, so merging it does not close the issue; Fishhawk posts remaining_scope on the issue at merge; and the plan gate surfaces the declaration to the approver as a PARTIAL DELIVERY plan_warnings advisory. Omit it for a full delivery, which is the normal case. Additive-optional within standard_v1 (no major bump).
remaining_scope string optional minLength: 1; maxLength: 2000 Optional (E83.52 / #4085). The short statement of what the triggering issue still needs after this delivery. REQUIRED when delivery is partial (enforced by the root if/then) and INVALID otherwise (plan.Parse rejects a remaining_scope paired with an absent or full delivery, and a whitespace-only one, which minLength:1 admits). Posted on the issue at merge and shown to the approver in the PARTIAL DELIVERY advisory.
counterfactuals array of counterfactual optional Optional (E85.3 / #4143). Structured counterfactuals the implement runner EXECUTES after the committed-tree verify gate passes: for each entry it applies the mutation to control_path in a scratch clone of the verified head, runs the named Go tests, and records whether they went RED. The runner’s observation (red | green | timeout | error) reaches the review gate as a counterfactual_observed audit row; a GREEN observation (the tests still pass with the control mutated, so nothing pins it) raises a server_check control_unpinned concern only a human can clear. Declare one entry per production control the change adds or tightens whose deletion a named test should catch. At most 8 entries (enforced semantically by plan.Validate and plan.Parse, not by the schema). Not executed for a plan carrying decomposition.sub_plans (the plan draws an advisory instead). Additive-optional within standard_v1 (no major bump).
Field Type Required Constraints Description
type string required enum: github_issue v0 closed set; v0.x adds linear_ticket and jira_ticket.
url string required format: uri Canonical URL of the originating ticket.
id string required minLength: 1 Stable identifier — e.g. “kuhlman-labs/fishhawk#1247”.
Field Type Required Constraints Description
agent string required minLength: 1 Agent identifier matching the workflow spec’s executor.agent.
model string required minLength: 1 Specific model used (e.g. claude-opus-4-7).
timestamp string required format: date-time RFC 3339 timestamp of generation. The runner wall-clock at agent invocation.
version string optional Optional. Model version or build SHA when agent reports it.
Field Type Required Constraints Description
files array of scope-file required minItems: 1 Files the agent intends to create, modify, or delete.
estimated_lines_changed integer optional min: 0 Rough estimate. Used by reviewers to gauge change size; not enforced.
Field Type Required Constraints Description
path string required minLength: 1 Repo-relative path.
operation string required enum: create, modify, delete
Field Type Required Constraints Description
step integer required min: 1
description string required minLength: 1
Field Type Required Constraints Description
test_strategy string required minLength: 1 How the change will be tested. Free-form prose; reviewers expect concrete tests, not just “add tests”.
rollback_plan string required minLength: 1 How a regression would be undone. “Revert PR” is a valid answer for purely-additive changes; data migrations need more.
acceptance_criteria array of acceptance-criterion optional Optional (ADR-049 #3, E31 wave 0). The structured, provenance-tagged acceptance-criteria contract. Each criterion’s id is the join key threaded across plan → acceptance execution → evidence → triage → feedback. Annotated x-intended-required: a future standard version promotes this to required after an E31 soak; additive-optional today.
out_of_scope array of string optional Optional. Statements of what this change deliberately does NOT cover, so reviewers and downstream acceptance don’t treat an omission as a gap.
acceptance_surface string optional enum: none Optional (E72.1 / #3325). Explicit declaration that the change exposes NO operator-observable surface — no HTTP route/response/status code, MCP tool result, CLI exit code/stderr/stdout, rendered prompt, or persisted audit row an acceptance agent could observe. When set, the plan gate records an acceptance_stage_omitted audit row and then omits the run’s acceptance stage on approval (marker first, then stage delete) instead of minting a not_validated short-circuit, and the all_criteria_skip_expected advisory is suppressed. Mutually exclusive with any drivable acceptance criterion: plan.Parse rejects it alongside a criterion that is not skip_expected with an expectation_basis. Observable-surface values are deliberately not enumerated — a drivable criterion needs no declaration, its verify_hint names the surface.

One acceptance criterion within verification.acceptance_criteria. The id is the unique join key across plan → acceptance execution → evidence → triage → feedback.

Field Type Required Constraints Description
id string required pattern: ^[a-z0-9][a-z0-9-]*$ Slug identifier, unique within acceptance_criteria. The join key that ties this criterion to its downstream execution, evidence, triage, and feedback records.
statement string required minLength: 1 The acceptance criterion itself — what must hold for the change to be accepted.
source string required enum: explicit, inferred Provenance: explicit = stated in the ticket/spec; inferred = the agent derived it. An inferred criterion must carry a rationale.
source_ref string optional Optional. Pointer to where an explicit criterion came from (e.g. an issue anchor or spec section).
rationale string optional Why the agent inferred this criterion. Required when source = inferred (enforced by the if/then conditional); optional otherwise.
blocking boolean optional default: true Whether failing this criterion blocks acceptance. Defaults to true; downstream consumers apply the default when omitted.
verify_hint string optional Optional. A hint to the acceptance executor on how to verify this criterion.
preconditions array of string optional Optional. Preconditions that must hold before this criterion can be verified.
skip_expected boolean optional Optional (#1748). Marks a criterion the acceptance agent cannot validate against the localhost preview — its trigger needs an external event the default-deny egress sandbox cannot produce. When true, expectation_basis is required (enforced by the presence-aware if/then conditional). A plan whose EVERY criterion is so marked short-circuits acceptance dispatch to a not_validated verdict (basis all-skip-with-basis), NEVER a passed one (#2347): the short-circuit verified ZERO criteria, so the recorded verdict says so — merge-eligible, but not certified. Omitted on a legacy criterion never triggers the then-branch.
expectation_basis string optional minLength: 1 Required when skip_expected is true (#1748). Cites where the criterion’s expectation is actually validated — e.g. the integration / end-to-end test with a fake that exercises the externally-triggered behavior.
requires_live_validation boolean optional Optional (#2045). Marks a criterion whose TRUE verification needs a LIVE forge/deploy/external target the default-deny sandbox lacks (not merely an external trigger event, which skip_expected covers). When true, the criterion should ALSO be marked skip_expected with an expectation_basis so acceptance short-circuits via all-skip-with-basis — recording a not_validated verdict, never a passed one (#2347) — instead of dispatching an unvalidatable live-target criterion to the acceptance agent. That short-circuit’s acceptance_outcome_recorded payload carries criteria_live_validation, the count of criteria marked here, so a skip with a tracked operator-validation walk is distinguishable from one skipped on any other basis. On plan approval the system auto-files-or-links an operator-validation walk so the live check is tracked rather than shipped silently unvalidated. Strictly additive: omitted on a legacy criterion changes nothing.
Field Type Required Constraints Description
title string required minLength: 1; maxLength: 200 Short title for this sub-plan. Must be unique within decomposition.sub_plans.
scope_hint string required Brief description of what this sub-plan covers. Helps the reviewer understand the split.
predicted_runtime_minutes integer required min: 1 Agent’s estimate of how long this sub-plan’s implement stage will take.
predicted_runtime_confidence string required enum: low, medium, high Agent’s confidence in this sub-plan’s predicted_runtime_minutes.
scope scope optional Optional. The files THIS sub-plan’s slice will touch. When present, the decomposition fan-out child for this sub-plan narrows its run scope (scope_handoff commit bounding and scope-drift detection) to these files instead of inheriting the parent plan’s full scope.files. Omitted means the child inherits the parent’s full scope.
depends_on array of integer optional 0-based indices of sibling sub_plans this slice depends on; run_children dispatches in topological waves (omitted/empty = no dependency, first wave).
model_recommendation model-recommendation optional Optional. The model recommendation for THIS sub-plan’s decomposition child. Resolved through the same chokepoint as the top-level model_recommendation, so a decomposed child ratifies its own slice’s recommendation at its plan gate. Omitted means the child has no per-slice recommendation.

One phase within split_proposal.phases (#2055). Carries its own scope.files (intended at or under the resolved implement-stage max_files_changed cap so each phase ships as its own within-cap plan) and depends_on edges; phases are shaped expand->migrate->contract for compile-atomic changes.

Field Type Required Constraints Description
title string required minLength: 1; maxLength: 200 Short title for this phase. Must be unique within split_proposal.phases.
scope scope required The files THIS phase will touch. Intended at or under the resolved implement-stage max_files_changed cap so each phase ships as its own within-cap plan.
scope_hint string optional Brief description of what this phase covers. Helps the reviewer understand the split.
depends_on array of integer optional 0-based indices of sibling phases this phase depends on; mirrors decomposition.sub_plans depends_on. Shaped expand(0) <- migrate(1) <- contract(2) for the canonical compile-atomic split.

Optional. The agent’s complexity-informed recommendation for which model should execute the implement stage (#1013). Advisory: the operator ratifies or overrides it at the plan-approval gate, and the resolved model is validated against the deployment’s per-adapter allowed-model set. Omitted means no recommendation — the implement-model resolver falls through to the workflow spec’s executor.model, then the deployment default.

Field Type Required Constraints Description
implement_model string required minLength: 1 Model identifier the agent recommends for the implement stage (e.g. claude-opus-4-8). Routed to the runner adapter as the model override when this rung wins the resolution ladder (deployment default < spec executor.model < plan model_recommendation < operator gate decision).
rationale string required minLength: 1 Why this model fits the assessed complexity. Rendered alongside the recommendation in the plan-review surface.
complexity_assessed string required enum: low, medium, high The agent’s assessment of the change’s complexity, informing the recommendation. Stamped onto calibration history so per-model-per-complexity outcomes accumulate.

One machine-readable surface-sweep exemption (#1544): a declaration that the named lockstep pattern’s named sibling correctly needs no change in this plan. The plan-gate surface sweep suppresses the missing-sibling finding only for a sibling actually absent from scope.files whose (pattern, sibling) matches; the applied exemption is surfaced to reviewers as a challengeable justification.

Field Type Required Constraints Description
pattern string required minLength: 1 The surface-sweep pattern’s name, exactly as surfaced in the plan-gate surface-coupling sibling map (e.g. “actor @-mention render surfaces”).
sibling string required minLength: 1 Repo-relative path of the pattern sibling that correctly needs no change in this plan.
reason string required minLength: 1 Why the sibling correctly needs no change. Rendered to plan reviewers as a challengeable justification, so a bogus reason is not silent.

One machine-readable scope removal (#2516): a declaration that the named path, present in the revision base plan, is deliberately dropped by this revision. The plan-gate scope-regression sweep suppresses the undeclared-narrowing refusal for exactly this path; every OTHER dropped path still refuses. The reason is surfaced to reviewers in the plan artifact as a challengeable justification.

Field Type Required Constraints Description
path string required minLength: 1 Repo-relative path, present in the revision base plan’s scope, that this revision deliberately drops.
reason string required minLength: 1 Why the revision constraint genuinely obsoletes this path. Rendered to plan reviewers as a challengeable justification, so a bogus reason is not silent.

A planner’s new-architectural-decision declaration (E78.4 / #3748). Read only by the plan-gate advisory pass, never by an enforcement site. A whitespace-only rationale, decision_summary or related_adrs entry satisfies minLength:1 but is rejected semantically by plan.Parse.

Field Type Required Constraints Description
rationale string required minLength: 1 Why this plan sets new architectural direction rather than working inside existing decisions. Surfaced to the captain in the plan-gate advisory.
related_adrs array of string required ADR identifiers this direction relates to or departs from (e.g. ‘ADR-082’ or ‘#3728’). An EMPTY array is valid and means no existing ADR covers the direction.
decision_summary string required minLength: 1 ONE line naming the direction the plan sets. Rendered as the lead of the plan-gate advisory.

One runner-executed counterfactual (E85.3 / #4143): mutate the production control at control_path and expect the named tests to go RED. plan.Validate and plan.Parse reject a control_path that is whitespace-only, absolute, contains a .. segment or a backslash, or names a _test.go file (a test-side control cannot be shown load-bearing by its own tests going RED — name the production control).

Field Type Required Constraints Description
control_path string required minLength: 1 Repo-relative path of the production file holding the control. The mutation is confined to this file.
mutation counterfactual-mutation required
tests counterfactual-tests required
expect string required enum: red The expected outcome of the mutated test run. Only red is defined: the named tests must FAIL with the control mutated.

How to mutate control_path. Exactly ONE form: find + replace (the runner replaces the single occurrence of find with replace; zero or several occurrences record an error, not a result), or patch (a unified diff applied with git apply, touching only control_path). The exactly-one-form rule is enforced semantically by plan.Validate and plan.Parse, not by the schema: find without replace, replace without find, replace equal to find, both forms, and neither form are all rejected.

Field Type Required Constraints Description
find string optional minLength: 1 The exact text to replace in control_path. Must occur exactly once in the verified head’s copy of the file.
replace string optional The replacement text. May be empty, which deletes the match. Must differ from find.
patch string optional minLength: 1 A unified diff applied with git apply. Every hunk must touch control_path only; a binary, mode-change, create, delete, rename or copy entry records an error.

The Go tests the runner executes (go test -json -count=1 -run <run> <package>), once on the unmutated verified head (the baseline, which must pass) and once with the mutation applied.

Field Type Required Constraints Description
run string required minLength: 1 The go test -run selector (a regular expression over test names), e.g. ^TestValidate_Counterfactuals$. Must match at least one test, or the runner records an error.
package string required minLength: 1 Repo-relative Go package path, e.g. ./backend/internal/plan or ./backend/internal/plan/.... Must not be absolute or contain a .. segment.