diff --git a/.fixtures/README.md b/.fixtures/README.md index b9ed9eb8f..f448882d6 100644 --- a/.fixtures/README.md +++ b/.fixtures/README.md @@ -18,9 +18,9 @@ for the current architecture. ``` .fixtures/ ├── seeds/ # Tracked reusable explicit-basis inputs -│ └── / +│ └── / │ ├── README.md -│ ├── .json +│ ├── .json │ └── _*.ts # Reproducible data-prep scripts, not product code ├── workbenches/ # Launchable local workspaces; .brunch/ is gitignored │ └── / @@ -46,13 +46,14 @@ Seed workbench state explicitly; `npm run dev` never seeds by implication. See catalog. From the repo root, load one tracked seed into one named workspace with: ```sh -npm run seed -- --workspace .fixtures/workbenches/live-graph-observer --seed workspace-spread/alpha-grounding -npm run dev -- --cwd .fixtures/workbenches/live-graph-observer +npm run seed -- --seed workspace-alpha-grounding/base --reset +npm run dev -- --workspace .fixtures/workbenches/workspace-alpha-grounding ``` The seed command writes only the target workspace's `.brunch/data.db` and reports -that destination path plus the `set/slug → specId` mapping. Add `--reset` to wipe -the target workspace's runtime state before seeding — `data.db` (+ `-wal`/`-shm`), +that destination path plus the `name/variant → specId` mapping. Add `--reset` to +wipe the target workspace's runtime state before seeding — `data.db` +(+ `-wal`/`-shm`), `sessions/`, `debug/`, and `workspace.json` — so a relaunch starts a fresh session instead of resuming a stale one; unknown files in `.brunch/` and the directory itself survive. Use `--all-seeds` only as an explicit opt-in when a manual diff --git a/.fixtures/seeds/README.md b/.fixtures/seeds/README.md index 8c74e7c46..1c35993d3 100644 --- a/.fixtures/seeds/README.md +++ b/.fixtures/seeds/README.md @@ -1,13 +1,23 @@ # `.fixtures/seeds/` -Tracked reusable graph seeds. Each seed set owns one or more explicit-basis spec -fixtures consumed by `src/graph/seed-fixtures.ts` through `CommandExecutor`. +Tracked reusable graph seeds. Each seed family owns one or more explicit-basis +spec fixtures consumed by `src/graph/seed-fixtures.ts` through +`CommandExecutor`. + +Seed refs are always `name/variant`: + +- `name` is the canonical family/workbench id; omitting `--workspace` derives + `.fixtures/workbenches//` +- `variant` is the starting graph state within that family; `base` is the + canonical full graph, and semantic variants like `grounded-intent` capture + alternate starting states for the same workbench Use a single named seed for normal workbench setup, or opt in to the whole catalog when building a broad manual workbench / probe-input database: ```sh -npm run seed -- --workspace .fixtures/workbenches/ --seed workspace-spread/alpha-grounding +npm run seed -- --seed workspace-alpha-grounding/base --reset +npm run dev -- --seed workspace-alpha-grounding/base --reset --open-web npm run seed -- --workspace .fixtures/workbenches/ --all-seeds --reset ``` @@ -18,14 +28,19 @@ than loading anything into the shell cwd. | Seed set | Disposition | Purpose | | --- | --- | --- | -| `bilal-port` | manual workbench | Rich ported prototype graphs for UI, renderer, and agent-context development against realistic messy specs. | -| `bilal-port-variants` | probe input | Small curated Bilal-derived bases for product-path fixture curation and proposal runs. | +| `bilal-code-health` | manual workbench | Rich Bilal-derived spec for renderer, context, and curation work around code-health material. | +| `bilal-explorer-ui` | manual workbench | Rich Bilal-derived spec for UI- and renderer-heavy exploration. | +| `bilal-macro-view` | probe input | Bilal-derived macro-view family; `base` is the full port and `grounded-intent` is the curated probe starting state. | | `brunch-self` | preview | Faithful Brunch planning graph used as a realistic all-planes anchor for renderer and graph previews. | -| `cook-port` | test | Small fully-grounded intent graphs that exercise fan-out, join/gate, and halt-isolation shapes. | +| `cook-layered-todo` | test | Small grounded intent graph that exercises fan-out, join, and cross-epic gate shape. | +| `cook-parallel-utils` | test | Small grounded intent graph that exercises pure scaffold-to-leaf fan-out. | +| `cook-resilient-pipeline` | test | Small grounded intent graph that exercises halt isolation and an unreachable join. | | `dumpchat` | preview | Faithful external-project graph that previews all-plane rendering over a compact real spec. | -| `edge-spread` | test | Synthetic edge-category and absence-case coverage for graph projections and renderers. | +| `edge-category-directions` | test | Synthetic edge-category and absence-case coverage for graph projections and renderers. | +| `edge-hub-neighborhood` | test | Synthetic neighborhood fixture centered on a high-degree hub for traversal and projection checks. | | `fable` | preview | Faithful external-project graph with broad all-plane coverage for realistic renderer/readback previews. | -| `kind-band-spread` | test | Compact synthetic coverage matrix for every graph kind and readiness band. | +| `kind-coverage-matrix` | test | Compact synthetic coverage matrix for every graph kind and readiness band. | | `rd-loop` | preview | Faithful harness graph that provides a second realistic all-planes anchor beside Brunch itself. | -| `workspace-spread` | test | Deterministic two-spec workspace inventory for workspace/spec projection tests and workbenches. | +| `workspace-alpha-grounding` | test | Small workspace-oriented grounding fixture used for smoke workbenches and projection tests. | +| `workspace-beta-commitments` | test | Small workspace-oriented commitments fixture paired with the alpha workbench family for multi-spec tests. | | `yamlbase` | preview | Faithful external-project graph used as a worked template for porting planning prose into seed truth. | diff --git a/.fixtures/seeds/bilal-port/code-health.json b/.fixtures/seeds/bilal-code-health/base.json similarity index 99% rename from .fixtures/seeds/bilal-port/code-health.json rename to .fixtures/seeds/bilal-code-health/base.json index d4f786f14..5325a1feb 100644 --- a/.fixtures/seeds/bilal-port/code-health.json +++ b/.fixtures/seeds/bilal-code-health/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "code-health", + "slug": "bilal-code-health", "name": "Code Health" }, "nodes": [ diff --git a/.fixtures/seeds/bilal-port/explorer-ui.json b/.fixtures/seeds/bilal-explorer-ui/base.json similarity index 99% rename from .fixtures/seeds/bilal-port/explorer-ui.json rename to .fixtures/seeds/bilal-explorer-ui/base.json index 7a53a2094..3879cf550 100644 --- a/.fixtures/seeds/bilal-port/explorer-ui.json +++ b/.fixtures/seeds/bilal-explorer-ui/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "explorer-ui", + "slug": "bilal-explorer-ui", "name": "Explorer UI" }, "nodes": [ diff --git a/.fixtures/seeds/bilal-port-variants/_variant-script.ts b/.fixtures/seeds/bilal-macro-view/_variant-script.ts similarity index 83% rename from .fixtures/seeds/bilal-port-variants/_variant-script.ts rename to .fixtures/seeds/bilal-macro-view/_variant-script.ts index 353712d53..122013838 100644 --- a/.fixtures/seeds/bilal-port-variants/_variant-script.ts +++ b/.fixtures/seeds/bilal-macro-view/_variant-script.ts @@ -27,13 +27,14 @@ interface SeedFixture { }>; } -const VARIANT_SLUG = 'macro-view-grounded-intent'; -const SOURCE_SLUG = 'macro-view'; +const VARIANT_SPEC_SLUG = 'bilal-macro-view'; +const VARIANT_FILENAME = 'grounded-intent.json'; +const SOURCE_SEED_NAME = 'bilal-macro-view'; const GROUNDED_SOURCE = /^(stakeholder|external-observed|technical-observed)\b/; async function main(): Promise { const here = dirname(fileURLToPath(import.meta.url)); - const sourcePath = join(here, '..', 'bilal-port', `${SOURCE_SLUG}.json`); + const sourcePath = join(here, '..', SOURCE_SEED_NAME, 'base.json'); const source = JSON.parse(await readFile(sourcePath, 'utf8')) as SeedFixture; const kept = source.nodes.filter( (node) => @@ -64,15 +65,15 @@ async function main(): Promise { }); const variant = { spec: { - slug: VARIANT_SLUG, - name: 'Macro View — grounded intent base', + slug: VARIANT_SPEC_SLUG, + name: 'Macro View — grounded intent', }, nodes, edges, } satisfies SeedFixture; await mkdir(here, { recursive: true }); - await writeFile(join(here, `${VARIANT_SLUG}.json`), `${JSON.stringify(variant, null, 2)}\n`, 'utf8'); + await writeFile(join(here, VARIANT_FILENAME), `${JSON.stringify(variant, null, 2)}\n`, 'utf8'); } main().catch((error: unknown) => { diff --git a/.fixtures/seeds/bilal-port/macro-view.json b/.fixtures/seeds/bilal-macro-view/base.json similarity index 99% rename from .fixtures/seeds/bilal-port/macro-view.json rename to .fixtures/seeds/bilal-macro-view/base.json index 4228ceeb4..287e460b3 100644 --- a/.fixtures/seeds/bilal-port/macro-view.json +++ b/.fixtures/seeds/bilal-macro-view/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "macro-view", + "slug": "bilal-macro-view", "name": "Macro View" }, "nodes": [ diff --git a/.fixtures/seeds/bilal-port-variants/macro-view-grounded-intent.json b/.fixtures/seeds/bilal-macro-view/grounded-intent.json similarity index 99% rename from .fixtures/seeds/bilal-port-variants/macro-view-grounded-intent.json rename to .fixtures/seeds/bilal-macro-view/grounded-intent.json index ef6878a8f..cc6bd6696 100644 --- a/.fixtures/seeds/bilal-port-variants/macro-view-grounded-intent.json +++ b/.fixtures/seeds/bilal-macro-view/grounded-intent.json @@ -1,7 +1,7 @@ { "spec": { - "slug": "macro-view-grounded-intent", - "name": "Macro View — grounded intent base" + "slug": "bilal-macro-view", + "name": "Macro View — grounded intent" }, "nodes": [ { diff --git a/.fixtures/seeds/bilal-port-variants/README.md b/.fixtures/seeds/bilal-port-variants/README.md deleted file mode 100644 index a7fad7a93..000000000 --- a/.fixtures/seeds/bilal-port-variants/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# `.fixtures/seeds/bilal-port-variants/` - -Small, reproducible base variants derived from the consolidated Bilal port for product-path fixture curation runs. - -## `macro-view-grounded-intent.json` - -Source: `../bilal-port/macro-view.json`. - -Profile: `grounded-intent`. - -Deterministic filter, implemented in [`_variant-script.ts`](./_variant-script.ts): - -- keep only intent-plane nodes -- keep only `basis: explicit` rows -- keep only nodes whose `source` starts with `stakeholder`, `external-observed`, or `technical-observed` -- keep only edges whose endpoints both survive the node filter -- rewrite `local_id` and edge endpoint ids densely from 1 in source order -- emit spec slug `macro-view-grounded-intent` - -The variant is curated starting truth for tracer runs. Product-created curation output is not merged back into this reusable seed; mixed-basis evidence belongs under `.fixtures/runs/fixture-curation//`. diff --git a/.fixtures/seeds/bilal-port/README.md b/.fixtures/seeds/bilal-port/README.md index 97ffebcc1..3f9f077ba 100644 --- a/.fixtures/seeds/bilal-port/README.md +++ b/.fixtures/seeds/bilal-port/README.md @@ -12,10 +12,10 @@ Not probe-run artifacts; sits under `.fixtures/seeds/` alongside Source: vendored under [`_originals/`](./_originals/) — copied from Bilal's spec-elicitation prototype `spec//graph/{nodes,edges}.json`. -Each `.json` is generated from `_originals/` by +Each sibling `bilal-*/base.json` is generated from `_originals/` by [`_port-script.ts`](./_port-script.ts) (a throwaway data-prep step, not product code). Re-runnable from this directory alone; each run -overwrites the `.json` files. +overwrites the sibling base fixtures. ## Transformation rules @@ -45,13 +45,15 @@ Curation flags carried in the `source` field: ``` bilal-port/ ├── README.md # this file (generated) -├── _port-script.ts # throwaway prep: _originals/ → .json +├── _port-script.ts # throwaway prep: _originals/ → sibling bilal-*/base.json ├── _originals/ # vendored Bilal source (reproducibility) │ └── /{nodes,edges}.json -└── .json # consolidated seed contract (× 3) +├── ../bilal-code-health/base.json +├── ../bilal-explorer-ui/base.json +└── ../bilal-macro-view/base.json ``` -Each `.json` is the seed contract consumed by the loader: +Each sibling `base.json` is the seed contract consumed by the loader: ``` { @@ -70,8 +72,8 @@ columns stay coherent under brunch's mutation contract. ## Stats -| Spec | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | duplicate-after-collapse drops | +| Seed family | spec slug | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | duplicate-after-collapse drops | | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | -| code-health | 335 | 600 | 277 | 446 | 117 | 1 | 0 | 74 | -| explorer-ui | 316 | 698 | 280 | 580 | 74 | 15 | 0 | 34 | -| macro-view | 265 | 568 | 232 | 461 | 68 | 0 | 0 | 43 | +| bilal-code-health | bilal-code-health | 335 | 600 | 277 | 446 | 117 | 1 | 0 | 74 | +| bilal-explorer-ui | bilal-explorer-ui | 316 | 698 | 280 | 580 | 74 | 15 | 0 | 34 | +| bilal-macro-view | bilal-macro-view | 265 | 568 | 232 | 461 | 68 | 0 | 0 | 43 | diff --git a/.fixtures/seeds/bilal-port/_port-script.ts b/.fixtures/seeds/bilal-port/_port-script.ts index b1ea521ba..2a8262525 100644 --- a/.fixtures/seeds/bilal-port/_port-script.ts +++ b/.fixtures/seeds/bilal-port/_port-script.ts @@ -83,7 +83,7 @@ * checkout. Anyone can regenerate the seed contracts from this directory alone. */ -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -98,12 +98,11 @@ import { dedupeSeedEdgesByPrecedence, type OriginTaggedEdge } from './duplicate- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const ORIGINALS_ROOT = resolve(SCRIPT_DIR, '_originals'); -const OUTPUT_ROOT = SCRIPT_DIR; -const SPECS: { source: string; slug: string; displayName: string }[] = [ - { source: 'code-health', slug: 'code-health', displayName: 'Code Health' }, - { source: 'explorer-ui', slug: 'explorer-ui', displayName: 'Explorer UI' }, - { source: 'macro-view', slug: 'macro-view', displayName: 'Macro View' }, +const SPECS: { source: string; seedName: string; specSlug: string; displayName: string }[] = [ + { source: 'code-health', seedName: 'bilal-code-health', specSlug: 'bilal-code-health', displayName: 'Code Health' }, + { source: 'explorer-ui', seedName: 'bilal-explorer-ui', specSlug: 'bilal-explorer-ui', displayName: 'Explorer UI' }, + { source: 'macro-view', seedName: 'bilal-macro-view', specSlug: 'bilal-macro-view', displayName: 'Macro View' }, ]; // --------------------------------------------------------------------------- @@ -720,11 +719,13 @@ function validateSeed(seed: SeedFixture): void { seedFixture(executor, seed); } -function writeSpec(seed: SeedFixture): void { - writeFileSync(resolve(OUTPUT_ROOT, `${seed.spec.slug}.json`), JSON.stringify(seed, null, 2) + '\n'); +function writeSpec(seedName: string, seed: SeedFixture): void { + const outputDir = resolve(SCRIPT_DIR, '..', seedName); + mkdirSync(outputDir, { recursive: true }); + writeFileSync(resolve(outputDir, 'base.json'), JSON.stringify(seed, null, 2) + '\n'); } -function writeReadme(results: { slug: string; displayName: string; stats: Record }[]): void { +function writeReadme(results: { seedName: string; specSlug: string; displayName: string; stats: Record }[]): void { const lines: string[] = [ '# `.fixtures/seeds/bilal-port/`', '', @@ -740,10 +741,10 @@ function writeReadme(results: { slug: string; displayName: string; stats: Record 'Source: vendored under [`_originals/`](./_originals/) — copied from', "Bilal's spec-elicitation prototype `spec//graph/{nodes,edges}.json`.", '', - 'Each `.json` is generated from `_originals/` by', + 'Each sibling `bilal-*/base.json` is generated from `_originals/` by', '[`_port-script.ts`](./_port-script.ts) (a throwaway data-prep step,', 'not product code). Re-runnable from this directory alone; each run', - 'overwrites the `.json` files.', + 'overwrites the sibling base fixtures.', '', '## Transformation rules', '', @@ -773,13 +774,15 @@ function writeReadme(results: { slug: string; displayName: string; stats: Record '```', 'bilal-port/', '├── README.md # this file (generated)', - '├── _port-script.ts # throwaway prep: _originals/ → .json', + '├── _port-script.ts # throwaway prep: _originals/ → sibling bilal-*/base.json', '├── _originals/ # vendored Bilal source (reproducibility)', '│ └── /{nodes,edges}.json', - '└── .json # consolidated seed contract (× 3)', + '├── ../bilal-code-health/base.json', + '├── ../bilal-explorer-ui/base.json', + '└── ../bilal-macro-view/base.json', '```', '', - 'Each `.json` is the seed contract consumed by the loader:', + 'Each sibling `base.json` is the seed contract consumed by the loader:', '', '```', '{', @@ -798,17 +801,17 @@ function writeReadme(results: { slug: string; displayName: string; stats: Record '', '## Stats', '', - '| Spec | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | duplicate-after-collapse drops |', + '| Seed family | spec slug | nodes in | edges in | nodes emitted | edges emitted | edges absorbed | self-after-collapse drops | unresolved-endpoint drops | duplicate-after-collapse drops |', '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', ]; for (const r of results) { const s = r.stats; lines.push( - `| ${r.slug} | ${s.nodes_in} | ${s.edges_in} | ${s.nodes_emitted} | ${s.edges_emitted} | ${s.edges_absorbed} | ${s.edges_dropped_self_after_collapse} | ${s.edges_dropped_unresolved_endpoint} | ${s.edges_dropped_duplicate_after_collapse} |`, + `| ${r.seedName} | ${r.specSlug} | ${s.nodes_in} | ${s.edges_in} | ${s.nodes_emitted} | ${s.edges_emitted} | ${s.edges_absorbed} | ${s.edges_dropped_self_after_collapse} | ${s.edges_dropped_unresolved_endpoint} | ${s.edges_dropped_duplicate_after_collapse} |`, ); } lines.push(''); - writeFileSync(resolve(OUTPUT_ROOT, 'README.md'), lines.join('\n')); + writeFileSync(resolve(SCRIPT_DIR, 'README.md'), lines.join('\n')); } // --------------------------------------------------------------------------- @@ -821,18 +824,23 @@ function main(): void { process.exit(1); } - const summaries: { slug: string; displayName: string; stats: Record }[] = []; + const summaries: { seedName: string; specSlug: string; displayName: string; stats: Record }[] = []; for (const spec of SPECS) { - console.log(`Porting ${spec.source} → ${spec.slug}.json...`); - const result = portSpec(spec.source, spec.slug, spec.displayName); + console.log(`Porting ${spec.source} → ../${spec.seedName}/base.json...`); + const result = portSpec(spec.source, spec.specSlug, spec.displayName); const seed = buildSeed(result, spec.displayName); validateSeed(seed); // throws if the seed would not commit cleanly - writeSpec(seed); - summaries.push({ slug: spec.slug, displayName: spec.displayName, stats: result.stats }); + writeSpec(spec.seedName, seed); + summaries.push({ + seedName: spec.seedName, + specSlug: spec.specSlug, + displayName: spec.displayName, + stats: result.stats, + }); console.log(` ${JSON.stringify(result.stats)}`); } writeReadme(summaries); - console.log(`\nDone. Output at ${OUTPUT_ROOT}`); + console.log(`\nDone. Output at ${resolve(SCRIPT_DIR, '..')}`); } main(); diff --git a/.fixtures/seeds/brunch-self/README.md b/.fixtures/seeds/brunch-self/README.md index d0e18e39e..cbe9db24c 100644 --- a/.fixtures/seeds/brunch-self/README.md +++ b/.fixtures/seeds/brunch-self/README.md @@ -2,7 +2,8 @@ A **faithful** spec graph hand-derived from this repository's own planning prose (`memory/SPEC.md` + `memory/PLAN.md`), as opposed to the synthetic -coverage/edge-spread fixtures. +coverage fixtures such as `kind-coverage-matrix/base` and +`edge-category-directions/base`. Purpose: @@ -23,8 +24,9 @@ Coverage (a by-product of being faithful, not the goal): Contents: -- `spec-graph.json` — one `planning_ready` spec describing Brunch itself. +- `base.json` — the canonical faithful Brunch graph; one `planning_ready` spec + describing Brunch itself. -Structural legality is enforced by the seed loader: `spec-graph` is committed -through `CommandExecutor` by `src/renderers/graph/previews.test.ts`, which fails -if any node/edge is structurally illegal. +Structural legality is enforced by the seed loader: `base` is committed through +`CommandExecutor` by `src/renderers/graph/previews.test.ts`, which fails if any +node/edge is structurally illegal. diff --git a/.fixtures/seeds/brunch-self/spec-graph.json b/.fixtures/seeds/brunch-self/base.json similarity index 100% rename from .fixtures/seeds/brunch-self/spec-graph.json rename to .fixtures/seeds/brunch-self/base.json diff --git a/.fixtures/seeds/cook-port/layered-todo-spec.json b/.fixtures/seeds/cook-layered-todo/base.json similarity index 99% rename from .fixtures/seeds/cook-port/layered-todo-spec.json rename to .fixtures/seeds/cook-layered-todo/base.json index 2e3c35f1e..c9c431ef8 100644 --- a/.fixtures/seeds/cook-port/layered-todo-spec.json +++ b/.fixtures/seeds/cook-layered-todo/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "layered-todo-spec", + "slug": "cook-layered-todo", "name": "layered-todo (reversed cook fixture)" }, "nodes": [ diff --git a/.fixtures/seeds/cook-port/parallel-utils-spec.json b/.fixtures/seeds/cook-parallel-utils/base.json similarity index 99% rename from .fixtures/seeds/cook-port/parallel-utils-spec.json rename to .fixtures/seeds/cook-parallel-utils/base.json index 6cb142416..6e5aa093f 100644 --- a/.fixtures/seeds/cook-port/parallel-utils-spec.json +++ b/.fixtures/seeds/cook-parallel-utils/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "parallel-utils-spec", + "slug": "cook-parallel-utils", "name": "parallel-utils (reversed cook fixture)" }, "nodes": [ diff --git a/.fixtures/seeds/cook-port/README.md b/.fixtures/seeds/cook-port/README.md index dffbaaec6..f67bed679 100644 --- a/.fixtures/seeds/cook-port/README.md +++ b/.fixtures/seeds/cook-port/README.md @@ -13,9 +13,9 @@ main-branch worktree) — the `parallelUtils*`, `layeredTodo*`, and design-answer strings, vendored verbatim inside [`_port-script.ts`](./_port-script.ts). -Each `.json` is generated by `_port-script.ts` (throwaway data-prep, -not product code). Re-runnable from this directory alone; each run -overwrites the `.json` files: +Each sibling `cook-*/base.json` is generated by `_port-script.ts` +(throwaway data-prep, not product code). Re-runnable from this directory +alone; each run overwrites the sibling base fixtures: ``` npx tsx .fixtures/seeds/cook-port/_port-script.ts @@ -44,14 +44,14 @@ Original item keys are preserved in `source` as `cook-port []`. ## Loading ``` -npm run seed -- --workspace --seed cook-port/ [--reset] -npx tsx src/graph/validate-fixture.ts cook-port/ +npm run seed -- --seed cook-parallel-utils/base [--reset] +npx tsx src/graph/validate-fixture.ts cook-parallel-utils/base ``` ## Specs | Spec | nodes | edges | shape demonstrated | | --- | ---: | ---: | --- | -| parallel-utils-spec | 31 | 18 | pure fan-out (scaffold → 8 leaves) | -| layered-todo-spec | 29 | 18 | fan-out → join + cross-epic gate | -| resilient-pipeline-spec | 25 | 18 | halt isolation + unreachable join | +| cook-parallel-utils | 31 | 18 | pure fan-out (scaffold → 8 leaves) | +| cook-layered-todo | 29 | 18 | fan-out → join + cross-epic gate | +| cook-resilient-pipeline | 25 | 18 | halt isolation + unreachable join | diff --git a/.fixtures/seeds/cook-port/_port-script.ts b/.fixtures/seeds/cook-port/_port-script.ts index f070e6b26..bda30751d 100644 --- a/.fixtures/seeds/cook-port/_port-script.ts +++ b/.fixtures/seeds/cook-port/_port-script.ts @@ -6,7 +6,7 @@ * The source data (items + edges per spec, plus the grounding/design * interview strings) is vendored inline below, verbatim from the sibling * file, so this script is re-runnable from this directory alone. Each run - * overwrites the `.json` files next to it. + * overwrites the sibling `cook-*/base.json` files. * * npx tsx .fixtures/seeds/cook-port/_port-script.ts * @@ -53,7 +53,7 @@ * captures. Nothing here adds such edges. */ -import { writeFileSync } from 'node:fs'; +import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -131,7 +131,7 @@ interface OutEdge { // --------------------------------------------------------------------------- const parallelUtils: SourceSpec = { - slug: 'parallel-utils-spec', + slug: 'cook-parallel-utils', name: 'parallel-utils (reversed cook fixture)', grounding: { question: @@ -326,7 +326,7 @@ const parallelUtils: SourceSpec = { }; const layeredTodo: SourceSpec = { - slug: 'layered-todo-spec', + slug: 'cook-layered-todo', name: 'layered-todo (reversed cook fixture)', grounding: { question: @@ -520,7 +520,7 @@ const layeredTodo: SourceSpec = { }; const resilientPipeline: SourceSpec = { - slug: 'resilient-pipeline-spec', + slug: 'cook-resilient-pipeline', name: 'resilient-pipeline (reversed cook fixture)', grounding: { question: @@ -866,7 +866,9 @@ function portSpec(spec: SourceSpec): { spec: { slug: string; name: string }; nod for (const source of [parallelUtils, layeredTodo, resilientPipeline]) { const fixture = portSpec(source); - const path = resolve(OUT_DIR, `${source.slug}.json`); + const outputDir = resolve(OUT_DIR, '..', source.slug); + mkdirSync(outputDir, { recursive: true }); + const path = resolve(outputDir, 'base.json'); writeFileSync(path, `${JSON.stringify(fixture, null, 2)}\n`); console.log(`wrote ${path} (${fixture.nodes.length} nodes, ${fixture.edges.length} edges)`); } diff --git a/.fixtures/seeds/cook-port/resilient-pipeline-spec.json b/.fixtures/seeds/cook-resilient-pipeline/base.json similarity index 99% rename from .fixtures/seeds/cook-port/resilient-pipeline-spec.json rename to .fixtures/seeds/cook-resilient-pipeline/base.json index 820d78e00..6d3ab7cad 100644 --- a/.fixtures/seeds/cook-port/resilient-pipeline-spec.json +++ b/.fixtures/seeds/cook-resilient-pipeline/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "resilient-pipeline-spec", + "slug": "cook-resilient-pipeline", "name": "resilient-pipeline (reversed cook fixture)" }, "nodes": [ diff --git a/.fixtures/seeds/dumpchat/README.md b/.fixtures/seeds/dumpchat/README.md index dbade67ff..6eb34e958 100644 --- a/.fixtures/seeds/dumpchat/README.md +++ b/.fixtures/seeds/dumpchat/README.md @@ -36,11 +36,11 @@ Coverage (a by-product of being faithful, not the goal): Contents: -- `spec-graph.json` — 41 nodes / 33 edges (40 / 31 in active context after the - superseded predecessor is hidden). +- `base.json` — the canonical faithful Dumpchat graph; 41 nodes / 33 edges + (40 / 31 in active context after the superseded predecessor is hidden). Validate with: ``` -npx tsx src/graph/validate-fixture.ts dumpchat/spec-graph +npx tsx src/graph/validate-fixture.ts dumpchat/base ``` diff --git a/.fixtures/seeds/dumpchat/spec-graph.json b/.fixtures/seeds/dumpchat/base.json similarity index 100% rename from .fixtures/seeds/dumpchat/spec-graph.json rename to .fixtures/seeds/dumpchat/base.json diff --git a/.fixtures/seeds/edge-spread/category-directions.json b/.fixtures/seeds/edge-category-directions/base.json similarity index 99% rename from .fixtures/seeds/edge-spread/category-directions.json rename to .fixtures/seeds/edge-category-directions/base.json index 2b6415e2a..1635f3098 100644 --- a/.fixtures/seeds/edge-spread/category-directions.json +++ b/.fixtures/seeds/edge-category-directions/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "category-directions", + "slug": "edge-category-directions", "name": "Category Directions" }, "nodes": [ diff --git a/.fixtures/seeds/edge-spread/hub-neighborhood.json b/.fixtures/seeds/edge-hub-neighborhood/base.json similarity index 99% rename from .fixtures/seeds/edge-spread/hub-neighborhood.json rename to .fixtures/seeds/edge-hub-neighborhood/base.json index 8717df71b..beb7fad59 100644 --- a/.fixtures/seeds/edge-spread/hub-neighborhood.json +++ b/.fixtures/seeds/edge-hub-neighborhood/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "hub-neighborhood", + "slug": "edge-hub-neighborhood", "name": "Hub Neighborhood" }, "nodes": [ diff --git a/.fixtures/seeds/edge-spread/README.md b/.fixtures/seeds/edge-spread/README.md deleted file mode 100644 index f19930d77..000000000 --- a/.fixtures/seeds/edge-spread/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# `.fixtures/seeds/edge-spread/` - -Hand-authored edge-category coverage fixture. - -Purpose: - -- exercise every D51-L edge category through the real seed loader -- keep a thesis-with-no-proof absence case available for future `IS_NOT` / omission renderers -- provide multiple directional exemplars so neighborhood/list projections can show incoming and outgoing relationships without depending on live curation - -Contents: - -- `category-directions.json` — one spec with explicit-basis nodes and paired edge-category exemplars diff --git a/.fixtures/seeds/fable/README.md b/.fixtures/seeds/fable/README.md index b3376326f..d2a59feb0 100644 --- a/.fixtures/seeds/fable/README.md +++ b/.fixtures/seeds/fable/README.md @@ -37,11 +37,11 @@ Coverage (a by-product of being faithful, not the goal): Contents: -- `spec-graph.json` — 67 nodes / 37 edges (66 / 36 in active context after the - superseded predecessor is hidden). +- `base.json` — the canonical faithful Fable graph; 67 nodes / 37 edges + (66 / 36 in active context after the superseded predecessor is hidden). Validate with: ``` -npx tsx src/graph/validate-fixture.ts fable/spec-graph +npx tsx src/graph/validate-fixture.ts fable/base ``` diff --git a/.fixtures/seeds/fable/spec-graph.json b/.fixtures/seeds/fable/base.json similarity index 100% rename from .fixtures/seeds/fable/spec-graph.json rename to .fixtures/seeds/fable/base.json diff --git a/.fixtures/seeds/kind-band-spread/README.md b/.fixtures/seeds/kind-band-spread/README.md deleted file mode 100644 index 33d513b3d..000000000 --- a/.fixtures/seeds/kind-band-spread/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# `.fixtures/seeds/kind-band-spread/` - -Hand-authored coverage fixture for renderer development. - -Purpose: - -- provide one explicit-basis node of every currently shipped graph kind -- guarantee all three readiness bands appear in one deterministic seed -- give graph-slice renderers a compact, legal seed that is not tied to any one real spec archive - -Contents: - -- `coverage-matrix.json` — one spec whose nodes cover every intent/oracle/design/plan kind - -The fixture is intentionally small and hand-curated. It exists to exercise projection and rendering breadth, not to mirror a realistic spec narrative. diff --git a/.fixtures/seeds/kind-band-spread/coverage-matrix.json b/.fixtures/seeds/kind-coverage-matrix/base.json similarity index 99% rename from .fixtures/seeds/kind-band-spread/coverage-matrix.json rename to .fixtures/seeds/kind-coverage-matrix/base.json index 77279d9c6..8ec5ece2d 100644 --- a/.fixtures/seeds/kind-band-spread/coverage-matrix.json +++ b/.fixtures/seeds/kind-coverage-matrix/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "coverage-matrix", + "slug": "kind-coverage-matrix", "name": "Coverage Matrix" }, "nodes": [ diff --git a/.fixtures/seeds/rd-loop/README.md b/.fixtures/seeds/rd-loop/README.md index b07a37277..010f9980a 100644 --- a/.fixtures/seeds/rd-loop/README.md +++ b/.fixtures/seeds/rd-loop/README.md @@ -27,9 +27,10 @@ Coverage (a by-product of being faithful): Contents: -- `spec-graph.json` — one `planning_ready` spec describing `rd-loop`. +- `base.json` — the canonical faithful `rd-loop` graph; one `planning_ready` + spec describing `rd-loop`. Most nodes map directly to doc prose; the two plan-plane **frontier** nodes are `source: "projected"` because the planning decomposition is synthesized from the docs' forward-looking POC/evolution path. Validate with -`npx tsx src/graph/validate-fixture.ts rd-loop/spec-graph`. +`npx tsx src/graph/validate-fixture.ts rd-loop/base`. diff --git a/.fixtures/seeds/rd-loop/spec-graph.json b/.fixtures/seeds/rd-loop/base.json similarity index 100% rename from .fixtures/seeds/rd-loop/spec-graph.json rename to .fixtures/seeds/rd-loop/base.json diff --git a/.fixtures/seeds/workspace-spread/alpha-grounding.json b/.fixtures/seeds/workspace-alpha-grounding/base.json similarity index 96% rename from .fixtures/seeds/workspace-spread/alpha-grounding.json rename to .fixtures/seeds/workspace-alpha-grounding/base.json index 6537dfd74..ad8c37049 100644 --- a/.fixtures/seeds/workspace-spread/alpha-grounding.json +++ b/.fixtures/seeds/workspace-alpha-grounding/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "alpha-grounding", + "slug": "workspace-alpha-grounding", "name": "Alpha Grounding" }, "nodes": [ diff --git a/.fixtures/seeds/workspace-spread/beta-commitments.json b/.fixtures/seeds/workspace-beta-commitments/base.json similarity index 97% rename from .fixtures/seeds/workspace-spread/beta-commitments.json rename to .fixtures/seeds/workspace-beta-commitments/base.json index 8937b6906..8f9abc5d8 100644 --- a/.fixtures/seeds/workspace-spread/beta-commitments.json +++ b/.fixtures/seeds/workspace-beta-commitments/base.json @@ -1,6 +1,6 @@ { "spec": { - "slug": "beta-commitments", + "slug": "workspace-beta-commitments", "name": "Beta Commitments" }, "nodes": [ diff --git a/.fixtures/seeds/workspace-spread/README.md b/.fixtures/seeds/workspace-spread/README.md deleted file mode 100644 index 0ac58c30e..000000000 --- a/.fixtures/seeds/workspace-spread/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# `.fixtures/seeds/workspace-spread/` - -Hand-authored multi-spec workspace seeds. - -Purpose: - -- give workspace-level projections a deterministic two-spec inventory in one seeded database -- provide distinct readiness grades so specs-overview and future sessions-overview renderers can exercise grade contrast without live curation -- keep the graph fixtures explicit-basis and small enough to pair with deterministic session creation in tests or probes - -Contents: - -- `alpha-grounding.json` — early-grade spec with grounding-oriented graph truth -- `beta-commitments.json` — later-grade spec with commitment-heavy graph truth - -These are graph seeds, not session transcripts. Future session-overview harnesses can deterministically bind one or more sessions onto these two specs and vary turn counts without changing the graph seed contract. diff --git a/.fixtures/seeds/yamlbase/README.md b/.fixtures/seeds/yamlbase/README.md index a58d4c26f..4af00ac2b 100644 --- a/.fixtures/seeds/yamlbase/README.md +++ b/.fixtures/seeds/yamlbase/README.md @@ -35,7 +35,7 @@ nodes carry `source: "projected"`. Validate: ``` -npx tsx src/graph/validate-fixture.ts yamlbase/spec-graph +npx tsx src/graph/validate-fixture.ts yamlbase/base ``` This seeds the fixture through the real `CommandExecutor` mutation boundary, so diff --git a/.fixtures/seeds/yamlbase/spec-graph.json b/.fixtures/seeds/yamlbase/base.json similarity index 100% rename from .fixtures/seeds/yamlbase/spec-graph.json rename to .fixtures/seeds/yamlbase/base.json diff --git a/.fixtures/workbenches/live-graph-observer/README.md b/.fixtures/workbenches/live-graph-observer/README.md deleted file mode 100644 index 6a2067291..000000000 --- a/.fixtures/workbenches/live-graph-observer/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Workbench — live-graph-observer - -A reusable cwd for manually exercising the `live-graph-observer` (FE-795) frontier -end-to-end. Treat this directory as the project cwd when launching `brunch` -so that `.brunch/` and `data.db` scaffold here rather than in the repo root. - -## Why it exists - -The frontier's middle/outer verification needs a stable, throwaway cwd where the -TUI writer and the web observer host can both run against a fresh -`.brunch/data.db`. Committing this directory (and only this README) guarantees -every contributor agrees on where the manual smoke happens. - -## How to use it - -From the repo root, seed a chosen starting graph explicitly, then launch against -this workbench cwd: - -```sh -npm run seed -- --workspace .fixtures/workbenches/live-graph-observer --seed workspace-spread/alpha-grounding - -# Dev build, against TS source (no build step needed) -npm run dev -- --cwd .fixtures/workbenches/live-graph-observer --mode print - -# Built bin (after `npm run build`) -node bin/brunch.js --cwd .fixtures/workbenches/live-graph-observer --mode print - -# Once installed (e.g. via `npm link` or a published install) -brunch --cwd .fixtures/workbenches/live-graph-observer --mode print -``` - -Brunch scaffolds a local `.brunch/` directory containing `data.db` and Pi session -files **inside this workbench directory**, not in the repo root. That state is -per-cwd by design and must not be committed. `npm run dev` only opens the named -workspace; it never loads seed fixtures implicitly. - -## What is and is not committed - -- ✅ This `README.md` is committed. -- 🚫 `.brunch/` (created on first launch) is ignored by the repo-level - `.gitignore` and must stay uncommitted. If anything else appears here later - (logs, scratch transcripts), prefer keeping them ignored rather than - whitelisting them. - -## Modes you will exercise here - -- `--mode print` — non-interactive workspace projection; smoke for CLI identity - and DB scaffolding. -- `--mode tui` — interactive writer session and the product-supported launch - path for the local web observer sidecar. The sidecar URL is printed but not - opened in a browser unless `--open-web` is passed. - -## Browser feedback loop - -Use `agent-browser` as the primary browser observer inside the agent-safehouse -sandbox. It keeps a daemon-backed Chrome instance alive across shell calls and -gives the agent accessibility-tree snapshots, clicks, form input, and screenshots -without becoming product runtime behavior. CDP-style tools remain useful for -console/network detail when needed. - -Launch the TUI sidecar against this workbench: - -```sh -# Terminal A: TUI writer plus web observer sidecar -npm run dev -- --cwd .fixtures/workbenches/live-graph-observer --mode tui -``` - -The host prints a localhost URL such as: - -```text -Brunch web sidecar listening on http://127.0.0.1:/spec/ -``` - -or, when no selected spec route is available yet: - -```text -Brunch web sidecar listening on http://127.0.0.1: -``` - -Open and inspect that URL with `agent-browser`: - -```sh -# Terminal B: browser observer -agent-browser close 2>/dev/null || true -agent-browser --args "--no-sandbox,--ignore-certificate-errors" open "http://127.0.0.1:/spec/" - -# Accessibility tree / page content with stable refs such as @e1, @e2, ... -agent-browser snapshot - -# Optional interaction and visual capture -agent-browser click @e2 -agent-browser screenshot /tmp/brunch-live-graph-observer.png -``` - -If you need console or network detail rather than interaction/page structure, -attach a CDP-style tool to the same URL: - -```sh -cdp-cli tabs - -# Runtime signals -cdp-cli console "127.0.0.1" -t error -d 2 -cdp-cli network "127.0.0.1" -d 2 -t fetch -``` - -If the page title or URL is ambiguous, use the page id from `cdp-cli tabs` -instead of the `127.0.0.1` title/URL substring in later commands. - -### Annotation tooling - -`agentation`, if used, is complementary to CDP tooling: CDP observes the browser -(console, network, accessibility tree, screenshots), while `agentation` annotates -a running browser so an agent can fetch human/agent notes through its own CLI. -This card does not enable `agentation`, add a dependency, or import it into -`src/web/*`. If a future slice wants annotated web UI feedback inside product -code, that slice owns the dependency/import change. - -### Current verification note - -- `npm run build` passed during the FE-795 tie-off check. -- `agent-browser` was verified on 2026-06-04 with the sandbox launch args above. -- A browser-observable FE-795 smoke opened a fresh selected-spec web dashboard, - observed empty graph state, committed a node through the default Brunch - runtime `mutate_graph` tool path with the shared product-update bus, and - observed the browser update without page reload. diff --git a/docs/README.md b/docs/README.md index 6516b8ce3..89e4ce046 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,7 +27,7 @@ planning state: ## Testing guides -- [`docs/testing/seeded-dev-rpc.md`](./testing/seeded-dev-rpc.md) — set up a seeded local Brunch workspace, inspect it over JSON-RPC, use the gated `dev.graph.mutateGraph` harness, and run the product-path fixture curation tracer. +- [`docs/testing/seeded-dev-rpc.md`](./testing/seeded-dev-rpc.md) — set up a seeded local Brunch workspace, inspect it over launcher-backed RPC reads, curate fixture truth through the explicit local mutate seam, and run the product-path fixture curation tracer. ## Behavioral kernels diff --git a/docs/architecture/poc-live-ship-runbook.md b/docs/architecture/poc-live-ship-runbook.md index a9e813043..7f369246d 100644 --- a/docs/architecture/poc-live-ship-runbook.md +++ b/docs/architecture/poc-live-ship-runbook.md @@ -24,14 +24,14 @@ mkdir -p "$WORKSPACE" "$RUN_DIR" # Seed two specs so selection/scope is visible before the live turn. # --reset is scoped to Brunch runtime state in this workbench. -npm run seed -- --workspace "$WORKSPACE" --seed workspace-spread/alpha-grounding --reset -npm run seed -- --workspace "$WORKSPACE" --seed workspace-spread/beta-commitments +npm run seed -- --workspace "$WORKSPACE" --seed workspace-alpha-grounding/base --reset +npm run seed -- --workspace "$WORKSPACE" --seed workspace-beta-commitments/base ``` -Launch the real product with dev mirrors enabled so prompt/posture artifacts are durable: +Launch the real product; source/dev runs mirror prompt/posture artifacts into `.brunch/debug/` automatically: ```bash -BRUNCH_DEV=1 npm run dev -- --cwd "$WORKSPACE" --mode tui --open-web +npm run dev -- --workspace "$WORKSPACE" --open-web ``` Record the sidecar URL printed by the TUI in `report.json` and open it in the browser. The launch path must be the product TUI sidecar, not a test harness or imported handler. @@ -137,8 +137,8 @@ cp "$WORKSPACE/.brunch/sessions/"*.jsonl "$RUN_DIR/session.jsonl" Use public RPC/read projections for JSON summaries; do not read SQLite directly: ```bash -npx tsx src/dev/workspace-rpc.ts --workspace "$WORKSPACE" graph.overview '{"specId":1}' > "$RUN_DIR/graph-summary-after.json" -npx tsx src/dev/workspace-rpc.ts --workspace "$WORKSPACE" session.runtimeState '{"specId":1,"sessionId":""}' > "$RUN_DIR/runtime-state-after.json" +npm run dev -- rpc graph.overview '{"specId":1}' --workspace "$WORKSPACE" > "$RUN_DIR/graph-summary-after.json" +npm run dev -- rpc session.runtimeState '{"specId":1,"sessionId":""}' --workspace "$WORKSPACE" > "$RUN_DIR/runtime-state-after.json" ``` If a human-readable transcript is useful during the run, use the workspace-local `.brunch/debug/transcript.md` emitted by the faux-harness/debug renderer. Do not add `transcript.md` as a default committed probe artifact; keep `session.jsonl` as the source evidence. diff --git a/docs/praxis/manual-testing.md b/docs/praxis/manual-testing.md index 061d6ad11..c73a01f32 100644 --- a/docs/praxis/manual-testing.md +++ b/docs/praxis/manual-testing.md @@ -19,10 +19,10 @@ Manual testing happens in a **workbench** — a launchable cwd under `.fixtures/ # sessions/, debug/, workspace.json — so the relaunch starts a fresh session # (seed + kick) instead of resuming a stale one. Unknown files in .brunch/ # and the directory itself survive. -npm run seed -- --workspace .fixtures/workbenches/live-graph-observer --seed workspace-spread/alpha-grounding --reset +npm run seed -- --seed workspace-alpha-grounding/base --reset # 2. Launch the TUI (plus web observer sidecar) against that workbench. -npm run dev -- --cwd .fixtures/workbenches/live-graph-observer --mode tui +npm run dev -- --workspace .fixtures/workbenches/workspace-alpha-grounding ``` Then: @@ -33,18 +33,20 @@ Then: 4. To test resume, quit and relaunch against the same workbench — state is per-cwd in `.brunch/data.db`. 5. To switch scenarios, stop the process and re-run step 1 with a different `--seed` (keep `--reset`). -For non-interactive smoke, `--mode print` projects the workspace and exits; `tsx src/dev/workspace-rpc.ts -w ` gives one-shot RPC reads (and dev-gated writes via `dev.graph.mutateGraph`). +For non-interactive smoke, `npm run dev -- --workspace --mode print` projects the workspace and exits; `npm run dev -- rpc [params-json] --workspace ` gives one-shot RPC reads; `npm run dev -- mutate --workspace --params-file ` is the explicit local curation seam. ## Choosing a seed -Tracked seeds live under `.fixtures/seeds//.json`; each set has a README describing its intent. Current sets: +Tracked seeds live under `.fixtures/seeds//.json`; each family has a README describing its intent. Current sets: -- `workspace-spread` — small multi-spec workspace states (`alpha-grounding`, `beta-commitments`); good default for workbench smoke -- `bilal-port` / `bilal-port-variants` — large real-world spec-elicitation graphs (hundreds of nodes); good for rendering and scale checks -- `edge-spread`, `kind-band-spread` — coverage matrices over edge categories / node kinds and readiness bands -- `brunch-self`, `dumpchat`, `fable`, `rd-loop`, `yamlbase` — captured spec graphs awaiting a disposition catalog (see the `dev-seed-fixtures` frontier in `memory/PLAN.md`) +- `workspace-alpha-grounding/base`, `workspace-beta-commitments/base` — small workspace-oriented smoke fixtures +- `bilal-code-health/base`, `bilal-explorer-ui/base`, `bilal-macro-view/base` — rich Bilal-derived workbench seeds +- `bilal-macro-view/grounded-intent` — the curated Bilal probe starting state +- `edge-category-directions/base`, `edge-hub-neighborhood/base`, `kind-coverage-matrix/base` — synthetic coverage fixtures +- `cook-parallel-utils/base`, `cook-layered-todo/base`, `cook-resilient-pipeline/base` — compact shape-focused intent fixtures +- `brunch-self/base`, `dumpchat/base`, `fable/base`, `rd-loop/base`, `yamlbase/base` — faithful project ports used for realistic preview coverage -Validate a seed against the current command layer with `npx tsx src/graph/validate-fixture.ts /`. +Validate a seed against the current command layer with `npx tsx src/graph/validate-fixture.ts /`. ## Capturing evidence from a manual session @@ -52,7 +54,7 @@ Durable evidence follows the harness/probe-first, JSONL-backed model (`docs/arch - Live dev-loop output lands in gitignored `.fixtures/scratch///`. - Promote a reviewed run deliberately: move it under `.fixtures/runs///`, add the probe report and source `session.jsonl` artifacts, then track it. -- In `BRUNCH_DEV` / faux-harness launches, the workspace's `.brunch/debug/` mirrors the latest system prompt, Brunch tool contents, and optional `transcript.md` debug rendering — an ephemeral inspection cache, not committed evidence. +- In source/dev launches and faux-harness boots, the workspace's `.brunch/debug/` mirrors the latest system prompt, Brunch tool contents, origination records, and optional `transcript.md` debug rendering — an ephemeral inspection cache, not committed evidence. Do not hand-author golden JSON or copy rows out of a workbench DB; workbench `.brunch/` state is local runtime, never canonical fixture truth. diff --git a/docs/testing/seeded-dev-rpc.md b/docs/testing/seeded-dev-rpc.md index 614dc0a44..6b98224bf 100644 --- a/docs/testing/seeded-dev-rpc.md +++ b/docs/testing/seeded-dev-rpc.md @@ -1,179 +1,124 @@ -# Seeded local dev RPC workflow +# Seeded Local Dev Workflow -Use this guide when you want a practical local Brunch workspace populated with reusable seed fixtures, while still being able to inspect and mutate that workspace from an agent conversation through JSON-RPC. +Use this guide when you want a practical local Brunch workspace populated with reusable seed fixtures, while still being able to inspect and curate that workspace from scripts or another coding agent. -This is a **local development harness**: +This is a local harness: -- reusable seed files under `.fixtures/seeds/**` are explicit starting truth; -- `dev.graph.mutateGraph` is opt-in and routes through `CommandExecutor`, but is not a product API; -- product-flow proof still comes from JSONL-backed harness/probe runs that use the real agent tools (`read_graph` / `mutate_graph`). +- reusable seed files under `.fixtures/seeds/**` are explicit starting truth +- the public RPC surface stays public; local curation is a separate explicit command +- product-path proof still comes from JSONL-backed runs that use the real agent tools (`read_graph` / `mutate_graph`) -## 0. Choose an isolated workspace +## 0. Choose an isolated workbench -Prefer a workbench directory so seeded `.brunch/` state does not mix with whatever is in the repo root. +Prefer a workbench directory so seeded `.brunch/` state does not mix with the repo root. ```bash REPO="$(git rev-parse --show-toplevel)" -DEV_WORKSPACE="$REPO/.fixtures/workbenches/seeded-dev-rpc" -mkdir -p "$DEV_WORKSPACE" +DEV_WORKSPACE="$REPO/.fixtures/workbenches/bilal-macro-view" ``` -To reset this scratch workspace only: +## 1. Seed explicit starting truth ```bash -rm -rf "$DEV_WORKSPACE/.brunch" +npm run seed -- --workspace "$DEV_WORKSPACE" --seed bilal-macro-view/grounded-intent --reset ``` -Do not run that cleanup command against a workspace whose Brunch sessions or graph data you care about. +`--reset` only clears Brunch runtime state in that workbench: `data.db`, WAL/SHM siblings, `sessions/`, `debug/`, and `workspace.json`. -## 1. Seed all current fixtures +## 2. Launch Brunch against that workbench -Run the seed loader from the target workspace. It loads every `.fixtures/seeds//.json` through `CommandExecutor` into `$DEV_WORKSPACE/.brunch/data.db`. +The friendly path is the dev launcher: ```bash -( - cd "$DEV_WORKSPACE" - "$REPO/node_modules/.bin/tsx" "$REPO/src/graph/seed-fixtures.ts" -) +npm run dev -- --workspace "$DEV_WORKSPACE" --open-web ``` -Current seed sets include: +Or just run `npm run dev` and answer the prompt flow. -- `bilal-port/*` — full Bilal-derived specs. -- `bilal-port-variants/macro-view-grounded-intent` — explicit-basis grounded-intent base variant for curation/proposal tests. +Notes: -The loader currently seeds all sets. Inspect the actual spec ids before issuing graph calls; do not assume a fixed id ordering. +- TUI is the default mode. +- Source/dev builds automatically mirror debug artifacts into `$DEV_WORKSPACE/.brunch/debug/`. +- Prompt-affecting dev surfaces stay explicit; add `--dev-tools` only when you want query tools or subagent affordances. -## 2. Define a one-shot dev RPC helper +## 3. Inspect the workspace over public RPC -`--mode=rpc` is a JSON-RPC line server over stdio. For command-line work, it is easiest to send one or more JSON lines and let the process exit at EOF. +The launcher exposes one-shot RPC reads without a separate helper script: ```bash -brunch_rpc() { - local payload="$1" - ( - cd "$DEV_WORKSPACE" - printf '%s\n' "$payload" | \ - BRUNCH_DEV=1 "$REPO/node_modules/.bin/tsx" "$REPO/src/app/brunch.ts" --mode=rpc - ) -} +npm run dev -- rpc workspace.selectionState --workspace "$DEV_WORKSPACE" +npm run dev -- rpc graph.overview '{"specId":1}' --workspace "$DEV_WORKSPACE" ``` -`BRUNCH_DEV=1` enables `dev.graph.mutateGraph`. Without that switch, the method is absent from discovery and calls return `Method not found`. - -RPC output may include `brunch.updated` notifications as separate JSON lines. Filter responses by `id` when scripting: - -```bash -brunch_rpc '{"jsonrpc":"2.0","id":1,"method":"rpc.discover"}' \ - | jq 'select(.id == 1).result.methods[].method' -``` +Projected node codes are rendered from `kind + kindOrdinal` (`G1`, `TH1`, `CTX1`, `CR1`, ...). Use `graph.overview` to discover the current code before addressing existing nodes by code in a curation payload. -For one-shot command-line work, prefer the dev helper. It sets `BRUNCH_DEV=1`, sends one request, filters notifications, and prints only the response result: +## 4. Curate graph truth through the explicit local seam -```bash -"$REPO/node_modules/.bin/tsx" "$REPO/src/dev/workspace-rpc.ts" \ - --workspace "$DEV_WORKSPACE" \ - graph.overview '{"specId":4}' -``` +`npm run dev -- mutate ...` is the replacement for the old gated `dev.graph.mutateGraph` RPC path. It still routes through `CommandExecutor.mutateGraph`; it is just no longer disguised as a public RPC method. -## 3. Inspect seeded specs +Example payload: ```bash -brunch_rpc '{"jsonrpc":"2.0","id":2,"method":"workspace.selectionState"}' \ - | jq 'select(.id == 2).result.specs[] | {id: .spec.id, title: .spec.title, sessions: (.sessions | length)}' -``` - -Pick the `specId` you want to inspect or mutate: - -```bash -SPEC_ID=1 -``` - -Read the graph overview: - -```bash -brunch_rpc "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"graph.overview\",\"params\":{\"specId\":$SPEC_ID}}" \ - | jq 'select(.id == 3).result | {nodeCount, edgeCount, lsn, goals: [.nodes[] | select(.kind == "goal") | {id, code: ("G" + (.kindOrdinal|tostring)), title}]}' -``` - -Projected node codes are not stored in the DB. They are rendered from `kind` + `kindOrdinal` using the graph labels (`G1`, `TH1`, `T1`, `CTX1`, `R1`, `CR1`, etc.). Use `graph.overview` to find the current `kindOrdinal` before referencing existing nodes by code. - -`lsn` is the selected spec's local graph-clock value. Compare freshness as -`{specId, lsn}`; seeded spec ids and bare LSN values do not imply workspace-wide -ordering. - -## 4. Activate a session when session methods matter - -Graph reads and `dev.graph.mutateGraph` take explicit `specId` and do not require a selected session. Session methods do. - -Create a new session for a seeded spec: +cat > /tmp/brunch-mutate.json <<'JSON' +{ + "specId": 1, + "createBasis": "explicit", + "ops": [ + { + "op": "create_node", + "ref": "th1", + "plane": "intent", + "kind": "thesis", + "title": "The macro view should make derivation history legible from structure alone.", + "body": "Manual fixture curation thesis for local testing.", + "source": "manual-dev-cli" + }, + { + "op": "create_edge", + "category": "rationale", + "support": { "existingCode": "G1" }, + "claim": "th1", + "stance": "for", + "rationale": "The existing goal motivates this thesis." + } + ] +} +JSON -```bash -brunch_rpc "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"workspace.activate\",\"params\":{\"decision\":{\"action\":\"newSession\",\"specId\":$SPEC_ID}}}" \ - | jq 'select(.id == 4).result | {status, spec, session}' +npm run dev -- mutate --workspace "$DEV_WORKSPACE" --params-file /tmp/brunch-mutate.json ``` -Then session calls such as `session.triggerExchange`, `session.pendingExchange`, `session.submitExchangeResponse`, and `session.runtimeState` operate on that selected session unless you pass an explicit session target where supported. - -## 5. Make a dev graph mutation - -Use `dev.graph.mutateGraph` for exact local curation or seam testing. Default to `createBasis: "explicit"` when you are manually authoring new fixture truth. - -The example below adds a thesis and connects it to an existing goal. Replace `G1` with a real code from your `graph.overview` output. +You can also pipe JSON on stdin: ```bash -cat > /tmp/brunch-dev-commit.json </.json" +npm run dev -- export --workspace "$DEV_WORKSPACE" --spec-id 1 --out "$REPO/.fixtures/seeds//.json" ``` For inspection without writing: ```bash -"$REPO/node_modules/.bin/tsx" "$REPO/src/graph/export-fixtures.ts" \ - --workspace "$DEV_WORKSPACE" \ - --spec-id "$SPEC_ID" \ - | jq '{spec, nodeCount:(.nodes|length), edgeCount:(.edges|length)}' +npm run dev -- export --workspace "$DEV_WORKSPACE" --spec-id 1 | jq '{spec, nodeCount:(.nodes|length), edgeCount:(.edges|length)}' ``` -### Basis rule of thumb - -- `explicit` — exact human-authored/manual curation or exact reviewed items. -- `implicit` — agent materialized specific graph items after concept-level acceptance. - -Do not use `dev.graph.mutateGraph` with `createBasis: "implicit"` as evidence that the product `propose-graph` flow works. Product proof requires a transcript with a real `mutate_graph` tool result. - ## 6. Run the product-path fixture curation tracer When you need proof that the agent/tool path can expand a seeded fixture, run the curation probe. It loads an explicit base variant and asks the real Brunch runtime to use `read_graph` then `mutate_graph`. @@ -181,30 +126,18 @@ When you need proof that the agent/tool path can expand a seeded fixture, run th ```bash "$REPO/node_modules/.bin/tsx" "$REPO/src/probes/fixture-curation-loop.ts" \ --fixture-root "$REPO/.fixtures" \ - --seed-set bilal-port-variants \ - --seed-slug macro-view-grounded-intent + --seed-name bilal-macro-view \ + --seed-variant grounded-intent ``` -A successful run writes: - -```text -.fixtures/runs/fixture-curation// -├── session.jsonl -├── report.json -└── graph-overview.json -``` - -The checked-in reference run `.fixtures/runs/fixture-curation/fixture-curation-2026-06-05T104440Z/` is historical pre-migration evidence from the earlier `commit_graph` tool. Fresh runs of `src/probes/fixture-curation-loop.ts` now use `mutate_graph` and should be preferred when you need current product-path proof. - -## 7. Browser/TUI notes +## 7. Browser and sidecar notes -The TUI-started web sidecar is read-only. It can observe graph updates from the same host, but it does not expose `dev.graph.mutateGraph`. +The TUI-started web sidecar is read-only. It observes graph updates from the same host, but it does not expose write methods. -For agent-addressable dev mutations, run a separate `BRUNCH_DEV=1 --mode=rpc` command against the same workspace directory. Keep to the one-writer discipline: do not run concurrent dev RPC writes and TUI/agent writes against the same workspace unless you are deliberately testing concurrency behavior. +If another coding agent needs to inspect or curate the same workbench, have it call the explicit launcher subcommands against that directory rather than talking to the sidecar directly. ## Troubleshooting -- `Method not found` for `dev.graph.mutateGraph`: check `BRUNCH_DEV=1` and ensure you are using `--mode=rpc`, not the TUI-started web sidecar. - `graph node code "G1" does not resolve`: inspect `graph.overview` for the selected `specId`; codes are spec-scoped. -- Empty `workspace.selectionState`: check that you seeded from the same `$DEV_WORKSPACE` directory you are using for RPC. -- Stale or surprising graph state: reset only the scratch workspace with `rm -rf "$DEV_WORKSPACE/.brunch"`, then reseed. +- Empty `workspace.selectionState`: check that you seeded and read from the same workbench directory. +- Stale or surprising graph state: re-run `npm run seed -- --workspace "$DEV_WORKSPACE" --seed / --reset`. diff --git a/memory/SPEC.md b/memory/SPEC.md index 9f3546178..931eab92b 100644 --- a/memory/SPEC.md +++ b/memory/SPEC.md @@ -292,7 +292,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c - **D69-L — Agent-input introspection is one read-only, dev-gated Brunch extension; mechanical and conversational modes are separate planes.** The architectural commitment is that introspection remains a single Brunch-owned, dev-gated, read-only extension family wired explicitly through the sealed Brunch Pi bundle: a passive final-payload observer plus separate read-only query tools, registered late enough to see post-mutation payloads, observing but never shaping product behavior, with registration distinct from advertisement. Current materialized state lives in [`src/dev/README.md`](src/dev/README.md), [`src/.pi/extensions/README.md`](src/.pi/extensions/README.md), [`src/.pi/extensions/dev-mode/introspection/README.md`](src/.pi/extensions/dev-mode/introspection/README.md), [`src/.pi/extensions/dev-mode/introspect-query/README.md`](src/.pi/extensions/dev-mode/introspect-query/README.md), [`src/.pi/extensions/dev-mode/session-query/README.md`](src/.pi/extensions/dev-mode/session-query/README.md), and [`src/app/pi-extensions.ts`](src/app/pi-extensions.ts). Direct diagnostic for the "Prompt-resource discretionary loading" blind spot (I38-L). Depends on: D39-L, D40-L, D58-L, D68-L, D70-L; I38-L. Supersedes: treating "how the model sees our tools/skills" as an outer-loop-only, non-instrumented concern, and the fixed structured self-report schema as the default conversational surface. - **D70-L — `.fixtures/` is a four-role tree (seeds / workbenches / runs / scratch); dev-loop artifacts decouple operating-cwd from artifact-root.** `.fixtures/` separates four lifecycles, each with its own git policy: **`seeds/`** — tracked, reusable explicit-basis starting truth consumed by the seed loader (INPUT), never local runtime DB state; **`workbenches/`** — launchable Brunch workspaces whose `.brunch/` is gitignored local state (the directories a dev `--cwd` targets, D71-L); **`runs/`** — tracked, *curated/promoted* probe evidence under `//`, probe-first per D68-L (EVIDENCE); **`scratch/`** — gitignored, ephemeral live dev-loop output under `//` (SCRATCH). Dev launchers (faux/introspection) must resolve their artifact root to the package-relative repo `.fixtures/scratch/`, **not** to the operating `cwd` — the same operating-cwd-vs-`fixtureRoot` decoupling the probe layer already uses (`mkdtemp` ephemeral cwd + repo-resolved `fixtureRoot`). This removes the `join(cwd, '.fixtures', …)` nesting defect where launching against a workbench would write `/.fixtures/…`. An exploratory scratch run becomes durable evidence only by explicit promotion (move `scratch///` → `runs///`, then track it), keeping curated `runs/` clean. `.fixtures/scratch/` is the chosen scratch home (over reusing `tmp/`) so promotion is a move within one tree. Depends on: D52-L, D68-L; the probe/transcript model. Supersedes: pinning dev-run artifacts to the operating cwd; treating all `.fixtures/runs/` output as tracked evidence; leaving the `workbenches/` role undocumented. - **D71-L — One `BRUNCH_DEV` switch gates all dev affordances; the main CLI accepts `--cwd`; introspection is present-but-dead in prod.** The over-specific `BRUNCH_DEV_RPC` env var is generalized to a single `BRUNCH_DEV` switch that, when set, enables dev affordances together: dev RPC methods (`dev.*`), registration of the read-only introspection extension (D69-L), and routing of dev-loop artifacts to `.fixtures/scratch/` (D70-L). `runBrunchCli` parses a `--cwd ` flag (defaulting to `process.cwd()`) so a dev session can target a `.fixtures/workbenches/` workspace without `cd`. Two independent prod-safety gates hold: (1) `src/dev/**` is build-excluded by `tsconfig.build.json`, so launchers/harness/alias never ship; (2) the introspection extension, though compiled into `dist` under `src/.pi/`, only *registers* when `createBrunchPiExtensions(..., { introspection: { enabled } })` opts in — and the TUI call site sets `enabled` from `BRUNCH_DEV` only, so absent the switch it is present-but-dead, never wired, honoring D39-L explicit-opt-in sealing (no ambient discovery). Brunch-launched TUI sessions keep Pi startup update suppression on in both product and `BRUNCH_DEV` runs by scoping `PI_OFFLINE=1` through `InteractiveMode.run()` unless the user already set a value; prior `PI_OFFLINE` / `PI_SKIP_VERSION_CHECK` state is restored in `finally`, never as a leaked global `process.env` mutation. Depends on: D39-L, D67-L, D68-L, D69-L, D70-L. Supersedes: the `BRUNCH_DEV_RPC`-only dev gate; relying on the operating cwd to choose the dev workspace; the assumption that the introspection extension needs build-exclusion (runtime opt-in suffices); lifting Pi offline mode in `BRUNCH_DEV` TUI sessions merely to enable live-provider behavior. -- **D79-L — Dev DB seeding is explicit, selected, and target-workspace-scoped; `npm run dev` never implies a seed.** A Brunch workspace DB is local runtime state under that launch cwd's `.brunch/`; running `npm run dev` against the repo root or a workbench may create/open that workspace, but it must not silently load reusable seed fixtures. Reusable graph seeds under `.fixtures/seeds//.json` are loaded only by an explicit seed command that names the target workspace and the seed set/slug (or an explicitly requested all-seeds batch); the loader remains a graph-domain utility over `seedFixture`/`CommandExecutor`, so seeded specs get normal `create_spec`/`mutate_graph` change-log entries, spec-local LSNs, elicitation-gap seeding, and structural validation. Workbenches under `.fixtures/workbenches//` are launchable cwd containers, not seed truth: their `.brunch/` may be reset or re-seeded locally, but tracked files must document which seed(s) a human or script should apply. Captured or newly-authored seed JSON is parked until it has at least one named consumer disposition (`test`, `preview`, `manual workbench`, `probe input`, or `parked`); existence under `seeds/` alone does not make it part of the default dev database. Depends on: D16-L, D20-L, D52-L, D70-L, D71-L. Supersedes: the catch-all `npm run seed` mental model that loads every seed into the current shell cwd; treating the repo-root `.brunch/` as canonical dev fixture state; auto-seeding because a dev host starts. +- **D79-L — Dev DB seeding is explicit, selected, and target-workspace-scoped; `npm run dev` never implies a seed.** A Brunch workspace DB is local runtime state under that launch cwd's `.brunch/`; running `npm run dev` against the repo root or a workbench may create/open that workspace, but it must not silently load reusable seed fixtures. Reusable graph seeds under `.fixtures/seeds//.json` are loaded only by an explicit seed command that names the target workspace and the seed ref (or an explicitly requested all-seeds batch); the loader remains a graph-domain utility over `seedFixture`/`CommandExecutor`, so seeded specs get normal `create_spec`/`mutate_graph` change-log entries, spec-local LSNs, elicitation-gap seeding, and structural validation. Workbenches under `.fixtures/workbenches//` are launchable cwd containers, not seed truth: their `.brunch/` may be reset or re-seeded locally, but tracked files must document which seed(s) a human or script should apply. Captured or newly-authored seed JSON is parked until it has at least one named consumer disposition (`test`, `preview`, `manual workbench`, `probe input`, or `parked`); existence under `seeds/` alone does not make it part of the default dev database. Depends on: D16-L, D20-L, D52-L, D70-L, D71-L. Supersedes: the catch-all `npm run seed` mental model that loads every seed into the current shell cwd; treating the repo-root `.brunch/` as canonical dev fixture state; auto-seeding because a dev host starts. - **D59-L — Suspended/retired: `goal` is not a runtime objective axis.** The earlier model treated a *goal* as what the session agent pursues via a strategy through a lens. D85-L first inlined the useful objective postures into the elicitor role prompt; D98-L now suspends the broader runtime-axis model. The useful residue is prompt guidance derived from readiness-band coverage (D64-L) rather than a stored grade: `grounding-advance` (fill grounding gaps and raise grounding coverage), `elicit-expand` (expand the elicited specification graph while ambiguity remains productive), `commit-converge` (reduce / lock down reviewable commitments), plus an always-on `capture-posture` (capture or confirm dev `posture`, D45-L). These are not pinned, AUTO-selected, or persisted; the SPEC-mode elicitor chooses its next move from pushed context, graph/gap state, and loaded references. `elicit-expand` and `commit-converge` intentionally form the diverge/converge pair for the elicitation diamond; `elicit-I` / `elicit-II` are retired because they were phase-like labels, not objectives. Depends on: D45-L, D57-L, D58-L, D64-L, D98-L. Refined by: D85-L and D98-L. Supersedes: conflating the elicit lifecycle objective with strategy selection, deriving the goal set from a stored readiness grade, and persisting `goal` as a runtime/manifest axis. - **D66-L — Structure-optional user turns feed SPEC-mode capture; `freestyle` is no longer runtime strategy state.** The durable commitment is that ordinary user-driven turns, pasted material, and structure-optional conversation are allowed without banning structured exchanges. D98-L supersedes the `freestyle` runtime-strategy framing: this behavior is part of SPEC-mode elicitation/capture conduct, not a separate op_mode, authority posture, AUTO strategy, or explicit user/system runtime pin. Current materialized state lives in [`src/agents/skills/strategies/README.md`](src/agents/skills/strategies/README.md), [`src/agents/skills/strategies/freestyle/SKILL.md`](src/agents/skills/strategies/freestyle/SKILL.md), [`src/agents/skills/methods/capture/SKILL.md`](src/agents/skills/methods/capture/SKILL.md), [`src/agents/runtime/policy.ts`](src/agents/runtime/policy.ts), [`src/.pi/extensions/commands/index.ts`](src/.pi/extensions/commands/index.ts), and [`src/session/README.md`](src/session/README.md). Depends on: D18-L, D25-L, D26-L, D40-L, D45-L, D49-L, D50-L, D59-L, D63-L, D65-L. Refines: R16. Refined by: D80-L, D81-L (2026-06-12 FE-861 grill: the capture half — submit-time capture wiring and the "directly-stated only" commitment line — is superseded by the banded capture sweep and the commitment gradient; capture runs on every elicitor turn over the un-swept tail, resolving the every-turn-vs-on-demand open question). Supersedes: treating offer-first (R16) as a universal per-turn session invariant; treating freestyle as a new operational mode or authority posture. - **D80-L — Generalized capture is the elicitor's banded capture sweep: in-turn, synchronous, over the un-swept transcript tail.** Capture is conduct of the foreground elicitor, not product wiring: there is no observer/auditor queue on the primary path (D18-L, reaffirmed — the v1 observer failed on structure-dependence and context starvation), no product-side LLM extraction pass on the submit paths, no gateway/translation/judgment layer between the agent and graph truth, and no capture subagent in the current block. The **banded capture sweep** is one band-ordered pass that walks intent-kind groups (the same typology the `elicitation_gaps` register references via `refersTo: NodeKind`, D65-L/D75-L), committing through the real role-named `mutateGraph` grammar (D53-L/A14-L) and moving gap dispositions through `update_elicitation_gaps`. Its input window is the **un-swept transcript tail** — all conversational and digest content since the last sweep, tracked by a **sweep watermark** (prior art: the I45-L assistant-visible watermark and the own-mutation stamp) — so capture is robust to RPC-submitted messages, interruptions, and multi-message bursts, and probes get a crisp invariant: after any elicitor turn, nothing conversational remains behind the watermark. Default is a single pass; bulk material (pastes, document reads, exploration digests) may engage an **escalation valve** — chunked/iterated sweeping within the same turn — without changing window or watermark semantics. Choreography is **capture-then-ask**: the sweep commits facts and moves gaps *before* the elicitor composes its next question, so the question provably benefits from what was just captured. Consequence: the deterministic labeled-prefix capture core (`graph/capture/structured-response.ts`), its `session.submitMessage`/`session.submitExchangeResponse` wiring, and the `capture-response-to-graph` proof are retired fossils — capture becomes turn-coupled (same agent for RPC transport clients, different moment; no coverage loss). Depends on: A14-L, A22-L, D18-L, D49-L, D53-L, D63-L, D65-L, D66-L; I45-L. Supersedes: submit-time product-side capture (the D66-L "exactly as the structured-response capture tracer does" wiring), the labeled-prefix extraction core, and the capture-quality spike's product-side extraction-pass shape. @@ -377,7 +377,7 @@ The POC's purpose is to prove three things: (a) that pi's coding-agent harness c | I45-L | A session's assistant-visible watermark advances only when a continuity entry naming a strictly higher spec-local LSN is inserted: a boot/context seed or whole-spec overview snapshot, a `worldUpdate` for any write not already assistant-visible through another carrier (naming only items with LSN strictly greater than the pre-update watermark, I4-L), or the session's own graph-mutation `toolResult`. `worldUpdate` covers foreign writes **and** same-session writes that did not ride an own-mutation `toolResult` (e.g. submit-time / freestyle capture); such a same-session capture advances `current_lsn` and is surfaced by the next `worldUpdate`, never silently swallowed. A freshly seeded session whose seed named the current snapshot LSN does not immediately synthesize a redundant `worldUpdate`. Narrow `getNodes`/`queryNodes` reads do not advance the global watermark (they update per-entity read ledgers only). When `current_lsn == watermark` no `worldUpdate` is synthesized, and the session's own already-visible mutations never produce a `worldUpdate`. The watermark is its own projection over the carrier set (distinct from `runtimeState.world.latestLsn`), projected from transcript continuity entries (D43-L), never a stored field. | covered (2026-06-11: all I45 Tier-2 scaffold rows run live through real `runBrunchTui` boot in `src/dev/tier-2-harness.test.ts`; the live `before_provider_request` guard delegates to `guardBeforeProviderRequest` retry semantics) | D43-L, D76-L, D77-L; I1-L, I4-L | | I46-L | Session origination never writes a fabricated user transcript entry. A new session inserts seed continuity entries and then kicks an assistant-authored opening turn (no product-fabricated exchange — D78-L 2026-06-12 revision) before idling; a resumed session decides the kick from the **latest unresolved conversational debt**, computed by ignoring trailing continuity-only entries — any reconciler-inserted notice owing no assistant continuation: seed / `worldUpdate` / `brunch.mention*` / `brunch.session_lifecycle` / side-task & reviewer drains — whether inserted this boot or persisted by a prior boot — it originates a turn iff that debt owed assistant continuation (a user message or an incomplete exchange-tuple awaiting the assistant), and otherwise rests at an assistant/system-originated leaf (I13-L). The kick decision is idempotent across crash/reboot: trailing continuity notices neither mask an older unanswered debt nor manufacture a kick over a satisfied leaf. AUTO never originates a `freestyle` turn (D66-L); only an explicit `freestyle` pin yields a wait-for-user idle. | covered (2026-06-11: new-session seed-then-kick plus all four resume rows run live through real boot/resume — pre-reconcile user-tail kick including after earlier completed exchanges, `request_*`/system leaves idle against the real result envelope (outcome is `answered`/`cancelled`/`unavailable` **key presence** per `projections/exchanges`, never a status string), crash-after-notice re-kick, drains neither manufacture nor mask debt; kick origin derives from projected transcript state, not entry counts. **FE-857 2026-06-11:** the seed's provider-visible content carries the spec overview + top-ranked open-gap framing (`composeContextSeedContent`), and pi's `buildSessionContext` surfaces it plus a real gap question in the LLM context. **Lifecycle closed 2026-06-11 (`origination-kick-live`):** the earlier "startup completeness" claim was harness-assisted (the oracle drove the turn the product never triggered — caught by manual walkthrough). Now the product completes the kick itself: a 'start' origination decision fires `session.sendCustomMessage(kickTurnMessage(origin), { triggerTurn: true })` after session creation, guarded on model availability so unauthenticated launches idle. The **product-originated-turn oracle** boots the real `runBrunchTui` path (new-spec and picker paths) with only the provider backend substituted and observes the provider call with **no harness prompt**; reboot over a kicked session stays idle; no-model boots append no kick. `brunch.kick` is a transcript custom message (I47-L), never a fabricated user entry. **D78-L revision landed 2026-06-12 (`origination-native-elicitation` card 1):** origination is seed-only — zero fabricated `present_*` offers in product transcripts; the deterministic generator is probe-land machinery for R24 evidence; `session.triggerExchange` is a kick surface reporting idle when no assistant-created exchange exists; resume-kick decision rows are proven as live product-originated turns over fixture transcripts (faux backend, no harness prompt); crash-after-kick reboot rests idle by assertion) | D66-L, D78-L; R16; I13-L | | I47-L | Continuity facts (seed/refresh, `worldUpdate`, `brunch.mention*`, `brunch.session_lifecycle`) persist only as Brunch custom transcript entries — never synthetic `toolCall`s, never prompt-only injection — so the D43-L projection can reconstruct them. Model-intent facts (`worldUpdate`, drains, staleness hints, context seed) ride pi's `CustomMessageEntry` (provider-visible `content` + structured `details`); ledger-only facts (`own_mutation`, `mention`, runtime state, binding, lifecycle) ride `CustomEntry` — both are custom transcript entries under this invariant (FE-857 carrier migration); boot/resume seeding is idempotent, deriving dedupe from projected transcript state (a seed/world-update already present is not re-emitted) rather than from hidden flags, and survives real restart/resume. The watermark must also survive compaction: the preserved-anchor set retains the latest watermark-carrier entry per spec so the projected global watermark never regresses after compaction+resume (which would otherwise spuriously re-emit `worldUpdate`). | covered (2026-06-11: boot/resume dedupe proven across an actual restart via `rebootTier2Runtime` — seed, kick, and `worldUpdate` non-duplicated, derived purely from transcript projection; compaction-anchor carrier preservation asserted at projection level; the Tier-2 scaffold has zero skipped/todo rows) | D17-L, D37-L, D43-L, D76-L, D78-L | -| I48-L | Dev seeding never mutates an unintended workspace and never loads unrelated reusable seeds by ambient default: the seed path is target-workspace-scoped, selected by seed set/slug unless an all-seeds batch is explicitly requested, routes through `CommandExecutor`, and reports the destination `.brunch/data.db`; dev launch (`npm run dev`, with or without `--cwd`) observes existing workspace DB state but does not imply seeding. | partially validated — seed CLI now requires unambiguous `--workspace` + safe `--seed /` input, rejects malformed/unknown/duplicate flags before opening a workspace DB, writes only the named workspace DB through `seedFixture`/`CommandExecutor`, reports destination + selected seed ref mapping, and product RPC `workspace.selectionState` through `--cwd` proves seeded-vs-sibling workspace isolation; explicit all-seeds opt-in and full seed disposition catalog remain `dev-seed-fixtures` follow-up. | D70-L, D71-L, D79-L; I1-L, I11-L | +| I48-L | Dev seeding never mutates an unintended workspace and never loads unrelated reusable seeds by ambient default: the seed path is target-workspace-scoped, selected by seed ref unless an all-seeds batch is explicitly requested, routes through `CommandExecutor`, and reports the destination `.brunch/data.db`; dev launch (`npm run dev`, with or without `--cwd`) observes existing workspace DB state but does not imply seeding. | partially validated — seed CLI now requires unambiguous `--workspace` + safe `--seed /` input, rejects malformed/unknown/duplicate flags before opening a workspace DB, writes only the named workspace DB through `seedFixture`/`CommandExecutor`, reports destination + selected seed ref mapping, and product RPC `workspace.selectionState` through `--cwd` proves seeded-vs-sibling workspace isolation; explicit all-seeds opt-in and full seed disposition catalog remain `dev-seed-fixtures` follow-up. | D70-L, D71-L, D79-L; I1-L, I11-L | | I49-L | The op_mode delegatable-set allowlist is the subagent write-safety boundary. A background agent's tool grant may exceed its spawning parent's, but an op_mode may spawn only the background agents named in its **code-owned** delegatable-set allowlist; a frontmatter-authored manifest can never self-advertise into an op_mode's delegatable set. Protected property: a read-only `elicit` session cannot spawn a write-capable child unless elicit's delegatable set explicitly names it. Ambient seal preserved alongside (D39-L): an injected-world background child still constructs in-memory auth/settings/session and performs no `~/.pi` discovery — world access is **injected, never discovered**. | covered (2026-06-24: `FOREGROUND_AGENT_ROSTER.elicit.foregroundAgent.canDelegate` is populated with the read-only background roster; `delegatableAgentsForRuntimeState` feeds `loadBrunchSubagents`; `registerBrunchSubagents` advertises and executes only `definitions ∩ delegatableAgents`; `subagents.test.ts` plants a test-only write-capable `worker` and proves `elicit` refuses it, while background frontmatter cannot author `canDelegate`) | D39-L, D40-L; D91-L, D92-L | | I50-L | The readiness-band axis has two carriers that must not re-couple: the elicitor's asking agenda reads `gap.band` (`elicitation_gaps`, elicitation super-type only — `grounding`/`elicitation`), and node-level filter/render/threshold reads node band membership derived from `plane` (D94-L). The gap-driver sort and the readiness-estimate rollup never read the node-kind table; node band membership is never stored per-kind where `plane` determines it. No reader gates graph truth or work on band (I31-L). | covered (`src/projections/session/__tests__/readiness-estimate.test.ts` source-guards both `readiness-estimate.ts` and `elicitation-driver.ts` against `NODE_KIND_METADATA` / `bandsForKind` / `schema/nodes` imports; `src/graph/__tests__/read-api.test.ts` proves `queryGraph(..., { bands })` uses derived projection/commitment/dual-band membership; `src/agents/contexts/graph/__tests__/graph-slice.test.ts` proves projection and band-less render handling; `src/graph/__tests__/command-executor.test.ts` proves `projection` is legal and unknown bands reject at the command boundary; `src/.pi/__tests__/graph-tools.test.ts` proves `read_graph` advertises the closed four-band enum) | D64-L, D94-L; I31-L, I35-L, I39-L | | I51-L | `present_candidates` is fan-out recognition only: it presents candidate graph expressions and records the chosen fan-in mode (`pick` / `synthesize` / `compose`, D96-L), but never commits graph truth itself. Generative commitment crosses into the graph only through the review-set path (`acceptReviewSet`, D27-L) or, for concept-accept direct commit, the `mutateGraph` grammar (D53-L); no candidate-presentation tool writes nodes/edges. | partially covered (2026-06-24 FE-1059 pick-only un-stub: `present_candidates` tool/projection/renderer carry no `CommandExecutor`/graph dependency, and `structured-exchange-present-request.test.ts` proves a pending `present_candidates` resolves to a `request_choice`/`capture_candidate` pick with no graph write; promoted run `.fixtures/runs/generate-fan-out/2026-06-24T16-51-13-704Z/` proves the live oracle fan-out turn emitted `present_candidates` while graph LSN/node/edge counts stayed unchanged and no `mutate_graph` or approved review result appeared; the `capture_candidate`→review-set/`mutateGraph` commit leg remains for later slices) | D26-L, D27-L, D53-L, D96-L | @@ -629,7 +629,7 @@ src/.pi/ | **Faux loop** | Deterministic in-process dev loop: an `AgentSession` driven by the pi faux provider with `.inMemory()` auth/registry/session/settings, scripting LLM turns via `setResponses`. The inner/middle-loop substrate for wrapper logic and regressions; no network, keys, or tokens (D68-L). | | **Introspection loop** | Real-provider dev loop that captures exactly what the model receives (system prompt, tool schemas, prompt-resource manifest) via the read-only D69-L extension, and pairs it with interactive interrogation of the model about clarity. Diagnoses I38-L discretionary-loading questions. | | **Dev front door** | The consolidated `src/dev/` surface owning the three DX loop launchers and the shared faux-harness factory (D68-L). Distinct from `src/probes/` product-verification probe runs. | -| **Seed fixture** | Tracked reusable explicit-basis starting graph truth under `.fixtures/seeds//.json`, consumed by the seed loader through `seedFixture`/`CommandExecutor` (D79-L). It is input data, not a workbench DB snapshot and not probe evidence; each seed needs a named consumer disposition before it becomes part of a default dev/test flow. | +| **Seed fixture** | Tracked reusable explicit-basis starting graph truth under `.fixtures/seeds//.json`, consumed by the seed loader through `seedFixture`/`CommandExecutor` (D79-L). It is input data, not a workbench DB snapshot and not probe evidence; each seed needs a named consumer disposition before it becomes part of a default dev/test flow. | | **Workbench** | A launchable Brunch workspace under `.fixtures/workbenches//` that a dev session targets with `--cwd` (D71-L). Its `.brunch/` runtime state is gitignored local state, not tracked evidence or reusable seed truth. The operating-cwd axis of a dev run, distinct from the artifact-root axis (D70-L); tracked workbench docs name which seed(s) to apply rather than committing the resulting DB. | | **Scratch run** | Gitignored ephemeral dev-loop output under `.fixtures/scratch///`, always resolved to the repo-root `.fixtures/` rather than the operating cwd (D70-L). Becomes durable evidence only by explicit promotion to a tracked `runs///`. | | **Promotion** | The explicit act of moving a `scratch///` run into tracked `runs///` evidence, the only path by which exploratory dev output becomes a curated probe run (D70-L). | @@ -795,7 +795,7 @@ The first required probe is M0: after manual TUI interaction, a checker proves ` | I45-L | Middle — watermark-projection property tests (own-write stamping vs foreign `worldUpdate`; strict-greater item set per I4-L; no-`worldUpdate` when `current==watermark`); **seed/full-overview snapshots advance the watermark while narrow `getNodes`/`queryNodes` reads do not**; **no redundant `worldUpdate` immediately after a seed that named the current snapshot LSN**; **same-session submit/capture write bumps `current_lsn` and is surfaced by the next `worldUpdate` (not swallowed)**; **a foreign write that lands between the snapshot read and seed insertion is not masked by the seed**; change-log-range fixtures driving a foreign writer (a second faux session or a direct `CommandExecutor` write) through the real boot. Inner — projection unit tests over synthetic transcript continuity entries. **Live 2026-06-11** — the coverage-first scaffold is fully flipped; no skipped/`todo` rows remain. | | I46-L | Middle — Tier-2 faux-turn-through-real-boot assertions: new session seeds-then-kicks before the first provider call; resumed-session kick decision classifies **latest unresolved conversational debt** (ignoring trailing continuity-only entries) and still fires when a user tail is followed by reconciler-inserted seed/staleness notices; **crash-after-notice-before-provider reboot still kicks when the underlying debt is an unanswered user/assistant turn** (idempotent re-boot); resumed-session kick stays silent when the latest debt already rests at a `request_*`/system leaf; no fabricated user entry in any path; AUTO never originates `freestyle`. Outer — manual walkthrough of opening-offer quality. **Live 2026-06-11** via `bootTier2RuntimeFromFixture` (real-boot-over-fixture resume chassis); the `request_*` idle proof uses fixtures built from the real result projections (key-presence envelope), not hand-built shapes. D98-L follow-up should replace the legacy AUTO/freestyle origination assertion with SPEC-mode offer/ambient-turn policy. | | I47-L | Middle — restart/resume idempotence property tests (repeated boot does not duplicate seed/`worldUpdate`; dedupe derived from projection); **compaction+resume preserves the projected watermark and does not spuriously re-emit `worldUpdate`** (preserved-anchor set retains the latest watermark carrier); carrier-discipline source/architecture tests (continuity facts are custom entries, not synthetic `toolCall`s or prompt-only). **Live 2026-06-11** via `rebootTier2Runtime` (actual restart over the same session file, Pi's deferred JSONL flushed first); the suite's sets-and-`{specId, lsn}` convention is enforced mechanically by a source scan banning golden matchers. | -| I48-L | Inner — seed CLI contract tests for target workspace resolution, seed set/slug filtering, explicit all-seeds mode, `CommandExecutor`/change-log routing, and destination reporting. Middle — fresh workbench tracer: seed one named fixture into `.fixtures/workbenches//.brunch/data.db`, launch `npm run dev -- --cwd .fixtures/workbenches/` (or print/RPC equivalent), and assert selected workspace state plus graph overview come only from that workbench DB. | +| I48-L | Inner — seed CLI contract tests for target workspace resolution, seed-ref filtering, explicit all-seeds mode, `CommandExecutor`/change-log routing, and destination reporting. Middle — fresh workbench tracer: seed one named fixture into `.fixtures/workbenches//.brunch/data.db`, launch `npm run dev -- --cwd .fixtures/workbenches/` (or print/RPC equivalent), and assert selected workspace state plus graph overview come only from that workbench DB. | | I49-L | Middle (covered by `subagent-reconciliation` slice 4) — negative-space invariant over the code-owned op_mode→delegatable-set allowlist: spawnable agents per op_mode equal the allowlist; a frontmatter-authored manifest cannot widen advertisement into a read-only mode; a **test-only write-capable background manifest** proves `elicit` refuses to spawn it, so the boundary is proven before the execute-mode write worker exists. Paired with the D91-L ambient-seal assertion (world is injected, not discovered: in-memory services, no `~/.pi`). See `src/.pi/extensions/subagents/subagents.test.ts` and §Verification Design subagent-reconciliation oracle battery (oracle 4). | ### Design Notes diff --git a/package-lock.json b/package-lock.json index 536ea3ee2..2c3936e8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "brunch": "bin/brunch.js" }, "devDependencies": { + "@clack/prompts": "^1.6.0", "@sinclair/typebox": "^0.34.49", "@tailwindcss/vite": "^4.3.1", "@testing-library/dom": "^10.4.1", @@ -632,6 +633,36 @@ "specificity": "bin/cli.js" } }, + "node_modules/@clack/core": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.2.tgz", + "integrity": "sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.6.0.tgz", + "integrity": "sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.2", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -10210,6 +10241,13 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index a056435f8..a593c1ec0 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "tag": "alpha" }, "scripts": { - "dev": "tsx src/app/brunch.ts", + "dev": "tsx scripts/dev.ts", + "dev:raw": "tsx src/app/brunch.ts", "probe:generate-fan-out": "PI_OFFLINE=0 PATH=\"$HOME/.local/share/mise/installs/node/lts/bin:$PATH\" tsx src/dev/generate-fan-out-witness.ts", "build": "tsc -p tsconfig.build.json && npm run build:info && npm run build:pi-assets && npm run build:web", "build:info": "node scripts/write-build-info.mjs", @@ -82,6 +83,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@clack/prompts": "^1.6.0", "@sinclair/typebox": "^0.34.49", "@tailwindcss/vite": "^4.3.1", "@testing-library/dom": "^10.4.1", diff --git a/scripts/dev.ts b/scripts/dev.ts new file mode 100644 index 000000000..955f67865 --- /dev/null +++ b/scripts/dev.ts @@ -0,0 +1,13 @@ +import process from 'node:process'; + +import { runDevCli } from '../src/dev/dev-cli.js'; + +async function main(): Promise { + process.exitCode = await runDevCli(); +} + +main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; +}); diff --git a/src/.pi/components/brunch-version.ts b/src/.pi/components/brunch-version.ts index cd78fc415..ff47dc7f4 100644 --- a/src/.pi/components/brunch-version.ts +++ b/src/.pi/components/brunch-version.ts @@ -1,7 +1,7 @@ -import { execSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; +import { resolveBrunchBuildInfo } from '../../build-info.js'; import type { BrunchVersionInfo } from './brunch-identity.js'; // Single source of truth for the TUI's version + dev-build marker. The startup @@ -10,21 +10,11 @@ import type { BrunchVersionInfo } from './brunch-identity.js'; // heuristics, which silently break once the package ships a real version. const PACKAGE_ROOT_URL = new URL('../../../', import.meta.url); const PACKAGE_JSON_URL = new URL('package.json', PACKAGE_ROOT_URL); -// Written by scripts/write-build-info.mjs during `npm run build`. Resolves to -// dist/build-info.json when running compiled output; from source (tsx, vitest) -// it points at src/build-info.json, which never exists. -const BUILD_INFO_URL = new URL('../../build-info.json', import.meta.url); interface PackageJson { version?: unknown; } -interface BuildInfo { - dev: boolean; - gitSha: string; - buildTime: string; -} - /** * Resolve the version line + dev marker. Compiled output ships build-info.json * (dev for local builds, `dev: false` for `RELEASE=true` builds). Running @@ -34,11 +24,7 @@ interface BuildInfo { export function resolveBrunchVersion(): BrunchVersionInfo { const pkg = readPackage(); const version = typeof pkg.version === 'string' ? pkg.version : '0.0.0'; - const buildInfo = readBuildInfo() ?? { - dev: true, - gitSha: getGitSha(), - buildTime: formatUtcBuildTime(new Date()), - }; + const buildInfo = resolveBrunchBuildInfo(); if (!buildInfo.dev) return { version: `v${version}`, dev: null }; const devMeta = [buildInfo.gitSha, buildInfo.buildTime ? `@ ${buildInfo.buildTime}` : ''] @@ -47,14 +33,6 @@ export function resolveBrunchVersion(): BrunchVersionInfo { return { version: `v${version}`, dev: devMeta ? `(dev ${devMeta})` : '(dev)' }; } -function formatUtcBuildTime(date: Date): string { - // toISOString is always UTC; keep the explicit suffix so it displays as such. - return date - .toISOString() - .replace('T', ' ') - .replace(/\.\d+Z$/, ' UTC'); -} - function readPackage(): PackageJson { try { return JSON.parse(readFileSync(fileURLToPath(PACKAGE_JSON_URL), 'utf8')) as PackageJson; @@ -62,28 +40,3 @@ function readPackage(): PackageJson { return {}; } } - -function readBuildInfo(): BuildInfo | null { - try { - const raw = JSON.parse(readFileSync(fileURLToPath(BUILD_INFO_URL), 'utf8')) as Partial; - return { - dev: raw.dev === true, - gitSha: typeof raw.gitSha === 'string' ? raw.gitSha : '', - buildTime: typeof raw.buildTime === 'string' ? raw.buildTime : '', - }; - } catch { - return null; - } -} - -function getGitSha(): string { - try { - return execSync('git rev-parse --short=7 HEAD', { - cwd: fileURLToPath(PACKAGE_ROOT_URL), - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - } catch { - return ''; - } -} diff --git a/src/.pi/extensions/__tests__/agent-runtime-system-prompts.test.ts b/src/.pi/extensions/__tests__/agent-runtime-system-prompts.test.ts index ad1f1b09e..083ce60df 100644 --- a/src/.pi/extensions/__tests__/agent-runtime-system-prompts.test.ts +++ b/src/.pi/extensions/__tests__/agent-runtime-system-prompts.test.ts @@ -428,7 +428,7 @@ describe('Brunch prompt-pack topology', () => { coordinator: {} as never, graphMentionSource: { listMentionCandidates: () => [] }, promptContext, - introspection: { enabled: true, store: createInMemoryBrunchIntrospectionStore() }, + introspection: { queryTools: true, store: createInMemoryBrunchIntrospectionStore() }, }, )({ on: (eventName: string, handler: (event: never, ctx?: never) => unknown) => { diff --git a/src/.pi/extensions/__tests__/brunch-data-context.test.ts b/src/.pi/extensions/__tests__/brunch-data-context.test.ts index 907081045..8d483281a 100644 --- a/src/.pi/extensions/__tests__/brunch-data-context.test.ts +++ b/src/.pi/extensions/__tests__/brunch-data-context.test.ts @@ -209,8 +209,8 @@ describe('context tools', () => { it('read_workspace_context returns a workspace overview for bound specs and sessions', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-context-overview-')); const executor = await openWorkspaceCommandExecutor(cwd); - const alpha = seedFixture(executor, await loadFixture('alpha-grounding', 'workspace-spread')); - const beta = seedFixture(executor, await loadFixture('beta-commitments', 'workspace-spread')); + const alpha = seedFixture(executor, await loadFixture('workspace-alpha-grounding')); + const beta = seedFixture(executor, await loadFixture('workspace-beta-commitments')); await mkdir(join(cwd, '.brunch', 'sessions'), { recursive: true }); await writeBoundSession(cwd, 'alpha-session', alpha.specId, [messageEntry('u1', 'user')]); @@ -251,8 +251,8 @@ describe('context tools', () => { it('read_specification_context returns the selected spec render through the registered tool', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-specification-tool-')); const executor = await openWorkspaceCommandExecutor(cwd); - const alpha = seedFixture(executor, await loadFixture('alpha-grounding', 'workspace-spread')); - const beta = seedFixture(executor, await loadFixture('beta-commitments', 'workspace-spread')); + const alpha = seedFixture(executor, await loadFixture('workspace-alpha-grounding')); + const beta = seedFixture(executor, await loadFixture('workspace-beta-commitments')); await mkdir(join(cwd, '.brunch', 'sessions'), { recursive: true }); await writeBoundSession(cwd, 'alpha-session', alpha.specId, [messageEntry('u1', 'user')]); @@ -346,9 +346,9 @@ describe('context tools', () => { }); }); -async function loadFixture(slug: string, set = 'bilal-port'): Promise { +async function loadFixture(name: string, variant = 'base'): Promise { const fixturePath = fileURLToPath( - new URL(`../../../../.fixtures/seeds/${set}/${slug}.json`, import.meta.url), + new URL(`../../../../.fixtures/seeds/${name}/${variant}.json`, import.meta.url), ); return JSON.parse(await import('node:fs/promises').then(({ readFile }) => readFile(fixturePath, 'utf8'))); } diff --git a/src/.pi/extensions/__tests__/dev-mode-introspection.test.ts b/src/.pi/extensions/__tests__/dev-mode-introspection.test.ts index e6d0a683f..6d7e02439 100644 --- a/src/.pi/extensions/__tests__/dev-mode-introspection.test.ts +++ b/src/.pi/extensions/__tests__/dev-mode-introspection.test.ts @@ -237,7 +237,7 @@ describe('Brunch introspection extension', () => { const devApi = createFakeExtensionApi(); await createBrunchPiExtensions(brunchChromeFixture, undefined, { coordinator: {} as never, - introspection: { enabled: true, store: createInMemoryBrunchIntrospectionStore() }, + introspection: { queryTools: true, store: createInMemoryBrunchIntrospectionStore() }, })(devApi.api as never); expect(devApi.commandNames.at(-1)).toBe(BRUNCH_INTROSPECTION_COMMAND); @@ -257,7 +257,7 @@ describe('Brunch introspection extension', () => { const devApi = createFakeExtensionApi(); await createBrunchPiExtensions(brunchChromeFixture, undefined, { coordinator: {} as never, - introspection: { enabled: true, store: createInMemoryBrunchIntrospectionStore() }, + introspection: { queryTools: true, store: createInMemoryBrunchIntrospectionStore() }, })(devApi.api as never); await devApi.emitBeforeAgentStart({ systemPrompt: 'base' }); diff --git a/src/.pi/extensions/__tests__/registry.test.ts b/src/.pi/extensions/__tests__/registry.test.ts index f5c35dcce..d7f65006d 100644 --- a/src/.pi/extensions/__tests__/registry.test.ts +++ b/src/.pi/extensions/__tests__/registry.test.ts @@ -151,7 +151,7 @@ describe('Brunch explicit Pi extension registry', () => { expect(sessionStartIndexes[0]).toBeLessThan(sessionStartIndexes[1] ?? -1); }); - it('registers the executor stub tool on the default product extension path', async () => { + it('keeps the default stub tool output aligned with its registered identity', async () => { const registeredTools: Array<{ name: string; execute: (toolCallId: string, params: unknown) => Promise<{ content: readonly { text: string }[] }>; @@ -175,9 +175,9 @@ describe('Brunch explicit Pi extension registry', () => { const stub = registeredTools.find((tool) => tool.name === BRUNCH_ORCHESTRATOR_STUB_TOOL); expect(stub).toBeDefined(); - await expect(stub!.execute('call-1', { message: 'standup' })).resolves.toMatchObject({ - content: [{ type: 'text', text: 'executor stub ran: standup' }], - }); + const result = await stub!.execute('call-1', { message: 'standup' }); + const toolLabel = BRUNCH_ORCHESTRATOR_STUB_TOOL.replaceAll('_', ' '); + expect(result.content[0]?.text).toBe(`${toolLabel} ran: standup`); }); it('registers both graph-register and elicitation-register tools when graph deps are provided', async () => { diff --git a/src/.pi/extensions/agent-runtime/orchestrator-stub/index.ts b/src/.pi/extensions/agent-runtime/orchestrator-stub/index.ts index 7ee4b392a..6818125ad 100644 --- a/src/.pi/extensions/agent-runtime/orchestrator-stub/index.ts +++ b/src/.pi/extensions/agent-runtime/orchestrator-stub/index.ts @@ -30,7 +30,7 @@ export function createOrchestratorStubTool(): ToolDefinition< parameters: OrchestratorStubParams, async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { return { - content: [{ type: 'text' as const, text: `executor stub ran: ${params.message}` }], + content: [{ type: 'text' as const, text: `orchestrator stub ran: ${params.message}` }], details: { message: params.message }, }; }, diff --git a/src/agents/contexts/README.md b/src/agents/contexts/README.md index 95adad387..97fdbe1eb 100644 --- a/src/agents/contexts/README.md +++ b/src/agents/contexts/README.md @@ -8,7 +8,6 @@ SPEC decisions: D52-L, D58-L, D60-L, D76-L, D78-L, D83-L, D91-L, D96-L ```text contexts/ -├── primitives/ markdown, TOON, tree, and section formatting helpers ├── references/ runtime-eligible shared context references cited by skills/prompts ├── seeds/ per-turn pushed context blocks and origination seed payloads ├── graph/ graph overview/neighborhood, related-node, mutation, reconciliation text @@ -19,6 +18,8 @@ contexts/ └── exchanges/ present_* / request_* structured-exchange result text ``` +Formatting primitives used by these renderers live in `src/agents/shared/`; they are shared helper substrate, not a child context surface. + ## Boundary rules ```pseudo diff --git a/src/agents/contexts/graph/__snapshots__/graph-overview-kind-band-spread.md b/src/agents/contexts/graph/__snapshots__/graph-overview-kind-coverage-matrix.md similarity index 100% rename from src/agents/contexts/graph/__snapshots__/graph-overview-kind-band-spread.md rename to src/agents/contexts/graph/__snapshots__/graph-overview-kind-coverage-matrix.md diff --git a/src/agents/contexts/graph/__tests__/graph-slice.test.ts b/src/agents/contexts/graph/__tests__/graph-slice.test.ts index e92743cbb..66affc67f 100644 --- a/src/agents/contexts/graph/__tests__/graph-slice.test.ts +++ b/src/agents/contexts/graph/__tests__/graph-slice.test.ts @@ -96,11 +96,11 @@ test('overview includes LSN on empty selected-spec graph', () => { ); }); -test('overview: kind-band fixture golden stays uncapped and sectioned', async () => { +test('overview: kind-coverage fixture golden stays uncapped and sectioned', async () => { const rendered = formatGraphOverview( - readGraphSliceFixture({ set: 'kind-band-spread', fixture: 'coverage-matrix' }), + readGraphSliceFixture({ name: 'kind-coverage-matrix', variant: 'base' }), ); - await expect(rendered).toMatchFileSnapshot('../__snapshots__/graph-overview-kind-band-spread.md'); + await expect(rendered).toMatchFileSnapshot('../__snapshots__/graph-overview-kind-coverage-matrix.md'); expect(rendered).toContain('Graph overview (LSN 2): 24 nodes, 7 edges'); expect(rendered).toContain('| S1 | 24 | Lock one neighborhood preview |'); }); diff --git a/src/agents/contexts/graph/__tests__/node-neighborhood.test.ts b/src/agents/contexts/graph/__tests__/node-neighborhood.test.ts index 0dbec391b..c4f919740 100644 --- a/src/agents/contexts/graph/__tests__/node-neighborhood.test.ts +++ b/src/agents/contexts/graph/__tests__/node-neighborhood.test.ts @@ -25,12 +25,12 @@ function expectNoStructuralLeak(rendered: string): void { expect(rendered).not.toContain('[Selected-spec node context]'); // old bracket header dialect } -const HUB = { set: 'edge-spread', fixture: 'hub-neighborhood', anchorCode: 'REQ1' } as const; -const SELF = { set: 'brunch-self', fixture: 'spec-graph' } as const; +const HUB = { name: 'edge-hub-neighborhood', variant: 'base', anchorCode: 'REQ1' } as const; +const SELF = { name: 'brunch-self', variant: 'base' } as const; test('neighborhood: real-port anchor (code-health REQ1)', async () => { const rendered = formatNeighborhood( - readNodeNeighborhoodFixture({ set: 'bilal-port', fixture: 'code-health', anchorCode: 'REQ1' }), + readNodeNeighborhoodFixture({ name: 'bilal-code-health', variant: 'base', anchorCode: 'REQ1' }), ); await expect(rendered).toMatchFileSnapshot('../__snapshots__/neighborhood-code-health-REQ1.md'); expectNoStructuralLeak(rendered); diff --git a/src/agents/contexts/graph/__tests__/related-nodes.test.ts b/src/agents/contexts/graph/__tests__/related-nodes.test.ts index 50fed0b40..ffcce168a 100644 --- a/src/agents/contexts/graph/__tests__/related-nodes.test.ts +++ b/src/agents/contexts/graph/__tests__/related-nodes.test.ts @@ -18,7 +18,7 @@ test('related nodes uses semantic relation labels instead of raw graph internals const rendered = formatRelatedNodesResult({ status: 'success', anchors: [ - readNodeNeighborhoodFixture({ set: 'edge-spread', fixture: 'hub-neighborhood', anchorCode: 'REQ1' }), + readNodeNeighborhoodFixture({ name: 'edge-hub-neighborhood', variant: 'base', anchorCode: 'REQ1' }), ], }); diff --git a/src/agents/contexts/spec/__tests__/spec-context.test.ts b/src/agents/contexts/spec/__tests__/spec-context.test.ts index a8866627c..ac2af528e 100644 --- a/src/agents/contexts/spec/__tests__/spec-context.test.ts +++ b/src/agents/contexts/spec/__tests__/spec-context.test.ts @@ -16,7 +16,7 @@ describe('renderSpecificationContext', () => { it('renders the approved specification house style', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-specification-context-')); const executor = await openWorkspaceCommandExecutor(cwd); - const seeded = seedFixture(executor, await loadFixture('alpha-grounding', 'workspace-spread')); + const seeded = seedFixture(executor, await loadFixture('workspace-alpha-grounding')); await mkdir(join(cwd, '.brunch', 'sessions'), { recursive: true }); await writeBoundSession(cwd, 'alpha-session', seeded.specId, [ messageEntry('u1', 'user'), @@ -63,9 +63,9 @@ describe('renderSpecificationContext', () => { }); }); -async function loadFixture(slug: string, set = 'bilal-port'): Promise { +async function loadFixture(name: string, variant = 'base'): Promise { const fixturePath = fileURLToPath( - new URL(`../../../../../.fixtures/seeds/${set}/${slug}.json`, import.meta.url), + new URL(`../../../../../.fixtures/seeds/${name}/${variant}.json`, import.meta.url), ); return JSON.parse(await import('node:fs/promises').then(({ readFile }) => readFile(fixturePath, 'utf8'))); } diff --git a/src/agents/shared/README.md b/src/agents/shared/README.md index d24034a4c..ca806613f 100644 --- a/src/agents/shared/README.md +++ b/src/agents/shared/README.md @@ -1,3 +1,3 @@ -# agents/contexts/primitives/ — context formatting helpers +# agents/shared/ — agent context formatting helpers Owns small formatting helpers used by agent-visible context renders: markdown escaping/tables, XML-style sections, TOON record blocks, and fenced ASCII trees. These helpers are formatting substrate only; they do not choose what facts enter context. diff --git a/src/app/__tests__/brunch-tui.test.ts b/src/app/__tests__/brunch-tui.test.ts index bec4ba09f..78b9b2692 100644 --- a/src/app/__tests__/brunch-tui.test.ts +++ b/src/app/__tests__/brunch-tui.test.ts @@ -30,6 +30,7 @@ import { runWithScopedBrunchOfflineDefault, startupHeaderForActivation, } from '../brunch-tui.js'; +import { runBrunchCli } from '../brunch.js'; import { BRUNCH_CONTINUE_COMMAND, BRUNCH_INTROSPECTION_COMMAND, @@ -206,76 +207,84 @@ describe('Brunch TUI boot', () => { ]); }); - it('threads BRUNCH_DEV introspection state into the interactive launch context', async () => { - const previous = process.env.BRUNCH_DEV; + it('mirrors debug cache by default in source runs and gates query tools behind developerTools', async () => { const workspace = readyWorkspace('/tmp/project', 'session-ready'); const observed: unknown[] = []; - try { - process.env.BRUNCH_DEV = '1'; - await runBrunchTui({ - cwd: '/tmp/project', - coordinator: { - inspectWorkspace: async () => ({ - cwd: '/tmp/project', - currentSpec: workspace.spec, - currentSessionFile: workspace.session.file, - needsNewSpec: false, - specs: [], - unavailableSessions: [], - }), - activateWorkspace: async () => workspace, - bindCurrentSpecToReplacementSession: async () => workspace, - }, - runWorkspaceDialogPreflight: async () => ({ - action: 'continue', - specId: workspace.spec.id, - sessionFile: workspace.session.file, + await runBrunchTui({ + cwd: '/tmp/project', + coordinator: { + inspectWorkspace: async () => ({ + cwd: '/tmp/project', + currentSpec: workspace.spec, + currentSessionFile: workspace.session.file, + needsNewSpec: false, + specs: [], + unavailableSessions: [], }), - webSidecarRunner: async () => null, - launchInteractive: async ({ dev }) => { - observed.push(dev?.introspection.enabled); - expect(dev?.introspection.store).toBeDefined(); - expect(dev?.introspection.debugCache).toEqual({ cwd: '/tmp/project' }); - }, - }); + activateWorkspace: async () => workspace, + bindCurrentSpecToReplacementSession: async () => workspace, + }, + developerTools: true, + runWorkspaceDialogPreflight: async () => ({ + action: 'continue', + specId: workspace.spec.id, + sessionFile: workspace.session.file, + }), + webSidecarRunner: async () => null, + launchInteractive: async ({ introspection }) => { + observed.push(introspection?.queryTools); + expect(introspection?.store).toBeDefined(); + expect(introspection?.debugCache).toEqual({ cwd: '/tmp/project' }); + }, + }); - delete process.env.BRUNCH_DEV; - await runBrunchTui({ - cwd: '/tmp/project', - coordinator: { - inspectWorkspace: async () => ({ - cwd: '/tmp/project', - currentSpec: workspace.spec, - currentSessionFile: workspace.session.file, - needsNewSpec: false, - specs: [], - unavailableSessions: [], - }), - activateWorkspace: async () => workspace, - bindCurrentSpecToReplacementSession: async () => workspace, - }, - runWorkspaceDialogPreflight: async () => ({ - action: 'continue', - specId: workspace.spec.id, - sessionFile: workspace.session.file, + await runBrunchTui({ + cwd: '/tmp/project', + coordinator: { + inspectWorkspace: async () => ({ + cwd: '/tmp/project', + currentSpec: workspace.spec, + currentSessionFile: workspace.session.file, + needsNewSpec: false, + specs: [], + unavailableSessions: [], }), - webSidecarRunner: async () => null, - launchInteractive: async ({ dev }) => { - observed.push(dev); - }, - }); - } finally { - if (previous === undefined) { - delete process.env.BRUNCH_DEV; - } else { - process.env.BRUNCH_DEV = previous; - } - } + activateWorkspace: async () => workspace, + bindCurrentSpecToReplacementSession: async () => workspace, + }, + debugMirror: false, + runWorkspaceDialogPreflight: async () => ({ + action: 'continue', + specId: workspace.spec.id, + sessionFile: workspace.session.file, + }), + webSidecarRunner: async () => null, + launchInteractive: async ({ introspection }) => { + observed.push(introspection); + }, + }); expect(observed).toEqual([true, undefined]); }); + it('lets programmatic callers enable developer tools when argv omits the flag', async () => { + let observedDeveloperTools: boolean | undefined; + + const code = await runBrunchCli({ + argv: [], + cwd: '/tmp/project', + coordinator: noOpWorkspaceCoordinator('/tmp/project') as never, + developerTools: true, + launchTui: async (options) => { + observedDeveloperTools = options?.developerTools; + }, + }); + + expect(code).toBe(0); + expect(observedDeveloperTools).toBe(true); + }); + it('registers TUI-gated introspection last when the launch context enables it', async () => { const events: string[] = []; const commands: string[] = []; @@ -286,7 +295,7 @@ describe('Brunch TUI boot', () => { undefined, { coordinator: noOpWorkspaceCoordinator('/tmp/project'), - introspection: { enabled: true, store }, + introspection: { store }, }, )({ on: (event: string) => events.push(event), @@ -1220,7 +1229,6 @@ describe('Brunch TUI boot', () => { expect(settingsManager.getImageAutoResize()).toBe(true); expect(settingsManager.getBlockImages()).toBe(false); expect(settingsManager.getTransport()).toBe('auto'); - expect(settingsManager.getThemeSetting()).toBeUndefined(); expect(settingsManager.getTheme()).toBeUndefined(); expect(settingsManager.getLastChangelogVersion()).toBeUndefined(); expect(settingsManager.getCollapseChangelog()).toBe(false); diff --git a/src/app/__tests__/brunch.test.ts b/src/app/__tests__/brunch.test.ts index 4d957410e..e71df840c 100644 --- a/src/app/__tests__/brunch.test.ts +++ b/src/app/__tests__/brunch.test.ts @@ -285,36 +285,26 @@ describe('Brunch CLI dispatch', () => { }); }); - it('gates dev RPC methods in CLI rpc mode behind BRUNCH_DEV=1', async () => { - const previous = process.env.BRUNCH_DEV; + it('keeps CLI rpc mode on the public method registry', async () => { const stdout = new PassThrough(); const chunks = collectStream(stdout); - process.env.BRUNCH_DEV = '1'; - try { - const code = await runBrunchCli({ - argv: ['--mode=rpc'], - cwd: '/tmp/brunch-project', - coordinator: coordinator(), - stdin: rpcRequest('rpc.discover'), - stdout, - }); - - expect(code).toBe(0); - expect(JSON.stringify(JSON.parse(chunks.join('')))).toContain('dev.graph.mutateGraph'); - } finally { - if (previous === undefined) { - delete process.env.BRUNCH_DEV; - } else { - process.env.BRUNCH_DEV = previous; - } - } + const code = await runBrunchCli({ + argv: ['--mode=rpc'], + cwd: '/tmp/brunch-project', + coordinator: coordinator(), + stdin: rpcRequest('rpc.discover'), + stdout, + }); + + expect(code).toBe(0); + expect(JSON.stringify(JSON.parse(chunks.join('')))).not.toContain('dev.graph.mutateGraph'); }); it('uses --cwd product RPC to inspect the named workspace rather than the shell cwd', async () => { const shellCwd = await mkdtemp(join(tmpdir(), 'brunch-shell-')); const seededWorkspace = await mkdtemp(join(tmpdir(), 'brunch-seeded-')); const emptySibling = await mkdtemp(join(tmpdir(), 'brunch-empty-')); await runSeedFixturesCli({ - argv: ['--workspace', seededWorkspace, '--seed', 'workspace-spread/alpha-grounding'], + argv: ['--workspace', seededWorkspace, '--seed', 'workspace-alpha-grounding/base'], cwd: shellCwd, stdout: () => {}, }); diff --git a/src/app/brunch-tui.ts b/src/app/brunch-tui.ts index e454c5542..027a4bb91 100644 --- a/src/app/brunch-tui.ts +++ b/src/app/brunch-tui.ts @@ -18,7 +18,7 @@ import { appendEntryContentToDebugCache, appendOriginationRecordToDebugCache, } from '../.pi/extensions/dev-mode/index.js'; -import { isBrunchDevEnabled } from '../dev/brunch-dev.js'; +import { isBrunchDevelopmentRuntime } from '../build-info.js'; import { openWorkspaceGraphRuntime, type EdgeCategory, @@ -104,7 +104,8 @@ export interface BrunchTuiLaunchContext { }; webSidecarUrl?: string; activationDecision?: SpecSessionActivationDecision; - dev?: BrunchTuiDevOptions; + introspection?: BrunchTuiIntrospectionOptions; + allowSubagents?: boolean; reportAsyncDiagnostic?: (diagnostic: { readonly type: 'warning'; readonly message: string }) => void; /** * Provider-backend substitution seam (faux provider in Tier-2 oracles). @@ -122,12 +123,10 @@ export interface BrunchAgentServicesOverride extends Pick< readonly model?: CreateAgentSessionFromServicesOptions['model']; } -export interface BrunchTuiDevOptions { - readonly introspection: { - readonly enabled: true; - readonly store: BrunchIntrospectionStore; - readonly debugCache: { readonly cwd: string }; - }; +export interface BrunchTuiIntrospectionOptions { + readonly store: BrunchIntrospectionStore; + readonly queryTools: boolean; + readonly debugCache?: { readonly cwd: string }; } export interface BrunchTuiOptions { @@ -141,6 +140,10 @@ export interface BrunchTuiOptions { webSidecarRunner?: (options: BrunchWebSidecarRunnerOptions) => Promise; /** Opt-in (`--open-web`): launch the web sidecar URL in the default browser. */ openWeb?: boolean; + /** Opt-in prompt-affecting developer tools such as query tools and subagents. */ + developerTools?: boolean; + /** Override the automatic source/dev-build debug-cache default. */ + debugMirror?: boolean; openBrowser?: (url: string) => Promise; advertiseWebSidecar?: (url: string) => void; } @@ -166,7 +169,11 @@ export async function runBrunchTui(options: BrunchTuiOptions = {}): Promise { process.stderr.write(`[brunch] ${diagnostic.message}\n`); }, @@ -213,14 +221,15 @@ export async function runBrunchTui(options: BrunchTuiOptions = {}): Promise { const specId = currentWorkspace.spec.id; @@ -464,11 +473,11 @@ export function createBrunchAgentSessionRuntimeFactory( strategy: agentState.agentStrategy === 'freestyle' ? 'freestyle' : 'auto', manager: sessionManager, }); - if (context.dev) { + if (context.introspection?.debugCache) { // Boot-time mirror is awaited (cheap, local fs) so a dev boot is // observable the moment the runtime exists; turn-time mirrors in the // reconciler/guard stay fire-and-forget. - const debugCache = context.dev.introspection.debugCache; + const debugCache = context.introspection.debugCache; for (const entry of origination.decision.seedEntries) { await appendEntryContentToDebugCache(debugCache, entry).catch(() => {}); } @@ -504,8 +513,8 @@ export function createBrunchAgentSessionRuntimeFactory( modelAvailable: services.modelRegistry.getAvailable().length > 0, sendCustomMessage: (message, options) => created.session.sendCustomMessage(message, options), onOutcome: (outcome) => { - if (context.dev) { - void appendOriginationRecordToDebugCache(context.dev.introspection.debugCache, { + if (context.introspection?.debugCache) { + void appendOriginationRecordToDebugCache(context.introspection.debugCache, { decision: origination.decision, outcome, }).catch(() => {}); diff --git a/src/app/brunch.ts b/src/app/brunch.ts index aea84d76d..dbb464295 100644 --- a/src/app/brunch.ts +++ b/src/app/brunch.ts @@ -4,7 +4,6 @@ import type { Readable, Writable } from 'node:stream'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; -import { isBrunchDevEnabled } from '../dev/brunch-dev.js'; import { projectWorkspaceState } from '../projections/workspace/workspace-state.js'; import { createRpcHandlers, runJsonRpcLineServer } from '../rpc/handlers.js'; import { createProductUpdatePublisher } from '../rpc/product-updates.js'; @@ -21,13 +20,16 @@ export interface BrunchCliOptions { coordinator?: WorkspaceSessionCoordinator; stdin?: Readable; stdout?: Writable | ((chunk: string) => void); + developerTools?: boolean; + debugMirror?: boolean; launchTui?: typeof runBrunchTui; } export async function runBrunchCli(options: BrunchCliOptions = {}): Promise { const argv = options.argv ?? process.argv.slice(2); - const { cwd: cwdFlag, mode, openWeb } = parseCliArgs(argv); + const { cwd: cwdFlag, mode, openWeb, developerTools: developerToolsFlag } = parseCliArgs(argv); const cwd = cwdFlag ?? options.cwd ?? process.cwd(); + const developerTools = developerToolsFlag ?? options.developerTools ?? false; const coordinator = options.coordinator ?? createWorkspaceSessionCoordinator({ cwd }); if (mode === 'print') { @@ -46,7 +48,6 @@ export async function runBrunchCli(options: BrunchCliOptions = {}): Promise void) | undefined): } as Writable; } -function parseCliArgs(argv: string[]): { cwd: string | undefined; mode: string; openWeb: boolean } { +function parseCliArgs(argv: string[]): { + cwd: string | undefined; + mode: string; + openWeb: boolean; + developerTools: boolean | undefined; +} { // node:util parseArgs accepts both `--flag value` and `--flag=value` forms and // fails loud on unknown or malformed flags. --open-web is a plain boolean whose // default is false, so there is no `=false` form to model: omit it to opt out. + // --dev-tools is optional so programmatic callers can supply the fallback. const { values } = parseArgs({ args: argv, options: { cwd: { type: 'string' }, mode: { type: 'string', default: 'tui' }, 'open-web': { type: 'boolean', default: false }, + 'dev-tools': { type: 'boolean' }, }, }); return { cwd: resolveCwdFlag(values.cwd), mode: values.mode, openWeb: values['open-web'], + developerTools: values['dev-tools'], }; } diff --git a/src/app/pi-extensions.ts b/src/app/pi-extensions.ts index ed96396b3..b020bf7e9 100644 --- a/src/app/pi-extensions.ts +++ b/src/app/pi-extensions.ts @@ -156,7 +156,7 @@ export interface BrunchPiExtensionsOptions extends Omit void | Promise; @@ -188,11 +188,11 @@ export function createBrunchPiExtensions( // dev introspection query tools (D69-L) and the `subagent` tool (D44-L). const hasDelegatableSubagents = (options.subagents?.delegatableAgents.length ?? 0) > 0; const optInAllowedToolNames = [ - ...(introspectionOptions?.enabled ? [BRUNCH_SESSION_QUERY_TOOL, BRUNCH_INTROSPECT_QUERY_TOOL] : []), + ...(introspectionOptions?.queryTools ? [BRUNCH_SESSION_QUERY_TOOL, BRUNCH_INTROSPECT_QUERY_TOOL] : []), ...(hasDelegatableSubagents ? [BRUNCH_SUBAGENT_TOOL] : []), ]; const devAllowedToolNames = optInAllowedToolNames.length > 0 ? optInAllowedToolNames : undefined; - const entryDebugCache = introspectionOptions?.enabled ? introspectionOptions.debugCache : undefined; + const entryDebugCache = introspectionOptions?.debugCache; const continuitySteps = options.graph ? [ createPrepareNextTurnContinuityStep(options.graph, options.continuityDrains, entryDebugCache), @@ -253,17 +253,19 @@ export function createBrunchPiExtensions( // graph register, but they read through the same workspace graph runtime deps. ...(options.graph ? [(api: ExtensionAPI) => registerBrunchElicitation(api, options.graph!)] : []), ...(options.graph ? [(api: ExtensionAPI) => registerBrunchReconciliation(api, options.graph!)] : []), - ...(introspectionOptions?.enabled + ...(introspectionOptions ? [ (api: ExtensionAPI) => { - const { store, clock, debugCache } = introspectionOptions; + const { store, clock, debugCache, queryTools } = introspectionOptions; const introspectionStore = registerBrunchIntrospection(api, { ...(store ? { store } : {}), ...(clock ? { clock } : {}), ...(debugCache ? { debugCache } : {}), }); - registerBrunchSessionQuery(api); - registerBrunchIntrospectQuery(api, { store: introspectionStore }); + if (queryTools) { + registerBrunchSessionQuery(api); + registerBrunchIntrospectQuery(api, { store: introspectionStore }); + } }, ] : []), diff --git a/src/app/pi-settings.ts b/src/app/pi-settings.ts index 5206babb4..374e23c63 100644 --- a/src/app/pi-settings.ts +++ b/src/app/pi-settings.ts @@ -61,7 +61,6 @@ export const BRUNCH_SETTINGS_AUDITED_GETTERS = [ 'getDefaultModel', 'getSteeringMode', 'getFollowUpMode', - 'getThemeSetting', 'getTheme', 'getDefaultThinkingLevel', 'getTransport', diff --git a/src/build-info.ts b/src/build-info.ts new file mode 100644 index 000000000..63e73fa17 --- /dev/null +++ b/src/build-info.ts @@ -0,0 +1,61 @@ +import { execSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const PACKAGE_ROOT_URL = new URL('../', import.meta.url); +const BUILD_INFO_URL = new URL('./build-info.json', import.meta.url); + +export interface BrunchBuildInfo { + readonly dev: boolean; + readonly gitSha: string; + readonly buildTime: string; +} + +export function isBrunchDevelopmentRuntime(): boolean { + return resolveBrunchBuildInfo().dev; +} + +export function resolveBrunchBuildInfo(): BrunchBuildInfo { + const buildInfo = readBuildInfo(); + if (buildInfo) { + return buildInfo; + } + return { + dev: true, + gitSha: getGitSha(), + buildTime: formatUtcBuildTime(new Date()), + }; +} + +export function formatUtcBuildTime(date: Date): string { + // toISOString is always UTC; keep the explicit suffix so it displays as such. + return date + .toISOString() + .replace('T', ' ') + .replace(/\.\d+Z$/, ' UTC'); +} + +function readBuildInfo(): BrunchBuildInfo | null { + try { + const raw = JSON.parse(readFileSync(fileURLToPath(BUILD_INFO_URL), 'utf8')) as Partial; + return { + dev: raw.dev === true, + gitSha: typeof raw.gitSha === 'string' ? raw.gitSha : '', + buildTime: typeof raw.buildTime === 'string' ? raw.buildTime : '', + }; + } catch { + return null; + } +} + +function getGitSha(): string { + try { + return execSync('git rev-parse --short=7 HEAD', { + cwd: fileURLToPath(PACKAGE_ROOT_URL), + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +} diff --git a/src/dev/README.md b/src/dev/README.md index 8fe47d118..310703aa0 100644 --- a/src/dev/README.md +++ b/src/dev/README.md @@ -1,66 +1,64 @@ -# Dev feedback loops +# Dev Feedback Loops -This directory owns Brunch-only development feedback loops. These helpers are not product runtime configuration and must not weaken the sealed Pi profile (D39-L). +This directory owns Brunch-only development loops and curation seams. Nothing here is product runtime configuration, and nothing here may silently widen the sealed Pi profile. -This README is a topology-local contract, not a tutorial. It records what `src/dev/**` owns, which proof loops live here, and which runtime substitutions are allowed. Operational notes are included only when they prevent a topology mistake. +## Ownership -## Pi source alias (D67-L) +`src/dev/**` owns four things: -Brunch tracks the latest published `@earendil-works/pi-*` line. Two resolution concerns are kept strictly separate: +- the human-facing dev launcher (`scripts/dev.ts` → `src/dev/dev-cli.ts`) +- the explicit graph-curation seam for fixture shaping (`graph-curation.ts`) +- faux/introspection/tier-2 harnesses used by tests and probes +- dev-only witnesses such as `generate-fan-out-witness.ts` -- **Types + default resolution → installed `dist`.** The published packages ship their own `dist/index.d.ts`, so `tsc`, `tsx`, the editor/LSP, oxlint type-aware lint, and ordinary runtime all resolve pi from `node_modules`. There are deliberately **no `paths` in `tsconfig.json`** — adding them would make a personal source checkout the unconditional default for everyone (tsconfig paths cannot be env-gated) and is unnecessary because the dist `.d.ts` are version-matched to the declared deps. -- **No-rebuild source iteration → runtime alias, gated by `PI_SOURCE`.** When you want edits in a sibling `pi-mono` checkout to take effect without rebuilding, set `PI_SOURCE=1`. `vite.config.ts`'s `piSourceAlias()` then redirects all four packages (`pi-ai`, `pi-agent-core`, `pi-coding-agent`, `pi-tui`) to `pi-mono` source for `vite` and `vitest`. `PI_SOURCE_ROOT` overrides the default checkout path (`join(os.homedir(), '.pi', 'pi-mono')`); the alias is inert if the checkout does not exist. +It does not own published CLI behavior, public RPC contracts, or database imports from outside `graph/`. -`pi-agent-core` is aliased even though Brunch never imports it directly: `pi-coding-agent`'s source imports it, so a partial alias would produce a mixed source/dist module graph. +## Launcher Surface -### tsx source mode (Cards 2–3, when needed) +`npm run dev` is the front door for local workbenches. -`vitest`/`vite` are covered by the alias above. The **`tsx`**-run loops (`npm run dev` TUI, probes) do **not** read `vite.config.ts`; tsx resolves through `tsconfig`. When a real-provider/TUI source-iteration loop actually needs no-rebuild pi edits, add an opt-in `tsconfig.dev.json` (extends `./tsconfig.json`, adds the pi `paths` + `allowImportingTsExtensions`) and run `tsx --tsconfig tsconfig.dev.json`. Do **not** add those paths to the base `tsconfig.json`. This is intentionally deferred — Card 1 only needs the vitest-level alias proven by `pi-source-alias.test.ts`. +- With no args, it prompts for a workbench, whether to start from current state or a reset seed, and whether to open the web sidecar. +- TUI is the default mode. +- Seeding is always explicit: the launcher only seeds when `--seed / --reset` is present or chosen in the prompt flow. +- `rpc`, `mutate`, and `export` are explicit subcommands for scripted reads, graph curation, and fixture export. +- `npm run dev:raw -- ...` remains the escape hatch to the underlying app entrypoint. -## Faux loop (D68-L) +Current subcommands: -`src/dev/index.ts` is the dev front door. It exports the shared faux-harness factory and the scripted faux launcher, plus the existing workspace RPC helper namespace. The tiny faux-provider config used by buildable probes lives in `src/probes/faux-provider.ts`; `src/dev/faux-harness.ts` re-exports it for dev-loop callers without making probes import build-excluded `src/dev/**` modules. +```text +npm run dev +npm run dev -- --seed workspace-alpha-grounding/base --reset --open-web +npm run dev -- rpc graph.overview '{"specId":1}' --workspace .fixtures/workbenches/workspace-alpha-grounding +npm run dev -- mutate --workspace .fixtures/workbenches/workspace-alpha-grounding --params-file /tmp/mutate.json +npm run dev -- export --workspace .fixtures/workbenches/workspace-alpha-grounding --spec-id 1 --out .fixtures/seeds/custom/example.json +``` -- `createBrunchFauxHarness()` boots an in-memory Pi `AgentSession` with in-memory auth, model registry, session manager, and a deterministic faux provider. By default it also uses in-memory settings and no active tools; Tier-1 callers may pass a Brunch-configured `resourceLoader`/`settingsManager` pair so the faux provider captures the real extension-mutated payload. It records `providerContexts` for Tier-1 assertions over exact provider messages and active tools after Brunch mutation. -- `runBrunchFauxTurn()` is the smoke launcher: it scripts one prompt→assistant turn with no network I/O and returns the assistant text plus provider call count. -- `brunchFauxProviderConfig()` defaults to the literal in-process dev key and accepts an explicit api-key override. Subprocess probes pass the pi 0.79 `$ENV` form themselves; the in-process harness does not mutate `process.env` to satisfy a subprocess concern. +## Debug Mirrors And Dev Tools -Product probes may import `src/probes/faux-provider.ts` when they need deterministic faux wiring, but they remain product-verification probes under `src/probes/`; they do not become dev loops merely because they share infrastructure. +Source runs and local dev builds automatically mirror debug artifacts into `/.brunch/debug/`. -## Introspection loop (D69-L) +- This automatic mirror is for passive observability only: system prompt captures, Brunch-owned tool content, origination records, and debug transcript rendering. +- Prompt-affecting dev surfaces stay explicit. `--dev-tools` is the opt-in for query tools and subagent affordances. +- TUI boots therefore have three states: product-default, debug-mirror-only, and debug-mirror plus dev tools. -`runBrunchIntrospectionTurn()` is the paired-run artifact writer for the dev-only introspection loop. The Pi side is the explicit, read-only `src/.pi/extensions/dev-mode/introspection/` registrar, included only when `createBrunchPiExtensions(..., { introspection: { enabled: true } })` is passed. Product Brunch sessions omit it by default and keep the D39-L offline default. The launcher does not mutate `process.env`; any future online real-provider lift belongs at session construction with save/restore scoping. +## Graph Curation -## Tier-2 real boot loop (FE-847) +`graph-curation.ts` is the canonical local fixture-shaping seam. -`runTier2RealBootFauxTurn()` is the real-boot harness for runtime choreography tests: it enters through `runBrunchTui`, drives one faux-provider turn, and exposes the captured provider context, active tool names, transcript entries, session file, and `.brunch/debug/transcript.md` path. The debug transcript is rendered from Pi's canonical context construction, then filtered to user, assistant, and Brunch-owned custom tool-result messages. `bootTier2RuntimeThroughRunBrunchTui()` owns real runtime boot proofs such as ready context and `BRUNCH_DEV`-gated query-tool registration. `resumeTier2Fixture()` writes a fixture JSONL transcript, reopens it through the workspace/session coordinator, and reports original vs resumed session-file state so restart/resume assertions do not need local fake boot helpers. `bootTier2RuntimeFromFixture()` is the resume-side real-boot chassis (pre-seed a fixture transcript, then boot the real runtime over it — the I46 resume-origination oracle), and `rebootTier2Runtime()` re-boots the real runtime over the same session file after flushing Pi's deferred JSONL (the I47 actual-restart idempotence oracle). The FE-847 coverage-first scaffold is fully live as of 2026-06-11 — no skipped/todo rows remain. Suites split across two files: kick/boot-path suites in `tier-2-harness.test.ts`, the coverage-first scaffold suites (I45/I46/I47) in `tier-2-scaffold.test.ts`, with shared transcript/assertion helpers in `tier-2-test-support.ts`. +- It accepts the former dev-mutate grammar: create/patch/delete node and edge ops, with projected node-code resolution scoped to one spec. +- It resolves those projected references before entering `CommandExecutor.mutateGraph`, so fixture shaping still uses the product-owned mutation boundary. +- It is intentionally in-process, not a hidden RPC host flag. External agents should call the explicit `npm run dev -- mutate ...` or `npm run dev -- rpc ...` commands instead of relying on a long-lived write-enabled sidecar. -Tier-2 means "real Brunch/Pi runtime boot", not "always a live provider" and not "always faux". The provider/auth source is a separate axis: +## Faux And Tier-2 Loops -- No `agentServices` override: Brunch builds product services through `createBrunchAgentSessionRuntimeFactory()`. The model registry can see real configured auth only when the boot uses the real Pi agent dir and the run opts out of offline mode. -- `agentServices` override: tests substitute only auth/model/provider services while keeping the Brunch runtime, extension registration, session manager, transcript, and origination choreography on the product path. This is still a real boot proof, but it is not a real-provider proof. -- Temporary `agentDir`: useful for isolation, but it intentionally hides ambient model auth. Do not use it for a real-provider witness that expects product-configured models. -- `PI_OFFLINE=1`: valid for deterministic/no-network loops; invalid for live-model evidence. Real-provider dev probes must set `PI_OFFLINE=0` explicitly. +- `createBrunchFauxHarness()` and `runBrunchFauxTurn()` are Tier-1 exact-payload loops. +- `runBrunchIntrospectionTurn()` writes paired subjective/mechanical artifacts for prompt inspection. +- `runTier2RealBootFauxTurn()`, `bootTier2RuntimeThroughRunBrunchTui()`, and `bootTier2RuntimeFromFixture()` own real-boot proofs: session activation, origination, restart/resume, and runtime registration. -## Generate fan-out witness (FE-1059) +Tier-2 means “real Brunch/Pi boot path,” not necessarily “real provider.” Tests may substitute only auth/model/provider services while keeping session construction, extension registration, transcript wiring, and origination choreography on the product path. -`runGenerateFanOutWitness()` is the dev-only real-model probe for the `elicitor-generate` oracle plane. It enters through `bootTier2RuntimeFromFixture()` with no `agentServices` override, pins the real `brunch:lens` command to `oracle`, seeds explicit intent/design graph truth through `CommandExecutor`, sends the P3 `generate-proposal` prompt under a bounded timeout, and writes JSONL-backed scratch artifacts to `.fixtures/scratch/generate-fan-out//`. Its report reads only canonical `session.jsonl` markers: `generate-proposal/SKILL.md`, `references/oracle.md`, `present_candidates`, and the I51-L no-write evidence (unchanged graph counts/LSN, no `mutate_graph`, no approved review result). `skipped` and `blocked` reports are evidence of environment/turn state, not A31-L passes; promoted evidence belongs under `.fixtures/runs/` only after human review. +## Dependency Posture -Run it with `npm run probe:generate-fan-out -- --timeout-ms 60000`. The script sets `PI_OFFLINE=0`, prepends mise's Node LTS install to `PATH` so native modules such as `better-sqlite3` load against the ABI they were built with, and boots with the real Pi agent dir so the product model registry can see configured model auth. +Brunch now resolves `@earendil-works/pi-*` through installed packages only. The old `PI_SOURCE` Vite/Vitest alias path was retired because it no longer affected the real `tsx` dev loop and had become a non-consequent maintenance surface. -## Proof ownership ledger - -- **Unit:** pure derivations and local policy tables (`projections/session/*`, `session/*` helpers). -- **Tier-1 faux session:** exact provider context after in-memory Brunch mutation, prompt/tool payloads, and hook mutation proof via `createBrunchFauxHarness().providerContexts`. -- **Tier-2 real boot:** `runBrunchTui` boot, ready context, runtime registration, transcript files, and restart/resume state via `tier-2-harness.ts`. -- **Probe/transport:** public JSON-RPC, CLI, subprocess, and cross-process parity claims. - -## Introspection loop (D69-L) - -The passive extension tap records the final `before_provider_request` payload. The launcher then drives a subjective `session.prompt(...)` turn and writes the correlated scratch run under repo-root `.fixtures/scratch/introspection//`, independent of the workspace cwd it targets: - -- `mechanical.json` — latest passive provider-payload capture plus optional `/introspect` base-prompt report -- `subjective.json` — assistant answer text from the subjective prompt -- `manifest.json` — paired summary keyed by the same captured turn id - -The `/introspect` command reports `ctx.getSystemPromptOptions()` base inputs plus the latest passive capture; it deliberately does not claim to reconstruct exact model input. Exactness belongs to the passive provider-payload tap registered last in the Brunch extension bundle. In `BRUNCH_DEV` real TUI launches, that same passive capture mirrors the latest final system prompt bytes into `.brunch/debug/system-prompt.md`; Brunch-owned text `tool_result` content appends to `.brunch/debug/tool-contents.md`. This is an ephemeral workspace debug cache, separate from repo-root `.fixtures/scratch/` evidence, and does not attempt `renderResult()` flattening. +If a future slice needs alternate-source Pi iteration again, that slice must re-establish it as a current, end-to-end consequence rather than reviving dead ambient flags. diff --git a/src/dev/__tests__/dev-cli.test.ts b/src/dev/__tests__/dev-cli.test.ts new file mode 100644 index 000000000..de1d55cb7 --- /dev/null +++ b/src/dev/__tests__/dev-cli.test.ts @@ -0,0 +1,201 @@ +import { resolve } from 'node:path'; +import { PassThrough } from 'node:stream'; + +import { describe, expect, it, vi } from 'vitest'; + +import type { BrunchCliOptions } from '../../app/brunch.js'; +import { runDevCli, type DevCliPrompts } from '../dev-cli.js'; + +const REPO_ROOT = process.cwd(); +const WORKBENCH = resolve(REPO_ROOT, '.fixtures/workbenches/workspace-alpha-grounding'); + +describe('runDevCli', () => { + it('derives the seed workbench before launching the default TUI flow', async () => { + const events: string[] = []; + const launchCalls: BrunchCliOptions[] = []; + + const code = await runDevCli({ + argv: ['--seed', 'workspace-alpha-grounding/base', '--reset', '--open-web'], + cwd: REPO_ROOT, + seedWorkspace: async (options) => { + events.push('seed'); + if (!options) throw new Error('expected seed cli options'); + expect(options.argv).toEqual([ + '--workspace', + WORKBENCH, + '--seed', + 'workspace-alpha-grounding/base', + '--reset', + ]); + return 0; + }, + launchBrunch: async (options) => { + events.push('launch'); + launchCalls.push(options); + return 17; + }, + }); + + expect(code).toBe(17); + expect(events).toEqual(['seed', 'launch']); + expect(launchCalls).toEqual([ + expect.objectContaining({ + cwd: WORKBENCH, + argv: ['--mode', 'tui', '--open-web'], + }), + ]); + }); + + it('uses the prompt flow when no workbench flag is provided', async () => { + const chooseWorkbench = vi.fn().mockResolvedValue(WORKBENCH); + const chooseSeed = vi.fn().mockResolvedValue('__current__'); + const confirmSeedReset = vi.fn(); + const confirmOpenWeb = vi.fn().mockResolvedValue(false); + const intro = vi.fn(); + const outro = vi.fn(); + const cancel = vi.fn(); + const launches: BrunchCliOptions[] = []; + const stdin = new PassThrough() as PassThrough & { isTTY: boolean }; + const stdout = new PassThrough() as PassThrough & { isTTY: boolean }; + stdin.isTTY = true; + stdout.isTTY = true; + + const code = await runDevCli({ + argv: [], + cwd: REPO_ROOT, + stdin, + stdout, + prompts: { + intro, + outro, + cancel, + chooseWorkbench, + chooseSeed, + confirmSeedReset, + confirmOpenWeb, + }, + launchBrunch: async (options) => { + launches.push(options); + return 0; + }, + }); + + expect(code).toBe(0); + expect(chooseWorkbench).toHaveBeenCalled(); + expect(chooseSeed).toHaveBeenCalledWith( + ['workspace-alpha-grounding/base'], + '.fixtures/workbenches/workspace-alpha-grounding', + ); + expect(confirmSeedReset).not.toHaveBeenCalled(); + expect(confirmOpenWeb).toHaveBeenCalledWith('.fixtures/workbenches/workspace-alpha-grounding'); + expect(intro).toHaveBeenCalledWith('Brunch dev launcher'); + expect(outro).toHaveBeenCalledWith('Launching .fixtures/workbenches/workspace-alpha-grounding.'); + expect(cancel).not.toHaveBeenCalled(); + expect(launches).toEqual([ + expect.objectContaining({ + cwd: WORKBENCH, + argv: ['--mode', 'tui'], + }), + ]); + }); + + it('treats prompt-selected seeding as an explicit reset before launch', async () => { + const chooseWorkbench = vi.fn().mockResolvedValue(WORKBENCH); + const chooseSeed = vi + .fn() + .mockResolvedValue('workspace-alpha-grounding/base'); + const confirmSeedReset = vi.fn().mockResolvedValue(true); + const confirmOpenWeb = vi.fn().mockResolvedValue(true); + const intro = vi.fn(); + const outro = vi.fn(); + const cancel = vi.fn(); + const stdin = new PassThrough() as PassThrough & { isTTY: boolean }; + const stdout = new PassThrough() as PassThrough & { isTTY: boolean }; + const events: string[] = []; + const launches: BrunchCliOptions[] = []; + stdin.isTTY = true; + stdout.isTTY = true; + + const code = await runDevCli({ + argv: [], + cwd: REPO_ROOT, + stdin, + stdout, + prompts: { + intro, + outro, + cancel, + chooseWorkbench, + chooseSeed, + confirmSeedReset, + confirmOpenWeb, + }, + seedWorkspace: async (options) => { + events.push('seed'); + if (!options) throw new Error('expected seed cli options'); + expect(options.argv).toEqual([ + '--workspace', + WORKBENCH, + '--seed', + 'workspace-alpha-grounding/base', + '--reset', + ]); + return 0; + }, + launchBrunch: async (options) => { + events.push('launch'); + launches.push(options); + return 0; + }, + }); + + expect(code).toBe(0); + expect(events).toEqual(['seed', 'launch']); + expect(confirmSeedReset).toHaveBeenCalledWith( + 'workspace-alpha-grounding/base', + '.fixtures/workbenches/workspace-alpha-grounding', + ); + expect(confirmOpenWeb).toHaveBeenCalledWith('.fixtures/workbenches/workspace-alpha-grounding'); + expect(outro).toHaveBeenCalledWith( + 'Launching .fixtures/workbenches/workspace-alpha-grounding from workspace-alpha-grounding/base.', + ); + expect(cancel).not.toHaveBeenCalled(); + expect(launches).toEqual([ + expect.objectContaining({ + cwd: WORKBENCH, + argv: ['--mode', 'tui', '--open-web'], + }), + ]); + }); + + it('documents canonical seed refs in usage text', async () => { + let stdout = ''; + + const code = await runDevCli({ + argv: ['help'], + cwd: REPO_ROOT, + stdout: (chunk) => { + stdout += chunk; + }, + }); + + expect(code).toBe(0); + expect(stdout).toContain('--seed /'); + expect(stdout).not.toContain(''); + }); + + it('rejects non-positive export spec ids loudly', async () => { + let stderr = ''; + + const code = await runDevCli({ + argv: ['export', '--workspace', WORKBENCH, '--spec-id', '0'], + cwd: REPO_ROOT, + stderr: (chunk) => { + stderr += chunk; + }, + }); + + expect(code).toBe(1); + expect(stderr).toContain('--spec-id must be a positive integer.'); + }); +}); diff --git a/src/dev/__tests__/faux-harness.test.ts b/src/dev/__tests__/faux-harness.test.ts index 528492f68..daa806c0a 100644 --- a/src/dev/__tests__/faux-harness.test.ts +++ b/src/dev/__tests__/faux-harness.test.ts @@ -131,7 +131,7 @@ describe('createBrunchFauxHarness', () => { latestLsn: () => 1, }, }), - introspection: { enabled: true, store }, + introspection: { queryTools: true, store }, }, ), ], diff --git a/src/dev/__tests__/graph-curation.test.ts b/src/dev/__tests__/graph-curation.test.ts new file mode 100644 index 000000000..03f62ae83 --- /dev/null +++ b/src/dev/__tests__/graph-curation.test.ts @@ -0,0 +1,171 @@ +import { mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { openWorkspaceGraphRuntime } from '../../graph/index.js'; +import { applyDevGraphMutation } from '../graph-curation.js'; + +describe('applyDevGraphMutation', () => { + it('applies create, patch, and delete curation ops through the shared mutation seam', async () => { + const fixture = await createFixture(); + + const created = await applyDevGraphMutation(fixture.cwd, { + specId: fixture.specAId, + createBasis: 'explicit', + ops: [ + { op: 'create_node', ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Curated thesis' }, + { + op: 'create_edge', + category: 'rationale', + support: { existingCode: 'REQ1' }, + claim: 'thesis', + stance: 'for', + rationale: 'Existing requirement supports the thesis.', + }, + ], + }); + + expect(created).toMatchObject({ + status: 'success', + createdNodes: { thesis: { code: 'TH1' } }, + createdEdges: [expect.any(Number)], + }); + if (created.status !== 'success') throw new Error('expected create success'); + const thesis = created.createdNodes.thesis; + if (!thesis) throw new Error('expected thesis node'); + + const patched = await applyDevGraphMutation(fixture.cwd, { + specId: fixture.specAId, + ops: [ + { + op: 'patch_node', + node: { existingCode: 'TH1' }, + patch: { title: 'Patched thesis', body: 'Patch through the shared curation seam.' }, + }, + { + op: 'patch_edge', + edgeId: created.createdEdges[0]!, + patch: { rationale: 'Patched rationale' }, + }, + ], + }); + + expect(patched).toMatchObject({ + status: 'success', + updatedNodes: [thesis.id], + updatedEdges: [created.createdEdges[0]], + }); + + const graph = await openWorkspaceGraphRuntime(fixture.cwd); + const overview = graph.forSpec(fixture.specAId).queryGraph(); + expect(overview.nodes).toEqual( + expect.arrayContaining([expect.objectContaining({ title: 'Patched thesis' })]), + ); + expect(overview.edges).toEqual( + expect.arrayContaining([ + expect.objectContaining({ category: 'rationale', rationale: 'Patched rationale' }), + ]), + ); + + const deleted = await applyDevGraphMutation(fixture.cwd, { + specId: fixture.specAId, + ops: [ + { op: 'delete_edge', edgeId: created.createdEdges[0]! }, + { op: 'delete_node', node: { existingCode: 'TH1' } }, + ], + }); + + expect(deleted).toMatchObject({ + status: 'success', + deletedEdges: [created.createdEdges[0]], + deletedNodes: [thesis.id], + }); + const afterDelete = graph.forSpec(fixture.specAId).queryGraph(); + expect(afterDelete.nodes).not.toEqual( + expect.arrayContaining([expect.objectContaining({ title: 'Patched thesis' })]), + ); + }); + + it('returns structural diagnostics without persisting invalid curation ops', async () => { + const fixture = await createFixture(); + + const invalidCode = await applyDevGraphMutation(fixture.cwd, { + specId: fixture.specAId, + ops: [{ op: 'patch_node', node: { existingCode: 'not-a-code' }, patch: { title: 'Bad patch' } }], + }); + expect(invalidCode).toMatchObject({ + status: 'structural_illegal', + diagnostics: expect.arrayContaining([ + expect.objectContaining({ message: expect.stringContaining('malformed graph node code') }), + ]), + }); + + const crossSpec = await applyDevGraphMutation(fixture.cwd, { + specId: fixture.specAId, + createBasis: 'explicit', + ops: [ + { op: 'create_node', ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Sibling code thesis' }, + { + op: 'create_edge', + category: 'rationale', + support: { existingCode: 'G1' }, + claim: 'thesis', + stance: 'for', + }, + ], + }); + expect(crossSpec).toMatchObject({ + status: 'structural_illegal', + diagnostics: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('does not resolve in the selected spec'), + }), + ]), + }); + + const graph = await openWorkspaceGraphRuntime(fixture.cwd); + const overview = graph.forSpec(fixture.specAId).queryGraph(); + expect(overview.lsn).toBe(fixture.specALsn); + expect(JSON.stringify(overview)).not.toContain('Sibling code thesis'); + }); +}); + +async function createFixture(): Promise<{ + cwd: string; + specAId: number; + specALsn: number; +}> { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-dev-curation-')); + const graph = await openWorkspaceGraphRuntime(cwd); + const specA = graph.commandExecutor.createSpec({ name: 'Spec A', slug: 'spec-a' }); + const specB = graph.commandExecutor.createSpec({ name: 'Spec B', slug: 'spec-b' }); + if (specA.status !== 'success' || specB.status !== 'success') { + throw new Error('failed to create graph-curation fixture specs'); + } + + const commitA = graph.commandExecutor.mutateGraph({ + specId: specA.specId, + createBasis: 'explicit', + ops: [ + { + op: 'create_node', + ref: 'requirement', + plane: 'intent', + kind: 'requirement', + title: 'Spec A requirement', + }, + ], + }); + const commitB = graph.commandExecutor.mutateGraph({ + specId: specB.specId, + createBasis: 'explicit', + ops: [{ op: 'create_node', ref: 'goal', plane: 'intent', kind: 'goal', title: 'Spec B goal' }], + }); + if (commitA.status !== 'success' || commitB.status !== 'success') { + throw new Error('failed to create graph-curation fixture graph'); + } + + return { cwd, specAId: specA.specId, specALsn: commitA.lsn }; +} diff --git a/src/dev/__tests__/pi-source-alias.test.ts b/src/dev/__tests__/pi-source-alias.test.ts deleted file mode 100644 index b128df795..000000000 --- a/src/dev/__tests__/pi-source-alias.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { homedir } from 'node:os'; -import { join } from 'node:path'; - -import { describe, expect, it } from 'vitest'; - -import { DEFAULT_PI_SOURCE_ROOT, piSourceAlias } from '../pi-source-alias.js'; - -function loadViteAlias(env: { PI_SOURCE?: string; PI_SOURCE_ROOT?: string }) { - const previous = { PI_SOURCE: process.env.PI_SOURCE, PI_SOURCE_ROOT: process.env.PI_SOURCE_ROOT }; - if (env.PI_SOURCE === undefined) delete process.env.PI_SOURCE; - else process.env.PI_SOURCE = env.PI_SOURCE; - if (env.PI_SOURCE_ROOT === undefined) delete process.env.PI_SOURCE_ROOT; - else process.env.PI_SOURCE_ROOT = env.PI_SOURCE_ROOT; - - try { - return piSourceAlias(); - } finally { - if (previous.PI_SOURCE === undefined) delete process.env.PI_SOURCE; - else process.env.PI_SOURCE = previous.PI_SOURCE; - if (previous.PI_SOURCE_ROOT === undefined) delete process.env.PI_SOURCE_ROOT; - else process.env.PI_SOURCE_ROOT = previous.PI_SOURCE_ROOT; - } -} - -describe('pi source alias', () => { - it('derives the default checkout path portably', () => { - expect(DEFAULT_PI_SOURCE_ROOT).toBe(join(homedir(), '.pi', 'pi-mono')); - }); - - it('types and default resolution stay on installed dist packages', () => { - // The published 0.79.0 packages ship their own dist/index.d.ts, so types and - // default runtime resolution come from node_modules — no tsconfig paths needed. - expect(import.meta.resolve('@earendil-works/pi-ai')).toContain( - 'node_modules/@earendil-works/pi-ai/dist/index.js', - ); - - // Without PI_SOURCE the vite alias is inert. - expect(loadViteAlias({})).toEqual([]); - }); - - it('points vite root aliases at pi-mono source only behind PI_SOURCE', () => { - // Use the current process cwd as a guaranteed-existing PI_SOURCE_ROOT so the - // existsSync guard passes on any machine; assert the alias mirrors that root. - const root = process.cwd(); - const alias = loadViteAlias({ PI_SOURCE: '1', PI_SOURCE_ROOT: root }); - - expect(alias).toEqual( - expect.arrayContaining([ - { find: /^@earendil-works\/pi-ai$/, replacement: `${root}/packages/ai/src/index.ts` }, - { find: /^@earendil-works\/pi-agent-core$/, replacement: `${root}/packages/agent/src/index.ts` }, - { - find: /^@earendil-works\/pi-coding-agent$/, - replacement: `${root}/packages/coding-agent/src/index.ts`, - }, - { find: /^@earendil-works\/pi-tui$/, replacement: `${root}/packages/tui/src/index.ts` }, - ]), - ); - }); - - it('resolves package subpaths to their source files', () => { - const root = process.cwd(); - const alias = loadViteAlias({ PI_SOURCE: '1', PI_SOURCE_ROOT: root }); - - expect(alias).toEqual( - expect.arrayContaining([ - { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: `${root}/packages/ai/src/oauth.ts` }, - { - find: /^@earendil-works\/pi-ai\/(.*)$/, - replacement: `${root}/packages/ai/src/$1.ts`, - }, - { - find: /^@earendil-works\/pi-coding-agent\/hooks$/, - replacement: `${root}/packages/coding-agent/src/core/hooks/index.ts`, - }, - { - find: /^@earendil-works\/pi-coding-agent\/(.*)$/, - replacement: `${root}/packages/coding-agent/src/$1.ts`, - }, - { - find: /^@earendil-works\/pi-agent-core\/(.*)$/, - replacement: `${root}/packages/agent/src/$1.ts`, - }, - { - find: /^@earendil-works\/pi-tui\/(.*)$/, - replacement: `${root}/packages/tui/src/$1.ts`, - }, - ]), - ); - }); - - it('stays inert when PI_SOURCE is set but the checkout is absent', () => { - expect(loadViteAlias({ PI_SOURCE: '1', PI_SOURCE_ROOT: '/nonexistent/pi-mono-checkout' })).toEqual([]); - }); -}); diff --git a/src/dev/__tests__/tier-2-harness.test.ts b/src/dev/__tests__/tier-2-harness.test.ts index efe4f2334..105a320ea 100644 --- a/src/dev/__tests__/tier-2-harness.test.ts +++ b/src/dev/__tests__/tier-2-harness.test.ts @@ -156,7 +156,7 @@ describe('origination-kick-live — the product originates the opening turn on i } }); - it('without BRUNCH_DEV no entry mirror is written', async () => { + it('without debug mirroring no entry mirror is written', async () => { const boot = await bootTier2RuntimeThroughRunBrunchTui({ dev: false }); try { await expect(readFile(`${boot.cwd}/.brunch/debug/entry-contents.md`, 'utf8')).rejects.toMatchObject({ @@ -278,7 +278,7 @@ describe('origination-kick-live — the product originates the opening turn on i }); describe('FE-847 Tier-2 real boot harness', () => { - it('owns real runtime boot proof for ready context and BRUNCH_DEV-gated query tools', async () => { + it('owns real runtime boot proof for ready context and developerTools-gated query tools', async () => { const productBoot = await bootTier2RuntimeThroughRunBrunchTui({ dev: false }); try { expect(productBoot.runtime.session.sessionManager.getHeader()).toMatchObject({ diff --git a/src/dev/brunch-dev.ts b/src/dev/brunch-dev.ts deleted file mode 100644 index dce67047c..000000000 --- a/src/dev/brunch-dev.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isBrunchDevEnabled(env: NodeJS.ProcessEnv = process.env): boolean { - return env.BRUNCH_DEV === '1'; -} diff --git a/src/dev/dev-cli.ts b/src/dev/dev-cli.ts new file mode 100644 index 000000000..4c86836f6 --- /dev/null +++ b/src/dev/dev-cli.ts @@ -0,0 +1,669 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'; +import process from 'node:process'; +import type { Readable, Writable } from 'node:stream'; +import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; + +import { + cancel as clackCancel, + confirm as clackConfirm, + intro as clackIntro, + isCancel, + outro as clackOutro, + select as clackSelect, +} from '@clack/prompts'; + +import { runBrunchCli, type BrunchCliOptions } from '../app/brunch.js'; +import { exportSeedFixtureFromWorkspace, formatSeedFixture } from '../graph/export-fixtures.js'; +import { + listTrackedSeedRefs, + parseSeedRef, + runSeedFixturesCli, + workbenchPathForSeed, +} from '../graph/seed-fixtures.js'; +import { createRpcHandlers } from '../rpc/handlers.js'; +import { createWorkspaceSessionCoordinator } from '../session/workspace-session-coordinator.js'; +import { applyDevGraphMutation, parseDevMutateGraphParams } from './graph-curation.js'; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const WORKBENCHES_ROOT = resolve(REPO_ROOT, '.fixtures', 'workbenches'); + +type TopLevelCommand = 'launch' | 'rpc' | 'mutate' | 'export' | 'help'; +type GraphVisibility = 'all' | 'active'; + +interface WorkbenchChoice { + readonly label: string; + readonly workspace: string; + readonly seedRefs: readonly string[]; +} + +interface LaunchPromptPlan { + readonly workspace: string; + readonly seed?: string; + readonly reset: boolean; + readonly openWeb: boolean; +} + +export interface DevCliPrompts { + intro(title: string): void; + outro(message: string): void; + cancel(message: string): void; + chooseWorkbench(options: readonly WorkbenchChoice[]): Promise; + chooseSeed(options: readonly string[], workspaceLabel: string): Promise; + confirmSeedReset(seed: string, workspaceLabel: string): Promise; + confirmOpenWeb(workspaceLabel: string): Promise; +} + +export interface DevCliOptions { + readonly argv?: readonly string[]; + readonly cwd?: string; + readonly stdin?: Readable; + readonly stdout?: Writable | ((chunk: string) => void); + readonly stderr?: Writable | ((chunk: string) => void); + readonly prompts?: DevCliPrompts; + readonly launchBrunch?: (options: BrunchCliOptions) => Promise; + readonly seedWorkspace?: typeof runSeedFixturesCli; +} + +interface LaunchFlags { + readonly workspace: string | undefined; + readonly seed: string | undefined; + readonly reset: boolean; + readonly mode: string; + readonly openWeb: boolean; + readonly developerTools: boolean; + readonly help: boolean; +} + +interface RpcFlags { + readonly workspace: string | undefined; + readonly fullResponse: boolean; + readonly help: boolean; + readonly method: string | undefined; + readonly paramsText: string | undefined; +} + +interface MutateFlags { + readonly workspace: string | undefined; + readonly help: boolean; + readonly paramsText: string | undefined; + readonly paramsFile: string | undefined; +} + +interface ExportFlags { + readonly workspace: string | undefined; + readonly specId: number | undefined; + readonly out: string | undefined; + readonly show: GraphVisibility | undefined; + readonly help: boolean; +} + +class DevCliUsageError extends Error {} + +const defaultPrompts: DevCliPrompts = { + intro: (title) => { + clackIntro(title); + }, + outro: (message) => { + clackOutro(message); + }, + cancel: (message) => { + clackCancel(message); + }, + chooseWorkbench: async (options) => + clackSelect({ + message: 'Which seed-derived workbench should Brunch use?', + options: options.map((option) => ({ value: option.workspace, label: option.label })), + maxItems: 8, + }), + chooseSeed: async (options, workspaceLabel) => + clackSelect({ + message: `How should ${workspaceLabel} start?`, + options: [ + { value: '__current__', label: 'Use the current workbench state' }, + ...options.map((seed) => ({ value: seed, label: `Reset and seed ${seed}` })), + ], + maxItems: 10, + }), + confirmSeedReset: async (seed, workspaceLabel) => + clackConfirm({ + message: + `Reset ${workspaceLabel}/.brunch/{data.db,data.db-wal,data.db-shm,sessions,debug,workspace.json} ` + + `and seed ${seed}?`, + initialValue: true, + }), + confirmOpenWeb: async (workspaceLabel) => + clackConfirm({ + message: `Open the web observer sidecar too for ${workspaceLabel}?`, + initialValue: false, + }), +}; + +export async function runDevCli(options: DevCliOptions = {}): Promise { + const argv = options.argv ?? process.argv.slice(2); + const cwd = options.cwd ?? process.cwd(); + const [command, commandArgs] = splitCommand(argv); + const stdout = options.stdout ?? process.stdout; + const stderr = options.stderr ?? process.stderr; + + try { + switch (command) { + case 'help': + writeStdout(stdout, devCliUsage()); + return 0; + case 'launch': + return await runLaunchCommand(commandArgs, { ...options, cwd }); + case 'rpc': + return await runRpcCommand(commandArgs, { ...options, cwd }); + case 'mutate': + return await runMutateCommand(commandArgs, { ...options, cwd }); + case 'export': + return await runExportCommand(commandArgs, { ...options, cwd }); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + writeStderr(stderr, `${message}\n\n${devCliUsage()}`); + return 1; + } +} + +function splitCommand(argv: readonly string[]): readonly [TopLevelCommand, readonly string[]] { + const [first, ...rest] = argv; + if (first === undefined) return ['launch', argv]; + if (first === '--help' || first === '-h' || first === 'help') return ['help', rest]; + if (first === 'launch' || first === 'rpc' || first === 'mutate' || first === 'export') { + return [first, rest]; + } + return ['launch', argv]; +} + +async function runLaunchCommand(args: readonly string[], options: DevCliOptions & { readonly cwd: string }) { + const flags = parseLaunchFlags(args, options.cwd); + if (flags.help) { + writeStdout(options.stdout ?? process.stdout, `${devCliUsage()}\n${launchUsage()}`); + return 0; + } + + const seedRef = flags.seed ? parseSeedRef(flags.seed) : null; + if (flags.seed && !seedRef) { + throw new DevCliUsageError('--seed must be a tracked seed ref in the form /.'); + } + + const currentWorkbench = currentWorkbenchForCwd(options.cwd); + const prompts = options.prompts ?? defaultPrompts; + let workspace = flags.workspace ?? (seedRef ? workbenchPathForSeed(seedRef) : currentWorkbench); + let seed = flags.seed; + let reset = flags.reset; + let openWeb = flags.openWeb; + + if (!workspace) { + if (flags.mode !== 'tui') { + throw new DevCliUsageError('A workbench is required for non-interactive launch modes.'); + } + if (!isInteractiveTerminal(options.stdin, options.stdout)) { + throw new DevCliUsageError('No workbench was provided and no interactive terminal is available.'); + } + const plan = await promptForLaunchPlan(prompts); + if (!plan) return 0; + workspace = plan.workspace; + seed = plan.seed; + reset = plan.reset; + openWeb = plan.openWeb; + } + + if (seed && !reset) { + throw new DevCliUsageError('Launch-time seeding requires --reset so the workspace state stays explicit.'); + } + if (!seed && reset) { + throw new DevCliUsageError('--reset only applies when paired with --seed.'); + } + + if (seed) { + const code = await (options.seedWorkspace ?? runSeedFixturesCli)({ + argv: ['--workspace', workspace, '--seed', seed, '--reset'], + cwd: options.cwd, + stdout: (chunk) => writeStdout(options.stdout ?? process.stdout, chunk), + stderr: (chunk) => writeStderr(options.stderr ?? process.stderr, chunk), + }); + if (code !== 0) return code; + } + + await mkdir(workspace, { recursive: true }); + + return await (options.launchBrunch ?? runBrunchCli)({ + argv: [ + '--mode', + flags.mode, + ...(openWeb ? ['--open-web'] : []), + ...(flags.developerTools ? ['--dev-tools'] : []), + ], + cwd: workspace, + ...(options.stdin ? { stdin: options.stdin } : {}), + ...(options.stdout ? { stdout: options.stdout } : {}), + developerTools: flags.developerTools, + }); +} + +async function promptForLaunchPlan(prompts: DevCliPrompts): Promise { + const workbenches = await listTrackedWorkbenches(); + if (workbenches.length === 0) { + throw new DevCliUsageError('No tracked seeds are available to derive workbenches from.'); + } + + prompts.intro('Brunch dev launcher'); + const workspace = + workbenches.length === 1 ? workbenches[0]!.workspace : await prompts.chooseWorkbench(workbenches); + if (isCancel(workspace)) { + prompts.cancel('Launch cancelled.'); + return null; + } + + const workspaceLabel = labelForWorkspace(workspace); + const selectedWorkbench = workbenches.find((choice) => choice.workspace === workspace); + if (!selectedWorkbench) { + throw new DevCliUsageError(`Unknown tracked workbench selected: ${workspaceLabel}`); + } + + const seedChoice = await prompts.chooseSeed(selectedWorkbench.seedRefs, workspaceLabel); + if (isCancel(seedChoice)) { + prompts.cancel('Launch cancelled.'); + return null; + } + + let seed: string | undefined; + if (seedChoice !== '__current__') { + const confirmed = await prompts.confirmSeedReset(seedChoice, workspaceLabel); + if (isCancel(confirmed) || confirmed !== true) { + prompts.cancel('Launch cancelled.'); + return null; + } + seed = seedChoice; + } + + const openWeb = await prompts.confirmOpenWeb(workspaceLabel); + if (isCancel(openWeb)) { + prompts.cancel('Launch cancelled.'); + return null; + } + + prompts.outro(`Launching ${workspaceLabel}${seed ? ` from ${seed}` : ''}.`); + return { workspace, ...(seed ? { seed } : {}), reset: seed !== undefined, openWeb }; +} + +async function runRpcCommand(args: readonly string[], options: DevCliOptions & { readonly cwd: string }) { + const flags = parseRpcFlags(args, options.cwd); + if (flags.help) { + writeStdout(options.stdout ?? process.stdout, `${devCliUsage()}\n${rpcUsage()}`); + return 0; + } + if (!flags.method) { + throw new DevCliUsageError('The rpc command requires a method name.'); + } + + const handlers = createRpcHandlers({ + coordinator: createWorkspaceSessionCoordinator({ cwd: flags.workspace ?? options.cwd }), + cwd: flags.workspace ?? options.cwd, + }); + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: flags.method, + ...(flags.paramsText === undefined ? {} : { params: parseJson(flags.paramsText, '--params') }), + }; + const response = await handlers.handle(request); + if ('error' in response && response.error != null) { + printJson(options.stdout ?? process.stdout, flags.fullResponse ? response : response.error); + return 1; + } + printJson( + options.stdout ?? process.stdout, + flags.fullResponse ? response : 'result' in response ? response.result : undefined, + ); + return 0; +} + +async function runMutateCommand(args: readonly string[], options: DevCliOptions & { readonly cwd: string }) { + const flags = parseMutateFlags(args, options.cwd); + if (flags.help) { + writeStdout(options.stdout ?? process.stdout, `${devCliUsage()}\n${mutateUsage()}`); + return 0; + } + + const paramsText = + flags.paramsText ?? + (flags.paramsFile + ? await readFile(resolve(options.cwd, flags.paramsFile), 'utf8') + : await readOptionalStdin(options.stdin)); + if (!paramsText) { + throw new DevCliUsageError('The mutate command requires --params, --params-file, or JSON on stdin.'); + } + + const parsedParams = parseDevMutateGraphParams(parseJson(paramsText, 'mutate params')); + if (!parsedParams) { + throw new DevCliUsageError( + 'The mutate params payload does not match the supported graph-curation schema.', + ); + } + + const result = await applyDevGraphMutation(flags.workspace ?? options.cwd, parsedParams); + printJson(options.stdout ?? process.stdout, result); + return result.status === 'success' ? 0 : 1; +} + +async function runExportCommand(args: readonly string[], options: DevCliOptions & { readonly cwd: string }) { + const flags = parseExportFlags(args, options.cwd); + if (flags.help) { + writeStdout(options.stdout ?? process.stdout, `${devCliUsage()}\n${exportUsage()}`); + return 0; + } + if (!flags.specId) { + throw new DevCliUsageError('The export command requires --spec-id.'); + } + + const workspace = flags.workspace ?? options.cwd; + const fixture = exportSeedFixtureFromWorkspace(workspace, { + specId: flags.specId, + ...(flags.show ? { show: flags.show } : {}), + }); + const rendered = formatSeedFixture(fixture); + + if (flags.out) { + const outPath = resolve(options.cwd, flags.out); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, rendered, 'utf8'); + writeStdout(options.stdout ?? process.stdout, `wrote ${outPath}\n`); + return 0; + } + + writeStdout(options.stdout ?? process.stdout, rendered); + return 0; +} + +function parseLaunchFlags(args: readonly string[], cwd: string): LaunchFlags { + const { values, positionals } = parseArgs({ + args, + allowPositionals: true, + options: { + workspace: { type: 'string', short: 'w' }, + cwd: { type: 'string' }, + seed: { type: 'string' }, + reset: { type: 'boolean', default: false }, + mode: { type: 'string', default: 'tui' }, + 'open-web': { type: 'boolean', default: false }, + 'dev-tools': { type: 'boolean', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + }); + if (positionals.length > 0) { + throw new DevCliUsageError(`Unexpected launch argument: ${positionals[0]}`); + } + return { + workspace: resolveWorkspaceOption(values.workspace, values.cwd, cwd), + seed: values.seed, + reset: values.reset, + mode: values.mode, + openWeb: values['open-web'], + developerTools: values['dev-tools'], + help: values.help, + }; +} + +function parseRpcFlags(args: readonly string[], cwd: string): RpcFlags { + const { values, positionals } = parseArgs({ + args, + allowPositionals: true, + options: { + workspace: { type: 'string', short: 'w' }, + cwd: { type: 'string' }, + 'full-response': { type: 'boolean', default: false }, + help: { type: 'boolean', short: 'h', default: false }, + }, + }); + if (positionals.length > 2) { + throw new DevCliUsageError('The rpc command accepts at most one params JSON argument.'); + } + return { + workspace: resolveWorkspaceOption(values.workspace, values.cwd, cwd), + fullResponse: values['full-response'], + help: values.help, + method: positionals[0], + paramsText: positionals[1], + }; +} + +function parseMutateFlags(args: readonly string[], cwd: string): MutateFlags { + const { values, positionals } = parseArgs({ + args, + allowPositionals: true, + options: { + workspace: { type: 'string', short: 'w' }, + cwd: { type: 'string' }, + params: { type: 'string' }, + 'params-file': { type: 'string' }, + help: { type: 'boolean', short: 'h', default: false }, + }, + }); + if (positionals.length > 0) { + throw new DevCliUsageError(`Unexpected mutate argument: ${positionals[0]}`); + } + if (values.params && values['params-file']) { + throw new DevCliUsageError('Use only one of --params or --params-file.'); + } + return { + workspace: resolveWorkspaceOption(values.workspace, values.cwd, cwd), + help: values.help, + paramsText: values.params, + paramsFile: values['params-file'], + }; +} + +function parseExportFlags(args: readonly string[], cwd: string): ExportFlags { + const { values, positionals } = parseArgs({ + args, + allowPositionals: true, + options: { + workspace: { type: 'string', short: 'w' }, + cwd: { type: 'string' }, + 'spec-id': { type: 'string' }, + out: { type: 'string', short: 'o' }, + show: { type: 'string' }, + help: { type: 'boolean', short: 'h', default: false }, + }, + }); + if (positionals.length > 0) { + throw new DevCliUsageError(`Unexpected export argument: ${positionals[0]}`); + } + const show = values.show; + if (show !== undefined && show !== 'all' && show !== 'active') { + throw new DevCliUsageError('--show must be all or active.'); + } + return { + workspace: resolveWorkspaceOption(values.workspace, values.cwd, cwd), + specId: values['spec-id'] ? parsePositiveInteger(values['spec-id'], '--spec-id') : undefined, + out: values.out, + show, + help: values.help, + }; +} + +function resolveWorkspaceOption( + workspace: string | undefined, + cwdFlag: string | undefined, + cwd: string, +): string | undefined { + if (workspace && cwdFlag) { + throw new DevCliUsageError('Use only one of --workspace or --cwd.'); + } + const value = workspace ?? cwdFlag; + if (!value) return undefined; + return isAbsolute(value) ? value : resolve(cwd, value); +} + +function parsePositiveInteger(value: string, flag: string): number { + if (value.length === 0 || value[0] === '0') { + throw new DevCliUsageError(`${flag} must be a positive integer.`); + } + + let parsed = 0; + for (const char of value) { + const digit = char.charCodeAt(0) - 48; + if (digit < 0 || digit > 9) { + throw new DevCliUsageError(`${flag} must be a positive integer.`); + } + parsed = parsed * 10 + digit; + if (!Number.isSafeInteger(parsed)) { + throw new DevCliUsageError(`${flag} must be a positive integer.`); + } + } + return parsed; +} + +function parseJson(text: string, label: string): unknown { + try { + return JSON.parse(text); + } catch (error) { + throw new DevCliUsageError( + `${label} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +function currentWorkbenchForCwd(cwd: string): string | undefined { + const resolvedCwd = resolve(cwd); + const relativePath = relative(WORKBENCHES_ROOT, resolvedCwd); + if (relativePath === '' || relativePath.startsWith('..') || isAbsolute(relativePath)) return undefined; + const [workbenchName] = relativePath.split(sep); + return workbenchName ? resolve(WORKBENCHES_ROOT, workbenchName) : undefined; +} + +async function listTrackedWorkbenches(): Promise { + const grouped = new Map(); + for (const seed of await listTrackedSeedRefs()) { + const workspace = workbenchPathForSeed(seed); + const seedRefs = grouped.get(workspace) ?? []; + seedRefs.push(seed.ref); + grouped.set(workspace, seedRefs); + } + + return [...grouped.entries()] + .map(([workspace, seedRefs]) => ({ + workspace, + label: labelForWorkspace(workspace), + seedRefs: seedRefs.sort((left, right) => left.localeCompare(right)), + })) + .sort((left, right) => left.label.localeCompare(right.label)); +} + +function labelForWorkspace(workspace: string): string { + const relativePath = relative(REPO_ROOT, workspace); + return relativePath.startsWith('..') ? workspace : relativePath; +} + +function isInteractiveTerminal( + stdin: Readable | undefined, + stdout: Writable | ((chunk: string) => void) | undefined, +): boolean { + const input = stdin ?? process.stdin; + const output = stdout && typeof stdout !== 'function' ? stdout : process.stdout; + return ( + readIsTty(input as { readonly isTTY?: boolean }) && readIsTty(output as { readonly isTTY?: boolean }) + ); +} + +function readIsTty(stream: { readonly isTTY?: boolean }): boolean { + return stream.isTTY === true; +} + +async function readOptionalStdin(stdin: Readable | undefined): Promise { + const input = stdin ?? process.stdin; + if ('isTTY' in input && input.isTTY === true) { + return undefined; + } + + let buffer = ''; + for await (const chunk of input) { + buffer += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk); + } + return buffer.trim() || undefined; +} + +function writeStdout(stdout: Writable | ((chunk: string) => void), chunk: string): void { + if (typeof stdout === 'function') { + stdout(chunk); + return; + } + stdout.write(chunk); +} + +function writeStderr(stderr: Writable | ((chunk: string) => void), chunk: string): void { + if (typeof stderr === 'function') { + stderr(chunk); + return; + } + stderr.write(chunk); +} + +function printJson(stdout: Writable | ((chunk: string) => void), value: unknown): void { + writeStdout(stdout, `${JSON.stringify(value, null, 2)}\n`); +} + +function devCliUsage(): string { + return [ + 'Usage:', + ' npm run dev', + ' npm run dev -- --seed / --reset [--open-web] [--dev-tools]', + ' npm run dev -- --workspace [--mode tui|print|rpc] [--open-web] [--dev-tools]', + ' npm run dev -- --workspace --seed / --reset [--open-web] [--dev-tools]', + ' npm run dev -- rpc [params-json] --workspace ', + ' npm run dev -- mutate --workspace (--params | --params-file )', + ' npm run dev -- export --workspace --spec-id [--out ] [--show all|active]', + '', + 'Notes:', + ' - Launch-time seeding never happens implicitly; pair --seed with --reset.', + ' - With --seed and no --workspace, the launcher derives .fixtures/workbenches//.', + ' - Source/dev builds mirror debug artifacts automatically into /.brunch/debug/.', + ' - --dev-tools opt into query tools and subagents; it is separate from debug mirroring.', + ' - For direct raw app access, use npm run dev:raw -- ...', + ].join('\n'); +} + +function launchUsage(): string { + return [ + '', + 'Launch examples:', + ' npm run dev', + ' npm run dev -- --seed workspace-alpha-grounding/base --reset --open-web', + ' npm run dev -- --workspace .fixtures/workbenches/workspace-alpha-grounding --open-web', + ].join('\n'); +} + +function rpcUsage(): string { + return [ + '', + 'RPC examples:', + ' npm run dev -- rpc workspace.selectionState --workspace .fixtures/workbenches/workspace-alpha-grounding', + ` npm run dev -- rpc graph.overview '{"specId":1}' --workspace .fixtures/workbenches/workspace-alpha-grounding`, + ].join('\n'); +} + +function mutateUsage(): string { + return [ + '', + 'Mutate examples:', + ' npm run dev -- mutate --workspace .fixtures/workbenches/workspace-alpha-grounding --params-file /tmp/mutate.json', + ' cat /tmp/mutate.json | npm run dev -- mutate --workspace .fixtures/workbenches/workspace-alpha-grounding', + '', + 'The mutate payload is the shared local graph-curation params object:', + ' {"specId":1,"createBasis":"explicit","ops":[...]}', + ].join('\n'); +} + +function exportUsage(): string { + return [ + '', + 'Export examples:', + ' npm run dev -- export --workspace .fixtures/workbenches/workspace-alpha-grounding --spec-id 1', + ' npm run dev -- export --workspace .fixtures/workbenches/workspace-alpha-grounding --spec-id 1 --out .fixtures/seeds/custom/example.json', + ].join('\n'); +} diff --git a/src/rpc/methods/dev-graph.ts b/src/dev/graph-curation.ts similarity index 76% rename from src/rpc/methods/dev-graph.ts rename to src/dev/graph-curation.ts index 2c819c3ee..0004c43d0 100644 --- a/src/rpc/methods/dev-graph.ts +++ b/src/dev/graph-curation.ts @@ -9,9 +9,10 @@ import type { GraphMutationNodeRef, GraphMutationOp, MutateGraphInput, + MutateGraphResult, NodePatch, StructuralIllegal, -} from '../../graph/command-executor/graph-mutation-types.js'; +} from '../graph/command-executor/graph-mutation-types.js'; import { authoredEdgeEndpointFields, CLAIM_FORM_JSON_SCHEMAS, @@ -23,13 +24,10 @@ import { NODE_KINDS, NODE_KINDS_REQUIRING_DETAIL, NODE_KINDS_WITH_FORM_DETAIL, + openWorkspaceGraphRuntime, parseGraphNodeCode, type NodeKindWithFormDetail, -} from '../../graph/index.js'; -import { graphMutationProductUpdates } from '../product-updates.js'; -import { createJsonRpcFailure, createJsonRpcSuccess, jsonRpcRequestId } from '../protocol.js'; -import type { RpcMethodContext, RpcMethodDefinition } from './registry.js'; -import { PositiveIntegerSchema } from './schemas.js'; +} from '../graph/index.js'; const BasisSchema = Type.Union([Type.Literal('explicit'), Type.Literal('implicit')]); const NodePlaneSchema = Type.Union([ @@ -160,7 +158,7 @@ const DevPatchNodeOpSchema = Type.Object( const DevPatchEdgeOpSchema = Type.Object( { op: Type.Literal('patch_edge'), - edgeId: PositiveIntegerSchema, + edgeId: Type.Integer({ minimum: 1 }), patch: DevEdgePatchSchema, }, { additionalProperties: false }, @@ -169,7 +167,7 @@ const DevPatchEdgeOpSchema = Type.Object( const DevDeleteEdgeOpSchema = Type.Object( { op: Type.Literal('delete_edge'), - edgeId: PositiveIntegerSchema, + edgeId: Type.Integer({ minimum: 1 }), }, { additionalProperties: false }, ); @@ -192,9 +190,9 @@ const DevMutateGraphOpSchema = Type.Union([ DevDeleteNodeOpSchema, ]); -const DevMutateGraphParamsSchema = Type.Object( +export const DevMutateGraphParamsSchema = Type.Object( { - specId: PositiveIntegerSchema, + specId: Type.Integer({ minimum: 1 }), createBasis: Type.Optional(BasisSchema), ops: Type.Array(DevMutateGraphOpSchema), }, @@ -262,118 +260,28 @@ type DevMutateGraphOp = | DevDeleteEdgeOp | DevDeleteNodeOp; -interface DevMutateGraphParams { +export interface DevMutateGraphParams { readonly specId: number; readonly createBasis?: DevBasis | undefined; readonly ops: readonly DevMutateGraphOp[]; } -const DiagnosticSchema = Type.Object( - { - field: Type.String(), - message: Type.String(), - }, - { additionalProperties: false }, -); - -const DevMutateGraphResultSchema = Type.Union([ - Type.Object( - { - status: Type.Literal('success'), - lsn: Type.Number(), - createdNodes: Type.Record( - Type.String(), - Type.Object( - { - id: Type.Number(), - code: Type.String(), - }, - { additionalProperties: false }, - ), - ), - createdEdges: Type.Array(Type.Number()), - updatedNodes: Type.Array(Type.Number()), - updatedEdges: Type.Array(Type.Number()), - deletedNodes: Type.Array(Type.Number()), - deletedEdges: Type.Array(Type.Number()), - }, - { additionalProperties: false }, - ), - Type.Object( - { - status: Type.Literal('structural_illegal'), - diagnostics: Type.Array(DiagnosticSchema), - }, - { additionalProperties: false }, - ), -]); - -export const devGraphRpcMethods: readonly RpcMethodDefinition[] = [ - { - method: 'dev.graph.mutateGraph', - access: 'write', - description: - 'Dev-only local curation harness: apply projected-code mutateGraph ops to one selected spec through CommandExecutor.', - paramsSchema: DevMutateGraphParamsSchema, - resultSchema: DevMutateGraphResultSchema, - examples: [ - { - jsonrpc: '2.0', - id: 90, - method: 'dev.graph.mutateGraph', - params: { - specId: 1, - createBasis: 'explicit', - ops: [ - { op: 'create_node', ref: 'n1', plane: 'intent', kind: 'thesis', title: 'Curated thesis' }, - { - op: 'create_edge', - category: 'rationale', - support: { existingCode: 'G1' }, - claim: 'n1', - stance: 'for', - }, - { op: 'patch_node', node: { existingCode: 'REQ1' }, patch: { body: 'Clarified body' } }, - { op: 'delete_edge', edgeId: 12 }, - ], - }, - }, - ], - async handle(context, request) { - const requestId = jsonRpcRequestId(request); - const params = parseDevMutateGraphParams(request.params); - if (!params.ok) { - return createJsonRpcFailure(requestId, -32602, 'Invalid params'); - } - - const graph = await context.getGraphRuntime(); - const scopedGraph = graph.forSpec(params.value.specId); - const input = translateDevMutateGraph(params.value, { - resolveNodeCode: scopedGraph.resolveNodeCode, - resolveEdgeId: scopedGraph.resolveEdgeId, - }); - const result = 'status' in input ? input : graph.commandExecutor.mutateGraph(input); - - if (result.status === 'success') { - context.productUpdates?.publish( - graphMutationProductUpdates({ specId: params.value.specId, lsn: result.lsn }), - ); - } - return createJsonRpcSuccess(requestId, result); - }, - }, -]; - -type DevMutateGraphParamsParseResult = - | { - ok: true; - value: DevMutateGraphParams; - } - | { ok: false }; +export function parseDevMutateGraphParams(value: unknown): DevMutateGraphParams | null { + if (!Value.Check(DevMutateGraphParamsSchema, value)) return null; + return Value.Parse(DevMutateGraphParamsSchema, value); +} -function parseDevMutateGraphParams(value: unknown): DevMutateGraphParamsParseResult { - if (!Value.Check(DevMutateGraphParamsSchema, value)) return { ok: false }; - return { ok: true, value: Value.Parse(DevMutateGraphParamsSchema, value) }; +export async function applyDevGraphMutation( + cwd: string, + params: DevMutateGraphParams, +): Promise { + const graph = await openWorkspaceGraphRuntime(cwd); + const scopedGraph = graph.forSpec(params.specId); + const input = translateDevMutateGraph(params, { + resolveNodeCode: scopedGraph.resolveNodeCode, + resolveEdgeId: scopedGraph.resolveEdgeId, + }); + return 'status' in input ? input : graph.commandExecutor.mutateGraph(input); } function translateDevMutateGraph( diff --git a/src/dev/index.ts b/src/dev/index.ts index 879d70b38..e83f8fc30 100644 --- a/src/dev/index.ts +++ b/src/dev/index.ts @@ -1,4 +1,10 @@ -export { piSourceAlias } from './pi-source-alias.js'; +export { runDevCli, type DevCliOptions, type DevCliPrompts } from './dev-cli.js'; +export { + DevMutateGraphParamsSchema, + applyDevGraphMutation, + parseDevMutateGraphParams, + type DevMutateGraphParams, +} from './graph-curation.js'; export { BRUNCH_FAUX_HARNESS_API_KEY, BRUNCH_FAUX_HARNESS_ENV_API_KEY, @@ -34,4 +40,3 @@ export { runTier2RealBootFauxTurn, type Tier2RealBootTurnResult, } from './tier-2-harness.js'; -export * as workspaceRpc from './workspace-rpc.js'; diff --git a/src/dev/pi-source-alias.ts b/src/dev/pi-source-alias.ts deleted file mode 100644 index 16a2692cc..000000000 --- a/src/dev/pi-source-alias.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { existsSync } from 'node:fs'; -import { homedir } from 'node:os'; -import { join, resolve } from 'node:path'; - -import { type AliasOptions } from 'vite'; - -export const DEFAULT_PI_SOURCE_ROOT = join(homedir(), '.pi', 'pi-mono'); - -/** - * Dev-only source alias for the pi packages (D67-L). - * - * Returns vite/vitest `resolve.alias` entries that redirect all four - * `@earendil-works/pi-*` packages to a sibling `pi-mono` source checkout so - * edits there take effect without rebuilding. Inert unless `PI_SOURCE=1` and the - * checkout exists, so the default (and every published build) resolves installed - * `dist`. `PI_SOURCE_ROOT` overrides the checkout path. - * - * `pi-agent-core` is included even though Brunch never imports it directly: - * `pi-coding-agent`'s source imports it, so a partial alias would yield a mixed - * source/dist module graph. - * - * Types are NOT handled here — the published packages ship their own - * `dist/index.d.ts`, so `tsconfig.json` deliberately carries no pi `paths`. - */ -export function piSourceAlias(): AliasOptions { - const piMonoRoot = process.env.PI_SOURCE_ROOT ?? DEFAULT_PI_SOURCE_ROOT; - if (process.env.PI_SOURCE !== '1' || !existsSync(piMonoRoot)) return []; - - return [ - { find: /^@earendil-works\/pi-ai$/, replacement: resolve(piMonoRoot, 'packages/ai/src/index.ts') }, - { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: resolve(piMonoRoot, 'packages/ai/src/oauth.ts') }, - { find: /^@earendil-works\/pi-ai\/(.*)$/, replacement: resolve(piMonoRoot, 'packages/ai/src/$1.ts') }, - { - find: /^@earendil-works\/pi-agent-core$/, - replacement: resolve(piMonoRoot, 'packages/agent/src/index.ts'), - }, - { - find: /^@earendil-works\/pi-agent-core\/(.*)$/, - replacement: resolve(piMonoRoot, 'packages/agent/src/$1.ts'), - }, - { - find: /^@earendil-works\/pi-coding-agent$/, - replacement: resolve(piMonoRoot, 'packages/coding-agent/src/index.ts'), - }, - { - find: /^@earendil-works\/pi-coding-agent\/hooks$/, - replacement: resolve(piMonoRoot, 'packages/coding-agent/src/core/hooks/index.ts'), - }, - { - find: /^@earendil-works\/pi-coding-agent\/(.*)$/, - replacement: resolve(piMonoRoot, 'packages/coding-agent/src/$1.ts'), - }, - { find: /^@earendil-works\/pi-tui$/, replacement: resolve(piMonoRoot, 'packages/tui/src/index.ts') }, - { find: /^@earendil-works\/pi-tui\/(.*)$/, replacement: resolve(piMonoRoot, 'packages/tui/src/$1.ts') }, - ]; -} diff --git a/src/dev/tier-2-harness.ts b/src/dev/tier-2-harness.ts index 7b3abde5f..820a40258 100644 --- a/src/dev/tier-2-harness.ts +++ b/src/dev/tier-2-harness.ts @@ -108,12 +108,14 @@ export async function bootTier2RuntimeThroughRunBrunchTui(options: { const agentDir = await mkdtemp(join(tmpdir(), 'brunch-agent-dir-')); await writeFile(join(cwd, 'boot-seam.md'), '# Boot seam\n'); - const restoreEnv = overrideBrunchDevEnv(options.dev ? '1' : undefined); + const restoreEnv = () => {}; let runtime: Awaited> | undefined; try { await runBrunchTui({ cwd, + debugMirror: options.dev, + developerTools: options.dev, runWorkspaceDialogPreflight: async () => ({ action: 'newSpec', title: 'Boot seam smoke' }), webSidecarRunner: async () => null, launchInteractive: async (context) => { @@ -170,7 +172,7 @@ export async function bootTier2RuntimeFromFixture(options: { const cwd = await mkdtemp(join(tmpdir(), 'brunch-tier-2-resume-boot-')); const agentDir = options.agentDir ?? (await mkdtemp(join(tmpdir(), 'brunch-agent-dir-'))); - const restoreEnv = overrideBrunchDevEnv(options.dev ? '1' : undefined); + const restoreEnv = () => {}; try { const coordinator = createWorkspaceSessionCoordinator({ cwd }); @@ -197,6 +199,8 @@ export async function bootTier2RuntimeFromFixture(options: { let runtime: Awaited> | undefined; await runBrunchTui({ cwd, + debugMirror: options.dev === true, + developerTools: options.dev === true, coordinator, runWorkspaceDialogPreflight: async () => ({ action: 'openSession', @@ -240,28 +244,6 @@ export async function bootTier2RuntimeFromFixture(options: { * `agentServices` override. Only the provider backend is substituted; the * session, extensions, and origination choreography stay product wiring. */ -/** - * Override BRUNCH_DEV for a harness boot and return the restore function. - * Pass a value to set it, `undefined` to clear it; restore reinstates the - * exact pre-override state (set vs unset). - */ -function overrideBrunchDevEnv(value: string | undefined): () => void { - const previous = process.env.BRUNCH_DEV; - const hadPrevious = Object.hasOwn(process.env, 'BRUNCH_DEV'); - if (value === undefined) { - delete process.env.BRUNCH_DEV; - } else { - process.env.BRUNCH_DEV = value; - } - return () => { - if (hadPrevious && previous !== undefined) { - process.env.BRUNCH_DEV = previous; - } else { - delete process.env.BRUNCH_DEV; - } - }; -} - /** * Run a test body against registered faux agent services, unregistering the * faux provider on the way out — the with-style form of @@ -343,7 +325,7 @@ export async function bootTier2ProductOriginatedTurn( const cwd = await mkdtemp(join(tmpdir(), 'brunch-kick-live-')); const agentDir = await mkdtemp(join(tmpdir(), 'brunch-agent-dir-')); - const restoreEnv = overrideBrunchDevEnv(options.dev ? '1' : undefined); + const restoreEnv = () => {}; const faux = createTier2FauxAgentServices( options.responseText === undefined ? {} : { responseText: options.responseText }, diff --git a/src/dev/workspace-rpc.ts b/src/dev/workspace-rpc.ts deleted file mode 100644 index 7bcb97872..000000000 --- a/src/dev/workspace-rpc.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * One-shot Brunch workspace RPC helper for local development. - * - * It hides the JSON-RPC stdio ceremony used by `src/app/brunch.ts --mode=rpc` and - * prints only the response result, filtering product-update notifications. - */ - -import { spawnSync } from 'node:child_process'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -interface CliArgs { - readonly workspace: string; - readonly method: string; - readonly params?: unknown; - readonly fullResponse: boolean; - readonly devRpc: boolean; -} - -interface JsonRpcResponse { - readonly jsonrpc: '2.0'; - readonly id?: number | string | null; - readonly result?: unknown; - readonly error?: unknown; -} - -function parseCliArgs(argv: readonly string[]): CliArgs { - let workspace = process.cwd(); - let fullResponse = false; - let devRpc = true; - const positional: string[] = []; - - for (let index = 0; index < argv.length; index++) { - const arg = argv[index]; - if (arg == null) throw new Error(`missing argument at index ${index}`); - if (arg === '--workspace' || arg === '-w') { - workspace = requiredValue(argv, ++index, arg); - } else if (arg === '--full-response') { - fullResponse = true; - } else if (arg === '--no-dev-rpc') { - devRpc = false; - } else if (arg === '--help' || arg === '-h') { - throw new UsageRequested(); - } else if (arg.startsWith('-')) { - throw new Error(`unknown argument: ${arg}`); - } else { - positional.push(arg); - } - } - - const [method, paramsText] = positional; - if (!method) throw new Error('method is required'); - if (positional.length > 2) throw new Error('expected at most one params JSON argument'); - - const base = { workspace, method, fullResponse, devRpc }; - return paramsText == null ? base : { ...base, params: parseParams(paramsText) }; -} - -function parseParams(text: string): unknown { - try { - return JSON.parse(text); - } catch (error) { - throw new Error(`params must be valid JSON: ${error instanceof Error ? error.message : String(error)}`); - } -} - -function requiredValue(argv: readonly string[], index: number, flag: string): string { - const value = argv[index]; - if (!value) throw new Error(`${flag} requires a value`); - return value; -} - -class UsageRequested extends Error {} - -function usage(): string { - return [ - 'Usage:', - ' tsx src/dev/workspace-rpc.ts --workspace [params-json]', - '', - 'Examples:', - ' tsx src/dev/workspace-rpc.ts -w .fixtures/workbenches/bilal-curation workspace.selectionState', - ' tsx src/dev/workspace-rpc.ts -w .fixtures/workbenches/bilal-curation graph.overview \'{"specId":4}\'', - '', - 'Options:', - ' -w, --workspace Brunch workspace directory (default: cwd)', - ' --full-response Print the full JSON-RPC response instead of result only', - ' --no-dev-rpc Do not set BRUNCH_DEV=1', - ].join('\n'); -} - -function repoRoot(): string { - return resolve(dirname(fileURLToPath(import.meta.url)), '../..'); -} - -function runRpc(args: CliArgs): JsonRpcResponse { - const root = repoRoot(); - const request = { - jsonrpc: '2.0' as const, - id: 1, - method: args.method, - ...(args.params === undefined ? {} : { params: args.params }), - }; - - const child = spawnSync( - resolve(root, 'node_modules/.bin/tsx'), - [resolve(root, 'src/app/brunch.ts'), '--mode=rpc'], - { - cwd: resolve(args.workspace), - input: `${JSON.stringify(request)}\n`, - encoding: 'utf8', - env: { - ...process.env, - ...(args.devRpc ? { BRUNCH_DEV: '1' } : {}), - }, - }, - ); - - if (child.status !== 0) { - if (child.stdout) process.stderr.write(child.stdout); - if (child.stderr) process.stderr.write(child.stderr); - throw new Error(`brunch RPC process exited with status ${child.status ?? 'unknown'}`); - } - - const response = child.stdout - .split('\n') - .filter((line) => line.trim()) - .map((line) => JSON.parse(line) as JsonRpcResponse) - .find((message) => message.id === 1); - - if (!response) { - if (child.stdout) process.stderr.write(child.stdout); - if (child.stderr) process.stderr.write(child.stderr); - throw new Error('RPC response with id 1 was not found'); - } - - return response; -} - -function printJson(value: unknown): void { - process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); -} - -function main(): void { - const args = parseCliArgs(process.argv.slice(2)); - const response = runRpc(args); - if (response.error != null) { - printJson(args.fullResponse ? response : response.error); - process.exit(1); - } - printJson(args.fullResponse ? response : response.result); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - try { - main(); - } catch (error) { - if (error instanceof UsageRequested) { - console.log(usage()); - process.exit(0); - } - console.error(error instanceof Error ? error.message : String(error)); - console.error(`\n${usage()}`); - process.exit(1); - } -} diff --git a/src/graph/__tests__/mutate-graph-edge-schema.test.ts b/src/graph/__tests__/mutate-graph-edge-schema.test.ts index 752e3f420..acf339e6f 100644 --- a/src/graph/__tests__/mutate-graph-edge-schema.test.ts +++ b/src/graph/__tests__/mutate-graph-edge-schema.test.ts @@ -6,12 +6,10 @@ import { MutateCreateEdgeSchema, MutateGraphParams, } from '../../.pi/extensions/brunch-data/graph/tool-schemas.js'; -import { devGraphRpcMethods } from '../../rpc/methods/dev-graph.js'; +import { DevMutateGraphParamsSchema } from '../../dev/graph-curation.js'; import { EDGE_CATEGORIES, EDGE_CATEGORY_METADATA, type EdgeCategory } from '../index.js'; -const devMutateGraphParamsSchema = devGraphRpcMethods.find( - (definition) => definition.method === 'dev.graph.mutateGraph', -)!.paramsSchema as TSchema; +const devMutateGraphParamsSchema = DevMutateGraphParamsSchema as TSchema; function roleNamedEdgeOp(category: EdgeCategory): Record { if (category === 'cross_reference') { diff --git a/src/graph/__tests__/seed-fixtures.test.ts b/src/graph/__tests__/seed-fixtures.test.ts index a35baef81..be8285843 100644 --- a/src/graph/__tests__/seed-fixtures.test.ts +++ b/src/graph/__tests__/seed-fixtures.test.ts @@ -19,13 +19,19 @@ import { CommandExecutor } from '../command-executor.js'; import { GROUNDING_FLOOR_KINDS } from '../schema/elicitation-gap-fixtures.js'; import { EDGE_CATEGORIES, type ReadinessBand } from '../schema/kinds.js'; import { NODE_KIND_METADATA, bandsForKind } from '../schema/nodes.js'; -import { runSeedFixturesCli, seedFixture, type SeedFixture } from '../seed-fixtures.js'; +import { + parseSeedRef, + runSeedFixturesCli, + seedFixture, + type SeedFixture, + workbenchPathForSeed, +} from '../seed-fixtures.js'; import { openWorkspaceCommandExecutor } from '../workspace-store.js'; const HERE = dirname(fileURLToPath(import.meta.url)); -function loadFixture(slug: string, set = 'bilal-port'): SeedFixture { - const path = resolve(HERE, `../../../.fixtures/seeds/${set}/${slug}.json`); +function loadFixture(name: string, variant = 'base'): SeedFixture { + const path = resolve(HERE, `../../../.fixtures/seeds/${name}/${variant}.json`); return JSON.parse(readFileSync(path, 'utf8')) as SeedFixture; } @@ -39,10 +45,10 @@ function trackedSeedRefs(): string[] { const seedsRoot = resolve(HERE, '../../../.fixtures/seeds'); return readdirSync(seedsRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) - .flatMap((set) => - readdirSync(join(seedsRoot, set.name)) + .flatMap((name) => + readdirSync(join(seedsRoot, name.name)) .filter((file) => file.endsWith('.json')) - .map((file) => `${set.name}/${file.slice(0, -'.json'.length)}`), + .map((file) => `${name.name}/${file.slice(0, -'.json'.length)}`), ) .sort(); } @@ -50,38 +56,31 @@ function trackedSeedRefs(): string[] { describe('seed fixture CLI', () => { it.each([ { name: 'missing args', argv: [] }, - { name: 'missing workspace value', argv: ['--workspace', '--seed', 'workspace-spread/alpha-grounding'] }, + { name: 'missing workspace value', argv: ['--workspace', '--seed', 'workspace-alpha-grounding/base'] }, { name: 'missing seed value', argv: ['--workspace', 'target', '--seed'] }, { name: 'unknown arg', - argv: ['--workspace', 'target', '--seed', 'workspace-spread/alpha-grounding', '--extra'], + argv: ['--workspace', 'target', '--seed', 'workspace-alpha-grounding/base', '--extra'], }, { name: 'duplicate workspace flag', - argv: ['--workspace', 'one', '--workspace', 'two', '--seed', 'workspace-spread/alpha-grounding'], + argv: ['--workspace', 'one', '--workspace', 'two', '--seed', 'workspace-alpha-grounding/base'], }, { name: 'duplicate seed flag', - argv: [ - '--workspace', - 'target', - '--seed', - 'workspace-spread/alpha-grounding', - '--seed', - 'yamlbase/spec-graph', - ], + argv: ['--workspace', 'target', '--seed', 'workspace-alpha-grounding/base', '--seed', 'yamlbase/base'], }, { - name: 'parent seed set', - argv: ['--workspace', 'target', '--seed', '../workspace-spread/alpha-grounding'], + name: 'parent seed family', + argv: ['--workspace', 'target', '--seed', '../workspace-alpha-grounding/base'], }, { - name: 'parent seed slug', - argv: ['--workspace', 'target', '--seed', 'workspace-spread/../alpha-grounding'], + name: 'parent seed variant', + argv: ['--workspace', 'target', '--seed', 'workspace-alpha-grounding/../base'], }, { name: 'absolute seed ref', - argv: ['--workspace', 'target', '--seed', '/workspace-spread/alpha-grounding'], + argv: ['--workspace', 'target', '--seed', '/workspace-alpha-grounding/base'], }, ])('rejects malformed input without creating a cwd DB: $name', async ({ argv }) => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-seed-cwd-')); @@ -96,7 +95,7 @@ describe('seed fixture CLI', () => { }); expect(code).toBe(1); - expect(stderr).toContain('Usage: npm run seed -- --workspace '); + expect(stderr).toContain('Usage: npm run seed -- (--seed /'); expect(existsSync(join(cwd, '.brunch', 'data.db'))).toBe(false); }); @@ -122,7 +121,7 @@ describe('seed fixture CLI', () => { let stderr = ''; const code = await runSeedFixturesCli({ - argv: ['--workspace', cwd, '--seed', 'workspace-spread/alpha-grounding', '--all-seeds'], + argv: ['--workspace', cwd, '--seed', 'workspace-alpha-grounding/base', '--all-seeds'], cwd, stderr: (chunk) => { stderr += chunk; @@ -130,7 +129,7 @@ describe('seed fixture CLI', () => { }); expect(code).toBe(1); - expect(stderr).toContain('Usage: npm run seed -- --workspace '); + expect(stderr).toContain('Usage: npm run seed -- (--seed /'); expect(existsSync(join(cwd, '.brunch', 'data.db'))).toBe(false); }); @@ -156,14 +155,14 @@ describe('seed fixture CLI', () => { let stdout = ''; const code = await runSeedFixturesCli({ - argv: ['--workspace', targetWorkspace, '--seed', 'workspace-spread/alpha-grounding', '--reset'], + argv: ['--workspace', targetWorkspace, '--seed', 'workspace-alpha-grounding/base', '--reset'], stdout: (chunk) => { stdout += chunk; }, }); expect(code).toBe(0); - expect(stdout).toContain('seeded workspace-spread/alpha-grounding → spec'); + expect(stdout).toContain('seeded workspace-alpha-grounding/base → spec'); expect(existsSync(join(targetWorkspace, '.brunch', 'data.db'))).toBe(true); }); @@ -171,7 +170,7 @@ describe('seed fixture CLI', () => { const targetWorkspace = await mkdtemp(join(tmpdir(), 'brunch-seed-target-')); const first = await runSeedFixturesCli({ - argv: ['--workspace', targetWorkspace, '--seed', 'workspace-spread/alpha-grounding'], + argv: ['--workspace', targetWorkspace, '--seed', 'workspace-alpha-grounding/base'], stdout: () => {}, }); expect(first).toBe(0); @@ -191,26 +190,35 @@ describe('seed fixture CLI', () => { await writeFile(join(brunchDir, 'notes.md'), 'keep me', 'utf8'); const second = await runSeedFixturesCli({ - argv: ['--workspace', targetWorkspace, '--seed', 'workspace-spread/beta-commitments', '--reset'], + argv: ['--workspace', targetWorkspace, '--seed', 'workspace-beta-commitments/base', '--reset'], stdout: () => {}, }); expect(second).toBe(0); const executor = await openWorkspaceCommandExecutor(targetWorkspace); - expect(executor.listSpecs().map((spec) => spec.slug)).toEqual(['beta-commitments']); + expect(executor.listSpecs().map((spec) => spec.slug)).toEqual(['workspace-beta-commitments']); expect(existsSync(sessionsDir)).toBe(false); expect(existsSync(join(brunchDir, 'workspace.json'))).toBe(false); expect(existsSync(debugDir)).toBe(false); expect(readFileSync(join(brunchDir, 'notes.md'), 'utf8')).toBe('keep me'); }); + it('maps a seed ref to its derived workbench path under .fixtures/workbenches', () => { + const seed = parseSeedRef('workspace-alpha-grounding/base'); + + expect(seed).not.toBeNull(); + expect(workbenchPathForSeed(seed!)).toBe( + resolve(HERE, '../../../.fixtures/workbenches/workspace-alpha-grounding'), + ); + }); + it('accepts equals-form flags when values are unambiguous and safe', async () => { const shellCwd = await mkdtemp(join(tmpdir(), 'brunch-seed-shell-')); const targetWorkspace = await mkdtemp(join(tmpdir(), 'brunch-seed-target-')); let stdout = ''; const code = await runSeedFixturesCli({ - argv: [`--workspace=${targetWorkspace}`, '--seed=workspace-spread/alpha-grounding'], + argv: [`--workspace=${targetWorkspace}`, '--seed=workspace-alpha-grounding/base'], cwd: shellCwd, stdout: (chunk) => { stdout += chunk; @@ -218,7 +226,7 @@ describe('seed fixture CLI', () => { }); expect(code).toBe(0); - expect(stdout).toContain('seeded workspace-spread/alpha-grounding → spec'); + expect(stdout).toContain('seeded workspace-alpha-grounding/base → spec'); expect(existsSync(join(shellCwd, '.brunch', 'data.db'))).toBe(false); expect(existsSync(join(targetWorkspace, '.brunch', 'data.db'))).toBe(true); }); @@ -256,14 +264,14 @@ describe('seed fixture CLI', () => { let stdout = ''; const code = await runSeedFixturesCli({ - argv: ['--workspace', targetWorkspace, '--seed', 'yamlbase/spec-graph'], + argv: ['--workspace', targetWorkspace, '--seed', 'yamlbase/base'], stdout: (chunk) => { stdout += chunk; }, }); expect(code).toBe(0); - expect(stdout).toContain('seeded yamlbase/spec-graph → spec'); + expect(stdout).toContain('seeded yamlbase/base → spec'); expect(stdout).not.toContain('seeded yamlbase/yamlbase → spec'); }); @@ -273,7 +281,7 @@ describe('seed fixture CLI', () => { let stdout = ''; const code = await runSeedFixturesCli({ - argv: ['--workspace', targetWorkspace, '--seed', 'workspace-spread/alpha-grounding'], + argv: ['--workspace', targetWorkspace, '--seed', 'workspace-alpha-grounding/base'], cwd: shellCwd, stdout: (chunk) => { stdout += chunk; @@ -281,18 +289,18 @@ describe('seed fixture CLI', () => { }); expect(code).toBe(0); - expect(stdout).toContain('seeded workspace-spread/alpha-grounding → spec'); + expect(stdout).toContain('seeded workspace-alpha-grounding/base → spec'); expect(stdout).toContain(`Destination: ${join(targetWorkspace, '.brunch', 'data.db')}`); expect(existsSync(join(shellCwd, '.brunch', 'data.db'))).toBe(false); expect(existsSync(join(targetWorkspace, '.brunch', 'data.db'))).toBe(true); const executor = await openWorkspaceCommandExecutor(targetWorkspace); const specRows = executor.listSpecs(); - expect(specRows.map((spec) => spec.slug)).toEqual(['alpha-grounding']); + expect(specRows.map((spec) => spec.slug)).toEqual(['workspace-alpha-grounding']); const alpha = specRows[0]!; const db = createDb(join(targetWorkspace, '.brunch', 'data.db')); expect(db.select().from(nodes).where(eq(nodes.spec_id, alpha.id)).all()).toHaveLength( - loadFixture('alpha-grounding', 'workspace-spread').nodes.length, + loadFixture('workspace-alpha-grounding').nodes.length, ); expect( db @@ -306,20 +314,20 @@ describe('seed fixture CLI', () => { }); describe('all tracked seeds remain structurally legal', () => { - // One-level /.json discovery: prep scripts (_*.ts), READMEs, and + // One-level /.json discovery: prep scripts (_*.ts), READMEs, and // raw-material subdirectories (e.g. bilal-port/_originals/) are excluded by // construction. No hand-maintained list to drift. const seedRefs = trackedSeedRefs(); it('discovers the tracked seed catalog', () => { expect(seedRefs.length).toBeGreaterThan(0); - expect(seedRefs).toContain('workspace-spread/alpha-grounding'); + expect(seedRefs).toContain('workspace-alpha-grounding/base'); expect(seedRefs.some((ref) => ref.includes('_originals'))).toBe(false); }); it.each(seedRefs.map((ref) => ({ ref })))('seeds $ref through the command layer', ({ ref }) => { - const [set, slug] = ref.split('/') as [string, string]; - const fixture = loadFixture(slug, set); + const [name, variant] = ref.split('/') as [string, string]; + const fixture = loadFixture(name, variant); const db = createDb(':memory:'); const result = seedFixture(new CommandExecutor(db), fixture); @@ -340,7 +348,7 @@ describe('all tracked seeds remain structurally legal', () => { } }); - it('documents every tracked seed set in the disposition catalog', () => { + it('documents every tracked seed family in the disposition catalog', () => { const catalogPath = resolve(HERE, '../../../.fixtures/seeds/README.md'); const catalog = readFileSync(catalogPath, 'utf8'); const allowedDispositions = new Set(['test', 'preview', 'manual workbench', 'probe input', 'parked']); @@ -375,7 +383,7 @@ describe('seedFixture', () => { it('seeds the code-health fixture into a real DB via the command layer', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); - const fixture = loadFixture('code-health'); + const fixture = loadFixture('bilal-code-health'); const result = seedFixture(executor, fixture); @@ -387,7 +395,7 @@ describe('seedFixture', () => { const specRows = db.select().from(specs).all(); expect(specRows).toHaveLength(1); expect(specRows[0]!.id).toBe(result.specId); - expect(specRows[0]!.slug).toBe('code-health'); + expect(specRows[0]!.slug).toBe('bilal-code-health'); // Node / edge rows persisted, all scoped to the seeded spec. const nodeRows = db.select().from(nodes).where(eq(nodes.spec_id, result.specId)).all(); @@ -410,7 +418,7 @@ describe('seedFixture', () => { it('loads the macro-view grounded-intent variant as explicit intent-only seed truth', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); - const fixture = loadFixture('macro-view-grounded-intent', 'bilal-port-variants'); + const fixture = loadFixture('bilal-macro-view', 'grounded-intent'); expect(fixture.nodes.length).toBeGreaterThan(0); expect(fixture.nodes.every((node) => node.plane === 'intent')).toBe(true); @@ -426,10 +434,10 @@ describe('seedFixture', () => { expect(nodeRows.every((row) => row.plane === 'intent' && row.basis === 'explicit')).toBe(true); }); - it('loads the kind-band spread fixture with every node kind and all readiness bands represented', () => { + it('loads the kind-coverage matrix fixture with every node kind and all readiness bands represented', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); - const fixture = loadFixture('coverage-matrix', 'kind-band-spread'); + const fixture = loadFixture('kind-coverage-matrix'); const result = seedFixture(executor, fixture); const nodeRows = db.select().from(nodes).where(eq(nodes.spec_id, result.specId)).all(); @@ -440,10 +448,10 @@ describe('seedFixture', () => { ); }); - it('loads the edge-spread fixture with every edge category and a thesis absence case', () => { + it('loads the edge-category-directions fixture with every edge category and a thesis absence case', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); - const fixture = loadFixture('category-directions', 'edge-spread'); + const fixture = loadFixture('edge-category-directions'); const result = seedFixture(executor, fixture); const specEdges = db.select().from(edges).where(eq(edges.spec_id, result.specId)).all(); @@ -455,15 +463,15 @@ describe('seedFixture', () => { expect(specEdges.some((row) => row.category === 'witness' && row.target_id === thesisId)).toBe(false); }); - it('loads the workspace-spread fixtures into one DB with distinct slugs', () => { + it('loads the workspace family fixtures into one DB with distinct slugs', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); - const alpha = seedFixture(executor, loadFixture('alpha-grounding', 'workspace-spread')); - const beta = seedFixture(executor, loadFixture('beta-commitments', 'workspace-spread')); + const alpha = seedFixture(executor, loadFixture('workspace-alpha-grounding')); + const beta = seedFixture(executor, loadFixture('workspace-beta-commitments')); const specRows = db.select({ slug: specs.slug }).from(specs).all(); - expect(specRows).toEqual([{ slug: 'alpha-grounding' }, { slug: 'beta-commitments' }]); + expect(specRows).toEqual([{ slug: 'workspace-alpha-grounding' }, { slug: 'workspace-beta-commitments' }]); expect(graphClockLsn(db, alpha.specId)).toBe(2); expect(graphClockLsn(db, beta.specId)).toBe(2); }); @@ -471,8 +479,8 @@ describe('seedFixture', () => { it('keeps seeded spec LSNs coherent independent of seed order', () => { const db: BrunchDb = createDb(':memory:'); const executor = new CommandExecutor(db); - const first = seedFixture(executor, loadFixture('code-health')); - const second = seedFixture(executor, loadFixture('macro-view-grounded-intent', 'bilal-port-variants')); + const first = seedFixture(executor, loadFixture('bilal-code-health')); + const second = seedFixture(executor, loadFixture('bilal-macro-view', 'grounded-intent')); expect(graphClockLsn(db, first.specId)).toBe(2); expect(graphClockLsn(db, second.specId)).toBe(2); diff --git a/src/graph/__tests__/spec-ownership.test.ts b/src/graph/__tests__/spec-ownership.test.ts index 0202ef430..2297256f8 100644 --- a/src/graph/__tests__/spec-ownership.test.ts +++ b/src/graph/__tests__/spec-ownership.test.ts @@ -4,7 +4,7 @@ * SPEC: D61-L (each spec owns its own intent graph; no workspace-global graph), * D52-L (graph/ owns the readers), D4-L/D20-L (CommandExecutor authority). * - * This is the card 1 tracer for live-graph-observer--graph-rpc-spine: every + * This is the card 1 tracer for the graph-rpc spine: every * graph projection and graph mutation targets exactly one spec. */ diff --git a/src/graph/__tests__/support/fixture-reads.ts b/src/graph/__tests__/support/fixture-reads.ts index 74cb65f0a..978def591 100644 --- a/src/graph/__tests__/support/fixture-reads.ts +++ b/src/graph/__tests__/support/fixture-reads.ts @@ -11,8 +11,8 @@ const HERE = dirname(fileURLToPath(import.meta.url)); const SEEDS_ROOT = resolve(HERE, '../../../../.fixtures/seeds'); export interface SeedFixtureRef { - readonly set: string; - readonly fixture: string; + readonly name: string; + readonly variant: string; } export function readGraphSliceFixture(ref: SeedFixtureRef): GraphSlice { @@ -29,14 +29,14 @@ export function readNodeNeighborhoodFixture( })[0]; if (!result || result.status === 'not_found') { - throw new Error(`Node code "${ref.anchorCode}" not found in ${ref.set}/${ref.fixture}`); + throw new Error(`Node code "${ref.anchorCode}" not found in ${ref.name}/${ref.variant}`); } return result; } function seedSelectedSpec(ref: SeedFixtureRef) { - const fixturePath = resolve(SEEDS_ROOT, ref.set, `${ref.fixture}.json`); + const fixturePath = resolve(SEEDS_ROOT, ref.name, `${ref.variant}.json`); const fixture = JSON.parse(readFileSync(fixturePath, 'utf8')) as SeedFixture; const db = createDb(':memory:'); const executor = new CommandExecutor(db); diff --git a/src/graph/export-fixtures.ts b/src/graph/export-fixtures.ts index 07b30c904..4257f0529 100644 --- a/src/graph/export-fixtures.ts +++ b/src/graph/export-fixtures.ts @@ -25,6 +25,14 @@ export interface ExportSeedFixtureInput { readonly show?: GraphVisibility; } +export function exportSeedFixtureFromWorkspace( + workspace: string, + input: ExportSeedFixtureInput, +): SeedFixture { + const db = createDb(join(resolve(workspace), '.brunch', 'data.db')); + return exportSeedFixture(db, input); +} + export function exportSeedFixture(db: BrunchDb, input: ExportSeedFixtureInput): SeedFixture { const spec = db.select().from(schema.specs).where(eq(schema.specs.id, input.specId)).get(); if (!spec) throw new Error(`exportSeedFixture: spec ${input.specId} does not exist`); @@ -151,8 +159,7 @@ function usage(): string { async function main(): Promise { const args = parseCliArgs(process.argv.slice(2)); - const db = createDb(join(resolve(args.workspace), '.brunch', 'data.db')); - const fixture = exportSeedFixture(db, { + const fixture = exportSeedFixtureFromWorkspace(args.workspace, { specId: args.specId, ...(args.show === undefined ? {} : { show: args.show }), }); diff --git a/src/graph/schema/__tests__/generate-ontology-ref.test.ts b/src/graph/schema/__tests__/generate-ontology-ref.test.ts index 4200619fd..a71854e39 100644 --- a/src/graph/schema/__tests__/generate-ontology-ref.test.ts +++ b/src/graph/schema/__tests__/generate-ontology-ref.test.ts @@ -7,6 +7,7 @@ import { GENERATED_ONTOLOGY_PATH, renderOntologyReference } from '../generate-on const projectRoot = new URL('../../../../', import.meta.url).pathname; const expectedReferencePath = `${projectRoot}src/agents/contexts/references/graph-ontology.md`; +const nodesSourcePath = `${projectRoot}src/graph/schema/nodes.ts`; import { EDGE_CATEGORIES, NODE_KINDS } from '../kinds.js'; import { bandsForKind, @@ -80,6 +81,18 @@ describe('ontology reference generator', () => { } }); + it('keeps the Gherkin then field owned by one plain schema constant', () => { + const source = readFileSync(nodesSourcePath, 'utf8'); + const gherkinSchema = CLAIM_FORM_JSON_SCHEMAS.gherkin; + + expect(gherkinSchema.required).toEqual([ + 'form', + ...Object.keys(gherkinSchema.properties).filter((key) => key === 'then'), + ]); + expect(source).toContain("const GHERKIN_THEN_FIELD = 'then';"); + expect(source).toContain("required: ['form', GHERKIN_THEN_FIELD]"); + }); + it('keeps the committed generated file in sync with the typed source (drift guard)', () => { const committed = readFileSync(GENERATED_ONTOLOGY_PATH, 'utf8'); expect(committed).toBe(markdown); diff --git a/src/graph/schema/nodes.ts b/src/graph/schema/nodes.ts index c9264e15b..e5b8cbb70 100644 --- a/src/graph/schema/nodes.ts +++ b/src/graph/schema/nodes.ts @@ -285,7 +285,7 @@ export interface GivenFormDetail { readonly statement: string; } -const GHERKIN_THEN_FIELD = `${'the'}n`; +const GHERKIN_THEN_FIELD = 'then'; export const CLAIM_FORM_JSON_SCHEMAS = { plain: { @@ -300,7 +300,7 @@ export const CLAIM_FORM_JSON_SCHEMAS = { gherkin: { type: 'object', additionalProperties: false, - required: ['form', 'then'], + required: ['form', GHERKIN_THEN_FIELD], properties: { form: { const: 'gherkin' }, given: { type: 'array', items: { type: 'string' }, description: 'Given preconditions.' }, diff --git a/src/graph/seed-fixtures.ts b/src/graph/seed-fixtures.ts index 9b4cc2439..452e513b5 100644 --- a/src/graph/seed-fixtures.ts +++ b/src/graph/seed-fixtures.ts @@ -2,7 +2,7 @@ * Seed loader for consolidated fixture specs. * * Reads the brunch-shaped seed contract produced under - * `.fixtures/seeds//.json` and commits each spec into a brunch + * `.fixtures/seeds//.json` and commits each spec into a brunch * SQLite database through the normal `CommandExecutor` mutation boundary, so * the graph clock, change log, and `*_lsn` columns stay coherent — seeded * data is indistinguishable from data an agent would have committed live. @@ -15,11 +15,12 @@ * The fixture-prep step that *produces* these files (porting Bilal's * spec-elicitation graphs) is a separate throwaway script vendored next to * the data at `.fixtures/seeds/bilal-port/_port-script.ts`; this loader only - * consumes the consolidated `.json` output and is unaware of any + * consumes the consolidated `.json` output and is unaware of any * upstream format. * * CLI (dev only, run via tsx): - * npm run seed -- --workspace --seed / [--reset] + * npm run seed -- --seed / [--reset] + * npm run seed -- --workspace --seed / [--reset] * npm run seed -- --workspace --all-seeds [--reset] * * `--reset` deletes the target workspace's **runtime state** before seeding @@ -32,7 +33,7 @@ * inside it. */ -import { readFile, readdir, rm } from 'node:fs/promises'; +import { mkdir, readFile, readdir, rm } from 'node:fs/promises'; import { dirname, isAbsolute, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -44,7 +45,7 @@ import type { NodeBasis, NodePlane } from './schema/nodes.js'; import { openWorkspaceCommandExecutor } from './workspace-store.js'; // --------------------------------------------------------------------------- -// Seed contract — shape of a consolidated `.json` fixture +// Seed contract — shape of a consolidated `.json` fixture // --------------------------------------------------------------------------- /** Spec header of a consolidated fixture. */ @@ -248,6 +249,7 @@ function roleNamedSeedEdgeDraft( // --------------------------------------------------------------------------- const SEEDS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../.fixtures/seeds'); +const WORKBENCHES_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../.fixtures/workbenches'); interface SeedCliOptions { readonly argv?: readonly string[]; @@ -269,10 +271,20 @@ interface ParsedSeedCliArgs { }; } -interface SeedRef { +export interface SeedRef { readonly ref: string; - readonly set: string; - readonly slug: string; + readonly name: string; + readonly variant: string; +} + +export function parseSeedRef(ref: string): SeedRef | null { + const [name, variant, extra] = ref.split('/'); + if (!safeSeedPart(name) || !safeSeedPart(variant) || extra) return null; + return { ref, name, variant }; +} + +export function workbenchPathForSeed(seed: SeedRef): string { + return join(WORKBENCHES_ROOT, seed.name); } /** @@ -293,24 +305,26 @@ function workspaceRuntimeState(workspace: string): { }; } -/** Read one `.json` fixture under a seed-set dir. */ -async function readSelectedSeed(set: string, slug: string): Promise { - const raw = await readFile(join(SEEDS_ROOT, set, `${slug}.json`), 'utf8'); +/** Read one `.json` fixture under a seed family dir. */ +async function readSelectedSeed(name: string, variant: string): Promise { + const raw = await readFile(join(SEEDS_ROOT, name, `${variant}.json`), 'utf8'); return JSON.parse(raw) as SeedFixture; } -async function trackedSeedRefs(): Promise { - const sets = await readdir(SEEDS_ROOT, { withFileTypes: true }); +export async function listTrackedSeedRefs(): Promise { + const names = await readdir(SEEDS_ROOT, { withFileTypes: true }); const refs = await Promise.all( - sets + names .filter((entry) => entry.isDirectory()) - .map(async (set) => { - const files = await readdir(join(SEEDS_ROOT, set.name)); + .map(async (name) => { + const files = await readdir(join(SEEDS_ROOT, name.name)); return files .filter((file) => file.endsWith('.json')) .map((file) => { - const slug = file.slice(0, -'.json'.length); - return { ref: `${set.name}/${slug}`, set: set.name, slug } satisfies SeedRef; + const variant = file.slice(0, -'.json'.length); + const seed = parseSeedRef(`${name.name}/${variant}`); + if (!seed) throw new Error(`Tracked seed has illegal ref shape: ${name.name}/${variant}`); + return seed; }); }), ); @@ -348,11 +362,12 @@ export async function runSeedFixturesCli(options: SeedCliOptions = {}): Promise< await rm(directory, { recursive: true, force: true }); } } + await mkdir(parsed.workspace, { recursive: true }); const executor = await openWorkspaceCommandExecutor(parsed.workspace); - const seeds = parsed.selection.kind === 'single' ? [parsed.selection.seed] : await trackedSeedRefs(); + const seeds = parsed.selection.kind === 'single' ? [parsed.selection.seed] : await listTrackedSeedRefs(); for (const seed of seeds) { - let fixture = await readSelectedSeed(seed.set, seed.slug); + let fixture = await readSelectedSeed(seed.name, seed.variant); if (parsed.selection.kind === 'all') fixture = fixtureForAllSeeds(seed, fixture); const result = seedFixture(executor, fixture); stdout( @@ -409,26 +424,33 @@ function parseSeedCliArgs(argv: readonly string[], cwd: string): ParsedSeedCliAr return null; } - const workspace = values.get('--workspace'); - const seed = values.get('--seed'); + const workspaceValue = values.get('--workspace'); + const seedValue = values.get('--seed'); const allSeeds = values.has('--all-seeds'); - if (!workspace || (!seed && !allSeeds) || (seed && allSeeds)) return null; + if ((!seedValue && !allSeeds) || (seedValue && allSeeds)) return null; + + const workspace = workspaceValue + ? isAbsolute(workspaceValue) + ? workspaceValue + : resolve(cwd, workspaceValue) + : undefined; if (allSeeds) { + if (!workspace) return null; return { - workspace: isAbsolute(workspace) ? workspace : resolve(cwd, workspace), + workspace, reset, selection: { kind: 'all' }, }; } - const [set, slug, extra] = seed!.split('/'); - if (!safeSeedPart(set) || !safeSeedPart(slug) || extra) return null; + const seed = parseSeedRef(seedValue!); + if (!seed) return null; return { - workspace: isAbsolute(workspace) ? workspace : resolve(cwd, workspace), + workspace: workspace ?? workbenchPathForSeed(seed), reset, - selection: { kind: 'single', seed: { ref: seed!, set, slug } }, + selection: { kind: 'single', seed }, }; } @@ -442,8 +464,9 @@ function safeSeedPart(value: string | undefined): value is string { function seedUsage(): string { return ( - 'Usage: npm run seed -- --workspace (--seed / | --all-seeds) [--reset]\n' + - ' --all-seeds opt in to seed every tracked fixture as its own spec\n' + + 'Usage: npm run seed -- (--seed / [--workspace ] | --workspace --all-seeds) [--reset]\n' + + ' --seed when --workspace is omitted, derive .fixtures/workbenches//\n' + + ' --all-seeds opt in to seed every tracked fixture as its own spec (requires --workspace)\n' + ' --reset delete the target workspace runtime state before seeding:\n' + ' .brunch/data.db (+ -wal/-shm), sessions/, debug/, and workspace.json\n' ); diff --git a/src/graph/validate-fixture.ts b/src/graph/validate-fixture.ts index 53533bb12..eee937cc7 100644 --- a/src/graph/validate-fixture.ts +++ b/src/graph/validate-fixture.ts @@ -1,10 +1,10 @@ /** * Dev CLI: validate one seed fixture against the real propose-graph validator. * - * Seeds `.fixtures/seeds//.json` into an in-memory database through - * the same `CommandExecutor` mutation boundary the live product uses, so a - * fixture that loads here is structurally legal (valid plane/kind, per-kind - * detail rules, edge category/stance rules, no self-loops, acyclic + * Seeds `.fixtures/seeds//.json` into an in-memory database + * through the same `CommandExecutor` mutation boundary the live product uses, + * so a fixture that loads here is structurally legal (valid plane/kind, + * per-kind detail rules, edge category/stance rules, no self-loops, acyclic * supersession). On rejection it prints the command-layer diagnostics; on * success it prints stored totals plus the active-context projection totals * (which hide superseded predecessors and their dangling edges). @@ -13,8 +13,8 @@ * fixtures — it touches no shared test file, so multiple fixtures can be * authored and validated independently. * - * npx tsx src/graph/validate-fixture.ts / - * npx tsx src/graph/validate-fixture.ts brunch-self/spec-graph + * npx tsx src/graph/validate-fixture.ts / + * npx tsx src/graph/validate-fixture.ts brunch-self/base */ import { readFileSync } from 'node:fs'; @@ -45,7 +45,7 @@ function validateFixture(ref: string): void { if (import.meta.url === `file://${process.argv[1]}`) { const ref = process.argv[2]; if (!ref) { - console.error('usage: tsx src/graph/validate-fixture.ts /'); + console.error('usage: tsx src/graph/validate-fixture.ts /'); process.exit(2); } try { diff --git a/src/probes/__tests__/fixture-curation-loop.test.ts b/src/probes/__tests__/fixture-curation-loop.test.ts index 56d2f9bdf..18bfb87ea 100644 --- a/src/probes/__tests__/fixture-curation-loop.test.ts +++ b/src/probes/__tests__/fixture-curation-loop.test.ts @@ -77,7 +77,7 @@ describe('fixture curation loop report', () => { runId: 'fixture-curation-test', generatedAt: '2026-06-05T00:00:00.000Z', cwd: '/tmp/brunch-fixture-curation-test', - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', selectedBaseProfile: 'grounded-intent', specId: 7, sessionId: 'session-1', @@ -125,7 +125,7 @@ describe('fixture curation loop report', () => { runId: 'fixture-curation-test', generatedAt: '2026-06-05T00:00:00.000Z', cwd: '/tmp/brunch-fixture-curation-test', - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', selectedBaseProfile: 'grounded-intent', specId: 7, sessionId: 'session-1', @@ -155,7 +155,7 @@ describe('fixture curation loop report', () => { runId: 'fixture-curation-test', generatedAt: '2026-06-05T00:00:00.000Z', cwd: fixtureRoot, - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', selectedBaseProfile: 'grounded-intent', specId: 7, sessionId: 'session-1', @@ -182,7 +182,7 @@ describe('fixture curation loop report', () => { '"toolName":"mutate_graph"', ); await expect(readFile(join(fixtureRoot, artifacts.reportJson), 'utf8')).resolves.toContain( - '"seedSlug": "macro-view-grounded-intent"', + '"seedVariant": "grounded-intent"', ); await expect(readFile(join(fixtureRoot, artifacts.graphOverviewJson), 'utf8')).resolves.toContain( '"basis": "implicit"', @@ -205,7 +205,7 @@ describe('fixture curation loop report', () => { runId: 'portable-run', generatedAt: '2026-06-05T00:00:00.000Z', cwd: fixtureRoot, - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', selectedBaseProfile: 'grounded-intent', specId: 7, sessionId: 'session-1', diff --git a/src/probes/__tests__/project-graph-review-cycle-proof.test.ts b/src/probes/__tests__/project-graph-review-cycle-proof.test.ts index d70a67da5..01dcb829d 100644 --- a/src/probes/__tests__/project-graph-review-cycle-proof.test.ts +++ b/src/probes/__tests__/project-graph-review-cycle-proof.test.ts @@ -156,7 +156,7 @@ describe('project-graph review-cycle proof report', () => { runId: 'project-graph-review-test', generatedAt: '2026-06-06T00:00:00.000Z', cwd: '/tmp/brunch-project-graph-review-test', - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', specId: 7, sessionId: 'session-1', prompt: 'Present a review set.', @@ -202,7 +202,7 @@ describe('project-graph review-cycle proof report', () => { runId: 'project-graph-review-test', generatedAt: '2026-06-06T00:00:00.000Z', cwd: '/tmp/brunch-project-graph-review-test', - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', specId: 7, sessionId: 'session-1', prompt: 'Present a review set.', @@ -233,7 +233,7 @@ describe('project-graph review-cycle proof report', () => { runId: 'artifact-run', generatedAt: '2026-06-06T00:00:00.000Z', cwd: fixtureRoot, - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', specId: 7, sessionId: 'session-1', prompt: 'Present a review set.', @@ -281,7 +281,7 @@ describe('project-graph review-cycle proof report', () => { runId: 'portable-run', generatedAt: '2026-06-06T00:00:00.000Z', cwd: fixtureRoot, - seedSlug: 'macro-view-grounded-intent', + seedVariant: 'grounded-intent', specId: 7, sessionId: 'session-1', prompt: 'Present a review set.', diff --git a/src/probes/__tests__/ship-gate-composition-proof.test.ts b/src/probes/__tests__/ship-gate-composition-proof.test.ts index 372cf7ede..e85f156e1 100644 --- a/src/probes/__tests__/ship-gate-composition-proof.test.ts +++ b/src/probes/__tests__/ship-gate-composition-proof.test.ts @@ -35,7 +35,7 @@ describe('ship gate composition proof contract', () => { cwd: '', setup: { publicSeedCli: 'node dist/graph/seed-fixtures.js', - seeds: ['workspace-spread/alpha-grounding', 'workspace-spread/beta-commitments'], + seeds: ['workspace-alpha-grounding/base', 'workspace-beta-commitments/base'], }, betaTitlesAbsentFromAlpha: true, runtimeStateObservable: true, diff --git a/src/probes/fixture-curation-loop.ts b/src/probes/fixture-curation-loop.ts index b2218e196..f15b3caaf 100644 --- a/src/probes/fixture-curation-loop.ts +++ b/src/probes/fixture-curation-loop.ts @@ -25,8 +25,9 @@ import { createWorkspaceSessionCoordinator } from '../session/workspace-session- import { assertPortableRunId, portableCwd } from './portable-report.js'; const PROBE_ID = 'fixture-curation' as const; -const DEFAULT_SEED_SET = 'bilal-port-variants'; -const DEFAULT_SEED_SLUG = 'macro-view-grounded-intent'; +const DEFAULT_SEED_NAME = 'bilal-macro-view'; +const DEFAULT_SEED_VARIANT = 'grounded-intent'; +const DEFAULT_SEED_REF = `${DEFAULT_SEED_NAME}/${DEFAULT_SEED_VARIANT}`; type FixtureCurationCommitStatus = | MutateGraphSuccess['status'] @@ -45,8 +46,8 @@ interface FixtureCurationRuntimeStateReport { interface FixtureCurationRunOptions { readonly cwd?: string; readonly fixtureRoot?: string; - readonly seedSet?: string; - readonly seedSlug?: string; + readonly seedName?: string; + readonly seedVariant?: string; readonly selectedBaseProfile?: string; readonly runId?: string; readonly prompt?: string; @@ -84,8 +85,8 @@ export interface FixtureCurationReport { readonly probeId: typeof PROBE_ID; readonly runId: string; readonly generatedAt: string; - readonly seedSet: string; - readonly seedSlug: string; + readonly seedName: string; + readonly seedVariant: string; readonly selectedBaseProfile: string; readonly cwd: string; readonly specId: number; @@ -114,8 +115,8 @@ export interface FixtureCurationSummaryInput { readonly runId: string; readonly generatedAt: string; readonly cwd: string; - readonly seedSet?: string; - readonly seedSlug: string; + readonly seedName?: string; + readonly seedVariant: string; readonly selectedBaseProfile: string; readonly specId: number; readonly sessionId: string; @@ -134,13 +135,13 @@ export async function runFixtureCurationLoop( const fixtureRoot = resolve( options.fixtureRoot ?? join(dirname(fileURLToPath(import.meta.url)), '../../.fixtures'), ); - const seedSet = options.seedSet ?? DEFAULT_SEED_SET; - const seedSlug = options.seedSlug ?? DEFAULT_SEED_SLUG; + const seedName = options.seedName ?? DEFAULT_SEED_NAME; + const seedVariant = options.seedVariant ?? DEFAULT_SEED_VARIANT; const selectedBaseProfile = options.selectedBaseProfile ?? 'grounded-intent'; const runId = assertPortableRunId(options.runId ?? defaultRunId()); const prompt = options.prompt ?? defaultCurationPrompt(); const generatedAt = new Date().toISOString(); - const fixture = await readSeedFixture(join(fixtureRoot, 'seeds', seedSet, `${seedSlug}.json`)); + const fixture = await readSeedFixture(join(fixtureRoot, 'seeds', seedName, `${seedVariant}.json`)); const graph = await openWorkspaceGraphRuntime(cwd); const seedResult = seedFixture(graph.commandExecutor, fixture); const coordinator = createWorkspaceSessionCoordinator({ cwd }); @@ -181,8 +182,8 @@ export async function runFixtureCurationLoop( runId, generatedAt, cwd, - seedSet, - seedSlug, + seedName, + seedVariant, selectedBaseProfile, specId: seedResult.specId, sessionId: activated.session.id, @@ -249,8 +250,8 @@ export function summarizeFixtureCurationRun(input: FixtureCurationSummaryInput): probeId: PROBE_ID, runId: input.runId, generatedAt: input.generatedAt, - seedSet: input.seedSet ?? DEFAULT_SEED_SET, - seedSlug: input.seedSlug, + seedName: input.seedName ?? DEFAULT_SEED_NAME, + seedVariant: input.seedVariant, selectedBaseProfile: input.selectedBaseProfile, cwd: input.cwd, specId: input.specId, @@ -360,7 +361,7 @@ function mutateGraphStatus(value: unknown): FixtureCurationCommitStatus { } function defaultCurationPrompt(): string { - return `Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named "${DEFAULT_SEED_SLUG}". + return `Brunch fixture-curation tracer: the selected spec is a Bilal-derived explicit base seed named "${DEFAULT_SEED_REF}". Use read_graph once in overview mode. Then use mutate_graph exactly once to add a small intent-plane expansion that improves launch/usefulness of this existing spec without duplicating base nodes. Create one to three new intent-plane nodes, connect them legally to existing graph truth when possible, use basis implicit through the propose-graph tool path, and stop after a successful mutate_graph result.`; } @@ -424,8 +425,8 @@ function parseCliArgs(argv: readonly string[]): FixtureCurationRunOptions { return { ...(options.cwd !== undefined ? { cwd: options.cwd } : {}), ...(options['fixture-root'] !== undefined ? { fixtureRoot: options['fixture-root'] } : {}), - ...(options['seed-set'] !== undefined ? { seedSet: options['seed-set'] } : {}), - ...(options['seed-slug'] !== undefined ? { seedSlug: options['seed-slug'] } : {}), + ...(options['seed-name'] !== undefined ? { seedName: options['seed-name'] } : {}), + ...(options['seed-variant'] !== undefined ? { seedVariant: options['seed-variant'] } : {}), ...(options['selected-base-profile'] !== undefined ? { selectedBaseProfile: options['selected-base-profile'] } : {}), diff --git a/src/probes/project-graph-review-cycle-proof.ts b/src/probes/project-graph-review-cycle-proof.ts index def2a3110..105f457c9 100644 --- a/src/probes/project-graph-review-cycle-proof.ts +++ b/src/probes/project-graph-review-cycle-proof.ts @@ -21,8 +21,9 @@ import { createWorkspaceSessionCoordinator } from '../session/workspace-session- import { assertPortableRunId, portableCwd } from './portable-report.js'; const PROBE_ID = 'project-graph-review-cycle' as const; -const DEFAULT_SEED_SET = 'bilal-port-variants'; -const DEFAULT_SEED_SLUG = 'macro-view-grounded-intent'; +const DEFAULT_SEED_NAME = 'bilal-macro-view'; +const DEFAULT_SEED_VARIANT = 'grounded-intent'; +const DEFAULT_SEED_REF = `${DEFAULT_SEED_NAME}/${DEFAULT_SEED_VARIANT}`; interface ProjectGraphReviewRuntimeStateReport { readonly operationalMode: 'elicit'; @@ -33,8 +34,8 @@ interface ProjectGraphReviewRuntimeStateReport { interface ProjectGraphReviewCycleProofOptions { readonly cwd?: string; readonly fixtureRoot?: string; - readonly seedSet?: string; - readonly seedSlug?: string; + readonly seedName?: string; + readonly seedVariant?: string; readonly runId?: string; readonly prompt?: string; readonly agentDir?: string; @@ -85,8 +86,8 @@ export interface ProjectGraphReviewCycleReport { readonly generatedAt: string; readonly mission: string; readonly evaluationFocus: string; - readonly seedSet: string; - readonly seedSlug: string; + readonly seedName: string; + readonly seedVariant: string; readonly cwd: string; readonly specId: number; readonly sessionId: string; @@ -131,8 +132,8 @@ export interface ProjectGraphReviewCycleSummaryInput { readonly runId: string; readonly generatedAt: string; readonly cwd: string; - readonly seedSet?: string; - readonly seedSlug: string; + readonly seedName?: string; + readonly seedVariant: string; readonly specId: number; readonly sessionId: string; readonly prompt: string; @@ -173,12 +174,12 @@ export async function runProjectGraphReviewCycleProof( const fixtureRoot = resolve( options.fixtureRoot ?? join(dirname(fileURLToPath(import.meta.url)), '../../.fixtures'), ); - const seedSet = options.seedSet ?? DEFAULT_SEED_SET; - const seedSlug = options.seedSlug ?? DEFAULT_SEED_SLUG; + const seedName = options.seedName ?? DEFAULT_SEED_NAME; + const seedVariant = options.seedVariant ?? DEFAULT_SEED_VARIANT; const runId = assertPortableRunId(options.runId ?? defaultRunId()); const prompt = options.prompt ?? defaultProjectGraphPrompt(); const generatedAt = new Date().toISOString(); - const fixture = await readSeedFixture(join(fixtureRoot, 'seeds', seedSet, `${seedSlug}.json`)); + const fixture = await readSeedFixture(join(fixtureRoot, 'seeds', seedName, `${seedVariant}.json`)); const graph = await openWorkspaceGraphRuntime(cwd); const seedResult = seedFixture(graph.commandExecutor, fixture); const baseOverview = graph.forSpec(seedResult.specId).queryGraph(); @@ -247,8 +248,8 @@ export async function runProjectGraphReviewCycleProof( runId, generatedAt, cwd, - seedSet, - seedSlug, + seedName, + seedVariant, specId: seedResult.specId, sessionId: activated.session.id, prompt, @@ -351,8 +352,8 @@ export function summarizeProjectGraphReviewCycleProof( 'Prove the project-graph capability path can present an exact review set and approve it through public RPC.', evaluationFocus: 'FE-809 real agent proposal → present_review_set → session.submitExchangeResponse approval → explicit graph readback.', - seedSet: input.seedSet ?? DEFAULT_SEED_SET, - seedSlug: input.seedSlug, + seedName: input.seedName ?? DEFAULT_SEED_NAME, + seedVariant: input.seedVariant, cwd: input.cwd, specId: input.specId, sessionId: input.sessionId, @@ -531,7 +532,7 @@ async function selectSpecForSetupSession(cwd: string, specId: number): Promise default full registry -│ ├── createRpcHandlers({devRpc}) -> full registry plus gated dev.* harnesses -│ ├── createReadOnlyRpcHandlers(...) -> read-only registry, never dev.* +│ ├── createReadOnlyRpcHandlers(...) -> read-only registry │ ├── createWebSidecarRpcHandlers(...) -> driver registry; live methods only with handles │ └── rpc.discover -> discovery over active registry └── methods/ @@ -68,7 +67,6 @@ rpc/ ├── session-driver.ts -> live AgentSession driver method ├── session-exchange-answer.ts -> live exchange answer method ├── graph.ts -> graph.* handlers - ├── dev-graph.ts -> gated dev.graph.* fixture-curation harness └── schemas.ts -> shared protocol schemas ``` @@ -91,15 +89,6 @@ full RPC host: session.submitExchangeResponse session.submitMessage -dev-enabled full RPC host only: - writes: - dev.graph.mutateGraph - absent unless: - createRpcHandlers({devRpc: true}) or BRUNCH_DEV=1 in CLI rpc mode - still absent from: - default full RPC discovery - TUI-started web sidecar - TUI-started web sidecar without live driver handle: reads: rpc.discover @@ -265,24 +254,6 @@ graph.nodeNeighborhood params: {specId, nodeId, hops?} result: success(anchor, neighbors, edges) | not_found source: SQLite graph reader for the explicit spec - -dev.graph.mutateGraph - access: write - params: - specId - createBasis?: explicit | implicit - ops: - - {op: create_node, ref, plane, kind, title, body?, source?, detail?} - - {op: create_edge, category, , stance?, rationale?} - role-named endpoints: batch ref | {existingCode} - - {op: patch_node, node: {existingCode}, patch} - - {op: patch_edge, edgeId, patch} - - {op: delete_edge, edgeId} - - {op: delete_node, node: {existingCode}, deleteIncidentEdges?} - result: success(lsn, createdNodes, createdEdges, updatedNodes, updatedEdges, deletedNodes, deletedEdges) | structural_illegal(diagnostics) - effects: resolves projected node codes / selected-spec edge ids at the boundary, commits atomically through `CommandExecutor.mutateGraph`, and publishes graph projection invalidations - gate: explicit local harness only; absent from default public RPC and read-only sidecars - caveat: local curation harness only; product-path proof still comes from transcript-backed `mutate_graph` tool runs ``` ## Product update notifications diff --git a/src/rpc/__tests__/handlers.test.ts b/src/rpc/__tests__/handlers.test.ts index 41730aa33..664294073 100644 --- a/src/rpc/__tests__/handlers.test.ts +++ b/src/rpc/__tests__/handlers.test.ts @@ -2344,7 +2344,7 @@ describe('JSON-RPC handlers', () => { }); }); - it('keeps dev graph commit RPC absent unless explicitly enabled', async () => { + it('keeps graph-curation RPC out of the public registry', async () => { const fixture = await createGraphRpcFixture(); const defaultHandlers = createRpcHandlers({ coordinator: coordinator(), @@ -2354,11 +2354,6 @@ describe('JSON-RPC handlers', () => { coordinator: coordinator(), cwd: fixture.cwd, }); - const devHandlers = createRpcHandlers({ - coordinator: coordinator(), - cwd: fixture.cwd, - devRpc: true, - }); await expect( defaultHandlers.handle({ jsonrpc: '2.0', id: 56, method: 'dev.graph.mutateGraph', params: {} }), @@ -2374,8 +2369,7 @@ describe('JSON-RPC handlers', () => { id: 58, method: 'rpc.discover', }); - const devDiscovery = await devHandlers.handle({ jsonrpc: '2.0', id: 59, method: 'rpc.discover' }); - if (!('result' in defaultDiscovery) || !('result' in readOnlyDiscovery) || !('result' in devDiscovery)) { + if (!('result' in defaultDiscovery) || !('result' in readOnlyDiscovery)) { throw new Error('expected discovery success'); } @@ -2388,10 +2382,6 @@ describe('JSON-RPC handlers', () => { expect(methodNames(defaultDiscovery)).not.toContain('dev.graph.mutateGraph'); expect(methodNames(readOnlyDiscovery)).not.toContain('dev.graph.mutateGraph'); - expect(methodNames(devDiscovery)).toContain('dev.graph.mutateGraph'); - expect(JSON.stringify(devDiscovery.result)).toContain('existingCode'); - expect(JSON.stringify(devDiscovery.result)).toContain('explicit'); - expect(JSON.stringify(devDiscovery.result)).toContain('implicit'); }); it('maps an attached-but-not-live sidecar driver to the public no-live-driver error', async () => { @@ -2419,437 +2409,6 @@ describe('JSON-RPC handlers', () => { }); }); - it('applies create mutateGraph ops through dev RPC and reads them back through public graph RPC', async () => { - const fixture = await createGraphRpcFixture(); - const productUpdates = createProductUpdatePublisher(); - const updates: unknown[] = []; - productUpdates.subscribe((batch) => updates.push(...batch)); - const handlers = createRpcHandlers({ - coordinator: coordinator(), - cwd: fixture.cwd, - productUpdates, - devRpc: true, - }); - - const response = await handlers.handle({ - jsonrpc: '2.0', - id: 60, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - createBasis: 'explicit', - ops: [ - { op: 'create_node', ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Dev RPC thesis' }, - { - op: 'create_edge', - category: 'rationale', - support: { existingCode: 'REQ1' }, - claim: 'thesis', - stance: 'for', - }, - ], - }, - }); - - expect(response).toMatchObject({ - jsonrpc: '2.0', - id: 60, - result: { - status: 'success', - lsn: expect.any(Number), - createdNodes: { thesis: { id: expect.any(Number), code: 'TH1' } }, - createdEdges: [expect.any(Number)], - }, - }); - if (!('result' in response)) throw new Error('expected commit success'); - const commitResult = response.result as { readonly lsn: number }; - - expect(updates).toEqual([ - { topic: 'graph.overview', specId: fixture.specAId, lsn: commitResult.lsn }, - { topic: 'graph.nodeNeighborhood', specId: fixture.specAId, lsn: commitResult.lsn }, - ]); - - const overview = await handlers.handle({ - jsonrpc: '2.0', - id: 61, - method: 'graph.overview', - params: { specId: fixture.specAId }, - }); - expect(overview).toMatchObject({ - jsonrpc: '2.0', - id: 61, - result: { - lsn: commitResult.lsn, - nodes: expect.arrayContaining([expect.objectContaining({ title: 'Dev RPC thesis' })]), - edges: expect.arrayContaining([expect.objectContaining({ category: 'rationale', stance: 'for' })]), - }, - }); - - const siblingOverview = await handlers.handle({ - jsonrpc: '2.0', - id: 62, - method: 'graph.overview', - params: { specId: fixture.specBId }, - }); - expect(siblingOverview).toMatchObject({ - jsonrpc: '2.0', - id: 62, - result: { - lsn: fixture.specBLsn, - }, - }); - }); - - it('applies patch and delete mutateGraph ops through dev RPC', async () => { - const fixture = await createGraphRpcFixture(); - const productUpdates = createProductUpdatePublisher(); - const handlers = createRpcHandlers({ - coordinator: coordinator(), - cwd: fixture.cwd, - productUpdates, - devRpc: true, - }); - - const createResponse = await handlers.handle({ - jsonrpc: '2.0', - id: 62, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - createBasis: 'explicit', - ops: [ - { - op: 'create_node', - ref: 'thesis', - plane: 'intent', - kind: 'thesis', - title: 'Dev RPC thesis', - body: 'Original thesis body', - }, - { - op: 'create_edge', - category: 'rationale', - support: { existingCode: 'REQ1' }, - claim: 'thesis', - stance: 'for', - rationale: 'Original rationale', - }, - ], - }, - }); - if ( - !('result' in createResponse) || - (createResponse.result as { status?: string }).status !== 'success' - ) { - throw new Error('expected create mutateGraph success'); - } - - const createResult = createResponse.result as { - readonly status: 'success'; - readonly createdNodes: { - readonly thesis: { - readonly id: number; - }; - }; - readonly createdEdges: readonly number[]; - }; - - const thesisId = createResult.createdNodes.thesis.id; - const edgeId = createResult.createdEdges[0]!; - - const patchResponse = await handlers.handle({ - jsonrpc: '2.0', - id: 63, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - ops: [ - { - op: 'patch_node', - node: { existingCode: 'TH1' }, - patch: { title: 'Patched thesis', body: 'Patched thesis body' }, - }, - { - op: 'patch_edge', - edgeId, - patch: { rationale: 'Patched rationale' }, - }, - ], - }, - }); - - expect(patchResponse).toMatchObject({ - jsonrpc: '2.0', - id: 63, - result: { - status: 'success', - updatedNodes: [thesisId], - updatedEdges: [edgeId], - }, - }); - - const deleteEdgeResponse = await handlers.handle({ - jsonrpc: '2.0', - id: 64, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - ops: [{ op: 'delete_edge', edgeId }], - }, - }); - - expect(deleteEdgeResponse).toMatchObject({ - jsonrpc: '2.0', - id: 64, - result: { - status: 'success', - deletedEdges: [edgeId], - }, - }); - - const deleteNodeResponse = await handlers.handle({ - jsonrpc: '2.0', - id: 65, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - ops: [{ op: 'delete_node', node: { existingCode: 'TH1' } }], - }, - }); - - expect(deleteNodeResponse).toMatchObject({ - jsonrpc: '2.0', - id: 65, - result: { - status: 'success', - deletedNodes: [thesisId], - }, - }); - - const overview = await handlers.handle({ - jsonrpc: '2.0', - id: 66, - method: 'graph.overview', - params: { specId: fixture.specAId }, - }); - - expect(overview).toMatchObject({ - jsonrpc: '2.0', - id: 66, - result: { - nodes: expect.not.arrayContaining([expect.objectContaining({ title: 'Patched thesis' })]), - edges: expect.not.arrayContaining([expect.objectContaining({ id: edgeId })]), - }, - }); - }); - - it('rejects invalid dev mutateGraph ops without partial persistence', async () => { - const fixture = await createGraphRpcFixture(); - const handlers = createRpcHandlers({ - coordinator: coordinator(), - cwd: fixture.cwd, - devRpc: true, - }); - - await expect( - handlers.handle({ - jsonrpc: '2.0', - id: 67, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - createBasis: 'accepted_review_set', - ops: [], - }, - }), - ).resolves.toMatchObject({ - jsonrpc: '2.0', - id: 67, - error: { code: -32602, message: 'Invalid params' }, - }); - - await expect( - handlers.handle({ - jsonrpc: '2.0', - id: 68, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - createBasis: 'explicit', - ops: [ - { - op: 'create_node', - ref: 'thesis', - plane: 'intent', - kind: 'thesis', - title: 'Invalid dev RPC thesis', - }, - { - op: 'create_edge', - category: 'rationale', - source: { existingCode: 'REQ1' }, - target: 'thesis', - stance: 'for', - }, - ], - }, - }), - ).resolves.toMatchObject({ - jsonrpc: '2.0', - id: 68, - error: { code: -32602, message: 'Invalid params' }, - }); - - const invalid = await handlers.handle({ - jsonrpc: '2.0', - id: 69, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - ops: [ - { - op: 'patch_node', - node: { existingCode: 'not-a-code' }, - patch: { title: 'Invalid dev RPC thesis' }, - }, - ], - }, - }); - - expect(invalid).toMatchObject({ - jsonrpc: '2.0', - id: 69, - result: { - status: 'structural_illegal', - diagnostics: expect.arrayContaining([ - expect.objectContaining({ message: expect.stringContaining('malformed graph node code') }), - ]), - }, - }); - - const siblingCode = await handlers.handle({ - jsonrpc: '2.0', - id: 70, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - createBasis: 'explicit', - ops: [ - { op: 'create_node', ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Sibling code thesis' }, - { - op: 'create_edge', - category: 'rationale', - support: { existingCode: 'G1' }, - claim: 'thesis', - stance: 'for', - }, - ], - }, - }); - - expect(siblingCode).toMatchObject({ - jsonrpc: '2.0', - id: 70, - result: { - status: 'structural_illegal', - diagnostics: expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('does not resolve in the selected spec'), - }), - ]), - }, - }); - - const overview = await handlers.handle({ - jsonrpc: '2.0', - id: 71, - method: 'graph.overview', - params: { specId: fixture.specAId }, - }); - expect(overview).toMatchObject({ - jsonrpc: '2.0', - id: 71, - result: { - lsn: fixture.specALsn, - }, - }); - if (!('result' in overview)) throw new Error('expected overview success'); - expect(JSON.stringify(overview.result)).not.toContain('Invalid dev RPC thesis'); - }); - - it('enables dev graph commits over newline-delimited JSON-RPC streams', async () => { - const fixture = await createGraphRpcFixture(); - const input = new PassThrough(); - const output = new PassThrough(); - const productUpdates = createProductUpdatePublisher(); - const chunks: string[] = []; - output.on('data', (chunk) => chunks.push(String(chunk))); - - const done = runJsonRpcLineServer({ - input, - output, - handlers: createRpcHandlers({ - coordinator: coordinator(), - cwd: fixture.cwd, - productUpdates, - devRpc: true, - }), - productUpdates, - }); - - input.end( - [ - { - jsonrpc: '2.0', - id: 65, - method: 'dev.graph.mutateGraph', - params: { - specId: fixture.specAId, - createBasis: 'explicit', - ops: [ - { op: 'create_node', ref: 'thesis', plane: 'intent', kind: 'thesis', title: 'Line RPC thesis' }, - { - op: 'create_edge', - category: 'rationale', - support: { existingCode: 'REQ1' }, - claim: 'thesis', - stance: 'for', - }, - ], - }, - }, - { jsonrpc: '2.0', id: 66, method: 'graph.overview', params: { specId: fixture.specAId } }, - ] - .map((message) => JSON.stringify(message)) - .join('\n') + '\n', - ); - await done; - - const messages = chunks - .join('') - .trim() - .split('\n') - .map((line) => JSON.parse(line) as Record); - expect(messages).toHaveLength(3); - expect(messages[0]).toMatchObject({ - jsonrpc: '2.0', - method: 'brunch.updated', - params: { - topics: ['graph.overview', 'graph.nodeNeighborhood'], - updates: [ - { topic: 'graph.overview', specId: fixture.specAId, lsn: expect.any(Number) }, - { topic: 'graph.nodeNeighborhood', specId: fixture.specAId, lsn: expect.any(Number) }, - ], - }, - }); - expect(messages[1]).toMatchObject({ - jsonrpc: '2.0', - id: 65, - result: { status: 'success', createdNodes: { thesis: { code: 'TH1' } } }, - }); - expect(JSON.stringify(messages[2])).toContain('Line RPC thesis'); - }); - it('returns parse errors over newline-delimited JSON-RPC streams', async () => { const input = new PassThrough(); const output = new PassThrough(); diff --git a/src/rpc/handlers.ts b/src/rpc/handlers.ts index 1b833f9e9..8e481e26e 100644 --- a/src/rpc/handlers.ts +++ b/src/rpc/handlers.ts @@ -7,7 +7,6 @@ import type { DefaultWorkspaceCoordinator, SpecSessionActivationCoordinator, } from '../session/workspace-session-coordinator.js'; -import { devGraphRpcMethods } from './methods/dev-graph.js'; import { graphRpcMethods } from './methods/graph.js'; import { discoverRpcMethods, @@ -65,12 +64,8 @@ export function createRpcHandlers(options: { coordinator: DefaultWorkspaceCoordinator & SpecSessionActivationCoordinator; cwd: string; productUpdates?: ProductUpdatePublisher; - devRpc?: boolean; }): RpcHandlers { - return createRpcHandlersForRegistry( - options, - options.devRpc ? [...FULL_RPC_METHOD_REGISTRY, ...devGraphRpcMethods] : FULL_RPC_METHOD_REGISTRY, - ); + return createRpcHandlersForRegistry(options, FULL_RPC_METHOD_REGISTRY); } function createRpcHandlersForRegistry( diff --git a/src/session/__tests__/workspace-overview-context.test.ts b/src/session/__tests__/workspace-overview-context.test.ts index 8714d4f81..535e88957 100644 --- a/src/session/__tests__/workspace-overview-context.test.ts +++ b/src/session/__tests__/workspace-overview-context.test.ts @@ -14,8 +14,8 @@ describe('inspectWorkspaceOverview', () => { it('returns a workspace overview with spec node counts and session turn counts', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-workspace-overview-')); const executor = await openWorkspaceCommandExecutor(cwd); - const alpha = seedFixture(executor, await loadFixture('alpha-grounding', 'workspace-spread')); - const beta = seedFixture(executor, await loadFixture('beta-commitments', 'workspace-spread')); + const alpha = seedFixture(executor, await loadFixture('workspace-alpha-grounding')); + const beta = seedFixture(executor, await loadFixture('workspace-beta-commitments')); await mkdir(join(cwd, '.brunch', 'sessions'), { recursive: true }); await writeBoundSession(cwd, 'alpha-session', alpha.specId, [messageEntry('u1', 'user')]); @@ -57,8 +57,10 @@ describe('inspectWorkspaceOverview', () => { }); }); -async function loadFixture(slug: string, set = 'bilal-port'): Promise { - const fixturePath = fileURLToPath(new URL(`../../../.fixtures/seeds/${set}/${slug}.json`, import.meta.url)); +async function loadFixture(name: string, variant = 'base'): Promise { + const fixturePath = fileURLToPath( + new URL(`../../../.fixtures/seeds/${name}/${variant}.json`, import.meta.url), + ); return JSON.parse(await import('node:fs/promises').then(({ readFile }) => readFile(fixturePath, 'utf8'))); } diff --git a/src/treedocs.yaml b/src/treedocs.yaml index a69ac8337..f9b060d88 100644 --- a/src/treedocs.yaml +++ b/src/treedocs.yaml @@ -170,7 +170,7 @@ tree: graph: README.md: 'Documents this source subtree.' __snapshots__: - graph-overview-kind-band-spread.md: 'Markdown resource.' + graph-overview-kind-coverage-matrix.md: 'Markdown resource.' neighborhood-brunch-self-MOD1-hops2.md: 'Markdown resource.' neighborhood-brunch-self-REQ1.md: 'Markdown resource.' neighborhood-code-health-REQ1.md: 'Markdown resource.' @@ -300,15 +300,14 @@ tree: dev: README.md: 'Documents this source subtree.' agent-messages.ts: 'Implements agent messages.' - brunch-dev.ts: 'Implements brunch dev.' + dev-cli.ts: 'Implements the dev launcher and curation CLI.' faux-harness.ts: 'Implements faux harness.' faux-launcher.ts: 'Implements faux launcher.' generate-fan-out-witness.ts: 'Implements generate fan out witness.' + graph-curation.ts: 'Implements local graph curation over the shared mutation seam.' index.ts: 'Exports the public module surface.' introspection-launcher.ts: 'Implements introspection launcher.' - pi-source-alias.ts: 'Implements pi source alias.' tier-2-harness.ts: 'Implements tier 2 harness.' - workspace-rpc.ts: 'Implements workspace rpc.' graph: README.md: 'Documents this source subtree.' atoms.ts: 'Implements atoms.' @@ -390,7 +389,6 @@ tree: README.md: 'Documents this source subtree.' handlers.ts: 'Implements handlers.' methods: - dev-graph.ts: 'Implements dev graph.' graph.ts: 'Implements graph.' registry.ts: 'Implements registry.' schemas.ts: 'Implements schemas.' diff --git a/vite.config.ts b/vite.config.ts index 0b3af25ea..a8b843b7b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,11 +4,6 @@ import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; -// `.ts` specifier: vite loads this config through its own esbuild loader, which -// resolves the real source file. (Source modules under src/ use the `.js` -// NodeNext convention instead.) -import { piSourceAlias } from './src/dev/pi-source-alias.ts'; - const { version } = createRequire(import.meta.url)('./package.json') as { version: string }; export default defineConfig(() => ({ @@ -16,9 +11,6 @@ export default defineConfig(() => ({ define: { __BRUNCH_VERSION__: JSON.stringify(version), }, - resolve: { - alias: piSourceAlias(), - }, build: { outDir: 'dist-web', emptyOutDir: true,