Seal the plan as a specification instead of four bullet lists

The plan artifact was Changes{Target,Intent} plus three string lists, every
entry capped at 500 single-line characters. That bound makes a specification
impossible: a phase cannot carry a code block, a paragraph of reasoning, or a
verification command with its own argument list. renderSealed then flattened
what little survived through collapse(), so an implement session received a
summary of a summary.

plan.md replaces it. Markdown, 128 KiB, no per-line cap, sealed through the
existing path under the existing PlanRef. The parser enforces the structure the
brief states: required sections, phases numbered from 1 with no gaps, Files,
Changes and Verification per phase, and at least one automated or manual check,
because a phase nobody can verify can never be established as done. Automated
entries are JSON argv arrays, so a pipe is a literal argument rather than an
operator. Headings inside fenced blocks are content, so a plan may show
markdown without parsing its own example.

Citations resolve at seal time against the accepted research, on the
coordinator, which is the only party holding ResearchRef. A plan resting on a
finding nobody recorded fails on the planner while its session is still alive
to be told.

The plan now renders byte for byte into the implement launch, and a rotated
successor receives the same complete document. That is the property the whole
change exists for. collapse() stays for research findings, which really are
short claims.

DecodeStoredPlan reads pre-markdown refs and renders them into the same type,
labelled, so nothing downstream branches on which era a plan came from. A
legacy plan carries no phases, which is honest: the old artifact never named an
executable unit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-28 11:36:45 +04:00
parent 822f086451
commit 57c028f94f
22 changed files with 1070 additions and 145 deletions
+69 -19
View File
@@ -53,7 +53,7 @@ type Input struct {
// Research and Plan are the sealed outputs of earlier phases. The
// implementation phase receives these, never the sessions that wrote them.
Research *workphase.Research
Plan *workphase.Plan
Plan *workphase.PlanDoc
// Evidence is the verified diff and quality-gate result a reviewing
// session works from. Orchestra verifies every field; none of it is the
// implementer's account of what it did.
@@ -156,7 +156,7 @@ var completionPhase = map[domain.WorkPhase]bool{domain.WorkPhaseReview: true}
// and the worker needs to check.
var phaseSealFile = map[domain.WorkPhase]string{
domain.WorkPhaseResearch: "research.json",
domain.WorkPhasePlan: "plan.json",
domain.WorkPhasePlan: "plan.md",
}
// phaseSealSchema is the shape of each sealed artifact, written out for the
@@ -171,12 +171,7 @@ var phaseSealSchema = map[domain.WorkPhase]string{
"dead_ends": [{"tried": "", "why_failed": ""}],
"unknowns": [""]
}`,
domain.WorkPhasePlan: ` {
"changes": [{"target": "", "intent": ""}],
"verification": [""],
"risks": [""],
"human_decisions_needed": [""]
}`,
domain.WorkPhasePlan: planSchema,
}
// askingBrief narrows step 4 per phase. The bar is not the same everywhere: a
@@ -452,18 +447,15 @@ func renderSealed(in Input) string {
}
}
if plan != nil {
// Verbatim, never collapsed. The plan is the execution map an
// implement session works from, and a rotated successor has to receive
// the same one: a phase specification flattened to a bullet line is a
// summary, and nobody can implement a summary. collapse() stays for
// research findings, which really are short claims.
b.WriteString("\n## Accepted plan\n\n")
for _, c := range plan.Changes {
fmt.Fprintf(&b, "- %s: %s\n", collapse(c.Target), collapse(c.Intent))
}
for _, v := range plan.Verification {
fmt.Fprintf(&b, "- verify: %s\n", collapse(v))
}
for _, r := range plan.Risks {
fmt.Fprintf(&b, "- risk: %s\n", collapse(r))
}
for _, d := range plan.DecisionsNeeded {
fmt.Fprintf(&b, "- needs a human decision: %s\n", collapse(d))
b.WriteString(plan.Markdown)
if !strings.HasSuffix(plan.Markdown, "\n") {
b.WriteString("\n")
}
}
return b.String()
@@ -548,3 +540,61 @@ func short(sha string) string {
}
return sha
}
// planSchema is the plan document's required outline, given to the planning
// session verbatim. Run 5 proved the planner follows a stated shape (F38), so
// this block is the delivery mechanism for the structure the seal enforces.
//
// It is markdown rather than JSON because a specification needs paragraphs,
// lists and fenced code, and the old artifact's 500-character single-line rule
// made all three impossible. What a phase needs is not "target and intent"
// but enough for a different session, with none of this one's context, to do
// the work and know when it is done.
const planSchema = "```markdown\n" + `# <what this plan implements>
## Overview
## Current state
## Desired end state
## Non-goals
## Approach
## Phase 1: <name>
### Files
- <path each change touches>
### Changes
<what changes in those files, and why>
### Verification
#### Automated
- run: ["go", "test", "./internal/foo/..."]
#### Manual
- <a step a human performs to confirm the phase>
## Phase 2: <name>
<same four subsections>
## Testing strategy
## Risks and edge cases
## Migration
## References
- research:r1 — <why this phase rests on it>
` + "```" + `
Rules the seal enforces, so a plan that breaks one is refused:
- Every section above is required, spelled exactly.
- Phases are numbered from 1 with no gaps, and each carries Files, Changes and
Verification.
- Every phase declares at least one automated or one manual check. A phase
nobody can verify can never be established as done.
- Each "- run:" line is a JSON array of arguments, not a shell command line.
There is no shell, so a pipe or a redirection would be a literal argument.
The project decides which commands may run; a command outside its policy is
refused when you seal, not later.
- Every "research:<id>" you cite must exist in the accepted research above.
- The whole document is at most 128 KiB. There is no per-line limit: write
paragraphs, code blocks and lists as the content needs.`