FE-800: Spec to orchestrator plan emitter — generate plan.yaml from a completed specification#167
FE-800: Spec to orchestrator plan emitter — generate plan.yaml from a completed specification#167kostandinang wants to merge 15 commits into
Conversation
Records two 2026-06-03 spikes proving a cook plan.yaml can be projected + planned from a completed intent graph (projection deterministic, ordering via LLM planning pass + reconciliation). Adds PLAN frontier spec-to-cook-plan, SPEC A97 + D160-K. Co-authored-by: Claude <noreply@anthropic.com>
Spike verdict (execution order isn't spec truth; FE-700 won't supply it) weakens Phase 3's premise. Flag FE-800 partially subsumes petri-graph-compilation in both frontier defs + the TRACK F tree; Phase 3 residual value is the Phase-4 simulation oracle. Co-authored-by: Claude <noreply@anthropic.com>
….yaml skeleton Pure projectCookPlanFromSpec(snapshot) → Plan in src/orchestrator/src/cook-plan-projection.ts maps each `requirement` knowledge item to one cook slice with stable `req-<kindOrdinal>` id, populates `verification` from incoming `criterion --verifies--> requirement` edges, attaches every slice to a single default epic, and leaves `depends_on` empty (graph-read execution ordering is intentionally dropped — slice 2's LLM planning pass owns it). 7 acceptance tests: - empty snapshot - N-requirement slice generation (kindOrdinal-ordered, stable ids) - verifies-edge linkage (criteria sorted by kindOrdinal) - depends_on edge between requirements does NOT populate slice.depends_on (regression pin for the deliberate non-goal) - determinism - loadPlan YAML round-trip + schema-conformance (slice.epic_id resolves) - brunch_graphs corpus fixture pinning the spike's "every requirement has ≥1 verifying criterion" oracle as a regression check Local CompletedSpecSnapshot type keeps the orchestrator package independent of @/server/* — the server-side snapshot builder + the orchestrator↔server transport are a separate slice. PLAN.md frontier status → active. CARDS.md slice 1 → done. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
…non-buildable detection
Pure planExecutionOrdering(plan, runModel) → Promise<PlanningResult> in
src/orchestrator/src/cook-plan-llm-planning.ts:
- Builds a prompt from the slice-1 projected Plan (instructions for
dependsOn DAG inference, epic grouping ~2-5 epics, conservative
non-buildable-constraint flagging; lists every available slice id).
- Calls injected runModel: (prompt: string) => Promise<unknown> seam.
- Parses output through Zod planningEnrichmentSchema (SHAPE only;
id-existence, cycles, dangling deps onto constraints deferred to
slice 3 reconciliation).
- Empty plan short-circuits without an LLM call.
- LLM exceptions + parse errors + schema violations all collapse to
{ status: 'failed', reason: string } so slice 3 can fall back
deterministically rather than crashing the pipeline.
defaultRunModel uses @ai-sdk/anthropic generateText + Output.object,
mirroring src/server/reconciliation-agent.ts:114. Model knob via
SPEC_TO_COOK_PLAN_MODEL env, defaulting to claude-sonnet-4-20250514
(matches the 2026-06-03 spike).
7 unit tests with stubbed runModel:
- success with well-formed enrichment
- failed on thrown runModel
- failed on missing required field
- failed on wrong-typed field
- succeeded on semantically wrong (hallucinated id) — regression pin
that slice 2 does NOT enforce existence
- prompt content includes every slice id and definition
- empty plan short-circuits without invoking runModel
1 opt-in real-LLM integration test gated on PLANNING_REAL_LLM=1 +
ANTHROPIC_API_KEY: feeds the brunch_graphs corpus fixture through
projection → planning, asserts succeeded + non-trivial signal
(at least one ordering edge OR one non-buildable flag).
PLAN.md frontier status: slice 3 (deterministic reconciliation) next.
CARDS.md slice 2 → done.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93
Co-authored-by: Amp <amp@ampcode.com>
…nrichment → cook-runnable Plan Pure `reconcileCookPlan(projected, enrichment)` that turns slice 1's projected Plan plus slice 2's PlanningEnrichment into a cook-runnable Plan + typed warnings: - drop dependsOn refs to nonexistent slice ids - drop self-loops - drop non-buildable slices (preserving definition in the warning) - drop dependsOn edges onto non-buildable slices - cycle-break via Kahn's algorithm with lex-smallest tie-break - assign orphan slices to a synthesized default epic - drop empty LLM-proposed epics - synthesize one `unit-test` verification per surviving slice at `tests/<sliceId>.test.ts`, matching cook's net-compiler reader (`net-compiler.ts:313`) - enrich `slice.definition` with the projected criterion text so the pi-agent has the test context when authoring the test file Every transformation surfaces as a typed `ReconciliationWarning` so a reviewer can audit slice 2's output before it drives cook. 12 unit tests cover each rule plus a brunch_graphs corpus end-to-end that round-trips the reconciled plan through `loadPlan` and a determinism pin. `npm run verify` green. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
…, surfaces warnings
Composes slices 1, 2, 3 into one end-to-end emitter and exposes it
via a new `brunch plan` command.
- New `src/orchestrator/src/cook-plan-emitter.ts` exports the pure
composition `emitCookPlanFromSnapshot(snapshot, { runModel? })`.
On LLM failure the planning result is preserved as
`{ status: 'failed', reason }` and reconciliation runs against an
empty enrichment so a usable (orderless) plan still emits.
- New `src/orchestrator/src/plan-cli.ts` exports `parsePlanArgs` and
`runPlan`. Reads a `CompletedSpecSnapshot` JSON, calls the emitter,
writes `<outDir>/.brunch/cook/plan.yaml` (creating the directory
if missing), and prints every reconciliation warning on stderr with
a ` ! ` prefix and a human-readable per-code format.
- `src/server/cli.ts` dispatches `brunch plan <snapshot.json>
[--out=<dir>] [--verbose]` to `runPlan` and lists it in --help.
9 unit tests cover the composition success path, LLM-failure
fallback, YAML round-trip, arg parsing (snapshot path, --out,
--verbose / -v, missing-snapshot usage error), end-to-end YAML
emission, and warning surfacing.
`npm run verify` green (one rerun for the known unrelated
`src/server/app.test.ts` flake, as in slices 1/2).
Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93
Co-authored-by: Amp <amp@ampcode.com>
…sis demoted, formatter co-located Addresses ln-review findings #2 (synthesized-target noise), #3 (planning failure across two return shapes), #5 (formatter colocation). - New `reconciliationWarningCategory(w): 'transformation' | 'synthesis'` and `formatReconciliationWarning(w): string` exported from `cook-plan-reconciliation.ts`, both exhaustive over the warning union (build-break enforcement on new codes). - New `EmitterWarning = ReconciliationWarning | { code: 'planning-failed'; reason: string }` widens the audit stream so callers iterate one source instead of forking on `planningResult.status`. The emitter pushes one `planning-failed` warning when the LLM throws and still falls back to reconciliation against an empty enrichment so a usable orderless plan emits. `planningResult` is preserved unchanged for callers that want the raw stage status. - `emitterWarningCategory` adds `'failure'` to the category vocabulary; `formatEmitterWarning` delegates to the reconciliation formatter for non-failure codes. - `runPlan` partitions display by category: failure + transformation always shown under the ` ! ` prefix; synthesis only with `--verbose`. The "N warnings" header counts only what gets printed so screen output stays self-consistent. Smoke-tested against `brunch_graphs` fixture: clean case now emits zero warning lines (was 5 synthesis-noise lines pre-slice). `--verbose` restores the synthesis lines for reviewers who want the full trace. 16 new reconciliation tests (8 category + 8 formatter), 3 new emitter tests (planning-failed presence/absence + category dispatch), 1 new + 2 reshaped plan-cli tests (verbose toggle + planning-failed in `!`-stream). `npm run verify` green on first try. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
…ommand) The plan emitter is an orchestrator-package capability that happens to produce a YAML the cook command later consumes. Prefixing the modules with `cook-` framed the artifact in terms of the consumer when it really belongs to the producer. This commit drops the `cook-` prefix from the FE-800 plan-emitter modules and their symbols. The orchestrator package is implicit from the directory; reframing leaves the cook command surface (`cook-cli.ts`, `runCook`, `parseCookArgs`, `CookOptions`, `brunch cook`, `.brunch/cook/plan.yaml` path) untouched. Files renamed (git mv, history preserved): - cook-plan-projection.ts → plan-projection.ts - cook-plan-llm-planning.ts → plan-llm-planning.ts - cook-plan-reconciliation.ts → plan-reconciliation.ts - cook-plan-emitter.ts → plan-emitter.ts (+ matching .test.ts files) Symbols renamed: - projectCookPlanFromSpec → projectPlanFromSpec - reconcilePlan (unchanged in name; was reconcileCookPlan) → reconcilePlan - emitCookPlanFromSnapshot → emitPlanFromSnapshot - EmitCookPlanResult → EmitPlanResult - EmitCookPlanOptions → EmitPlanOptions Memory docs (CARDS.md, PLAN.md) updated for path/symbol references and descriptive prose. Frontier id `spec-to-cook-plan` and branch name `ka/fe-800-spec-to-cook-plan` kept as stable identifiers. 285 orchestrator tests green after rename; `npm run verify` green. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
Slice 6: replace 'brunch plan <snapshot.json>' with 'brunch plan <specId>'.
- src/server/db/completed-spec-snapshot.ts: buildCompletedSpecSnapshot(db, specId)
maps accepted requirements/criteria (kind_ordinal → kindOrdinal) and
active-path relationships (filtered to accepted endpoints) into the
orchestrator's CompletedSpecSnapshot shape. Uses
getEntitiesForSpecificationOnActivePath; orchestrator stays pure
(type-only import).
- src/server/plan-runner.ts: parsePlanArgs(<specId>, --out, --verbose)
+ runPlan({ specId, snapshot, outDir, verbose, runModel?, log? }).
Header prints spec id; display rules (failure + transformation always,
synthesis only with --verbose) preserved from slice 5.
- src/server/cli.ts: resolves project + opens DB, calls
buildCompletedSpecSnapshot then runPlan; closes DB on resolve/reject.
buildCompletedSpecSnapshot is statically imported so db.ts stays
bundled inline with cli.js (avoids drizzle migrations chunk-path break).
- Orchestrator src/orchestrator/src/plan-cli.ts + plan-cli.test.ts deleted.
- memory/PLAN.md: FE-800 status → done (all six slices).
- memory/CARDS.md: retired (queue exhausted).
Tests: 3 new for buildCompletedSpecSnapshot (accepted-id filter, edge
filter + relation enum preservation, empty spec), 8 new for plan-runner
(parsePlanArgs spec-id parsing + flags + usage errors, runPlan cycle/
synthesis/planning-failed paths). npm run verify green (1636 tests).
Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93
Co-authored-by: Amp <amp@ampcode.com>
Slice 7 (ln-review follow-up) — bundles four hardening findings on
the just-landed brunch plan <specId> surface:
1. Empty-snapshot guard. CLI now rejects nonexistent specs
('specification <N> not found') and specs with no accepted
requirements ('specification <N> has no accepted requirements
— confirm the requirements phase before planning') before
emission, instead of producing a default-epic plan that pretends
to have planned a completed spec.
2. Strict arg parser. parsePlanArgs rejects unknown flags
('--bogus'), bare hyphen tokens ('-1'), --out without =, and
a second positional argument. Each path throws a usage error
mentioning the offending token. Existing accepted shapes
unchanged.
3. Unified error boundary. The plan branch in src/server/cli.ts
wraps parse + project resolve + db open + snapshot build + runPlan
in one try/finally. All failure paths print the friendly 'Failed
to run brunch plan: <message>' prefix and the DB is closed
exactly once.
4. CLI surface oracle. Three new tests in src/server/cli.test.ts
(packaged-bin suite) pin: --help mentions 'plan <specId>',
'brunch plan' with no args fails with usage error, 'brunch plan
abc' fails with usage error, 'brunch plan 999' against empty
.brunch/ fails with spec-not-found.
5. Lexicon normalization. Internal identifiers settle on
specificationId (PlanOptions.specificationId, RunPlanArgs.
specificationId, seed-helper return key). User-facing CLI token
stays <specId>. Test helper variable 'project' → 'specification'.
Verify green (1641 tests, 1 skipped). Live smoke: 'brunch plan 23'
still emits the working plan; 'brunch plan 999' / 'brunch plan' /
'brunch plan 23 --bogus' all exit 1 with friendly messages.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93
Co-authored-by: Amp <amp@ampcode.com>
brunch plan <id> now writes .brunch/cook/specs/<id>/plan.yaml so multiple specs can coexist on the same project. brunch cook gains --spec=<id> and resolves plans in this precedence: 1. <dir>/plan.yaml (fixture) 2. --spec=<id> -> .brunch/cook/specs/<id>/plan.yaml (error if missing) 3. newest .brunch/cook/specs/*/plan.yaml by mtime 4. legacy .brunch/cook/plan.yaml (authored single-plan fallback) 5. otherwise error Parser rejects non-integer / non-positive --spec values. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
…yout
Both brunch plan (writer) and brunch cook (resolver) were independently
constructing .brunch/cook/specs/<id>/plan.yaml and re-implementing
positive-integer validation for spec ids. Extract a small orchestrator
module that owns:
- specPlanPath(dir, specId)
- specsRootDir(dir)
- resolveLatestSpecPlanPath(dir) (newest by mtime; ignores non-int dirs)
- parseSpecId(raw, flagLabel)
plan-runner and cook-cli now delegate; the inline findNewestSpecPlan
helper and the duplicated spec-id parser are gone. Help text under
'Plan flags' also names the spec-scoped layout.
Migrated the cook-cli mtime test off execFileSync('touch', ...) to
fs.utimesSync — no subprocess, no BSD/GNU date-format risk.
Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93
Co-authored-by: Amp <amp@ampcode.com>
…ition Live cook lines now read 'req-4 · users-can-drag-nodes' instead of bare 'req-4', so the operator can tell which requirement is being processed without cross-referencing plan.yaml. Display-only — slice ids stay canonical for branches, depends_on, reports, and tests. Slug rule: lowercase, cut at first clause boundary, drop stop words and sub-3-char fragments, take first 4 surviving words, cap on word boundary at 32 chars. Falls back to bare id when no usable slug emerges. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
…ition - slice-label.ts header example and JSDoc now match the implementation (stop words dropped anywhere, not just leading; example matches R4 smoke output). - pi-actions assess-semantic binds 'label' once like the other four actions instead of inlining the call. - plan-projection.test.ts pins the now-display-load-bearing invariant that every projected slice has a non-empty definition. Amp-Thread-ID: https://ampcode.com/threads/T-019e8dea-2ba3-776f-8d0d-57c94acf5f93 Co-authored-by: Amp <amp@ampcode.com>
The first brownfield cook of spatial_graph_layout produced orphan, unwired feature code that satisfied criteria via a Ladle story, because the emitter's convention-synthesized verification.target is integration-blind. Records the integration-oracle direction (distinct from petri-simulation-oracle's net reachability) and the run-output promotion dependency as an FE-800 follow-on. Post-demo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Confirmed it's from this branch, not downstack: it fails on fe-813 (#170) with that branch's own changes stashed, i.e. on the fe-800 tip itself. Likely introduced by a recent fe-800 commit. Since the stack inherits it, anything based on fe-800 (e.g. #170) shows red in CI until it's triaged here at the source. |
PR SummaryMedium Risk Overview
The emitter in the orchestrator package is pure: projection (req → slices,
Cook progress logs use Reviewed by Cursor Bugbot for commit 3d21c13. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3d21c13. Configure here.
| throw new Error( | ||
| `specification ${opts.specificationId} has no accepted requirements — confirm the requirements phase before planning`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Plan skips criteria confirmation
Medium Severity
brunch plan only rejects specs with zero accepted requirements; it does not require a confirmed criteria phase or any accepted criteria, so emitted plans can omit verifies linkage the pipeline treats as part of a completed spec.
Reviewed by Cursor Bugbot for commit 3d21c13. Configure here.



Summary
This PR closes the loop between a confirmed brunch specification and the orchestrator:
brunch plan <specId>emits a runnableplan.yamlfrom a completed spec, multiple specs can coexist on the same project under.brunch/cook/specs/<specId>/plan.yaml, andbrunch cookpicks which one to run via--spec=<id>or auto-pick.The emitter is a three-stage projection — deterministic graph read → single LLM planning pass → deterministic reconciliation — and every meaningful transformation surfaces as a typed warning on a single audit stream. Spec-scoped storage is owned by one helper that both the writer and the resolver delegate to.
What Changed
brunch plan <specId>(server-side), driven by a snapshot built from the completed spec's accepted requirements, criteria, and active-path edges.EmitterWarningaudit stream; failure paths fall back to a usable orderless plan..brunch/cook/specs/<specId>/plan.yamlso multiple specs coexist on the same project.brunch cook --spec=<id>with resolution precedence:<dir>/plan.yaml→ explicit spec → newest spec by mtime → legacy.brunch/cook/plan.yaml.spec-plan-pathsas the single owner of the spec-scoped layout, latest-by-mtime selection, and spec-id parsing.Scope
The generated plan is a reviewable artifact, not a silent input — it round-trips through
loadPlanand drivesbrunch cook --petrinaut-streamend-to-end. The orchestrator package stays pure of server-side code; the server depends on the orchestrator, never the reverse.Verification
npm run verifypasses: format and lint checks, full test suite, production build. Live smoke against the working project confirmsbrunch plan 23emits the expected spec-scoped plan andbrunch cook --spec=23resolves it.