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 16 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 New CLI and storage: Emitter pipeline (orchestrator): deterministic projection (requirements→slices, Polish and docs: cook progress logs use Reviewed by Cursor Bugbot for commit cdd876b. Bugbot is set up for automated code reviews on this repo. Configure here. |
Co-authored-by: Cursor <cursoragent@cursor.com>
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 cdd876b. Configure here.
| const mtimeMs = statSync(planPath).mtimeMs; | ||
| if (!newest || mtimeMs > newest.mtimeMs) { | ||
| newest = { path: planPath, mtimeMs }; | ||
| } |
There was a problem hiding this comment.
Auto-pick ties break arbitrarily
Low Severity
resolveLatestSpecPlanPath only replaces the chosen plan when mtimeMs is strictly greater than the current best, so two spec plans with the same modification time leave which plan wins to readdir order. Default brunch cook resolution can pick different specs across runs on the same tree.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cdd876b. 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.