@eir-labs/coltrane 0.24.30 → 0.24.32

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 (87) hide show
  1. package/agents/change-verifier.json +4 -2
  2. package/agents/deploy-scout.json +2 -2
  3. package/agents/red-law-reviewer.json +54 -0
  4. package/agents/red-spec-attester.json +53 -0
  5. package/agents/red-spec-builder.json +56 -0
  6. package/agents/red-spec-drafter.json +1 -1
  7. package/agents/spec-reviewer.json +1 -1
  8. package/dist/src/chart.js +4 -0
  9. package/dist/src/chart.js.map +1 -1
  10. package/dist/src/chat_completions_port.d.ts +41 -0
  11. package/dist/src/chat_completions_port.js +221 -0
  12. package/dist/src/chat_completions_port.js.map +1 -0
  13. package/dist/src/claude_invoker.d.ts +18 -0
  14. package/dist/src/claude_invoker.js +771 -22
  15. package/dist/src/claude_invoker.js.map +1 -1
  16. package/dist/src/cli.d.ts +1 -1
  17. package/dist/src/cli.js +49 -8
  18. package/dist/src/cli.js.map +1 -1
  19. package/dist/src/completions_invoker.d.ts +79 -0
  20. package/dist/src/completions_invoker.js +220 -0
  21. package/dist/src/completions_invoker.js.map +1 -0
  22. package/dist/src/composition.d.ts +13 -0
  23. package/dist/src/composition.js +16 -0
  24. package/dist/src/composition.js.map +1 -1
  25. package/dist/src/genome_schema.d.ts +200 -0
  26. package/dist/src/genome_schema.js +37 -0
  27. package/dist/src/genome_schema.js.map +1 -1
  28. package/dist/src/genome_store.d.ts +30 -2
  29. package/dist/src/genome_store.js +51 -8
  30. package/dist/src/genome_store.js.map +1 -1
  31. package/dist/src/index.d.ts +5 -0
  32. package/dist/src/index.js +9 -0
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/invoker_selection.d.ts +58 -0
  35. package/dist/src/invoker_selection.js +110 -0
  36. package/dist/src/invoker_selection.js.map +1 -0
  37. package/dist/src/ledger.d.ts +45 -2
  38. package/dist/src/ledger.js +23 -3
  39. package/dist/src/ledger.js.map +1 -1
  40. package/dist/src/mcp.js +2 -2
  41. package/dist/src/mcp.js.map +1 -1
  42. package/dist/src/outputs.d.ts +24 -0
  43. package/dist/src/outputs.js +1 -0
  44. package/dist/src/outputs.js.map +1 -1
  45. package/dist/src/registry.js +16 -0
  46. package/dist/src/registry.js.map +1 -1
  47. package/dist/src/repo_index.d.ts +13 -0
  48. package/dist/src/repo_index.js +90 -10
  49. package/dist/src/repo_index.js.map +1 -1
  50. package/dist/src/reside_backing.d.ts +3 -2
  51. package/dist/src/reside_backing.js +8 -2
  52. package/dist/src/reside_backing.js.map +1 -1
  53. package/dist/src/reuse.d.ts +8 -0
  54. package/dist/src/reuse.js.map +1 -1
  55. package/dist/src/run_deps.d.ts +10 -10
  56. package/dist/src/run_deps.js +25 -15
  57. package/dist/src/run_deps.js.map +1 -1
  58. package/dist/src/runtime.d.ts +205 -109
  59. package/dist/src/runtime.js +908 -145
  60. package/dist/src/runtime.js.map +1 -1
  61. package/dist/src/server.js +148 -45
  62. package/dist/src/server.js.map +1 -1
  63. package/dist/src/server_relay.js +28 -0
  64. package/dist/src/server_relay.js.map +1 -1
  65. package/dist/src/skill_runner.mjs +9 -2
  66. package/dist/src/skill_subprocess.d.ts +20 -1
  67. package/dist/src/skill_subprocess.js +11 -4
  68. package/dist/src/skill_subprocess.js.map +1 -1
  69. package/dist/src/transcript_store.d.ts +14 -0
  70. package/dist/src/transcript_store.js +39 -0
  71. package/dist/src/transcript_store.js.map +1 -0
  72. package/dist/src/turn_loop.d.ts +173 -0
  73. package/dist/src/turn_loop.js +283 -0
  74. package/dist/src/turn_loop.js.map +1 -0
  75. package/dist/src/version.d.ts +1 -1
  76. package/dist/src/version.js +1 -1
  77. package/dist/src/worker.js +6 -0
  78. package/dist/src/worker.js.map +1 -1
  79. package/dist/src/worker_env.d.ts +4 -2
  80. package/dist/src/worker_env.js +42 -0
  81. package/dist/src/worker_env.js.map +1 -1
  82. package/domain_types/change-set.json +33 -22
  83. package/domain_types/red-spec.json +32 -23
  84. package/domain_types/seat-primer.json +65 -0
  85. package/package.json +1 -1
  86. package/standards/build-from-red-spec-v0.json +93 -0
  87. package/standards/draft-red-laws-v0.json +68 -0
@@ -14,7 +14,7 @@ import { RUNNING_GIGS_METHOD, ABORT_FOR_RESTART_METHOD } from "./server_relay.js
14
14
  import { MCP_TOOLS, requiresApproval, AGENT_STATUS_ORDER, STANDARD_STATUS_ORDER, SKILL_STATUS_ORDER, checkPromotion, PromotionError, } from "./mcp.js";
15
15
  import { loadRegistry, domainTypeDefect } from "./registry.js";
16
16
  import { resolveGenome } from "./loader.js";
17
- import { SkillSchema, AgentObjectSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, VenueObjectSchema, venueDefect } from "./genome_schema.js";
17
+ import { SkillSchema, AgentObjectSchema, StandardSchema, DomainTypeSchema, ChartSchema, VenueSchema, VenueObjectSchema, venueDefect, EffortSchema } from "./genome_schema.js";
18
18
  import { composeChart, runChart, chartHash, chartEntrySeedTypes, dispatchTarget, } from "./chart.js";
19
19
  import { runSkillFixtures, executeSkill, loadFixtures } from "./skill_subprocess.js";
20
20
  import { evolveSkill } from "./skills.js";
@@ -27,11 +27,12 @@ import { standardSimulate } from "./simulate.js";
27
27
  import { runGig, BudgetExhausted, GigAborted, ResumeRefused, partialGigUsage, partialBudgetState } from "./runtime.js";
28
28
  import { assembleRunDeps, resolveWorkingRepo } from "./run_deps.js";
29
29
  import { createCheckpointStore, createReuseStore } from "./reuse.js";
30
- import { makeClaudeInvoker, killLiveChairChildren } from "./claude_invoker.js";
30
+ import { killLiveChairChildren } from "./claude_invoker.js";
31
+ import { selectChairInvoker } from "./invoker_selection.js";
31
32
  import { dockerComposeRealizer } from "./venue_realizer.js";
32
33
  import { institutionPlacementResolver } from "./placement_institutions.js";
33
34
  import { isDepth, DEPTHS } from "./pricing.js";
34
- import { ENGINE_MCP_SERVER } from "./tool_providers.js";
35
+ import { ENGINE_MCP_SERVER, isHostBuiltin, toolBaseName } from "./tool_providers.js";
35
36
  import { gigScopeRefusal, missingWorkerEnv, } from "./venue_credential.js";
36
37
  import { composeStandard, defineAgent, CompositionError } from "./composition.js";
37
38
  import { PRIMITIVE_OUTPUT_TYPE } from "./core_types.js";
@@ -134,6 +135,21 @@ function readDepth(v) {
134
135
  return { error: `unknown depth "${String(v)}" — expected one of: ${DEPTHS.join(", ")}` };
135
136
  return { depth: v };
136
137
  }
138
+ /**
139
+ * #seat-effort (F2) — read an optional `effort` argument, mirroring `readDepth`. Absent/empty → no
140
+ * dispatch effort (the agent's own `effort` or its tier default stands). Present but not one of the
141
+ * five levels → an ERROR that names the field and the value, refused BEFORE anything runs: an
142
+ * unlisted effort must never quietly run at a guessed level. Validated against the one Zod source
143
+ * (EffortSchema), so the door and the genome load refuse the same set.
144
+ */
145
+ function readEffort(v) {
146
+ if (v === undefined || v === null || v === "")
147
+ return {};
148
+ const parsed = EffortSchema.safeParse(v);
149
+ if (!parsed.success)
150
+ return { error: `unknown effort "${String(v)}" — expected one of: ${EffortSchema.options.join(", ")}` };
151
+ return { effort: parsed.data };
152
+ }
137
153
  /**
138
154
  * Normalize the user-passed `schema` for type_register (Rob #131).
139
155
  *
@@ -763,16 +779,28 @@ async function runImpl(slug, args, deps, approval) {
763
779
  };
764
780
  }
765
781
  }
766
- // Optional budget arg — when present, runtime enforces per-gig cost-budget
767
- // and raises BudgetExhausted on depletion (PR for T10 gap, see runtime.ts).
768
- const budgetArg = args["budget"];
782
+ // Optional budget arg — a per-gig ceiling in US DOLLARS, { max_usd }. Validated HERE, before
783
+ // anything runs (F1/F2): a malformed budget, or one carrying a retired append-unit field, is
784
+ // REFUSED naming the offending field/value — never silently run with no ceiling.
785
+ const budgetRaw = args["budget"];
769
786
  let budget;
770
- if (budgetArg && typeof budgetArg["opening"] === "number") {
771
- budget = { opening: budgetArg["opening"] };
772
- if (typeof budgetArg["base_cost"] === "number")
773
- budget.base_cost = budgetArg["base_cost"];
774
- if (typeof budgetArg["k"] === "number")
775
- budget.k = budgetArg["k"];
787
+ if (budgetRaw !== undefined) {
788
+ if (typeof budgetRaw !== "object" || budgetRaw === null || Array.isArray(budgetRaw)) {
789
+ return { ok: false, requires_approval: approval, error: `gig_dispatch: budget must be an object naming max_usd in USD; got ${String(budgetRaw)} set { max_usd: <dollars> }` };
790
+ }
791
+ const b = budgetRaw;
792
+ const retired = ["opening", "base_cost", "k", "pool"].filter((f) => f in b);
793
+ if (retired.length > 0) {
794
+ return { ok: false, requires_approval: approval, error: `gig_dispatch: budget carries the retired append-unit field(s) ${retired.map((f) => `"${f}"`).join(", ")} — budgets are now { max_usd } in US dollars; pass max_usd instead` };
795
+ }
796
+ const mu = b["max_usd"];
797
+ if (typeof mu !== "number") {
798
+ return { ok: false, requires_approval: approval, error: `gig_dispatch: budget.max_usd is required and must be a number of USD; got ${mu === undefined ? "undefined" : String(mu)}` };
799
+ }
800
+ if (!Number.isFinite(mu) || mu <= 0) {
801
+ return { ok: false, requires_approval: approval, error: `gig_dispatch: budget.max_usd must be a positive finite number of USD; got ${String(mu)}` };
802
+ }
803
+ budget = { max_usd: mu };
776
804
  }
777
805
  const gigInput = args["input"] ?? {};
778
806
  // #237 — `depth` was advertised here and never read. Every dispatch ran at full depth,
@@ -781,6 +809,25 @@ async function runImpl(slug, args, deps, approval) {
781
809
  if (depthArg.error)
782
810
  return { ok: false, requires_approval: approval, error: depthArg.error };
783
811
  const depth = depthArg.depth;
812
+ // #seat-effort (O1/F2) — `effort` is advertised on gig_dispatch (src/mcp.ts) and read here.
813
+ // An out-of-range value is refused BEFORE the chart/standard path spawns anything (F2); a
814
+ // valid one threads into runGig beside `depth` so the resolver (resolveEffort) sees it.
815
+ const effortArg = readEffort(args["effort"]);
816
+ if (effortArg.error)
817
+ return { ok: false, requires_approval: approval, error: effortArg.error };
818
+ const effort = effortArg.effort;
819
+ // contract-seat-context-ceiling-v1 (O1/F1) — `max_context_tokens` is advertised on gig_dispatch
820
+ // (src/mcp.ts) and read here. A non-positive-integer is refused BEFORE anything spawns, naming
821
+ // the field and the offending value; a valid one threads into runGig beside `effort` so the
822
+ // resolver (resolveMaxContextTokens) sees it. Absent ⇒ no dispatch ceiling.
823
+ const rawCap = args["max_context_tokens"];
824
+ let max_context_tokens;
825
+ if (rawCap !== undefined && rawCap !== null) {
826
+ if (typeof rawCap !== "number" || !Number.isInteger(rawCap) || rawCap <= 0) {
827
+ return { ok: false, requires_approval: approval, error: `gig_dispatch: max_context_tokens must be a positive integer; got ${String(rawCap)}` };
828
+ }
829
+ max_context_tokens = rawCap;
830
+ }
784
831
  // ── reuse a sealed output instead of re-deriving it ──────────────────────────────
785
832
  // Both halves are opt-in, and both are named on the dispatch call so the decision is
786
833
  // recorded where the run is requested rather than inferred from server configuration.
@@ -879,7 +926,12 @@ async function runImpl(slug, args, deps, approval) {
879
926
  // onto each chair's spawn. Absent = the substrate is skipped (server-less venues, or a
880
927
  // bare deps without a realizer wired).
881
928
  ...(deps.venueRealizer ? { venueRealizer: deps.venueRealizer } : {}),
882
- ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
929
+ // The address-stamping tree (records-by-address), carried into the performance so each
930
+ // movement stamps against the same repository root the server was bootstrapped with —
931
+ // runChart forwards it to every movement via `...deps`. Threaded only when present, never
932
+ // process.cwd().
933
+ ...(deps.genome_dir ? { tree_root: deps.genome_dir } : {}),
934
+ ...(depth ? { depth } : {}), ...(effort ? { effort } : {}), ...(max_context_tokens !== undefined ? { max_context_tokens } : {}), ...reuseWiring, ...humanWiring,
883
935
  };
884
936
  /** The ARRANGEMENT's manifest. A chart has no single genome_hash or run_fingerprint — it
885
937
  * has a chart_hash and one run per movement — so the reply says what a chart run is
@@ -956,7 +1008,7 @@ async function runImpl(slug, args, deps, approval) {
956
1008
  if (e instanceof BudgetExhausted) {
957
1009
  const partial = partialGigUsage(e);
958
1010
  return { ok: false, requires_approval: approval, error: e.message,
959
- data: { budget_exhausted: true, agent_slug: e.agent_slug, balance: e.balance, cost: e.cost, budget_state: e.state,
1011
+ data: { budget_exhausted: true, agent_slug: e.agent_slug, unit: e.unit, max_usd: e.max_usd, spent_usd: e.spent_usd, budget_state: e.state,
960
1012
  ...(partial ? { usage: partial } : {}) } };
961
1013
  }
962
1014
  throw e;
@@ -1093,6 +1145,11 @@ async function runImpl(slug, args, deps, approval) {
1093
1145
  toolProviders: deps.toolProviders, mcpServerConfigs: deps.mcpServerConfigs, // dispatch preflight resolves against the invoker's environment
1094
1146
  venue, venues: deps.venues, venueRealizer: deps.venueRealizer,
1095
1147
  repoUrl: dispatchRepoUrl,
1148
+ // The address-stamping tree (records-by-address): the repository root this server was
1149
+ // bootstrapped with. The CLI reaches this same door through `dispatchTool`, so its stamps
1150
+ // resolve against the bootstrapped root too. Never process.cwd(): absent genome_dir → no
1151
+ // tree_root, and a laws/changes seal then refuses `tree_root_unknown`.
1152
+ tree_root: deps.genome_dir,
1096
1153
  });
1097
1154
  // The gig id this run seals under — minted ONCE for both doors so the single-flight lock
1098
1155
  // names the same holder whether the caller blocked (wait:true) or polled (async default).
@@ -1133,7 +1190,7 @@ async function runImpl(slug, args, deps, approval) {
1133
1190
  try {
1134
1191
  const res = await runGig(standard, gigInput, {
1135
1192
  ...dispatchDeps, gig_id: gigId,
1136
- ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
1193
+ ...(depth ? { depth } : {}), ...(effort ? { effort } : {}), ...(max_context_tokens !== undefined ? { max_context_tokens } : {}), ...reuseWiring, ...humanWiring,
1137
1194
  });
1138
1195
  // Terminal (complete) frees the tree; a parked gig (awaiting_approval) RETAINS it.
1139
1196
  if (releaseLock && res.status !== "awaiting_approval")
@@ -1176,7 +1233,7 @@ async function runImpl(slug, args, deps, approval) {
1176
1233
  // stopped, and the operator needs them in the same reply as the depletion notice.
1177
1234
  const partial = partialGigUsage(e);
1178
1235
  return { ok: false, requires_approval: approval, error: e.message,
1179
- data: { budget_exhausted: true, agent_slug: e.agent_slug, balance: e.balance, cost: e.cost, budget_state: e.state,
1236
+ data: { budget_exhausted: true, agent_slug: e.agent_slug, unit: e.unit, max_usd: e.max_usd, spent_usd: e.spent_usd, budget_state: e.state,
1180
1237
  ...(partial ? { usage: partial } : {}) } };
1181
1238
  }
1182
1239
  throw e;
@@ -1226,7 +1283,7 @@ async function runImpl(slug, args, deps, approval) {
1226
1283
  // A wire added to the shared assembler reaches this default async path by construction.
1227
1284
  const runPromise = runGig(standard, gigInput, {
1228
1285
  ...dispatchDeps,
1229
- gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
1286
+ gig_id: gigId, onProgress, signal: controller.signal, ...(depth ? { depth } : {}), ...(effort ? { effort } : {}), ...(max_context_tokens !== undefined ? { max_context_tokens } : {}), ...reuseWiring, ...humanWiring,
1230
1287
  });
1231
1288
  // A REFUSED resume must be answered in THIS reply, not discovered later by polling. The
1232
1289
  // gate throws in runGig's SYNCHRONOUS phase — before its first `await`, which is exactly
@@ -1657,8 +1714,35 @@ async function runImpl(slug, args, deps, approval) {
1657
1714
  const extension = args["extension"] ?? undefined;
1658
1715
  const addProps = extension?.schema?.properties ??
1659
1716
  args["fields_to_add"] ?? {};
1660
- const nextProps = { ...baseProps, ...addProps };
1661
- const nextRequired = extension?.schema?.required ?? baseDef.required_fields;
1717
+ // RECORDS BY ADDRESS — a field can be RETIRED through the genome's mouth, not only added. Each
1718
+ // named field is removed from `properties` AND dropped from `required_fields` in the SAME call
1719
+ // that applies any additions; the merge below then versions through proposeTypeChange, which
1720
+ // classifies a removal as `breaking` (approval required). Two refusals guard it, BEFORE the
1721
+ // version bumps and BEFORE the "changes nothing" guard: a name the type does not DECLARE cannot
1722
+ // be retired, and a field cannot be retired while the same call still REQUIRES it (an explicit
1723
+ // `extension.schema.required` naming it) — a type that requires a field it no longer declares is
1724
+ // the contradiction domainTypeDefect would catch downstream, refused here by name instead.
1725
+ const retire = Array.isArray(args["fields_to_retire"]) ? args["fields_to_retire"] : [];
1726
+ const undeclared = retire.filter((f) => !Object.hasOwn(baseProps, f));
1727
+ if (undeclared.length > 0) {
1728
+ return {
1729
+ ok: false, requires_approval: approval,
1730
+ error: `type_extend cannot retire ${undeclared.map((f) => `"${f}"`).join(", ")} from "${baseDef.slug}" — ` +
1731
+ `the type does not declare ${undeclared.length > 1 ? "those fields" : "that field"}. A retirement names a property the type currently has.`,
1732
+ };
1733
+ }
1734
+ const explicitRequired = extension?.schema?.required;
1735
+ const retiredButRequired = explicitRequired ? retire.filter((f) => explicitRequired.includes(f)) : [];
1736
+ if (retiredButRequired.length > 0) {
1737
+ return {
1738
+ ok: false, requires_approval: approval,
1739
+ error: `type_extend cannot both retire and require ${retiredButRequired.map((f) => `"${f}"`).join(", ")} in one call ` +
1740
+ `on "${baseDef.slug}" — a field cannot be retired while the same call still lists it in required_fields. Drop it from \`required\` to retire it.`,
1741
+ };
1742
+ }
1743
+ const retired = new Set(retire);
1744
+ const nextProps = Object.fromEntries(Object.entries({ ...baseProps, ...addProps }).filter(([k]) => !retired.has(k)));
1745
+ const nextRequired = (explicitRequired ?? baseDef.required_fields).filter((f) => !retired.has(f));
1662
1746
  // A MUTATION THAT CHANGES NOTHING SAYS SO. `addProps` reads exactly two shapes —
1663
1747
  // `extension.schema.properties` and `fields_to_add`. An `extension` supplied in any OTHER
1664
1748
  // shape (top-level JSON Schema keywords, say) matches neither, so addProps is {} and the
@@ -1669,7 +1753,10 @@ async function runImpl(slug, args, deps, approval) {
1669
1753
  // Sibling of the agent_evolve no-op reported in #325; both refuse here rather than guess.
1670
1754
  const addedNothing = Object.keys(addProps).length === 0;
1671
1755
  const requiredUnchanged = JSON.stringify([...nextRequired].sort()) === JSON.stringify([...baseDef.required_fields].sort());
1672
- if (addedNothing && requiredUnchanged) {
1756
+ // A retirement (already validated to remove a declared property) IS a change, even with no
1757
+ // additions and no required-set drift — so the "changes nothing" guard must not fire when one
1758
+ // is present, or a lawful retirement would be refused as a no-op.
1759
+ if (addedNothing && requiredUnchanged && retire.length === 0) {
1673
1760
  return {
1674
1761
  ok: false,
1675
1762
  requires_approval: approval,
@@ -2255,16 +2342,22 @@ async function runImpl(slug, args, deps, approval) {
2255
2342
  built[key] = args[key];
2256
2343
  }
2257
2344
  const def = built;
2258
- // Governance gate: each allowed_tools slug must be registered. tool_propose
2259
- // alone does NOT register; tool_register lands the slug. Unknown slugs are
2260
- // rejected so the cage cannot grant scope to a tool the registry doesn't know.
2345
+ // Governance gate: each allowed_tools slug must be registered OR be a host builtin the
2346
+ // invoker knows (Read, Write(src/**), Bash(npx vitest run:*) judged on the base name).
2347
+ // tool_propose alone does NOT register; tool_register lands the slug. Unknown names are
2348
+ // rejected so the cage cannot grant scope to a tool nothing provides. Host builtins were
2349
+ // refused here until 2026-09-16, so no code-touching seat could be authored through this
2350
+ // surface at all (every one in the genome was committed as a file instead). Registering one
2351
+ // is not the cure: the registry is the engine's own tool surface, and `Read` is not an engine
2352
+ // tool. Admitting a host builtin grants nothing by itself — dispatch still resolves every
2353
+ // grant and fails closed, and a venue still narrows the set to its equipment.
2261
2354
  if (def.allowed_tools && def.allowed_tools.length > 0) {
2262
- const unknown = def.allowed_tools.filter((s) => !REGISTERED_TOOL_SLUGS.has(s));
2355
+ const unknown = def.allowed_tools.filter((s) => !REGISTERED_TOOL_SLUGS.has(s) && !isHostBuiltin(toolBaseName(s)));
2263
2356
  if (unknown.length > 0) {
2264
2357
  return {
2265
2358
  ok: false,
2266
2359
  requires_approval: approval,
2267
- error: `agent_define: unknown/unregistered allowed_tools slug${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")} — call tool_propose then tool_register first`,
2360
+ error: `agent_define: unknown/unregistered allowed_tools slug${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")} — not a host builtin and not registered; call tool_propose then tool_register first`,
2268
2361
  };
2269
2362
  }
2270
2363
  }
@@ -3796,27 +3889,37 @@ export function bootstrapServerDeps(genomeRoot) {
3796
3889
  // Silence admits, so a genome with no institutions/ (which is most of them) is unaffected: every
3797
3890
  // placement is admitted and the run is byte-identical.
3798
3891
  placementResolver: institutionPlacementResolver(genome.institutions ?? new Map()),
3799
- invoke: makeClaudeInvoker({
3892
+ // ONE selector for this door AND the drain (src/invoker_selection.ts). A completions URL in the
3893
+ // environment seats the cheap chat-completions port so `coltrane dispatch` / `gig_dispatch` can
3894
+ // reach a model priced in cents, exactly as `coltrane work` already could; its absence keeps the
3895
+ // host-tool invoker below, configured byte-for-byte as it was — the Claude options are handed to
3896
+ // the selector WHOLE and passed through untouched on that path, so this door's Claude behaviour
3897
+ // is unchanged. The selector also loads the deployment's price table (COLTRANE_PRICES_FILE),
3898
+ // failing startup if it is malformed — the propagation this bootstrap owes.
3899
+ invoke: selectChairInvoker(process.env, {
3800
3900
  registry,
3801
- model: process.env["COLTRANE_MODEL"],
3802
- // #185 — per-agent grant resolution wires each agent's MCP servers into its spawn (coltrane's
3803
- // own server + any the deployment registers in .mcp.json). An unresolvable grant fails closed.
3804
- mcpServerConfigs,
3805
- toolProviders, // the genome→provider bridge (above) makes in_house grants resolvable
3806
- // The production seal path: a model chair SEALS IN-BAND by calling output_write (validated at
3807
- // the full write boundary, corrected in-band), and the invoker captures what passed. The
3808
- // engine server config above is bridged into the spawn and its validate-mode env set, so the
3809
- // chair's output_write adjudicates-not-persists and the runtime seals exactly once.
3810
- sealVia: "output_write",
3811
- // per-chair wall-clock bound; COLTRANE_CHAIR_TIMEOUT_MS overrides for slow deployments
3812
- ...(process.env["COLTRANE_CHAIR_TIMEOUT_MS"] ? { timeout_ms: Number(process.env["COLTRANE_CHAIR_TIMEOUT_MS"]) } : {}),
3813
- // The reserve grant (#329) had no reachable caller: it was built, tested, and set by nothing,
3814
- // so a chair that spent its budget still died silently at the cap. This is the operator-level
3815
- // door to it. Absent = no reserve, which is the prior behaviour exactly an extension nobody
3816
- // asked for is spend nobody authorised. The DURABLE fix is a per-chair `turn_reserve` declared
3817
- // in the standard (PR #331), because a budget is a property of the work rather than of the
3818
- // player; this env is the deployment-level stopgap until that lands, not a substitute for it.
3819
- ...(process.env["COLTRANE_TURN_RESERVE"] ? { turn_reserve: Number(process.env["COLTRANE_TURN_RESERVE"]) } : {}),
3901
+ claude: {
3902
+ registry,
3903
+ model: process.env["COLTRANE_MODEL"],
3904
+ // #185 — per-agent grant resolution wires each agent's MCP servers into its spawn (coltrane's
3905
+ // own server + any the deployment registers in .mcp.json). An unresolvable grant fails closed.
3906
+ mcpServerConfigs,
3907
+ toolProviders, // the genome→provider bridge (above) makes in_house grants resolvable
3908
+ // The production seal path: a model chair SEALS IN-BAND by calling output_write (validated at
3909
+ // the full write boundary, corrected in-band), and the invoker captures what passed. The
3910
+ // engine server config above is bridged into the spawn and its validate-mode env set, so the
3911
+ // chair's output_write adjudicates-not-persists and the runtime seals exactly once.
3912
+ sealVia: "output_write",
3913
+ // per-chair wall-clock bound; COLTRANE_CHAIR_TIMEOUT_MS overrides for slow deployments
3914
+ ...(process.env["COLTRANE_CHAIR_TIMEOUT_MS"] ? { timeout_ms: Number(process.env["COLTRANE_CHAIR_TIMEOUT_MS"]) } : {}),
3915
+ // The reserve grant (#329) had no reachable caller: it was built, tested, and set by nothing,
3916
+ // so a chair that spent its budget still died silently at the cap. This is the operator-level
3917
+ // door to it. Absent = no reserve, which is the prior behaviour exactly an extension nobody
3918
+ // asked for is spend nobody authorised. The DURABLE fix is a per-chair `turn_reserve` declared
3919
+ // in the standard (PR #331), because a budget is a property of the work rather than of the
3920
+ // player; this env is the deployment-level stopgap until that lands, not a substitute for it.
3921
+ ...(process.env["COLTRANE_TURN_RESERVE"] ? { turn_reserve: Number(process.env["COLTRANE_TURN_RESERVE"]) } : {}),
3922
+ },
3820
3923
  }),
3821
3924
  model_version: process.env["COLTRANE_MODEL"] ?? "claude-cli-default",
3822
3925
  skills: genome.skills, // ← skill substrate — runGig resolves agent.skill_slugs into prompt