@eir-labs/coltrane 0.5.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/src/bifrost_invoker.js +5 -3
  2. package/dist/src/bifrost_invoker.js.map +1 -1
  3. package/dist/src/claude_invoker.d.ts +20 -3
  4. package/dist/src/claude_invoker.js +30 -5
  5. package/dist/src/claude_invoker.js.map +1 -1
  6. package/dist/src/cli.d.ts +1 -1
  7. package/dist/src/conduct_daemon.d.ts +59 -0
  8. package/dist/src/conduct_daemon.js +178 -0
  9. package/dist/src/conduct_daemon.js.map +1 -0
  10. package/dist/src/genome_schema.d.ts +366 -0
  11. package/dist/src/genome_schema.js +129 -0
  12. package/dist/src/genome_schema.js.map +1 -1
  13. package/dist/src/genome_store.d.ts +29 -0
  14. package/dist/src/genome_store.js +326 -0
  15. package/dist/src/genome_store.js.map +1 -0
  16. package/dist/src/hosted_tools.d.ts +23 -0
  17. package/dist/src/hosted_tools.js +170 -0
  18. package/dist/src/hosted_tools.js.map +1 -0
  19. package/dist/src/index.d.ts +3 -0
  20. package/dist/src/index.js +3 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/mcp.js +10 -1
  23. package/dist/src/mcp.js.map +1 -1
  24. package/dist/src/output_mirror.d.ts +70 -0
  25. package/dist/src/output_mirror.js +381 -0
  26. package/dist/src/output_mirror.js.map +1 -0
  27. package/dist/src/outputs.d.ts +2 -0
  28. package/dist/src/outputs.js +37 -9
  29. package/dist/src/outputs.js.map +1 -1
  30. package/dist/src/play_worker.d.ts +24 -0
  31. package/dist/src/play_worker.js +49 -0
  32. package/dist/src/play_worker.js.map +1 -0
  33. package/dist/src/registry.d.ts +2 -0
  34. package/dist/src/registry.js +63 -33
  35. package/dist/src/registry.js.map +1 -1
  36. package/dist/src/runtime.js +19 -1
  37. package/dist/src/runtime.js.map +1 -1
  38. package/dist/src/seal_drill.d.ts +32 -0
  39. package/dist/src/seal_drill.js +138 -0
  40. package/dist/src/seal_drill.js.map +1 -0
  41. package/dist/src/server.d.ts +28 -1
  42. package/dist/src/server.js +243 -16
  43. package/dist/src/server.js.map +1 -1
  44. package/dist/src/skill_subprocess.js +12 -4
  45. package/dist/src/skill_subprocess.js.map +1 -1
  46. package/dist/src/supabase_genome.d.ts +28 -0
  47. package/dist/src/supabase_genome.js +49 -0
  48. package/dist/src/supabase_genome.js.map +1 -0
  49. package/dist/src/tool_surface.d.ts +5 -0
  50. package/dist/src/tool_surface.js +15 -0
  51. package/dist/src/tool_surface.js.map +1 -0
  52. package/dist/src/version.d.ts +1 -1
  53. package/dist/src/version.js +1 -1
  54. package/package.json +18 -3
@@ -9,12 +9,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
9
9
  import { MCP_TOOLS, requiresApproval, AGENT_STATUS_ORDER, STANDARD_STATUS_ORDER, SKILL_STATUS_ORDER, checkPromotion, PromotionError, } from "./mcp.js";
10
10
  import { loadRegistry, domainTypeDefect } from "./registry.js";
11
11
  import { resolveGenome } from "./loader.js";
12
- import { SkillSchema, AgentSchema, StandardSchema } from "./genome_schema.js";
12
+ import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema } from "./genome_schema.js";
13
13
  import { runSkillFixtures, executeSkill, loadFixtures } from "./skill_subprocess.js";
14
14
  import { evolveSkill } from "./skills.js";
15
15
  import { sealAgentDefinition, sealDefinition, sealSkillPackage, recordIdentity } from "./genome_writer.js";
16
16
  import { createOutputStore, defaultOutputsPersistDir } from "./outputs.js";
17
+ import { createOutputMirror, defaultMirrorDir, outputPreview, mirrorStorageRef } from "./output_mirror.js";
17
18
  import { FileLedger, LedgerError, LEDGER_SCHEMA_VERSION, defaultLedgerPath, } from "./ledger.js";
19
+ import { sealDrill } from "./seal_drill.js";
18
20
  import { standardSimulate } from "./simulate.js";
19
21
  import { runGig, BudgetExhausted, GigAborted, ResumeRefused, partialGigUsage, partialBudgetState } from "./runtime.js";
20
22
  import { createCheckpointStore, createReuseStore } from "./reuse.js";
@@ -172,6 +174,46 @@ function syncMap(target, source, before) {
172
174
  }
173
175
  return { added, modified, removed };
174
176
  }
177
+ // A queryable output row: the Tier-1 metadata projection (+ preview + storage_ref), with the
178
+ // full `data` payload only when the caller wants it. Keeps output_query's default shape
179
+ // backward-compatible (data present) while making the compact traversal and the deep second
180
+ // pass first-class.
181
+ function outputRow(rec, deps, withData) {
182
+ const row = {
183
+ id: rec.id,
184
+ gig_id: rec.gig_id,
185
+ agent_slug: rec.agent_slug,
186
+ from_role: rec.from_role,
187
+ phase: rec.phase,
188
+ primitive: rec.primitive,
189
+ core_type: rec.core_type,
190
+ domain_type: rec.domain_type,
191
+ domain_type_version: rec.domain_type_version,
192
+ domain: rec.domain,
193
+ content_sha: rec.content_sha,
194
+ input_refs: rec.input_refs,
195
+ input_shas: rec.input_shas,
196
+ cost_usd: rec.cost_usd,
197
+ tokens_used: rec.tokens_used,
198
+ duration_ms: rec.duration_ms,
199
+ model: rec.model,
200
+ model_tier: rec.model_tier,
201
+ created_at: rec.created_at,
202
+ preview: outputPreview(rec.data),
203
+ storage_ref: deps.output_mirror ? mirrorStorageRef(rec.content_sha) : "",
204
+ };
205
+ if (withData)
206
+ row["data"] = rec.data;
207
+ if (rec.skill_provenance !== undefined)
208
+ row["skill_provenance"] = rec.skill_provenance;
209
+ if (rec.reused_from !== undefined)
210
+ row["reused_from"] = rec.reused_from;
211
+ return row;
212
+ }
213
+ // A Tier-1 metadata row (from the mirror) shaped as an output_query row.
214
+ function metaToRow(meta) {
215
+ return { ...meta };
216
+ }
175
217
  /**
176
218
  * Pure tool dispatcher. Routes a tool call to its implementation. No transport,
177
219
  * no I/O beyond the injected deps — fully unit-testable.
@@ -347,28 +389,70 @@ async function runImpl(slug, args, deps, approval) {
347
389
  ...(std ? { standard: { slug: std.slug, phases: std.phases.map((p) => ({ name: p.name, chairs: p.chairs.length })) } } : {}),
348
390
  ...(observed.length > 0 ? { observed_costs_usd: observed } : {}),
349
391
  });
350
- return { ok: true, requires_approval: approval, data: res };
392
+ // WU-0008 the seal drill: before quoting a price, prove every chair contract
393
+ // can seal AT ALL. A cost estimate for a run whose terminal chair is doomed is
394
+ // worse than useless (it prices a failure as if it were work). Failures ride in
395
+ // the SUCCESS payload loudly; dispatch stays unchanged — the operator decides.
396
+ const drill = sealDrill({ phases: std.phases.map((p) => ({ name: p.name, chairs: p.chairs.map((c) => ({ role: c.role, output_contract: c.output_contract })) })) }, deps.registry);
397
+ return { ok: true, requires_approval: approval, data: { ...res, seal_drill: drill } };
351
398
  }
352
399
  case "output_query": {
353
- let outs = deps.outputs.all();
400
+ const mirror = deps.output_mirror;
401
+ // SECOND PASS — a single output's FULL payload by id or content_sha. This is the deeper
402
+ // Tier-2 read: prefer the content-addressed artifact tier (local mirror, or remote when
403
+ // drained), falling back to the store. Returns exactly the one output, with its data.
404
+ const wantId = args["output_id"] ? String(args["output_id"]) : undefined;
405
+ const wantSha = args["content_sha"] ? String(args["content_sha"]) : undefined;
406
+ if (wantId || wantSha) {
407
+ const rec = wantId
408
+ ? deps.outputs.get(wantId)
409
+ : deps.outputs.all().find((o) => o.content_sha === wantSha);
410
+ let row = rec ? outputRow(rec, deps, true) : undefined;
411
+ // Store miss (or a mirror-only payload): read the artifact tier directly.
412
+ if ((!row || row["data"] === undefined) && mirror) {
413
+ const sel = {};
414
+ if (wantId)
415
+ sel.id = wantId;
416
+ if (wantSha)
417
+ sel.content_sha = wantSha;
418
+ const hit = mirror.readPayload(sel);
419
+ if (hit?.meta)
420
+ row = { ...metaToRow(hit.meta), data: hit.data };
421
+ else if (row && hit?.data !== undefined)
422
+ row["data"] = hit.data;
423
+ }
424
+ const outputs = row ? [row] : [];
425
+ return { ok: true, requires_approval: approval, data: { outputs, total_count: outputs.length } };
426
+ }
427
+ // LIST — Tier-1 traversal. The store is now cross-process-fresh (the `fullyHydrated`
428
+ // latch is gone), so a gig sealed by a separate CLI process is visible here. `data` is
429
+ // carried by default (backward-compat) and on `data_filter`; pass `include_data:false`
430
+ // for the compact Tier-1 rows (metadata + preview + storage_ref, no payload).
431
+ const dataFilter = args["data_filter"];
432
+ const hasDataFilter = Boolean(dataFilter && typeof dataFilter === "object" && !Array.isArray(dataFilter));
433
+ const includeData = args["include_data"] === false ? false : true;
434
+ let recs = deps.outputs.all();
354
435
  if (args["domain_type"])
355
- outs = outs.filter((o) => o.domain_type === args["domain_type"]);
436
+ recs = recs.filter((o) => o.domain_type === args["domain_type"]);
356
437
  if (args["gig_id"])
357
- outs = outs.filter((o) => o.gig_id === args["gig_id"]);
438
+ recs = recs.filter((o) => o.gig_id === args["gig_id"]);
358
439
  if (args["agent_slug"])
359
- outs = outs.filter((o) => o.agent_slug === args["agent_slug"]);
440
+ recs = recs.filter((o) => o.agent_slug === args["agent_slug"]);
360
441
  // #234 — `data_filter` was advertised and ignored, so a caller narrowing a query by
361
442
  // payload got the UNFILTERED set back and a `total_count` describing it. Every key must
362
443
  // match (AND), compared structurally so an object or array value filters as written.
363
- const dataFilter = args["data_filter"];
364
- if (dataFilter && typeof dataFilter === "object" && !Array.isArray(dataFilter)) {
444
+ if (hasDataFilter) {
365
445
  const entries = Object.entries(dataFilter);
366
- outs = outs.filter((o) => {
446
+ recs = recs.filter((o) => {
367
447
  const data = (o.data ?? {});
368
448
  return entries.every(([k, v]) => canonJson(data[k]) === canonJson(v));
369
449
  });
370
450
  }
371
- return { ok: true, requires_approval: approval, data: { outputs: outs, total_count: outs.length } };
451
+ // Payload is dropped only when the caller explicitly asks for the compact rows AND is not
452
+ // filtering by payload (a data_filter needs the payload to have been read).
453
+ const withData = includeData || hasDataFilter;
454
+ const outputs = recs.map((o) => outputRow(o, deps, withData));
455
+ return { ok: true, requires_approval: approval, data: { outputs, total_count: outputs.length } };
372
456
  }
373
457
  case "output_trace": {
374
458
  const id = String(args["output_id"] ?? "");
@@ -501,6 +585,19 @@ async function runImpl(slug, args, deps, approval) {
501
585
  const warnings = stdStatus === "deprecated"
502
586
  ? [`standard "${slug2}" is deprecated — it still runs, but should not be built on.`]
503
587
  : [];
588
+ // WU-0008 preflight: run the same sealDrill used by standard_simulate BEFORE spending
589
+ // on any chair. A structurally-unsealable standard is refused here (pennies) instead of
590
+ // after a chair runs and aborts. Gate is placed once, above the wait/async split, so a
591
+ // single check covers both runGig call-sites below.
592
+ const drill = sealDrill({ phases: standard.phases.map((p) => ({ name: p.name, chairs: p.chairs.map((c) => ({ role: c.role, output_contract: c.output_contract })) })) }, deps.registry);
593
+ if (!drill.ok) {
594
+ return {
595
+ ok: false, requires_approval: approval,
596
+ error: `standard "${slug2}" cannot seal: ` +
597
+ drill.failures.map((f) => `${f.phase}/${f.role} → ${f.domain_type} (${f.errors.join("; ")})`).join(", "),
598
+ data: { seal_drill: drill },
599
+ };
600
+ }
504
601
  // Optional budget arg — when present, runtime enforces per-gig cost-budget
505
602
  // and raises BudgetExhausted on depletion (PR for T10 gap, see runtime.ts).
506
603
  const budgetArg = args["budget"];
@@ -935,6 +1032,9 @@ async function runImpl(slug, args, deps, approval) {
935
1032
  slug: baseDef.slug,
936
1033
  extends: baseDef.extends,
937
1034
  schema: { properties: nextProps },
1035
+ // 2026-08-08 — the undeclared-required check needs the extension's required
1036
+ // set, or an extend could version-in a requirement no producer can see.
1037
+ required_fields: nextRequired,
938
1038
  });
939
1039
  if (extendDefect) {
940
1040
  return { ok: false, requires_approval: approval, error: `type_extend rejected: ${extendDefect}` };
@@ -1620,6 +1720,46 @@ async function runImpl(slug, args, deps, approval) {
1620
1720
  // production status, and never RUN, TESTED, LISTED or REVISED through the engine. The
1621
1721
  // fixture gate on promotion made that gap sharper — you could be refused for failing
1622
1722
  // fixtures with no way to run them and see why.
1723
+ // ── discoverability parity — a dispatcher must be able to FIND a slug over MCP ──────
1724
+ // (tests/genome_browse_parity.test.ts). Backed by the deps maps, so the same handler
1725
+ // serves a working-tree load and a hosted store load identically; no filesystem.
1726
+ case "standard_browse": {
1727
+ if (!deps.standards)
1728
+ return { ok: false, not_implemented: true, requires_approval: approval, error: "standard_browse needs a standards map (bootstrap from a genome)" };
1729
+ let list = [...deps.standards.values()];
1730
+ if (args["domain"])
1731
+ list = list.filter((s) => s.domain === args["domain"]);
1732
+ if (args["status"])
1733
+ list = list.filter((s) => (s.status ?? "active") === args["status"]);
1734
+ const standards = list
1735
+ .map((s) => ({
1736
+ slug: s.slug, domain: s.domain, status: s.status ?? "active",
1737
+ phases: s.phases.map((p) => p.name), phase_count: s.phases.length,
1738
+ chair_count: s.phases.reduce((n, p) => n + p.chairs.length, 0),
1739
+ input_types: s.input_types ?? [], output_types: s.output_types ?? [],
1740
+ eval_slugs: s.eval_slugs ?? [], description: s.description ?? null,
1741
+ }))
1742
+ .sort((a, b) => (a.slug < b.slug ? -1 : 1));
1743
+ return { ok: true, requires_approval: approval, data: { standards, count: standards.length } };
1744
+ }
1745
+ case "agent_browse": {
1746
+ if (!deps.agents)
1747
+ return { ok: false, not_implemented: true, requires_approval: approval, error: "agent_browse needs an agents map (bootstrap from a genome)" };
1748
+ let list = [...deps.agents.values()];
1749
+ if (args["domain"])
1750
+ list = list.filter((a) => a.domain === args["domain"]);
1751
+ if (args["primitive"])
1752
+ list = list.filter((a) => a.primitives.includes(args["primitive"]));
1753
+ const agents = list
1754
+ .map((a) => ({
1755
+ slug: a.slug, primitives: a.primitives, domain: a.domain,
1756
+ input_types: a.input_types, output_types: a.output_types,
1757
+ behavioral_primitives: a.behavioral_primitives,
1758
+ skill_slugs: a.skill_slugs ?? [], model_tier: a.model_tier ?? null,
1759
+ }))
1760
+ .sort((x, y) => (x.slug < y.slug ? -1 : 1));
1761
+ return { ok: true, requires_approval: approval, data: { agents, count: agents.length } };
1762
+ }
1623
1763
  case "skill_browse": {
1624
1764
  if (!deps.skills)
1625
1765
  return { ok: false, not_implemented: true, requires_approval: approval, error: "skill_browse needs a skills map (bootstrap from a genome)" };
@@ -2153,18 +2293,99 @@ async function runImpl(slug, args, deps, approval) {
2153
2293
  return { ok: false, requires_approval: approval, error: e instanceof Error ? e.message : String(e) };
2154
2294
  }
2155
2295
  }
2156
- /** Build the low-level MCP Server with ListTools + CallTool wired to the dispatcher. */
2296
+ // Tools whose implementation is inherently local-process. Each entry names WHY, so the
2297
+ // hosted refusal teaches instead of stonewalling.
2298
+ const HOSTED_BLOCKED = {
2299
+ server_restart: "server_restart is a local-process concern (the stdio relay restarts its child); a hosted, per-request surface has no server process to restart",
2300
+ skill_execute: "skill_execute runs a skill's code half in a local subprocess against its on-disk package; hosted skills carry no local package — run code skills where the genome files live",
2301
+ skill_evolve: "skill_evolve runs candidate code against local fixtures in a subprocess; hosted skills carry no local package to evolve",
2302
+ charter_read: "charter_read reads a charter file from a local path; a hosted surface has no filesystem",
2303
+ gig_logs: "gig_logs tails per-chair log files under the local outputs dir; hosted runs are executed by the drain worker and their record lives in the store",
2304
+ genome_reload: "genome_reload re-reads genome files from disk; a hosted surface loads its genome from the store per-request, so there is nothing to reload",
2305
+ };
2306
+ // The genome-mutation tools whose success must ALSO land in the hosted store, and the class
2307
+ // each persists as. Payload is filtered by the class's own Zod schema key list (the same
2308
+ // single source the handlers copy from), so the store payload can't drift from the schema.
2309
+ const HOSTED_UPSERT = {
2310
+ agent_define: { cls: "agent", keys: Object.keys(AgentSchema.shape) },
2311
+ standard_compose: { cls: "standard", keys: Object.keys(StandardSchema.shape) },
2312
+ type_register: { cls: "domain_type", keys: Object.keys(DomainTypeSchema.shape) },
2313
+ skill_define: { cls: "skill", keys: Object.keys(SkillSchema.shape) },
2314
+ };
2315
+ async function callSurfaceTool(slug, args, deps) {
2316
+ if (deps.hosted) {
2317
+ const blocked = HOSTED_BLOCKED[slug];
2318
+ if (blocked)
2319
+ return { ok: false, hosted_unsupported: true, error: blocked };
2320
+ if (slug === "gig_dispatch") {
2321
+ // Hosted dispatch NEVER spawns. With a queue seam it queues (the gig table is the
2322
+ // queue; a drain worker claims and runs); without one it says so, typed.
2323
+ if (deps.queueGig) {
2324
+ try {
2325
+ const data = await deps.queueGig(args);
2326
+ return { ok: true, data };
2327
+ }
2328
+ catch (e) {
2329
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
2330
+ }
2331
+ }
2332
+ return {
2333
+ ok: false,
2334
+ hosted_unsupported: true,
2335
+ error: "hosted dispatch goes through the queue RPC (coltrane_gig_dispatch) — nothing spawns in a hosted " +
2336
+ "surface. Wire deps.queueGig (e.g. postgrestQueueGig(ctx) from ./genome_store) to queue the gig " +
2337
+ "for the drain worker.",
2338
+ };
2339
+ }
2340
+ }
2341
+ const result = await dispatchTool(slug, args, deps);
2342
+ // Hosted persistence: without a genome_dir the handlers compute + validate + seal identity
2343
+ // but write no file (the validation path). The store upsert is the hosted write half; a
2344
+ // refused upsert fails the mutation loudly — a definition that only ever lived in this
2345
+ // request's memory must not report success.
2346
+ const up = HOSTED_UPSERT[slug];
2347
+ if (deps.hosted && deps.store && result.ok && up) {
2348
+ const payload = {};
2349
+ for (const k of up.keys)
2350
+ if (args[k] !== undefined)
2351
+ payload[k] = args[k];
2352
+ try {
2353
+ await deps.store.upsert(up.cls, payload);
2354
+ }
2355
+ catch (e) {
2356
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
2357
+ }
2358
+ }
2359
+ return result;
2360
+ }
2361
+ /** The engine's FULL MCP tool surface as a transport-agnostic registry. The stdio server
2362
+ * below consumes this internally; a hosted host mounts it per-request with hosted deps. */
2363
+ export function createToolSurface(deps) {
2364
+ return MCP_TOOLS.map((t) => ({
2365
+ name: t.slug,
2366
+ description: `${t.category} tool`,
2367
+ input_schema: t.input_schema,
2368
+ call: (args) => callSurfaceTool(t.slug, args, deps),
2369
+ }));
2370
+ }
2371
+ /** Build the low-level MCP Server with ListTools + CallTool wired to the tool surface. */
2157
2372
  export function createColtraneServer(deps, recorder) {
2158
2373
  const server = new Server({ name: "coltrane", version: COLTRANE_VERSION }, { capabilities: { tools: {} } });
2374
+ const surface = createToolSurface(deps);
2375
+ const byName = new Map(surface.map((t) => [t.name, t]));
2159
2376
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
2160
- tools: MCP_TOOLS.map((t) => ({
2161
- name: t.slug,
2162
- description: `${t.category} tool`,
2377
+ tools: surface.map((t) => ({
2378
+ name: t.name,
2379
+ description: t.description,
2163
2380
  inputSchema: t.input_schema,
2164
2381
  })),
2165
2382
  }));
2166
2383
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
2167
- const result = await dispatchTool(req.params.name, (req.params.arguments ?? {}), deps);
2384
+ const args = (req.params.arguments ?? {});
2385
+ const tool = byName.get(req.params.name);
2386
+ // A name outside the surface falls through to the dispatcher's own unknown-tool answer,
2387
+ // keeping the reply byte-identical to the pre-surface path.
2388
+ const result = tool ? await tool.call(args) : await dispatchTool(req.params.name, args, deps);
2168
2389
  if (recorder) {
2169
2390
  recorder.recordToolCall(req.params.name);
2170
2391
  recorder.recordObservability(`call:${req.params.name}`, { ok: result.ok });
@@ -2234,13 +2455,19 @@ export function bootstrapServerDeps(genomeRoot) {
2234
2455
  // the tool as mcp__coltrane__<slug> — the name the server exposes — so a bare-slug grant (the form
2235
2456
  // agent_define accepts) is actually callable instead of a silent dead name that seals nothing.
2236
2457
  const toolProviders = new Map([...REGISTERED_TOOL_SLUGS].map((slug) => [slug, { tool: slug, kind: "in_house", server: ENGINE_MCP_SERVER }]));
2458
+ // The two-tier local mirror, rooted at `<root>/.coltrane` (gitignored, beside the ledger).
2459
+ // The SAME instance is handed to the OutputStore and exposed as `output_mirror`, so every
2460
+ // sealed output — whether this process ran the gig or a separate CLI process did — lands in
2461
+ // one content-addressed store MCP retrieval reads. COLTRANE_MIRROR_DIR overrides (tests).
2462
+ const output_mirror = createOutputMirror(defaultMirrorDir(root));
2237
2463
  return {
2238
2464
  registry,
2239
2465
  toolProviders,
2466
+ output_mirror,
2240
2467
  // PR #78 follow-up: persist outputs to disk so the audit chain survives an
2241
2468
  // MCP session close (Rob cold-trial requirement). COLTRANE_OUTPUTS_DIR
2242
2469
  // overrides the default ~/.eir/coltrane_outputs path (tests + sandboxes).
2243
- outputs: createOutputStore(registry, { persistDir: defaultOutputsPersistDir() }),
2470
+ outputs: createOutputStore(registry, { persistDir: defaultOutputsPersistDir(), mirror: output_mirror }),
2244
2471
  // #209 — the audit spine is durable by default. The line above gives OUTPUTS a persistDir
2245
2472
  // under an explicit "the audit chain must survive an MCP session close" requirement
2246
2473
  // (PR #78); the ledger sat in RAM directly beneath it, which made absence-of-row mean