Skip to content

FE-800: Spec to orchestrator plan emitter — generate plan.yaml from a completed specification#167

Open
kostandinang wants to merge 15 commits into
ka/fe-764-petri-sync-serverfrom
ka/fe-800-spec-to-cook-plan
Open

FE-800: Spec to orchestrator plan emitter — generate plan.yaml from a completed specification#167
kostandinang wants to merge 15 commits into
ka/fe-764-petri-sync-serverfrom
ka/fe-800-spec-to-cook-plan

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented Jun 3, 2026

Summary

This PR closes the loop between a confirmed brunch specification and the orchestrator: brunch plan <specId> emits a runnable plan.yaml from a completed spec, multiple specs can coexist on the same project under .brunch/cook/specs/<specId>/plan.yaml, and brunch cook picks 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

  • Adds brunch plan <specId> (server-side), driven by a snapshot built from the completed spec's accepted requirements, criteria, and active-path edges.
  • Adds projection → planning → reconciliation as three pure stages with a typed EmitterWarning audit stream; failure paths fall back to a usable orderless plan.
  • Emits plans to .brunch/cook/specs/<specId>/plan.yaml so multiple specs coexist on the same project.
  • Adds brunch cook --spec=<id> with resolution precedence: <dir>/plan.yaml → explicit spec → newest spec by mtime → legacy .brunch/cook/plan.yaml.
  • Extracts spec-plan-paths as the single owner of the spec-scoped layout, latest-by-mtime selection, and spec-id parsing.
  • Adds regression oracles for projection determinism, reconciliation, emitter fallback, snapshot building, parser surface, and resolver precedence.

Scope

The generated plan is a reviewable artifact, not a silent input — it round-trips through loadPlan and drives brunch cook --petrinaut-stream end-to-end. The orchestrator package stays pure of server-side code; the server depends on the orchestrator, never the reverse.

Verification

npm run verify passes: format and lint checks, full test suite, production build. Live smoke against the working project confirms brunch plan 23 emits the expected spec-scoped plan and brunch cook --spec=23 resolves it.

kostandinang and others added 10 commits June 3, 2026 15:05
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>
@kostandinang kostandinang changed the title FE-800: Spec to cook-plan emitter — project and plan a cook plan.yaml from a completed intent graph FE-800: Spec to orchestrator plan emitter — generate plan.yaml from a completed specification Jun 4, 2026
kostandinang and others added 4 commits June 4, 2026 10:15
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>
@kostandinang
Copy link
Copy Markdown
Contributor Author

⚠️ Heads-up — a test is failing on this branch's tip (3d21c13c), independent of any downstack work:

src/client/__tests__/build-boundary.test.ts"keeps rich markdown rendering lazy and preserves route-level code splitting in the production build"
AssertionError: expected [ …(10) ] to deeply equal [ …(10) ] (a code-splitting / chunk-set mismatch).

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.

@kostandinang kostandinang marked this pull request as ready for review June 4, 2026 16:27
@cursor
Copy link
Copy Markdown

cursor Bot commented Jun 4, 2026

PR Summary

Medium Risk
Introduces a production LLM planning step and changes how cook resolves plans, but reconciliation is deterministic with fallbacks and legacy plan paths remain supported.

Overview
FE-800 wires the Bristol demo front half: a completed brunch specification can become a runnable orchestrator plan.yaml without hand-authoring.

brunch plan <specId> (server) loads accepted requirements/criteria and active-path edges from the project DB via buildCompletedSpecSnapshot, runs the three-stage emitter, and writes .brunch/cook/specs/<specId>/plan.yaml. Warnings (LLM failure, cycle breaks, dropped deps, etc.) go through one EmitterWarning stream; synthesis noise stays behind --verbose.

The emitter in the orchestrator package is pure: projection (req → slices, verifies → criterion linkage, no graph-read depends_on) → LLM planning pass (injected runModel, empty enrichment on failure) → reconciliation (acyclicity, non-buildable slices, epic grouping, synthesized tests/<sliceId>.test.ts targets). plan-cli is gone; plan-runner owns the DB-backed CLI.

brunch cook gains --spec=<id> and expanded resolveCookMode: fixture plan.yaml → explicit spec plan → newest spec plan by mtime → legacy .brunch/cook/plan.yaml. Shared layout lives in spec-plan-paths.

Cook progress logs use sliceLabel for readable slice lines. PLAN.md, SPEC.md (A97, D160-K), and the retired CARDS.md queue reflect FE-800 done and petri-graph-compilation partially subsumed.

Reviewed by Cursor Bugbot for commit 3d21c13. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/server/cli.ts
throw new Error(
`specification ${opts.specificationId} has no accepted requirements — confirm the requirements phase before planning`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3d21c13. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant