@eir-labs/coltrane 0.10.0 → 0.11.0

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 (138) hide show
  1. package/agents/bandleader.json +54 -0
  2. package/agents/bill.json +4 -2
  3. package/agents/change-verifier.json +2 -1
  4. package/agents/code-implementer.json +7 -4
  5. package/agents/john.json +5 -3
  6. package/agents/lineage-scout-external.json +19 -6
  7. package/agents/lineage-scribe.json +20 -9
  8. package/agents/lineage-weaver.json +26 -11
  9. package/agents/miles.json +2 -1
  10. package/agents/players/bandleader.md +70 -0
  11. package/agents/pr-publisher.json +4 -2
  12. package/agents/red-spec-drafter.json +12 -6
  13. package/agents/room-prober.json +29 -0
  14. package/agents/source-walker.json +1 -1
  15. package/charts/room-probe-chart-v1.json +14 -0
  16. package/charts/worker-contract-docker-v2.json +135 -0
  17. package/charts/worker-contract-v3.json +206 -0
  18. package/dist/src/boundary_check.d.ts +18 -0
  19. package/dist/src/boundary_check.js +70 -0
  20. package/dist/src/boundary_check.js.map +1 -0
  21. package/dist/src/boundary_scan.d.ts +62 -0
  22. package/dist/src/boundary_scan.js +93 -0
  23. package/dist/src/boundary_scan.js.map +1 -0
  24. package/dist/src/chart.js +34 -1
  25. package/dist/src/chart.js.map +1 -1
  26. package/dist/src/claude_invoker.d.ts +2 -1
  27. package/dist/src/claude_invoker.js +150 -17
  28. package/dist/src/claude_invoker.js.map +1 -1
  29. package/dist/src/cli.d.ts +1 -1
  30. package/dist/src/cli.js +61 -26
  31. package/dist/src/cli.js.map +1 -1
  32. package/dist/src/drain_preflight.d.ts +33 -0
  33. package/dist/src/drain_preflight.js +97 -0
  34. package/dist/src/drain_preflight.js.map +1 -0
  35. package/dist/src/genome_schema.d.ts +390 -9
  36. package/dist/src/genome_schema.js +163 -8
  37. package/dist/src/genome_schema.js.map +1 -1
  38. package/dist/src/genome_store.js +9 -0
  39. package/dist/src/genome_store.js.map +1 -1
  40. package/dist/src/genome_writer.d.ts +1 -1
  41. package/dist/src/genome_writer.js +10 -2
  42. package/dist/src/genome_writer.js.map +1 -1
  43. package/dist/src/gig_conformance.d.ts +66 -0
  44. package/dist/src/gig_conformance.js +153 -0
  45. package/dist/src/gig_conformance.js.map +1 -0
  46. package/dist/src/gig_tracker.js +8 -0
  47. package/dist/src/gig_tracker.js.map +1 -1
  48. package/dist/src/grounding.d.ts +44 -0
  49. package/dist/src/grounding.js +127 -0
  50. package/dist/src/grounding.js.map +1 -0
  51. package/dist/src/index.d.ts +1 -0
  52. package/dist/src/index.js +1 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/institution_loader.d.ts +29 -5
  55. package/dist/src/institution_loader.js +67 -6
  56. package/dist/src/institution_loader.js.map +1 -1
  57. package/dist/src/lineage_adoption.d.ts +82 -0
  58. package/dist/src/lineage_adoption.js +82 -0
  59. package/dist/src/lineage_adoption.js.map +1 -0
  60. package/dist/src/lineage_persist.d.ts +34 -0
  61. package/dist/src/lineage_persist.js +60 -0
  62. package/dist/src/lineage_persist.js.map +1 -0
  63. package/dist/src/loader.d.ts +13 -0
  64. package/dist/src/loader.js +43 -2
  65. package/dist/src/loader.js.map +1 -1
  66. package/dist/src/mcp.js +37 -4
  67. package/dist/src/mcp.js.map +1 -1
  68. package/dist/src/org_hire.d.ts +23 -0
  69. package/dist/src/org_hire.js +42 -0
  70. package/dist/src/org_hire.js.map +1 -0
  71. package/dist/src/outputs.d.ts +17 -0
  72. package/dist/src/outputs.js +86 -1
  73. package/dist/src/outputs.js.map +1 -1
  74. package/dist/src/player_to_claude_code.js +26 -2
  75. package/dist/src/player_to_claude_code.js.map +1 -1
  76. package/dist/src/registry.d.ts +3 -1
  77. package/dist/src/registry.js +43 -3
  78. package/dist/src/registry.js.map +1 -1
  79. package/dist/src/repo_index.d.ts +58 -0
  80. package/dist/src/repo_index.js +232 -0
  81. package/dist/src/repo_index.js.map +1 -0
  82. package/dist/src/reuse.d.ts +44 -4
  83. package/dist/src/reuse.js +0 -0
  84. package/dist/src/reuse.js.map +1 -1
  85. package/dist/src/runtime.d.ts +82 -0
  86. package/dist/src/runtime.js +254 -25
  87. package/dist/src/runtime.js.map +1 -1
  88. package/dist/src/server.d.ts +37 -0
  89. package/dist/src/server.js +422 -11
  90. package/dist/src/server.js.map +1 -1
  91. package/dist/src/server_relay.d.ts +40 -2
  92. package/dist/src/server_relay.js +186 -19
  93. package/dist/src/server_relay.js.map +1 -1
  94. package/dist/src/tool_providers.d.ts +27 -0
  95. package/dist/src/tool_providers.js +48 -0
  96. package/dist/src/tool_providers.js.map +1 -1
  97. package/dist/src/venue_credential.d.ts +41 -0
  98. package/dist/src/venue_credential.js +57 -0
  99. package/dist/src/venue_credential.js.map +1 -0
  100. package/dist/src/venue_realize.d.ts +23 -0
  101. package/dist/src/venue_realize.js +43 -3
  102. package/dist/src/venue_realize.js.map +1 -1
  103. package/dist/src/venue_realizer.d.ts +224 -0
  104. package/dist/src/venue_realizer.js +889 -0
  105. package/dist/src/venue_realizer.js.map +1 -0
  106. package/dist/src/version.d.ts +1 -1
  107. package/dist/src/version.js +1 -1
  108. package/dist/src/worker.d.ts +37 -0
  109. package/dist/src/worker.js +127 -24
  110. package/dist/src/worker.js.map +1 -1
  111. package/dist/src/worker_env.d.ts +79 -0
  112. package/dist/src/worker_env.js +322 -0
  113. package/dist/src/worker_env.js.map +1 -0
  114. package/dist/src/workspace.d.ts +12 -1
  115. package/dist/src/workspace.js +10 -3
  116. package/dist/src/workspace.js.map +1 -1
  117. package/domain_types/change-context.json +59 -13
  118. package/domain_types/lineage-adoption-target.json +22 -0
  119. package/domain_types/lineage-map.json +7 -2
  120. package/domain_types/lineage-record.json +55 -5
  121. package/domain_types/prior-art-hit.json +16 -1
  122. package/domain_types/red-spec.json +5 -3
  123. package/domain_types/woodshed-record.json +149 -0
  124. package/evals/gist-present.json +7 -0
  125. package/institutions/coltrane.json +178 -6
  126. package/package.json +6 -3
  127. package/skills/non-vacuity-probe/fixtures/fixture-001.json +17 -0
  128. package/skills/non-vacuity-probe/meta.json +12 -0
  129. package/skills/non-vacuity-probe/skill.md +27 -0
  130. package/skills/non-vacuity-probe/skill.mjs +81 -0
  131. package/standards/lineage-adopt-v0.json +37 -0
  132. package/standards/room-probe-v1.json +28 -0
  133. package/standards/software-change-pr-v1.json +15 -15
  134. package/standards/software-change-red-first-v0.json +183 -0
  135. package/standards/spec-review-and-sequence-v0.json +112 -0
  136. package/standards/studio-session-v0.json +101 -0
  137. package/tours/coltrane.json +72 -0
  138. package/venues/engine-room-v1.json +32 -0
@@ -6,15 +6,19 @@
6
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
+ import { z } from "zod";
10
+ // The wire names of the reserved relay↔child methods (restart guard, venue/8). Defined by the
11
+ // relay — the relay owns the conversation — and answered by the child handlers registered below.
12
+ import { RUNNING_GIGS_METHOD, ABORT_FOR_RESTART_METHOD } from "./server_relay.js";
9
13
  import { MCP_TOOLS, requiresApproval, AGENT_STATUS_ORDER, STANDARD_STATUS_ORDER, SKILL_STATUS_ORDER, checkPromotion, PromotionError, } from "./mcp.js";
10
14
  import { loadRegistry, domainTypeDefect } from "./registry.js";
11
15
  import { resolveGenome } from "./loader.js";
12
- import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, venueDefect } from "./genome_schema.js";
16
+ import { SkillSchema, AgentSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, VenueObjectSchema, venueDefect } from "./genome_schema.js";
13
17
  import { composeChart, runChart, chartHash, chartEntrySeedTypes, dispatchTarget, } from "./chart.js";
14
18
  import { runSkillFixtures, executeSkill, loadFixtures } from "./skill_subprocess.js";
15
19
  import { evolveSkill } from "./skills.js";
16
20
  import { sealAgentDefinition, sealDefinition, sealSkillPackage, recordIdentity } from "./genome_writer.js";
17
- import { createOutputStore, defaultOutputsPersistDir, } from "./outputs.js";
21
+ import { createOutputStore, defaultOutputsPersistDir, performanceRoot, } from "./outputs.js";
18
22
  import { createOutputMirror, defaultMirrorDir, outputPreview, mirrorStorageRef } from "./output_mirror.js";
19
23
  import { FileLedger, LedgerError, LEDGER_SCHEMA_VERSION, defaultLedgerPath, } from "./ledger.js";
20
24
  import { sealDrill } from "./seal_drill.js";
@@ -22,8 +26,10 @@ import { standardSimulate } from "./simulate.js";
22
26
  import { runGig, BudgetExhausted, GigAborted, ResumeRefused, partialGigUsage, partialBudgetState } from "./runtime.js";
23
27
  import { createCheckpointStore, createReuseStore } from "./reuse.js";
24
28
  import { makeClaudeInvoker, killLiveChairChildren } from "./claude_invoker.js";
29
+ import { dockerComposeRealizer } from "./venue_realizer.js";
25
30
  import { isDepth, DEPTHS } from "./pricing.js";
26
31
  import { ENGINE_MCP_SERVER } from "./tool_providers.js";
32
+ import { gigScopeRefusal, missingWorkerEnv, } from "./venue_credential.js";
27
33
  import { composeStandard, defineAgent, CompositionError } from "./composition.js";
28
34
  import { PRIMITIVE_OUTPUT_TYPE } from "./core_types.js";
29
35
  import { proposeTypeChange } from "./type_versioning.js";
@@ -526,9 +532,13 @@ async function runImpl(slug, args, deps, approval) {
526
532
  throw new Error(verdict.reason ??
527
533
  `output rejected: "${domain_type || core_type}" did not satisfy its output contract`);
528
534
  }
535
+ // Name what did NOT happen. Validation genuinely succeeded (ok:true, validated:true), but
536
+ // the runtime is the one sealer — this branch does NOT persist. A truthful compose chair
537
+ // once read `validated` as `sealed` and filed a false completion; `sealed:false` makes
538
+ // VALIDATED unmistakable from SEALED so no reader can launder one into the other.
529
539
  return {
530
540
  ok: true, requires_approval: approval,
531
- data: { validated: true, validation_result: { valid: true } },
541
+ data: { validated: true, sealed: false, validation_result: { valid: true } },
532
542
  };
533
543
  }
534
544
  const rec = deps.outputs.write({
@@ -585,6 +595,23 @@ async function runImpl(slug, args, deps, approval) {
585
595
  // player; an unseated one produces a gig that can only fail at genome load.
586
596
  const actingFor = args["acting_for"] === undefined || args["acting_for"] === null ? undefined : String(args["acting_for"]);
587
597
  void actingFor; // forwarded to the queue seam via `args` below; named here to be legible.
598
+ // WHERE it plays, as distinct from what it plays. This is a CEILING, and the whole design is
599
+ // fail-closed — so the one outcome the contract forbids is proceeding as if no venue was
600
+ // asked. It is threaded into BOTH runGig calls below (the sync and async paths) via the same
601
+ // conditional-spread trio the chart path uses (server.ts:935-941), so `runGig`'s own venue
602
+ // block either realizes the room or refuses fail-closed. It used to be read into this local
603
+ // and then DISCARDED (`void venue`, with a comment claiming the queue seam forwarded it — it
604
+ // did not for a local run): gig a77f6f7f dispatched room-probe-v1 with a named venue and got
605
+ // NO room, NO refusal, and a 'complete' status whose output was byte-identical to the
606
+ // venue-less control. An unnamed gig carries `undefined` here and is threaded to nothing —
607
+ // the venue-less path stays byte-identical.
608
+ const venue = args["venue"] === undefined || args["venue"] === null ? undefined : String(args["venue"]);
609
+ // WHAT REPOSITORY it works on, as distinct from WHERE it plays. The SUBJECT of the run, named
610
+ // EXPLICITLY at dispatch and threaded to runGig (the same conditional-spread trio venue uses)
611
+ // so the realized room's workspace is populated with THIS repository — never process.cwd() or
612
+ // any ambient host path. A venue is at rest and serves many repositories, so the repository
613
+ // belongs on the RUN, not the room. Absent → the room declines to populate (empty workspace).
614
+ const repoUrl = args["repo_url"] === undefined || args["repo_url"] === null ? undefined : String(args["repo_url"]);
588
615
  const target = dispatchTarget({
589
616
  standard_slug: args["standard_slug"] === undefined || args["standard_slug"] === null ? undefined : String(args["standard_slug"]),
590
617
  chart_slug: args["chart_slug"] === undefined || args["chart_slug"] === null ? undefined : String(args["chart_slug"]),
@@ -704,10 +731,18 @@ async function runImpl(slug, args, deps, approval) {
704
731
  if (reuseOn && !deps.reuse) {
705
732
  return { ok: false, requires_approval: approval, error: `gig_dispatch: reuse was requested but this server has no reuse store wired` };
706
733
  }
734
+ // #20 — the CLI signals an OMITTED --input as a boolean on args, so a human-only resume can
735
+ // inherit the checkpoint's gig_input_sha instead of drifting to sha256('{}') and refusing.
736
+ // It is advertised in gig_dispatch's input_schema (src/mcp.ts): a control the handler reads
737
+ // must be discoverable by a caller (#234). Only true when the caller stated no payload; an
738
+ // explicit `{}` is a supplied value and leaves this false, so a disagreeing payload still
739
+ // gates (see src/runtime.ts).
740
+ const gigInputOmitted = args["gig_input_omitted"] === true;
707
741
  const reuseWiring = {
708
742
  ...(deps.checkpoints ? { checkpoints: deps.checkpoints } : {}),
709
743
  ...(resumeArg !== undefined ? { resume_from: resumeArg } : {}),
710
744
  ...(reuseOn && deps.reuse ? { reuse: deps.reuse } : {}),
745
+ ...(gigInputOmitted ? { gig_input_omitted: true } : {}),
711
746
  };
712
747
  // ── the human seat's door ────────────────────────────────────────────────────────
713
748
  // A chair marked `human: true` parks the run until its incumbent's verdict arrives
@@ -758,6 +793,21 @@ async function runImpl(slug, args, deps, approval) {
758
793
  outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
759
794
  model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
760
795
  toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // each movement's preflight resolves against the invoker's environment
796
+ // THE ROOM THE ARRANGEMENT NAMED, carried into the run.
797
+ //
798
+ // Without this the venue branch in `runGig` is unreachable in production: `composeChart`
799
+ // checks the ceiling at AUTHORING time (R10) and `deps.venues` is passed here only so it
800
+ // can, but the chart's own `venue` was never threaded into the run deps — so no shipped
801
+ // caller ever set `deps.venue`, and `resolveAndRealize` ran for tests alone. A room that
802
+ // is checked when the chart is written and forgotten when it is performed is a ceiling
803
+ // on paper.
804
+ ...(chartDef.venue ? { venue: chartDef.venue } : {}),
805
+ ...(deps.venues ? { venues: deps.venues } : {}),
806
+ // The substrate seam, carried beside the room the arrangement named: when that room
807
+ // declares mcp_servers, runGig stands it up on this realizer and threads its transports
808
+ // onto each chair's spawn. Absent = the substrate is skipped (server-less venues, or a
809
+ // bare deps without a realizer wired).
810
+ ...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
761
811
  ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
762
812
  };
763
813
  /** The ARRANGEMENT's manifest. A chart has no single genome_hash or run_fingerprint — it
@@ -916,6 +966,15 @@ async function runImpl(slug, args, deps, approval) {
916
966
  outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
917
967
  model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
918
968
  toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // dispatch preflight resolves against the invoker's environment
969
+ // THE ROOM THIS DISPATCH NAMED, threaded so runGig realizes it or refuses fail-closed —
970
+ // the same conditional-spread trio the chart path uses (server.ts:935-941). `venue` is
971
+ // threaded only when non-empty so runGig's `deps.venue !== undefined` gate is never
972
+ // tripped by an absent one; `venues`/`venueRealizer` come from bootstrap (ServerDeps).
973
+ ...(venue ? { venue } : {}),
974
+ ...(deps.venues ? { venues: deps.venues } : {}),
975
+ ...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
976
+ // The repository the run names, threaded so runGig populates the room's tree from it.
977
+ ...(repoUrl ? { repoUrl } : {}),
919
978
  ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
920
979
  });
921
980
  return {
@@ -1002,6 +1061,15 @@ async function runImpl(slug, args, deps, approval) {
1002
1061
  outputs: deps.outputs, ledger: deps.ledger, invoke: deps.invoke,
1003
1062
  model_version: deps.model_version, skills: deps.skills, skill_dirs: deps.skill_dirs, evals: deps.evals, budget,
1004
1063
  toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // dispatch preflight resolves against the invoker's environment
1064
+ // Same venue trio as the sync path above — the DEFAULT dispatch mode must honour a named
1065
+ // room too, or the fix covers only the deterministic wait:true test path and leaves the
1066
+ // path the product actually dispatches through discarding the venue.
1067
+ ...(venue ? { venue } : {}),
1068
+ ...(deps.venues ? { venues: deps.venues } : {}),
1069
+ ...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
1070
+ // Same repository wire as the sync path above — the default async dispatch must populate a
1071
+ // named room's tree too, not only the deterministic wait:true path.
1072
+ ...(repoUrl ? { repoUrl } : {}),
1005
1073
  gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
1006
1074
  });
1007
1075
  // A REFUSED resume must be answered in THIS reply, not discovered later by polling. The
@@ -1104,8 +1172,22 @@ async function runImpl(slug, args, deps, approval) {
1104
1172
  // Prefer the live state map (async runs). Falls back to the ledger/outputs read for a
1105
1173
  // synchronously-completed gig (or one from a prior server lifetime, not in the map).
1106
1174
  const live = deps.gig_runs?.get(gid);
1175
+ // A MOVEMENT SEALS UNDER ITS OWN ID, so a raw `===` can never match a chart's records: a
1176
+ // movement runs as `<performance>.m.<movement_id>` (src/chart.ts `movementGigId`), and the
1177
+ // id an operator holds — the one `gig_dispatch` returned — is the performance's. Resolve
1178
+ // through `performanceRoot`, which src/outputs.ts:142-147 declares the ONE owner of that
1179
+ // scheme, rather than re-deriving the separator here; that second copy is the drift the
1180
+ // note there warns about, and it is what `output_trace` already had to fix (#248,
1181
+ // tests/cross_movement_trace.test.ts). A plain gig id is its own root, so a
1182
+ // single-standard run is unmoved, and a shared PREFIX is not a root — the infix carries
1183
+ // dots on both sides precisely so a uuid cannot be mistaken for a parent of another.
1184
+ // BOTH READINGS RESOLVE, and the exact match is not redundant: asking about a MOVEMENT's
1185
+ // own id must keep naming its own records, and `performanceRoot(<perf>.m.<mv>)` is
1186
+ // `<perf>`, which would not equal the movement id the caller asked about. A root-only
1187
+ // predicate trades one false negative for another.
1188
+ const ofPerformance = (o) => o.gig_id === gid || performanceRoot(o.gig_id) === gid;
1107
1189
  if (live) {
1108
- const outs = deps.outputs.all().filter((o) => o.gig_id === gid);
1190
+ const outs = deps.outputs.all().filter(ofPerformance);
1109
1191
  return {
1110
1192
  ok: true, requires_approval: approval,
1111
1193
  data: {
@@ -1141,7 +1223,10 @@ async function runImpl(slug, args, deps, approval) {
1141
1223
  },
1142
1224
  };
1143
1225
  }
1144
- const outs = deps.outputs.all().filter((o) => o.gig_id === gid);
1226
+ // The non-live path is the DAMAGING one: `status` and `phases_complete` below are derived
1227
+ // from this list, so an unresolved chart id did not merely omit the records — it answered
1228
+ // "unknown" and "0 phases" about a performance that had demonstrably done work.
1229
+ const outs = deps.outputs.all().filter(ofPerformance);
1145
1230
  const entry = deps.ledger.query({ kind: "gig", gig_id: gid })[0];
1146
1231
  return {
1147
1232
  ok: true, requires_approval: approval,
@@ -1201,7 +1286,12 @@ async function runImpl(slug, args, deps, approval) {
1201
1286
  // plain string slugs. Now: when an entry is a string or a slug-only
1202
1287
  // object, look it up in deps.agents (populated by bootstrap from the
1203
1288
  // loaded genome). Full Agent objects are passed through unchanged.
1204
- const sAgentsRaw = args["agents"] ?? [];
1289
+ // The input_schema advertises BOTH keys: `agents` is the compose-input shape; `agent_slugs`
1290
+ // is the shape the PERSISTED standards/<slug>.json file carries (written at line ~1413).
1291
+ // Reading only `agents` made `agent_slugs` a silent no-op that failed via the misleading
1292
+ // "agent not found" error — and broke the read-file/re-compose round-trip. Fall back to
1293
+ // `agent_slugs` when `agents` is absent so the documented parameter actually composes.
1294
+ const sAgentsRaw = args["agents"] ?? args["agent_slugs"] ?? [];
1205
1295
  const sAgents = sAgentsRaw.map((a) => {
1206
1296
  if (typeof a === "string") {
1207
1297
  const loaded = deps.agents?.get(a);
@@ -1313,6 +1403,20 @@ async function runImpl(slug, args, deps, approval) {
1313
1403
  ...(args["credential_surface"] !== undefined ? { credential_surface: args["credential_surface"] } : {}),
1314
1404
  ...(args["lifecycle"] !== undefined ? { lifecycle: args["lifecycle"] } : {}),
1315
1405
  ...(args["responsible_chair"] !== undefined ? { responsible_chair: args["responsible_chair"] } : {}),
1406
+ // The worker-contract fields, passed through by the same rule: only what was stated, so the
1407
+ // schema keeps ownership of every default (an unstated `mcp_servers`/`devices`/`architectures`
1408
+ // is the EMPTY list, an unstated `substrate`/`floor`/`max_concurrent_chairs` the deployment
1409
+ // default). Read here so a room authored through this tool actually carries its substrate,
1410
+ // and so the advertised schema and the handler stay one statement of the same fact (#234).
1411
+ ...(args["substrate"] !== undefined ? { substrate: args["substrate"] } : {}),
1412
+ ...(args["mcp_servers"] !== undefined ? { mcp_servers: args["mcp_servers"] } : {}),
1413
+ ...(args["devices"] !== undefined ? { devices: args["devices"] } : {}),
1414
+ ...(args["architectures"] !== undefined ? { architectures: args["architectures"] } : {}),
1415
+ ...(args["max_concurrent_chairs"] !== undefined ? { max_concurrent_chairs: args["max_concurrent_chairs"] } : {}),
1416
+ ...(args["floor"] !== undefined ? { floor: args["floor"] } : {}),
1417
+ // NO repo_url here: the repository is the SUBJECT of a RUN, named at dispatch, not an
1418
+ // at-rest venue field. A venue serves many repositories; pinning one on the room would mint
1419
+ // a venue per repository. See VenueObjectSchema's gap note.
1316
1420
  };
1317
1421
  const parsedVenue = VenueSchema.safeParse(venueInputDef);
1318
1422
  if (!parsedVenue.success) {
@@ -1391,8 +1495,32 @@ async function runImpl(slug, args, deps, approval) {
1391
1495
  args["fields_to_add"] ?? {};
1392
1496
  const nextProps = { ...baseProps, ...addProps };
1393
1497
  const nextRequired = extension?.schema?.required ?? baseDef.required_fields;
1498
+ // A MUTATION THAT CHANGES NOTHING SAYS SO. `addProps` reads exactly two shapes —
1499
+ // `extension.schema.properties` and `fields_to_add`. An `extension` supplied in any OTHER
1500
+ // shape (top-level JSON Schema keywords, say) matches neither, so addProps is {} and the
1501
+ // caller's intent is DROPPED — while this handler went on to bump the version and seal
1502
+ // content/effective hashes, reporting "additive: +0 field(s)" as success. The caller is told
1503
+ // the genome moved when it did not, and learns otherwise only downstream, when the thing
1504
+ // they authored never fires. Observed 2026-08-20 authoring a conditional constraint.
1505
+ // Sibling of the agent_evolve no-op reported in #325; both refuse here rather than guess.
1506
+ const addedNothing = Object.keys(addProps).length === 0;
1507
+ const requiredUnchanged = JSON.stringify([...nextRequired].sort()) === JSON.stringify([...baseDef.required_fields].sort());
1508
+ if (addedNothing && requiredUnchanged) {
1509
+ return {
1510
+ ok: false,
1511
+ requires_approval: approval,
1512
+ error: `type_extend would change nothing about "${baseDef.slug}" — no field was added and ` +
1513
+ `required_fields is unchanged, so no version is warranted. Field additions are read ` +
1514
+ `from \`extension.schema.properties\` or \`fields_to_add\`; an extension supplied in ` +
1515
+ `any other shape (e.g. top-level JSON Schema keywords) is not read by this verb.`,
1516
+ };
1517
+ }
1394
1518
  const base = {
1395
- slug: baseDef.slug, version: 1, extends: baseDef.extends, domain: baseDef.domain,
1519
+ // Read the base's OWN version (widened onto DomainType) so proposeTypeChange computes
1520
+ // next_version = base.version + 1 from reality, not a constant. `?? 1` covers the freshly
1521
+ // registered projection that carries no version (always v1). The hardcoded 1 here forced
1522
+ // every extend to next_version 2 — the first half of PR #433 AC6's second-extend defect.
1523
+ slug: baseDef.slug, version: baseDef.version ?? 1, extends: baseDef.extends, domain: baseDef.domain,
1396
1524
  status: "active", schema: { type: "object", properties: baseProps }, required_fields: baseDef.required_fields,
1397
1525
  };
1398
1526
  // THE THIRD DOOR. `{...baseProps, ...addProps}` above is the exact merge #264 is
@@ -1418,10 +1546,37 @@ async function runImpl(slug, args, deps, approval) {
1418
1546
  };
1419
1547
  const proposal = proposeTypeChange(base, next);
1420
1548
  const newFields = Object.keys(nextProps).length - Object.keys(baseProps).length;
1421
- // substrate seal: the new version's identity is recorded in the ledger (file
1422
- // materialization of versioned types follows the version-aware loader path).
1549
+ // substrate seal: PERSIST the extended definition the way type_register does, rather
1550
+ // than only recording its identity in the ledger. The prior `recordIdentity` path
1551
+ // deferred file materialization to a "version-aware loader path" that does not exist —
1552
+ // so `ok:true` named a version nothing wrote: a fresh load still resolved the old
1553
+ // definition, and the sealed genome_mutation row asserted a state the genome did not
1554
+ // hold. `sealDefinition` seals-before-writes and materializes a loadable file, so an
1555
+ // `ok:true` now means the type is resolvable. The loader keys domain types by version
1556
+ // from file CONTENT and DomainTypeMap.get returns the highest-version record, so the
1557
+ // in-place overwrite of `domain_types/<slug>.json` at the bumped version resolves today
1558
+ // with no versioned filename — an `@v` file would be the same defect wearing a version
1559
+ // number. The ledger subject stays the VERSIONED identity `<slug>@v<n>` (its content
1560
+ // hash), while the file is the bare `<slug>.json` the loader reads.
1423
1561
  const versioned = { ...next, version: proposal.next_version };
1424
- const tx = deps.genome_dir ? recordIdentity("type_extend", `${base.slug}@v${proposal.next_version}`, versioned, deps.ledger, args["reason"] != null ? { reason: args["reason"] } : undefined) : undefined;
1562
+ const tx = deps.genome_dir
1563
+ ? sealDefinition("type_extend", `${base.slug}@v${proposal.next_version}`, versioned, deps.ledger, deps.genome_dir, "domain_types", args["reason"] != null ? { reason: args["reason"] } : undefined, base.slug)
1564
+ : undefined;
1565
+ // Refresh the in-memory registry to match what was just sealed to disk. sealDefinition
1566
+ // overwrites domain_types/<slug>.json at the bumped version, but the registry map still
1567
+ // holds the pre-extend entry — so a SECOND type_extend on the same slug would re-read the
1568
+ // stale version and compute next_version from it again (v1→v2→v2 instead of v3). That is
1569
+ // the second half of PR #433 AC6's defect: the write became real, so the collision became
1570
+ // real. Swap just this slug's entry (bumped version + the merged schema/required_fields
1571
+ // just sealed) into a snapshot and replaceTypes it. replaceTypes — not registerType — is
1572
+ // used deliberately: registerType runs reuse enforcement via score(), and an extended type
1573
+ // scores >=80 against its own prior version (score() never inspects slug), so registerType
1574
+ // would REFUSE it. replaceTypes is the existing bypass (genome_reload uses it) and is a
1575
+ // sync, not authorship. See rationale in the sealed change-set re: score()'s slug-blindness.
1576
+ const snapshot = deps.registry.listTypes().map((t) => t.slug === base.slug
1577
+ ? { ...t, version: proposal.next_version, schema: { type: "object", properties: nextProps }, required_fields: [...nextRequired] }
1578
+ : t);
1579
+ deps.registry.replaceTypes(snapshot);
1425
1580
  return { ok: true, requires_approval: proposal.approval_required, data: { new_version: proposal.next_version, changelog_entry: `${proposal.change_class}: +${newFields} field(s)`, change_class: proposal.change_class, effective_hash: tx?.effective_hash, content_hash: tx?.content_hash } };
1426
1581
  }
1427
1582
  case "charter_read": {
@@ -1574,6 +1729,9 @@ async function runImpl(slug, args, deps, approval) {
1574
1729
  domain: d.domain,
1575
1730
  schema: d.schema,
1576
1731
  required_fields: [...d.required_fields],
1732
+ // Carry the on-disk version through the reload so a version decision reads it (mirrors
1733
+ // loadRegistry). Editing domain_types/ and reloading must not reset a type to version-less.
1734
+ version: d.version,
1577
1735
  }));
1578
1736
  const typeDiff = deps.registry.replaceTypes(typeDefs);
1579
1737
  // standards — mutate in place so callers holding deps.standards see updates.
@@ -1948,6 +2106,12 @@ async function runImpl(slug, args, deps, approval) {
1948
2106
  }
1949
2107
  // The substrate loop: validate → canonical hash → (if genome_dir) persist + ledger-seal.
1950
2108
  const sealed = sealAgentDefinition(def, deps.ledger, deps.genome_dir);
2109
+ // Write-through to the LIVE map so a same-session standard_compose resolves this agent
2110
+ // WITHOUT a genome_reload. agent_define wrote agents/<slug>.json but never refreshed
2111
+ // deps.agents, so standard_compose (which resolves slugs from deps.agents) denied an agent
2112
+ // this very tool had just created. This mirrors the write-through standard_compose does for
2113
+ // deps.standards and tool_register does for deps.toolProviders.
2114
+ deps.agents?.set(sealed.agent.slug, sealed.agent);
1951
2115
  return {
1952
2116
  ok: true,
1953
2117
  requires_approval: approval,
@@ -2025,6 +2189,20 @@ async function runImpl(slug, args, deps, approval) {
2025
2189
  const evolveSlug = typeof args["slug"] === "string" ? args["slug"] : undefined;
2026
2190
  const changes = (args["changes"] && typeof args["changes"] === "object")
2027
2191
  ? args["changes"] : undefined;
2192
+ // A MUTATION THAT CHANGES NOTHING SAYS SO (#325). Field edits are read ONLY from a
2193
+ // `changes` object. A caller who puts them at the top level leaves `changes` undefined, the
2194
+ // guarded block below is skipped, and this case used to fall through to `ok: true` with a
2195
+ // `new_version` — success, a version bump, and nothing changed. Refuse instead, naming the
2196
+ // shape, so the caller learns in one call rather than discovering it when the edit is absent.
2197
+ if (evolveSlug && !changes) {
2198
+ return {
2199
+ ok: false,
2200
+ requires_approval: approval,
2201
+ error: `agent_evolve: no \`changes\` object was supplied for "${evolveSlug}", so nothing ` +
2202
+ `would be applied. Wrap the field edits in a \`changes\` object — top-level fields are ` +
2203
+ `not read by this verb, and a call that changes nothing warrants no new version.`,
2204
+ };
2205
+ }
2028
2206
  if (evolveSlug && changes && (deps.genome_dir || deps.agents?.has(evolveSlug))) {
2029
2207
  // The base definition: the genome file when a working tree exists, else the loaded
2030
2208
  // agents map (a hosted surface has no filesystem — the STORE genome is the base,
@@ -2832,6 +3010,49 @@ async function runImpl(slug, args, deps, approval) {
2832
3010
  },
2833
3011
  };
2834
3012
  }
3013
+ case "org_hire": {
3014
+ // NOT the live path — admission is intercepted in callSurfaceTool (which holds the caller
3015
+ // identity and the deps.hireMember backend that dispatchTool never receives). This block
3016
+ // exists, like venue_credential_mint's, so the verb's advertised schema has a matching
3017
+ // handler that reads exactly its two arguments (advertised_args_are_read.test.ts), and it
3018
+ // must live INSIDE this case body so those reads are attributed to org_hire, not the
3019
+ // preceding case. Reaching this at runtime means the surface interception was bypassed —
3020
+ // answer honestly rather than pretend a hire happened.
3021
+ //
3022
+ // ORDER MATTERS: this case sits BEFORE venue_credential_mint so venue_credential_mint stays
3023
+ // the LAST string case in dispatchTool. The advertised_args_are_read parser slices the last
3024
+ // case's body to end-of-file, sweeping in callSurfaceTool's own `args["…"]` reads; keeping
3025
+ // venue last means that slurped tail's bracket reads stay {org_slug, instance} — exactly
3026
+ // venue's schema. (The org_hire callSurfaceTool intercept reads its args by dot access for
3027
+ // the same reason, so it contributes nothing to that tail.)
3028
+ const org_slug = String(args["org_slug"] ?? "");
3029
+ const agent_slug = String(args["agent_slug"] ?? "");
3030
+ return {
3031
+ ok: false, refusal: "no_backend", requires_approval: approval,
3032
+ error: `org_hire is served by the tool surface (createToolSurface), which wires the caller and ` +
3033
+ `the deps.hireMember backend; the bare dispatcher cannot admit agent "${agent_slug}" to ` +
3034
+ `org "${org_slug}". Call it through the surface.`,
3035
+ };
3036
+ }
3037
+ case "venue_credential_mint": {
3038
+ // NOT the live path — minting is intercepted in callSurfaceTool (which holds the caller
3039
+ // identity and the deps.mintVenueCredential backend that dispatchTool never receives).
3040
+ // This block exists so the verb's advertised schema has a matching handler reading exactly
3041
+ // its two arguments, and so it must live INSIDE this case body, not before the label:
3042
+ // tests/advertised_args_are_read.test.ts slices each case's body from its own label to the
3043
+ // next one, so any argument-read text placed above this label would be attributed to the
3044
+ // PRECEDING case (learning_synthesize) — a control reported against a tool that never took
3045
+ // it. Reaching this block at runtime means the surface interception was bypassed — answer
3046
+ // honestly rather than pretend a mint happened.
3047
+ const org_slug = String(args["org_slug"] ?? "");
3048
+ const instance = String(args["instance"] ?? "");
3049
+ return {
3050
+ ok: false, refusal: "no_backend", requires_approval: approval,
3051
+ error: `venue_credential_mint is served by the tool surface (createToolSurface), which wires the ` +
3052
+ `caller and the deps.mintVenueCredential backend; the bare dispatcher cannot mint for ` +
3053
+ `org "${org_slug}" instance "${instance}". Call it through the surface.`,
3054
+ };
3055
+ }
2835
3056
  default:
2836
3057
  return { ok: false, not_implemented: true, requires_approval: approval, error: `"${slug}" has no v0 handler` };
2837
3058
  }
@@ -2876,9 +3097,147 @@ const HOSTED_UPSERT = {
2876
3097
  // class travels the port it is supposed to travel, and the missing half announces itself instead
2877
3098
  // of the engine quietly declining to try. (Store-side work: two tables + two upsert branches.)
2878
3099
  chart_define: { cls: "chart", keys: Object.keys(ChartSchema.shape) },
2879
- venue_define: { cls: "venue", keys: Object.keys(VenueSchema.shape) },
3100
+ venue_define: { cls: "venue", keys: Object.keys(VenueObjectSchema.shape) },
2880
3101
  };
2881
3102
  async function callSurfaceTool(slug, args, deps) {
3103
+ if (slug === "venue_credential_mint") {
3104
+ // The engine half of the venue credential: shape validation and the three refusals around
3105
+ // whatever backend a deployment injects. This fires for ALL callers, not only hosted, because
3106
+ // its refusals are structural facts about the credential — not a hosted-transport concern.
3107
+ //
3108
+ // (a) A gig-scoped caller may NOT mint a venue credential. This is decided from caller identity
3109
+ // alone, BEFORE the backend is reached, because the escalation (a one-lease gig token
3110
+ // minting an org-scoped key that outlives every gig) is a credential-scope fact no store-side
3111
+ // gate catches. A refused mint must never touch the backend.
3112
+ const escalation = gigScopeRefusal(deps.caller);
3113
+ if (escalation) {
3114
+ return {
3115
+ ok: false,
3116
+ refusal: escalation,
3117
+ error: "a gig-scoped credential may not mint a venue credential: a gig token is issued to one " +
3118
+ "agent for one gig and expires with that gig's lease, while a venue credential is " +
3119
+ "org-scoped and outlives every gig. Mint from a member or venue credential instead.",
3120
+ };
3121
+ }
3122
+ // (b) No backend wired → the verb answers honestly rather than throwing, naming the seam to
3123
+ // wire (the same shape gig_dispatch/gig_approve/gig_cancel use when their store seams are
3124
+ // absent). A caller that cannot tell "minting is unwired here" from "your request was bad"
3125
+ // retries the wrong thing forever.
3126
+ if (!deps.mintVenueCredential) {
3127
+ return {
3128
+ ok: false,
3129
+ refusal: "no_backend",
3130
+ error: "no minting backend is wired on this surface — venue_credential_mint ships its schema " +
3131
+ "and refusals, but a deployment supplies the credential. Wire deps.mintVenueCredential " +
3132
+ "(parallel to deps.queueGig) to stand up the worker environment.",
3133
+ };
3134
+ }
3135
+ // (c) Mint, then check the grant is COMPLETE. A backend that answers with a half-set is refused,
3136
+ // not forwarded — handing an incomplete environment back moves the assembly problem to the
3137
+ // caller while looking like success, which is the failure this verb exists to end.
3138
+ let grant;
3139
+ try {
3140
+ grant = await deps.mintVenueCredential({
3141
+ org_slug: String(args["org_slug"] ?? ""),
3142
+ instance: String(args["instance"] ?? ""),
3143
+ });
3144
+ }
3145
+ catch (e) {
3146
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
3147
+ }
3148
+ const missing = missingWorkerEnv(grant.env);
3149
+ if (missing.length) {
3150
+ return {
3151
+ ok: false,
3152
+ refusal: "incomplete_env",
3153
+ error: `the minted grant is missing required worker environment: ${missing.join(", ")}. A grant ` +
3154
+ "that is not complete is refused, not returned — a half-set moves the assembly problem to " +
3155
+ "the caller while looking like success.",
3156
+ };
3157
+ }
3158
+ // (d) The class names pass through UNCHANGED — the engine does not validate class vocabulary
3159
+ // (that is the room contract's job, checked by realize before dispatch). The grant is the
3160
+ // answer, returned exactly once; the engine does not persist it and there is no read-back.
3161
+ return { ok: true, data: grant };
3162
+ }
3163
+ if (slug === "org_hire") {
3164
+ // The engine half of org admission: the two engine-decided refusals and the ledger seal around
3165
+ // whatever backend a deployment injects. Intercepted here (like venue_credential_mint) for ALL
3166
+ // callers, BEFORE the hosted check, so its refusals are structural facts about the act — not a
3167
+ // hosted-transport concern — and so it never reaches a store upsert path.
3168
+ //
3169
+ // These two args are read by DOT access, not `args["…"]`, on purpose: the
3170
+ // advertised_args_are_read parser slurps the LAST dispatchTool case's body to end-of-file and
3171
+ // counts every bracket-with-string-literal read in it as that case's reads (this comment must
3172
+ // not spell that pattern out, or the parser counts the EXPLANATION as an instance of the thing
3173
+ // it explains — which is exactly how it first went red). venue_credential_mint is the last
3174
+ // case (org_hire sits before it precisely to keep it so); a bracket read of `agent_slug` here
3175
+ // would be miscounted as venue reading an arg it does not advertise. Dot access is invisible to
3176
+ // that parser and keeps the slurped tail's reads honestly {org_slug, instance}.
3177
+ const org_slug = String(args.org_slug ?? "");
3178
+ const agent_slug = String(args.agent_slug ?? "");
3179
+ // (a) HIRING IS NEVER SELF-SERVICE. Only a human 'member' caller may admit an agent; a
3180
+ // player/venue/gig token is an AGENT token, and an agent may not admit an agent — that is
3181
+ // the whole point of the verb. Decided from caller identity ALONE, before the backend is
3182
+ // reached, so a refused hire never touches deps.hireMember. Absent caller (a bare surface)
3183
+ // is not a member either, so it is refused too — the gate fails closed.
3184
+ if (deps.caller?.kind !== "member") {
3185
+ return {
3186
+ ok: false,
3187
+ refusal: "not_a_human_member",
3188
+ error: "hiring is never self-service: only a human member may admit an agent to an org. This " +
3189
+ "caller presented an agent token, and an agent may not hire an agent. ADMISSION IS NOT " +
3190
+ "AUTHORITY — a member performs the hire.",
3191
+ };
3192
+ }
3193
+ // (b) No backend wired → the verb answers honestly rather than throwing, naming the seam to
3194
+ // wire (the same shape venue_credential_mint uses). A caller that cannot tell "hiring is
3195
+ // unwired here" from "your request was bad" retries the wrong thing forever.
3196
+ if (!deps.hireMember) {
3197
+ return {
3198
+ ok: false,
3199
+ refusal: "no_backend",
3200
+ error: "no admission backend is wired on this surface — org_hire ships its schema and refusals, " +
3201
+ "but a deployment supplies the insert. Wire deps.hireMember (parallel to " +
3202
+ "deps.mintVenueCredential) to admit the agent to the org.",
3203
+ };
3204
+ }
3205
+ // (c) Admit, then map the store's TYPED answer. Existence (`unknown_agent`) and idempotency
3206
+ // (`already_member`) are facts only the store holds — the engine never checks the
3207
+ // agent_record's status ('named'/'active'), because governance and naming are separate acts
3208
+ // (coltrane-proposer is active yet was never named). Existence is the ONLY precondition, and
3209
+ // it is the store's answer, not the engine's.
3210
+ let result;
3211
+ try {
3212
+ result = await deps.hireMember({ org_slug, agent_slug });
3213
+ }
3214
+ catch (e) {
3215
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
3216
+ }
3217
+ if (!result.ok) {
3218
+ // A refused hire seals NOTHING — the ledger row is written only inside the {ok:true} branch
3219
+ // below, so a hire that did not happen leaves no trace claiming it did.
3220
+ return { ok: false, refusal: result.code };
3221
+ }
3222
+ // (d) SEAL THE ACT to the ledger BEFORE reporting success — a kind:"genome_mutation" row via
3223
+ // recordIdentity (ledger-only, NOT sealDefinition: a hire writes no genome file), so
3224
+ // who-hired-whom, when, and on whose authority lives in the append-only chain rather than
3225
+ // only in a database row. subject_slug is the agent admitted; event is 'org_hire'; org_slug
3226
+ // rides in the hashed detail. #218 — if the audit row does not land, say so (the store row
3227
+ // may already exist), rather than reporting a success whose seal never happened.
3228
+ try {
3229
+ recordIdentity("org_hire", agent_slug, { org_slug, agent_slug }, deps.ledger, { org_slug });
3230
+ }
3231
+ catch (e) {
3232
+ if (e instanceof LedgerError) {
3233
+ return { ok: false, audit_write_failed: true, error: `audit write failed — "org_hire" was NOT sealed: ${e.message}` };
3234
+ }
3235
+ throw e;
3236
+ }
3237
+ // (e) Admission REPORTS the belonging it created — the {org_slug, agent_slug} pair, and nothing
3238
+ // carrying authority.
3239
+ return { ok: true, data: { org_slug, agent_slug } };
3240
+ }
2882
3241
  if (deps.hosted) {
2883
3242
  const blocked = HOSTED_BLOCKED[slug];
2884
3243
  if (blocked)
@@ -2984,6 +3343,13 @@ export function createToolSurface(deps) {
2984
3343
  call: (args) => callSurfaceTool(t.slug, args, deps),
2985
3344
  }));
2986
3345
  }
3346
+ // Reserved relay↔child request schemas (restart guard, venue/8). The MCP SDK keys a request
3347
+ // handler by the `method` literal in its schema; `.passthrough()` lets the full JSON-RPC envelope
3348
+ // (jsonrpc/id/params) pass validation. These carry no params — the relay asks, the child answers
3349
+ // from deps.gig_runs. Registering them as request handlers (not tools) is what keeps them off
3350
+ // tools/list, so they are invisible to Claude Code and callable only by the parent relay.
3351
+ const RunningGigsRequestSchema = z.object({ method: z.literal(RUNNING_GIGS_METHOD) }).passthrough();
3352
+ const AbortForRestartRequestSchema = z.object({ method: z.literal(ABORT_FOR_RESTART_METHOD) }).passthrough();
2987
3353
  /** Build the low-level MCP Server with ListTools + CallTool wired to the tool surface. */
2988
3354
  export function createColtraneServer(deps, recorder) {
2989
3355
  const server = new Server({ name: "coltrane", version: COLTRANE_VERSION }, { capabilities: { tools: {} } });
@@ -3011,6 +3377,45 @@ export function createColtraneServer(deps, recorder) {
3011
3377
  isError: !result.ok,
3012
3378
  };
3013
3379
  });
3380
+ // ── Restart guard (venue/8): the child's half of the relay↔child conversation ──────────────
3381
+ // The stdio relay (src/server_relay.ts) holds NO gig state, so before it kills this child to pick
3382
+ // up new bytes it ASKS the child what a restart would destroy. These two reserved methods answer.
3383
+ // They are deliberately NOT MCP tools — they are not in `createToolSurface`, so they never appear
3384
+ // in tools/list and Claude Code cannot call them; only the parent relay, over the same stdio pipe,
3385
+ // does. That is what keeps the relay's blindness structural: it asks, the child answers from the
3386
+ // ONE authority (deps.gig_runs), the relay acts on the answer.
3387
+ const runningGigIds = () => [...(deps.gig_runs?.entries() ?? [])]
3388
+ .filter(([, s]) => s.status === "running")
3389
+ .map(([id]) => id);
3390
+ // "Which gigs are running right now?" — the pre-restart check. Answering with the empty list is a
3391
+ // POSITIVE statement ("nothing in flight, restart freely"); the relay only treats a NON-answer
3392
+ // (timeout) as unhealthy, never an empty answer.
3393
+ server.setRequestHandler(RunningGigsRequestSchema, async () => ({ running: runningGigIds() }));
3394
+ // The FORCE path: the operator chose to restart with gigs in flight. Abort each running gig and —
3395
+ // the whole point — LEDGER the abort BEFORE this child dies, so a killed gig is a recorded fact
3396
+ // rather than an absence (the defect: two publish seats, gigs 8146142e / 18726459, died mid-phase
3397
+ // with sealed outputs banked and no row saying they were killed). This reuses the EXACT sanctioned
3398
+ // gig_abort path: mark the run, abort its controller, then governanceRow('gig_abort', …).
3399
+ server.setRequestHandler(AbortForRestartRequestSchema, async () => {
3400
+ const aborted = [];
3401
+ for (const [gid, live] of deps.gig_runs?.entries() ?? []) {
3402
+ if (live.status !== "running")
3403
+ continue;
3404
+ live.abort_requested = true;
3405
+ live.abort_reason = "server_restart override";
3406
+ let cancelled = false;
3407
+ if (live.controller) {
3408
+ try {
3409
+ live.controller.abort(live.abort_reason);
3410
+ }
3411
+ catch { /* an already-aborted signal is fine */ }
3412
+ cancelled = true;
3413
+ }
3414
+ deps.ledger.append(governanceRow("gig_abort", gid, { reason: "server_restart override", status: "aborting", cancelled }, gid));
3415
+ aborted.push(gid);
3416
+ }
3417
+ return { aborted };
3418
+ });
3014
3419
  return server;
3015
3420
  }
3016
3421
  /**
@@ -3101,6 +3506,12 @@ export function bootstrapServerDeps(genomeRoot) {
3101
3506
  standards: genome.standards, // ← gig_dispatch can now resolve file-defined standards
3102
3507
  charts: genome.charts, // ← gig_dispatch resolves a chart_slug; chart_browse lists them
3103
3508
  venues: genome.venues, // ← the ceiling a chart's venue imposes has to resolve to something
3509
+ // The production construction of a realizer — the wire from dispatch to the container substrate.
3510
+ // A venue-with-mcp_servers gig is stood up on this (real docker by default; the seam's `run` is
3511
+ // the daemon-free test substitute). Before this, dockerComposeRealizer was defined and reachable
3512
+ // from nowhere in src/, so a venue-named gig got no room. runGig only realizes when the venue
3513
+ // declares servers, so wiring it here changes no server-less venue's behaviour.
3514
+ venueRealizer: dockerComposeRealizer(),
3104
3515
  invoke: makeClaudeInvoker({
3105
3516
  registry,
3106
3517
  model: process.env["COLTRANE_MODEL"],