57c028f94f
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
193 lines
5.7 KiB
Go
193 lines
5.7 KiB
Go
package workphase
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const goodPlan = "# Quiet flag implementation plan\n" + `
|
|
## Overview
|
|
Add a --quiet flag.
|
|
|
|
## Current state
|
|
The script prints unconditionally. See research:r1.
|
|
|
|
## Desired end state
|
|
--quiet suppresses the success line.
|
|
|
|
## Non-goals
|
|
No change to exit codes.
|
|
|
|
## Approach
|
|
Parse the flag in the existing loop.
|
|
|
|
## Phase 1: Parse the flag
|
|
|
|
### Files
|
|
- scripts/orchestra_e2e_healthcheck.sh
|
|
|
|
### Changes
|
|
Add a quiet local and set it in the argument loop.
|
|
|
|
### Verification
|
|
|
|
#### Automated
|
|
- run: ["bash", "-n", "scripts/orchestra_e2e_healthcheck.sh"]
|
|
|
|
#### Manual
|
|
- Run the script with --quiet and confirm it prints nothing.
|
|
|
|
## Phase 2: Document the flag
|
|
|
|
### Files
|
|
- scripts/orchestra_e2e_healthcheck.sh
|
|
|
|
### Changes
|
|
State --quiet in the --help output, per research:r2.
|
|
|
|
### Verification
|
|
|
|
#### Automated
|
|
- run: ["bash", "scripts/orchestra_e2e_healthcheck.sh", "--help"]
|
|
|
|
## Testing strategy
|
|
Shell syntax check plus a manual run.
|
|
|
|
## Risks and edge cases
|
|
An unknown flag is still ignored.
|
|
|
|
## Migration
|
|
None.
|
|
|
|
## References
|
|
- research:r1
|
|
- research:r2
|
|
`
|
|
|
|
func TestParsePlanAcceptsACompleteSpecification(t *testing.T) {
|
|
doc, err := ParsePlan([]byte(goodPlan))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if doc.Title != "Quiet flag implementation plan" {
|
|
t.Fatalf("title %q", doc.Title)
|
|
}
|
|
if len(doc.Phases) != 2 {
|
|
t.Fatalf("got %d phases, want 2", len(doc.Phases))
|
|
}
|
|
one, ok := doc.Phase("phase-1")
|
|
if !ok {
|
|
t.Fatal("phase-1 is not addressable")
|
|
}
|
|
if len(one.Automated) != 1 || one.Automated[0][0] != "bash" || len(one.Automated[0]) != 3 {
|
|
t.Fatalf("phase-1 automated: %v", one.Automated)
|
|
}
|
|
if len(one.Manual) != 1 {
|
|
t.Fatalf("phase-1 manual: %v", one.Manual)
|
|
}
|
|
// A command is attributed to the phase that contains it, never to the
|
|
// previous one.
|
|
two, _ := doc.Phase("phase-2")
|
|
if len(two.Automated) != 1 || two.Automated[0][2] != "--help" {
|
|
t.Fatalf("phase-2 automated: %v", two.Automated)
|
|
}
|
|
if len(two.Manual) != 0 {
|
|
t.Fatalf("phase-2 borrowed manual steps: %v", two.Manual)
|
|
}
|
|
if strings.Join(doc.References, ",") != "r1,r2" {
|
|
t.Fatalf("references %v", doc.References)
|
|
}
|
|
// The document survives byte for byte. An implement session receives this,
|
|
// not a reconstruction of it.
|
|
if doc.Markdown != goodPlan {
|
|
t.Fatal("the sealed document was not retained verbatim")
|
|
}
|
|
}
|
|
|
|
func TestParsePlanRefusesStructuralGaps(t *testing.T) {
|
|
cases := map[string]struct {
|
|
mutate func(string) string
|
|
names string
|
|
}{
|
|
"no title": {func(s string) string { return strings.Replace(s, "# Quiet flag implementation plan", "", 1) }, "level-one"},
|
|
"missing section": {func(s string) string { return strings.Replace(s, "## Non-goals", "## Notgoals", 1) }, "Non-goals"},
|
|
"no phases": {func(s string) string { return strings.ReplaceAll(s, "## Phase ", "## Step ") }, "Phase 1"},
|
|
"phase without files": {func(s string) string { return strings.Replace(s, "### Files\n- scripts", "### Filez\n- scripts", 1) }, "Files"},
|
|
"unparsable run": {func(s string) string {
|
|
return strings.Replace(s, `- run: ["bash", "-n", "scripts/orchestra_e2e_healthcheck.sh"]`, `- run: bash -n scripts/x.sh`, 1)
|
|
}, "argv array"},
|
|
}
|
|
for name, c := range cases {
|
|
_, err := ParsePlan([]byte(c.mutate(goodPlan)))
|
|
if err == nil {
|
|
t.Errorf("%s: accepted, want a refusal", name)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), c.names) {
|
|
t.Errorf("%s: error %q does not name %q", name, err, c.names)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A phase nobody can check can never leave in_progress. Refusing it at seal
|
|
// time puts the failure on the planner, while the implementer would otherwise
|
|
// discover it as a task that never finishes.
|
|
func TestParsePlanRefusesAPhaseWithNoVerification(t *testing.T) {
|
|
stripped := strings.Replace(goodPlan,
|
|
"#### Automated\n- run: [\"bash\", \"scripts/orchestra_e2e_healthcheck.sh\", \"--help\"]\n", "", 1)
|
|
_, err := ParsePlan([]byte(stripped))
|
|
if err == nil || !strings.Contains(err.Error(), "phase-2") {
|
|
t.Fatalf("expected phase-2 to be refused, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParsePlanRefusesNoncontiguousPhaseNumbers(t *testing.T) {
|
|
renumbered := strings.Replace(goodPlan, "## Phase 2:", "## Phase 4:", 1)
|
|
_, err := ParsePlan([]byte(renumbered))
|
|
if err == nil || !strings.Contains(err.Error(), "Phase 4") {
|
|
t.Fatalf("expected a gap to be refused, got %v", err)
|
|
}
|
|
}
|
|
|
|
// A plan that shows example markdown must not parse its own example.
|
|
func TestParsePlanIgnoresHeadingsInsideFences(t *testing.T) {
|
|
withFence := strings.Replace(goodPlan, "## Testing strategy\n",
|
|
"## Testing strategy\n\n```\n## Phase 9: not a real phase\n#### Automated\n- run: [\"rm\", \"-rf\", \"/\"]\n```\n\n", 1)
|
|
doc, err := ParsePlan([]byte(withFence))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(doc.Phases) != 2 {
|
|
t.Fatalf("a fenced example became %d phases", len(doc.Phases))
|
|
}
|
|
}
|
|
|
|
func TestParsePlanBoundsTheDocument(t *testing.T) {
|
|
if _, err := ParsePlan(nil); err == nil {
|
|
t.Fatal("an empty document was accepted")
|
|
}
|
|
huge := goodPlan + strings.Repeat("x", MaxPlanBytes)
|
|
if _, err := ParsePlan([]byte(huge)); err == nil {
|
|
t.Fatal("an oversized document was accepted")
|
|
}
|
|
}
|
|
|
|
func TestResolveReferencesNamesUnknownFindings(t *testing.T) {
|
|
doc, err := ParsePlan([]byte(goodPlan))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
full := Research{Findings: []Finding{
|
|
{ID: "r1", Confidence: Fact, Claim: "c", Evidence: "e"},
|
|
{ID: "r2", Confidence: Inference, Claim: "d", Evidence: "f"},
|
|
}}
|
|
if missing := doc.ResolveReferences(full); len(missing) != 0 {
|
|
t.Fatalf("resolved research reported missing: %v", missing)
|
|
}
|
|
partial := Research{Findings: []Finding{{ID: "r1", Confidence: Fact, Claim: "c", Evidence: "e"}}}
|
|
missing := doc.ResolveReferences(partial)
|
|
if len(missing) != 1 || missing[0] != "r2" {
|
|
t.Fatalf("want r2 reported missing, got %v", missing)
|
|
}
|
|
}
|