@eir-labs/coltrane 0.6.2 → 0.7.3

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 (106) hide show
  1. package/README.md +23 -0
  2. package/agents/bill.json +59 -0
  3. package/agents/deploy-agent.json +68 -0
  4. package/agents/deploy-scout.json +40 -0
  5. package/agents/john.json +42 -0
  6. package/agents/lineage-scout-external.json +25 -0
  7. package/agents/lineage-scout-internal.json +32 -0
  8. package/agents/lineage-scribe.json +21 -0
  9. package/agents/lineage-weaver.json +23 -0
  10. package/agents/miles.json +44 -0
  11. package/charts/software-delivery-v1.json +9 -0
  12. package/charts/software-delivery-v2.json +39 -0
  13. package/dist/src/canonical_form.d.ts +23 -0
  14. package/dist/src/canonical_form.js +53 -0
  15. package/dist/src/canonical_form.js.map +1 -1
  16. package/dist/src/chart.d.ts +254 -0
  17. package/dist/src/chart.js +897 -0
  18. package/dist/src/chart.js.map +1 -0
  19. package/dist/src/cli.d.ts +19 -4
  20. package/dist/src/cli.js +132 -9
  21. package/dist/src/cli.js.map +1 -1
  22. package/dist/src/composition.d.ts +24 -0
  23. package/dist/src/composition.js +50 -5
  24. package/dist/src/composition.js.map +1 -1
  25. package/dist/src/genome_schema.d.ts +1203 -166
  26. package/dist/src/genome_schema.js +344 -34
  27. package/dist/src/genome_schema.js.map +1 -1
  28. package/dist/src/genome_store.d.ts +53 -3
  29. package/dist/src/genome_store.js +316 -139
  30. package/dist/src/genome_store.js.map +1 -1
  31. package/dist/src/gig_tracker.d.ts +11 -1
  32. package/dist/src/gig_tracker.js +5 -0
  33. package/dist/src/gig_tracker.js.map +1 -1
  34. package/dist/src/index.d.ts +1 -0
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/ledger.d.ts +22 -0
  38. package/dist/src/ledger.js +4 -0
  39. package/dist/src/ledger.js.map +1 -1
  40. package/dist/src/loader.d.ts +9 -1
  41. package/dist/src/loader.js +105 -5
  42. package/dist/src/loader.js.map +1 -1
  43. package/dist/src/mcp.js +54 -4
  44. package/dist/src/mcp.js.map +1 -1
  45. package/dist/src/output_mirror.d.ts +1 -1
  46. package/dist/src/outputs.d.ts +75 -1
  47. package/dist/src/outputs.js +142 -28
  48. package/dist/src/outputs.js.map +1 -1
  49. package/dist/src/reuse.d.ts +71 -0
  50. package/dist/src/reuse.js +0 -0
  51. package/dist/src/reuse.js.map +1 -1
  52. package/dist/src/runtime.d.ts +91 -2
  53. package/dist/src/runtime.js +236 -18
  54. package/dist/src/runtime.js.map +1 -1
  55. package/dist/src/server.d.ts +16 -0
  56. package/dist/src/server.js +600 -64
  57. package/dist/src/server.js.map +1 -1
  58. package/dist/src/version.d.ts +1 -1
  59. package/dist/src/version.js +1 -1
  60. package/dist/src/worker.d.ts +228 -0
  61. package/dist/src/worker.js +737 -0
  62. package/dist/src/worker.js.map +1 -0
  63. package/domain_types/alignment-plan.json +16 -0
  64. package/domain_types/branch-state.json +21 -0
  65. package/domain_types/change-context.json +39 -0
  66. package/domain_types/change-decision.json +36 -0
  67. package/domain_types/change-plan.json +47 -0
  68. package/domain_types/change-request.json +24 -0
  69. package/domain_types/change-set.json +46 -0
  70. package/domain_types/change-verdict.json +26 -0
  71. package/domain_types/deploy-verdict.json +23 -0
  72. package/domain_types/design-brief.json +37 -0
  73. package/domain_types/design-concept.json +36 -0
  74. package/domain_types/design-definition.json +37 -0
  75. package/domain_types/design-question.json +23 -0
  76. package/domain_types/design-verdict.json +27 -0
  77. package/domain_types/internal-inventory.json +26 -0
  78. package/domain_types/lineage-hit.json +19 -0
  79. package/domain_types/lineage-map.json +32 -0
  80. package/domain_types/lineage-question.json +17 -0
  81. package/domain_types/lineage-record.json +19 -0
  82. package/domain_types/lineage-verdict.json +16 -0
  83. package/domain_types/preview-deployment.json +32 -0
  84. package/institutions/quartet.json +344 -0
  85. package/package.json +4 -1
  86. package/skills/vercel-api/fixtures/error.json +10 -0
  87. package/skills/vercel-api/fixtures/ready.json +10 -0
  88. package/skills/vercel-api/fixtures/unsettled.json +10 -0
  89. package/skills/vercel-api/meta.json +10 -0
  90. package/skills/vercel-api/skill.mjs +63 -0
  91. package/standards/lineage-pass-v1.json +96 -0
  92. package/standards/preview-deploy-v1.json +89 -0
  93. package/standards/product-design-v1.json +122 -0
  94. package/standards/promote-v1.json +41 -0
  95. package/standards/software-change-v1.json +147 -0
  96. package/venues/ci-deploy-room-v1.json +28 -0
  97. package/venues/empty-room-v1.json +19 -0
  98. package/dist/src/conduct_daemon.d.ts +0 -59
  99. package/dist/src/conduct_daemon.js +0 -178
  100. package/dist/src/conduct_daemon.js.map +0 -1
  101. package/dist/src/play_worker.d.ts +0 -24
  102. package/dist/src/play_worker.js +0 -49
  103. package/dist/src/play_worker.js.map +0 -1
  104. package/dist/src/supabase_genome.d.ts +0 -28
  105. package/dist/src/supabase_genome.js +0 -49
  106. package/dist/src/supabase_genome.js.map +0 -1
@@ -0,0 +1,737 @@
1
+ // worker.ts — the drain worker's verb set. The gig table IS the queue; this module is the
2
+ // consumer that turns a queued row into a completed one.
3
+ //
4
+ // The credential model is two keys with two jobs (governor rulings, 2026-08-10):
5
+ // * a ctk_ AGENT token — per-agent, authenticates WHO is working. Claim, genome read, and
6
+ // failure reporting all speak through it; authorization derives from the CHAIR CONTRACT
7
+ // the agent is seated on (the store enforces it — "standards should be authorized on
8
+ // chair contract"). A token narrows the office, never widens it.
9
+ // * a cdk_ DRAIN key — per-ORGANIZATION, because the org is the resource boundary. It is
10
+ // the write path for results (outputs + gig header), consumed by the engine's drain
11
+ // layer via COLTRANE_DRAIN_URL / COLTRANE_DRAIN_KEY, not by this module directly.
12
+ //
13
+ // workOnce runs the claimed gig UNDER THE CLAIMED GIG'S ID (deps.gig_id), so the drained
14
+ // header completes the queue row itself — one record per gig, no parallel bookkeeping. A
15
+ // run that throws is recorded as failed through coltrane_mcp_gig_fail; a worker crash
16
+ // leaves only an expiring lease, which the claim RPC hands to the next worker.
17
+ //
18
+ // The third outcome is the HUMAN SEAT. A run that reaches a chair a person holds parks:
19
+ // coltrane_mcp_gig_park sets the row to awaiting_approval and clears the lease, so the store's
20
+ // approve RPC can re-queue it at once. The approval comes back on the next claim payload, and
21
+ // the durable checkpoint (workerStateRoot) means the approved re-claim RESTORES the chairs that
22
+ // already sealed instead of paying for them twice.
23
+ //
24
+ // THAT LOCAL CHECKPOINT IS ONLY THE FAST PATH. A human-in-the-loop delay is measured in hours
25
+ // or days, and the worker that re-claims is a different process — often on a different machine
26
+ // — from the one that parked. `workerStateRoot()` cannot travel, so on its own it makes the
27
+ // saving conditional on the accident of which box picked the row up.
28
+ //
29
+ // The SINK already is the checkpoint. Every sealed output drained to the org store carries its
30
+ // content_sha, its input_shas, its phase, its agent_slug and its whole `data`; the drained gig
31
+ // header carries the run's genome_hash. So a worker with no local record reconstructs one from
32
+ // the sink (`resumeStateFromDrain` below) and resumes from that. Resume state's home is the
33
+ // store; the local checkpoint demotes to a fast path.
34
+ import * as fs from "node:fs";
35
+ import { homedir, tmpdir } from "node:os";
36
+ import { join } from "node:path";
37
+ import { runGig, ResumeRefused, genomeHash, CORE_TO_PRIMITIVE } from "./runtime.js";
38
+ import { loadRegistry } from "./registry.js";
39
+ import { createOutputStore } from "./outputs.js";
40
+ import { MemoryLedger } from "./ledger.js";
41
+ import { rpcGenomeStore } from "./genome_store.js";
42
+ import { createOutputMirror } from "./output_mirror.js";
43
+ import { PRIMITIVE_OUTPUT_TYPE } from "./core_types.js";
44
+ import { sha256Hex, canonJson, outputContentHash, CANONICAL_FORM_VERSION } from "./canonical_form.js";
45
+ import { createCheckpointStore, producersSha, CHECKPOINT_SCHEMA_VERSION, } from "./reuse.js";
46
+ /**
47
+ * The worker's durable state root: `checkpoints/` for the resume records, `outputs/` + `refs/`
48
+ * for the sealed rows those records name — the sibling layout `createCheckpointStore` documents.
49
+ *
50
+ * BOTH halves have to outlive the process. A worker is a short-lived consumer: it claims one
51
+ * row and exits, and the approved re-claim is a DIFFERENT process. A checkpoint whose outputs
52
+ * the next process cannot read refuses the resume it exists to permit, so the run would be
53
+ * paid for twice — which is the whole cost this store exists to avoid.
54
+ */
55
+ export function workerStateRoot() {
56
+ const override = process.env["COLTRANE_WORKER_CHECKPOINTS"];
57
+ if (override && override.length > 0)
58
+ return override;
59
+ return join(homedir(), ".coltrane", "worker-checkpoints");
60
+ }
61
+ /** Default worker-state TTL, in days. A checkpoint older than this is presumed abandoned. */
62
+ export const DEFAULT_WORKER_STATE_TTL_DAYS = 7;
63
+ /** Resolve the reaper TTL from `COLTRANE_WORKER_STATE_TTL_DAYS`, falling back to the default. */
64
+ export function workerStateTtlDays() {
65
+ const raw = process.env["COLTRANE_WORKER_STATE_TTL_DAYS"];
66
+ if (raw && raw.trim().length > 0) {
67
+ const n = Number(raw);
68
+ if (Number.isFinite(n) && n > 0)
69
+ return n;
70
+ }
71
+ return DEFAULT_WORKER_STATE_TTL_DAYS;
72
+ }
73
+ /**
74
+ * Bound the worker state root's growth. The runtime deletes a checkpoint on SUCCESS, but a
75
+ * FAILED / awaiting-approval / abandoned gig leaves its `checkpoints/<gig>.json` (plus the
76
+ * `outputs/<gig>.jsonl` + `refs/<gig>.jsonl` it names) behind forever — an unbounded disk
77
+ * leak. This drops what is old enough to be presumed abandoned.
78
+ *
79
+ * WHAT IT TOUCHES:
80
+ * - A `checkpoints/<gig>.json` whose MTIME is older than the TTL, together with that gig's
81
+ * sibling `outputs/<gig>.jsonl` and `refs/<gig>.jsonl` — they are dead weight once the
82
+ * checkpoint that named them is gone.
83
+ * - An ORPHAN `outputs/<gig>.jsonl` / `refs/<gig>.jsonl` (no checkpoint file for that gig at
84
+ * all — e.g. a completed gig whose checkpoint was already dropped on success) older than the
85
+ * TTL. This is the other half of the leak: success removes the checkpoint but leaves the rows.
86
+ *
87
+ * WHAT IT NEVER TOUCHES:
88
+ * - Any gig whose checkpoint is FRESHER than the TTL. A parked / awaiting-approval gig's
89
+ * checkpoint is LOAD-BEARING for the approved resume, and the TTL is the "reasonable window"
90
+ * that protects it — the reaper cannot see the org store's row status locally, so mtime is
91
+ * the proxy: recent state is kept. A fresh checkpoint's outputs are never swept even if the
92
+ * outputs file itself looks old (a restore-only resume re-touches the checkpoint but appends
93
+ * no new rows), because the sweep is driven from checkpoint age, not output age.
94
+ *
95
+ * Best-effort by construction: every filesystem op is caught and recorded in `errors`; the
96
+ * function never throws, so a reap failure can never fail the claim it runs ahead of.
97
+ */
98
+ export function reapWorkerState(root, opts) {
99
+ const result = { checkpoints_removed: [], orphans_removed: [], kept: 0, errors: [] };
100
+ const ttlDays = opts?.ttlDays ?? workerStateTtlDays();
101
+ const now = opts?.now ?? Date.now();
102
+ const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
103
+ const cpDir = join(root, "checkpoints");
104
+ const outDir = join(root, "outputs");
105
+ const refDir = join(root, "refs");
106
+ const listDir = (dir) => {
107
+ try {
108
+ return fs.existsSync(dir) ? fs.readdirSync(dir) : [];
109
+ }
110
+ catch (e) {
111
+ result.errors.push(`readdir ${dir}: ${e instanceof Error ? e.message : String(e)}`);
112
+ return [];
113
+ }
114
+ };
115
+ const ageMs = (file) => {
116
+ try {
117
+ return now - fs.statSync(file).mtimeMs;
118
+ }
119
+ catch (e) {
120
+ result.errors.push(`stat ${file}: ${e instanceof Error ? e.message : String(e)}`);
121
+ return null;
122
+ }
123
+ };
124
+ const rm = (file) => {
125
+ try {
126
+ fs.rmSync(file, { force: true });
127
+ }
128
+ catch (e) {
129
+ result.errors.push(`rm ${file}: ${e instanceof Error ? e.message : String(e)}`);
130
+ }
131
+ };
132
+ // Pass 1 — checkpoint-driven. A gig id is exactly a checkpoint file's basename.
133
+ const liveGigs = new Set();
134
+ for (const name of listDir(cpDir)) {
135
+ if (!name.endsWith(".json"))
136
+ continue;
137
+ const gig = name.slice(0, -".json".length);
138
+ const age = ageMs(join(cpDir, name));
139
+ if (age === null) {
140
+ liveGigs.add(gig);
141
+ continue;
142
+ } // couldn't stat → keep, don't guess
143
+ if (age > ttlMs) {
144
+ rm(join(cpDir, name));
145
+ rm(join(outDir, `${gig}.jsonl`));
146
+ rm(join(refDir, `${gig}.jsonl`));
147
+ result.checkpoints_removed.push(gig);
148
+ }
149
+ else {
150
+ // Fresh checkpoint — load-bearing. Its gig's rows are protected regardless of their own age.
151
+ liveGigs.add(gig);
152
+ result.kept += 1;
153
+ }
154
+ }
155
+ // Pass 2 — orphan rows: an outputs/refs file for a gig that has NO checkpoint at all.
156
+ const sweepOrphans = (dir) => {
157
+ for (const name of listDir(dir)) {
158
+ if (!name.endsWith(".jsonl"))
159
+ continue;
160
+ const gig = name.slice(0, -".jsonl".length);
161
+ if (liveGigs.has(gig))
162
+ continue; // a live checkpoint owns it — keep
163
+ if (result.checkpoints_removed.includes(gig))
164
+ continue; // already removed as a sibling
165
+ const age = ageMs(join(dir, name));
166
+ if (age === null || age <= ttlMs)
167
+ continue;
168
+ rm(join(dir, name));
169
+ if (!result.orphans_removed.includes(gig))
170
+ result.orphans_removed.push(gig);
171
+ }
172
+ };
173
+ sweepOrphans(outDir);
174
+ sweepOrphans(refDir);
175
+ return result;
176
+ }
177
+ async function workerRpc(ctx, fn, body) {
178
+ const res = await fetch(`${ctx.baseUrl}/rest/v1/rpc/${fn}`, {
179
+ method: "POST",
180
+ headers: {
181
+ apikey: ctx.anonKey,
182
+ // A ctk_ bearer is not a JWT — it authenticates inside the definer RPC via the body;
183
+ // the transport rides the anon key.
184
+ Authorization: `Bearer ${ctx.anonKey}`,
185
+ "Content-Type": "application/json",
186
+ },
187
+ body: JSON.stringify(body),
188
+ });
189
+ const text = await res.text();
190
+ if (!res.ok) {
191
+ let message = text || `store error ${res.status}`;
192
+ try {
193
+ const parsed = JSON.parse(text);
194
+ if (parsed.message)
195
+ message = parsed.message;
196
+ }
197
+ catch { /* keep the raw text */ }
198
+ throw new Error(`${fn}: ${message}`);
199
+ }
200
+ return text ? JSON.parse(text) : null;
201
+ }
202
+ /** Atomically claim the oldest runnable gig (queued, or running with an expired lease) the
203
+ * seated agent's chair contract authorizes. Null means the queue holds nothing for us. */
204
+ export async function claimNextGig(ctx) {
205
+ const out = await workerRpc(ctx, "coltrane_mcp_claim", {
206
+ p_bearer: ctx.agentToken,
207
+ p_worker: ctx.worker ?? null,
208
+ });
209
+ return out ?? null;
210
+ }
211
+ /**
212
+ * Release the lease on a PARKED gig: the row goes `awaiting_approval` and its lease clears, so
213
+ * an approval can re-queue it immediately instead of waiting the lease out. Deliberately NOT
214
+ * `gig_fail` — a run waiting on a person is not a failed run, and recording it as one both lies
215
+ * to the operator and takes the row out of the approve→requeue path.
216
+ *
217
+ * False means the store did not record the release — including a store that has not deployed
218
+ * the RPC yet. The run's own drained header already carries `awaiting_approval`, so an absent
219
+ * release is a missing convenience, not a lost fact, and must not fail the claim.
220
+ */
221
+ export async function parkGig(ctx, gig_id) {
222
+ try {
223
+ const out = await workerRpc(ctx, "coltrane_mcp_gig_park", {
224
+ p_bearer: ctx.agentToken,
225
+ p_gig: gig_id,
226
+ });
227
+ return out === true;
228
+ }
229
+ catch (e) {
230
+ const message = e instanceof Error ? e.message : String(e);
231
+ // PostgREST answers an undeployed function with PGRST202 / "Could not find the function".
232
+ if (/PGRST202|could not find the function|does not exist/i.test(message))
233
+ return false;
234
+ throw e;
235
+ }
236
+ }
237
+ /** Record a failed run on the claimed row. True iff the store recorded it (row was running). */
238
+ export async function failGig(ctx, gig_id, error) {
239
+ const out = await workerRpc(ctx, "coltrane_mcp_gig_fail", {
240
+ p_bearer: ctx.agentToken,
241
+ p_gig: gig_id,
242
+ p_error: error,
243
+ });
244
+ return out === true;
245
+ }
246
+ /** The sink's sealed rows for one gig. `[]` covers "no drain to read" as well as "nothing drained". */
247
+ export async function fetchDrainedOutputs(ctx, gig_id) {
248
+ let out;
249
+ try {
250
+ out = await workerRpc(ctx, "coltrane_mcp_gig_outputs", { p_bearer: ctx.agentToken, p_gig: gig_id });
251
+ }
252
+ catch (e) {
253
+ // Every failure here is the same kind of event: resume state could not be read. The queue
254
+ // row is still runnable work, so this never fails the claim — it costs a cold run, which is
255
+ // exactly what the worker did before this path existed.
256
+ return { rows: [], error: e instanceof Error ? e.message : String(e) };
257
+ }
258
+ if (!Array.isArray(out))
259
+ return { rows: [] };
260
+ return { rows: out.filter((r) => !!r && typeof r === "object") };
261
+ }
262
+ /**
263
+ * The `genome_hash` the sink recorded on this gig's drained HEADER.
264
+ *
265
+ * This is the only identity a drained gig carries, and reading it is what keeps the
266
+ * reconstruction from being a splice. `genomeHash` folds the standard's whole phase graph and
267
+ * every bound agent's type surface — chair `depends_on`, `input_contract`, an added or removed
268
+ * phase — none of which reaches an individual row's `content_sha`. Without this check a
269
+ * pipeline could be re-wired between the park and the approval and the restored outputs would
270
+ * be consumed by chairs that never produced them, with nothing in the manifest recording it.
271
+ *
272
+ * STATED GAP: the header carries no `producers_sha`, so a rewritten agent `method` (or a
273
+ * rewritten skill under a stable version) is invisible to this path — the very hole
274
+ * `RunIdentity.producers_sha` exists to close for a LOCAL checkpoint. A drain-reconstructed
275
+ * resume is therefore a weaker gate than a local one by exactly that much, and the strongest
276
+ * available check is the one applied: the sink's structural hash plus a per-row re-seal.
277
+ */
278
+ export async function fetchDrainedGenomeHash(ctx, gig_id) {
279
+ let out;
280
+ try {
281
+ out = await workerRpc(ctx, "coltrane_mcp_gig_status", { p_bearer: ctx.agentToken, p_gig: gig_id });
282
+ }
283
+ catch (e) {
284
+ return { error: e instanceof Error ? e.message : String(e) };
285
+ }
286
+ // The RPC may answer with the header row or a single-row array; both are the same fact.
287
+ const row = Array.isArray(out) ? out[0] : out;
288
+ if (!row || typeof row !== "object")
289
+ return {};
290
+ const gh = row["genome_hash"];
291
+ // 64 hex or nothing: the header stores `null` for a run with no hash, and the ledger writes
292
+ // "n/a" in places. Neither is a genome identity, and treating one as if it were is the bug.
293
+ return typeof gh === "string" && /^[0-9a-f]{64}$/.test(gh) ? { genome_hash: gh } : {};
294
+ }
295
+ /**
296
+ * Re-derive every chair's seal specs from the loaded standard.
297
+ *
298
+ * This mirrors the runtime's own derivation chair-kind for chair-kind (`outputSpecsFor`, the
299
+ * skill-backed branch of `prepareChair`, and the human-seat branch of the phase loop), because
300
+ * `content_sha` folds exactly what it produces. Divergence here would not be a cosmetic
301
+ * mismatch — it would compute a different sha for an unchanged row and refuse every resume.
302
+ */
303
+ function chairSeats(standard, outputs) {
304
+ const seats = [];
305
+ for (const phase of standard.phases) {
306
+ for (const chair of phase.chairs) {
307
+ const specs = new Map();
308
+ if (chair.human === true && (chair.agent_slug ?? "") === "") {
309
+ const dt = chair.output_contract[0] ?? "Judgment";
310
+ const core = outputs.coreTypeOf(dt) ?? dt;
311
+ specs.set(dt, { core_type: core, primitive: CORE_TO_PRIMITIVE[core] ?? "JUDGE", domain: standard.domain });
312
+ seats.push({ chair, phase: phase.name, producer: { kind: "human" }, specs });
313
+ continue;
314
+ }
315
+ if (chair.skill_slug && (chair.agent_slug ?? "") === "") {
316
+ const dt = chair.output_contract[0] ?? "Signal";
317
+ const core = outputs.coreTypeOf(dt) ?? "Signal";
318
+ specs.set(dt, { core_type: core, primitive: CORE_TO_PRIMITIVE[core] ?? "SENSE", domain: standard.domain });
319
+ seats.push({ chair, phase: phase.name, producer: { kind: "skill", slug: chair.skill_slug }, specs });
320
+ continue;
321
+ }
322
+ const agent = standard.agents.find((a) => a.slug === chair.agent_slug);
323
+ const fallback = agent?.primitives[0];
324
+ // A chair whose agent the genome no longer holds could not have sealed anything under
325
+ // THIS genome. Skipping it means its rows find no seat and the reconstruction refuses,
326
+ // which is the honest outcome.
327
+ if (!agent || !fallback)
328
+ continue;
329
+ const wanted = chair.output_contract.length
330
+ ? agent.output_types.filter((t) => chair.output_contract.includes(t))
331
+ : agent.output_types;
332
+ const domain = agent.domain ?? standard.domain;
333
+ for (const dt of wanted) {
334
+ const core = outputs.coreTypeOf(dt) ?? PRIMITIVE_OUTPUT_TYPE[fallback];
335
+ specs.set(dt, { core_type: core, primitive: CORE_TO_PRIMITIVE[core] ?? fallback, domain });
336
+ }
337
+ seats.push({ chair, phase: phase.name, producer: { kind: "agent", slug: agent.slug }, specs });
338
+ }
339
+ }
340
+ return seats;
341
+ }
342
+ /**
343
+ * The run identity a COLD run of this claim would compute, field for field.
344
+ *
345
+ * Kept adjacent to the `runGig` call in `workOnce` on purpose: `model_version` and `depth` are
346
+ * that call's defaults (it passes neither), and `skills: []` is what `resolvedSkillHashes()`
347
+ * folds for a worker that registers no `skill_dirs` — a store-loaded skill has no code half by
348
+ * construction. Change what `workOnce` passes and this has to move with it, or every
349
+ * reconstruction is refused for identity drift against its own run.
350
+ */
351
+ function coldRunIdentity(standard, gigInput) {
352
+ return {
353
+ standard_slug: standard.slug,
354
+ genome_hash: genomeHash(standard),
355
+ producers_sha: producersSha({ agents: standard.agents, skills: [] }),
356
+ gig_input_sha: sha256Hex(canonJson(gigInput)),
357
+ model_version: "unknown",
358
+ depth: "",
359
+ canonical_form_version: CANONICAL_FORM_VERSION,
360
+ };
361
+ }
362
+ /** Accumulate one restored record onto its chair's checkpoint role, whether written or adopted. */
363
+ function noteRole(byRole, p, output_id, content_sha) {
364
+ const role = p.seat.chair.role;
365
+ const cur = byRole.get(role) ?? {
366
+ role, phase: p.seat.phase,
367
+ output_ids: [], content_shas: [], domain_types: [], type_fingerprints: [],
368
+ sealed_at: p.row.created_at,
369
+ };
370
+ cur.output_ids.push(output_id);
371
+ cur.content_shas.push(content_sha);
372
+ cur.domain_types.push(p.row.domain_type);
373
+ cur.type_fingerprints.push(p.fingerprint);
374
+ if (p.row.created_at > cur.sealed_at)
375
+ cur.sealed_at = p.row.created_at;
376
+ byRole.set(role, cur);
377
+ }
378
+ /**
379
+ * Turn the sink's sealed rows into a resume checkpoint — or refuse, with a reason.
380
+ *
381
+ * ALL-OR-NOTHING, in two passes. Pass one derives and verifies every row while nothing is
382
+ * durable; pass two writes. A gig whose second row fails must not leave its first one in the
383
+ * local store seeding a half-resume, which is the same invariant #243 gave a single chair, one
384
+ * scope up.
385
+ *
386
+ * ROLE MAPPING. The sink does not record `from_role`, so each row is mapped to a chair by
387
+ * `phase` + the chair's SEAT: `agent_slug` for an agent chair, the skill slug for a skill-backed
388
+ * one, and name-agnostically for a human chair (its record seals under the approving principal,
389
+ * whom the genome cannot know). The row's `domain_type` narrows further — a chair that does not
390
+ * seal that type is not a candidate. Zero candidates or MORE THAN ONE both refuse: a guess about
391
+ * which chair produced a sealed output is a guess about the provenance chain.
392
+ *
393
+ * SHA VERIFICATION. Every row is re-sealed under the derived core/primitive/domain and the sha
394
+ * compared to the one the sink recorded. A mismatch refuses the WHOLE reconstruction — a sink
395
+ * row that no longer hashes to its claimed sha must never silently seed a resume, and one such
396
+ * row is evidence about the sink, not about that row alone.
397
+ */
398
+ export function resumeStateFromDrain(args) {
399
+ const { gig_id, standard, identity, rows, outputs } = args;
400
+ if (rows.length === 0)
401
+ return { ok: false, reason: "the sink holds no sealed outputs for this gig" };
402
+ const seats = chairSeats(standard, outputs);
403
+ // created_at order, so a row's in-gig predecessors are already re-written when it is written.
404
+ const ordered = [...rows].sort((a, b) => (a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : 0));
405
+ const planned = [];
406
+ for (const row of ordered) {
407
+ const phase = typeof row.phase === "string" && row.phase !== "" ? row.phase : undefined;
408
+ if (phase === undefined) {
409
+ return { ok: false, reason: `sink row "${row.id}" records no phase, so it cannot be mapped to a chair` };
410
+ }
411
+ if (typeof row.content_sha !== "string" || !row.data || typeof row.data !== "object") {
412
+ return { ok: false, reason: `sink row "${row.id}" is not a sealed output shape (content_sha + data)` };
413
+ }
414
+ const candidates = seats.filter((s) => s.phase === phase && s.specs.has(row.domain_type) &&
415
+ (s.producer.kind === "human" || s.producer.slug === row.agent_slug));
416
+ if (candidates.length === 0) {
417
+ return {
418
+ ok: false,
419
+ reason: `no chair in phase "${phase}" seals "${row.domain_type}" for producer "${row.agent_slug}" — the standard has moved since that output sealed`,
420
+ };
421
+ }
422
+ if (candidates.length > 1) {
423
+ return {
424
+ ok: false,
425
+ reason: `chairs [${candidates.map((c) => c.chair.role).join(", ")}] in phase "${phase}" could each have sealed "${row.domain_type}" — the sink records no from_role, so the mapping is ambiguous`,
426
+ };
427
+ }
428
+ const seat = candidates[0];
429
+ const spec = seat.specs.get(row.domain_type);
430
+ // The registry moves independently of the standard, so genome_hash does not see a type that
431
+ // changed shape. Same fingerprint tool the local resume gate and the reuse cache use.
432
+ const fingerprint = outputs.typeFingerprint(row.domain_type);
433
+ if (fingerprint === "") {
434
+ return { ok: false, reason: `the registry can no longer describe type "${row.domain_type}", so the sink's row cannot be checked` };
435
+ }
436
+ const gate = outputs.validateWrite({ core_type: spec.core_type, domain_type: row.domain_type, data: row.data });
437
+ if (!gate.valid) {
438
+ return { ok: false, reason: `sink row "${row.id}" would not pass the seal boundary today — ${gate.reason}` };
439
+ }
440
+ const sha = outputContentHash({
441
+ core_type: spec.core_type,
442
+ domain_type: row.domain_type,
443
+ domain_type_version: 1,
444
+ domain: spec.domain,
445
+ primitive: spec.primitive,
446
+ phase,
447
+ agent_slug: row.agent_slug,
448
+ data: row.data,
449
+ });
450
+ if (sha !== row.content_sha) {
451
+ return {
452
+ ok: false,
453
+ reason: `sink row "${row.id}" ("${row.domain_type}", phase "${phase}") re-seals to a different content_sha than the sink recorded — the row no longer hashes to its claimed content_sha`,
454
+ };
455
+ }
456
+ planned.push({ row, seat, spec, fingerprint });
457
+ }
458
+ // Pass two — durable. The resume gate resolves a checkpoint's `output_ids` against the LOCAL
459
+ // output store, so the sink's rows have to become local records before they can be restored.
460
+ //
461
+ // IDEMPOTENT. The local store may already hold these rows: the checkpoint file can go missing
462
+ // while `outputs/<gig_id>.jsonl` survives (a swallowed checkpoint write, a cleared checkpoints
463
+ // dir), and appending second copies would leave `output_query` and `output_trace` reporting a
464
+ // gig that sealed each record twice. An existing IN-GIG record with the same content_sha IS
465
+ // that record — content_sha folds core, type, version, domain, primitive, phase, agent_slug and
466
+ // data, so an identical sha is an identical derivation — and it is adopted by id.
467
+ const alreadyHeld = new Map();
468
+ for (const rec of outputs.all()) {
469
+ if (rec.gig_id === gig_id && !alreadyHeld.has(rec.content_sha))
470
+ alreadyHeld.set(rec.content_sha, rec.id);
471
+ }
472
+ const byRole = new Map();
473
+ const shaToId = new Map();
474
+ for (const p of planned) {
475
+ const adopted = alreadyHeld.get(p.row.content_sha);
476
+ if (adopted !== undefined) {
477
+ shaToId.set(p.row.content_sha, adopted);
478
+ noteRole(byRole, p, adopted, p.row.content_sha);
479
+ continue;
480
+ }
481
+ const inputShas = (p.row.input_shas ?? []).map((s) => (typeof s === "string" ? s : ""));
482
+ const mapped = inputShas.map((s) => shaToId.get(s));
483
+ // Object identity is machine-local: the sink's own output ids name nothing here, and the ids
484
+ // of the rows we are writing are fresh. The HASH chain does travel — each `input_sha` names
485
+ // the content a row consumed — so remap ids only when every entry resolves, and otherwise
486
+ // keep the engine-stamped hashes and leave `input_refs` empty rather than emit a
487
+ // half-aligned pair.
488
+ const remapped = mapped.every((id) => typeof id === "string") ? mapped : undefined;
489
+ let rec;
490
+ try {
491
+ rec = outputs.write({
492
+ core_type: p.spec.core_type,
493
+ domain_type: p.row.domain_type,
494
+ domain_type_version: 1,
495
+ domain: p.spec.domain,
496
+ gig_id,
497
+ agent_slug: p.row.agent_slug,
498
+ from_role: p.seat.chair.role,
499
+ phase: p.seat.phase,
500
+ primitive: p.spec.primitive,
501
+ data: p.row.data,
502
+ input_refs: remapped ?? [],
503
+ input_shas: inputShas,
504
+ });
505
+ }
506
+ catch (e) {
507
+ // Unreachable: `validateWrite` above is the same gate `write` runs, from one implementation.
508
+ return { ok: false, reason: `sink row "${p.row.id}" could not be written locally — ${e instanceof Error ? e.message : String(e)}` };
509
+ }
510
+ if (rec.content_sha !== p.row.content_sha) {
511
+ return { ok: false, reason: `sink row "${p.row.id}" sealed locally to a different content_sha than the sink recorded` };
512
+ }
513
+ shaToId.set(rec.content_sha, rec.id);
514
+ noteRole(byRole, p, rec.id, rec.content_sha);
515
+ }
516
+ return {
517
+ ok: true,
518
+ checkpoint: {
519
+ schema_version: CHECKPOINT_SCHEMA_VERSION,
520
+ gig_id,
521
+ identity,
522
+ // The sink's own timestamps. `prior_usage` is deliberately absent: the outputs RPC carries
523
+ // no cost fields, so what the earlier attempt spent is genuinely unknown here, and an
524
+ // invented zero would be the "not captured reported as $0.00" defect #235/#236 removed.
525
+ started_at: ordered[0].created_at,
526
+ updated_at: ordered[ordered.length - 1].created_at,
527
+ roles: [...byRole.values()],
528
+ },
529
+ };
530
+ }
531
+ /** Read the sink and rebuild this claim's resume state, or say why it cannot be rebuilt. */
532
+ async function rebuildFromDrain(ctx, claim, standard, outputs) {
533
+ const drained = await fetchDrainedOutputs(ctx, claim.gig_id);
534
+ if (drained.error !== undefined)
535
+ return { ok: false, reason: `the sink's outputs could not be read — ${drained.error}` };
536
+ if (drained.rows.length === 0)
537
+ return { ok: false, reason: "the sink holds no sealed outputs for this gig" };
538
+ const header = await fetchDrainedGenomeHash(ctx, claim.gig_id);
539
+ const current = genomeHash(standard);
540
+ if (header.genome_hash === undefined) {
541
+ // A miss is free; a wrong hit is not. An identity that cannot be checked resolves to doing
542
+ // the work — the same asymmetry every other substitution gate in this engine resolves on.
543
+ return {
544
+ ok: false,
545
+ reason: `the sink reports no genome_hash for this gig${header.error !== undefined ? ` (${header.error})` : ""}, so the pipeline those outputs sealed under cannot be checked`,
546
+ };
547
+ }
548
+ if (header.genome_hash !== current) {
549
+ return {
550
+ ok: false,
551
+ reason: `the genome moved since those outputs sealed (sink genome_hash="${header.genome_hash}" current="${current}")`,
552
+ };
553
+ }
554
+ return resumeStateFromDrain({
555
+ gig_id: claim.gig_id,
556
+ standard,
557
+ identity: coldRunIdentity(standard, claim.input),
558
+ rows: drained.rows,
559
+ outputs,
560
+ });
561
+ }
562
+ /**
563
+ * Map the claim's per-role approval entries onto runGig's two arguments.
564
+ *
565
+ * The store keys each verdict by ROLE (a standard may hold more than one human chair) while
566
+ * runGig takes a single `approved_by` for the run. So the name is read from the entry for the
567
+ * chair this claim will actually reach — the first human chair the checkpoint does not already
568
+ * hold — and falls back to the first entry when that is not discernible. Attribution on a seal
569
+ * is not decoration: the approval output carries it as its `agent_slug`.
570
+ */
571
+ export function approvalWiring(approvals, standard, completedRoles = []) {
572
+ const entries = Object.entries(approvals ?? {}).filter(([, e]) => e && typeof e.verdict === "object" && e.verdict !== null);
573
+ if (entries.length === 0)
574
+ return {};
575
+ const held = new Set(completedRoles);
576
+ const awaitingRole = standard.phases
577
+ .flatMap((p) => p.chairs)
578
+ .find((c) => c.human === true && !held.has(c.role))?.role;
579
+ const named = (awaitingRole === undefined ? undefined : entries.find(([role]) => role === awaitingRole)) ?? entries[0];
580
+ const approved_by = named[1].approved_by;
581
+ return {
582
+ approvals: Object.fromEntries(entries.map(([role, e]) => [role, e.verdict])),
583
+ ...(typeof approved_by === "string" && approved_by !== "" ? { approved_by } : {}),
584
+ };
585
+ }
586
+ /** One unit of work: claim → load the org genome (as the agent) → run under the claimed
587
+ * gig's id → results drain via the org drain key (engine drain layer, env-configured), or
588
+ * the failure is recorded. Never throws for a run failure — a thrown claim/store error
589
+ * means the worker itself could not speak to the store.
590
+ *
591
+ * A run that reaches an unapproved human chair PARKS: the row is released (parkGig) and the
592
+ * outcome is `awaiting_approval` — its own status, because it is neither finished nor broken. */
593
+ export async function workOnce(ctx, deps) {
594
+ const log = deps.log ?? (() => { });
595
+ // Best-effort bounded reap of the worker state root before we claim. Wrapped so a reap
596
+ // failure can never fail a claim — the money-losing outcome is a run refused, not a file kept.
597
+ try {
598
+ const reaped = reapWorkerState(workerStateRoot());
599
+ if (reaped.checkpoints_removed.length > 0 || reaped.orphans_removed.length > 0) {
600
+ log(`reaped worker state: ${reaped.checkpoints_removed.length} checkpoint(s), ${reaped.orphans_removed.length} orphan row(s)`);
601
+ }
602
+ }
603
+ catch (e) {
604
+ log(`worker-state reap skipped: ${e instanceof Error ? e.message : String(e)}`);
605
+ }
606
+ const claim = await claimNextGig(ctx);
607
+ if (!claim)
608
+ return { claimed: false };
609
+ log(`claimed ${claim.gig_id} (${claim.standard_slug}, ${claim.mode}) as ${claim.acting_for}`);
610
+ try {
611
+ const genome = await rpcGenomeStore(ctx).load();
612
+ const standard = genome.standards.get(claim.standard_slug);
613
+ if (!standard) {
614
+ throw new Error(`claimed standard "${claim.standard_slug}" is not in the org genome this token can read` +
615
+ (genome.load_errors.length ? ` (${genome.load_errors.length} load error(s) — system_health has them)` : ""));
616
+ }
617
+ const registry = loadRegistry(genome);
618
+ const stateRoot = workerStateRoot();
619
+ // The mirror tier is where OUTPUT drain lives (the header drains from the runtime
620
+ // directly) — without it a worker's sealed outputs never reach the sink. Found live:
621
+ // the first worker run drained its failure header and none of its sealed phases.
622
+ //
623
+ // `persistDir` is the RESUME half of the same concern: the checkpoint names sealed rows by
624
+ // id, and the process that resumes is not the process that sealed them. Without a durable
625
+ // row store every re-claim is refused ("the output store no longer holds") and pays again.
626
+ const outputs = createOutputStore(registry, {
627
+ persistDir: stateRoot,
628
+ mirror: createOutputMirror(join(tmpdir(), "coltrane-worker-mirror")),
629
+ });
630
+ const ledger = new MemoryLedger();
631
+ const invoke = deps.makeInvoke(registry, genome);
632
+ const checkpoints = createCheckpointStore(stateRoot);
633
+ // ORDER OF PREFERENCE: the local checkpoint (fast path — same box, nothing to fetch), then
634
+ // the DRAIN reconstruction (a different box, or a state root that was cleared), then cold.
635
+ // Exactly one line is logged for whichever path is taken, including the reason for cold.
636
+ let checkpoint;
637
+ let resumeSource = "local";
638
+ let coldReason = "no local checkpoint and no drain to rebuild one from";
639
+ try {
640
+ checkpoint = checkpoints.read(claim.gig_id);
641
+ }
642
+ catch (e) {
643
+ // A damaged checkpoint is not a reason to fail a runnable row — it is a reason to pay for
644
+ // a cold run, and to say so.
645
+ log(`checkpoint for ${claim.gig_id} unreadable, running cold: ${e instanceof Error ? e.message : String(e)}`);
646
+ }
647
+ if (!checkpoint) {
648
+ const rebuilt = await rebuildFromDrain(ctx, claim, standard, outputs);
649
+ if (rebuilt.ok) {
650
+ // Written to the LOCAL store because that is where the runtime's resume gate reads a
651
+ // checkpoint from. The reconstruction seeds that gate; the gate remains the authority —
652
+ // it re-resolves every output id, re-checks every content_sha and type fingerprint, and
653
+ // compares the identity itself, so a bad reconstruction is refused by the same code a
654
+ // bad local checkpoint is.
655
+ checkpoints.write(rebuilt.checkpoint);
656
+ checkpoint = rebuilt.checkpoint;
657
+ resumeSource = "drain";
658
+ }
659
+ else {
660
+ coldReason = rebuilt.reason;
661
+ }
662
+ }
663
+ const human = approvalWiring(claim.approvals, standard, checkpoint?.roles.map((r) => r.role) ?? []);
664
+ const run = (resume) => runGig(standard, claim.input, {
665
+ outputs,
666
+ ledger,
667
+ invoke,
668
+ gig_id: claim.gig_id, // ← the run IS the queue row; the drained header completes it
669
+ skills: genome.skills,
670
+ // Store-loaded skills carry no local package dir (no code half) by construction, so
671
+ // no skill_dirs: a skill-BACKED chair in a store standard fails precisely at prep
672
+ // with the runtime's own "no skill_dir is registered" error, not a confabulated run.
673
+ checkpoints,
674
+ ...(resume ? { resume_from: claim.gig_id } : {}),
675
+ ...human,
676
+ });
677
+ let res;
678
+ if (checkpoint) {
679
+ // An approved re-claim is the common case for this branch: the chairs before the human
680
+ // seat already sealed and were already paid for, so replaying them is money spent twice.
681
+ log(`resuming ${claim.gig_id} from its ${resumeSource === "drain" ? "DRAIN-reconstructed" : "local"} ` +
682
+ `checkpoint (${checkpoint.roles.length} chair(s) recorded)`);
683
+ try {
684
+ res = await run(true);
685
+ }
686
+ catch (e) {
687
+ if (!(e instanceof ResumeRefused))
688
+ throw e;
689
+ // A refusal is the engine declining to splice two runs together (the genome, the
690
+ // payload or a type moved). The queue row is still work that must happen, so it is run
691
+ // COLD and the second payment is stated rather than hidden.
692
+ log(`resume refused for ${claim.gig_id} — running it COLD: ${e.message}`);
693
+ res = await run(false);
694
+ }
695
+ }
696
+ else {
697
+ log(`running ${claim.gig_id} COLD — ${coldReason}`);
698
+ res = await run(false);
699
+ }
700
+ if (res.status === "awaiting_approval") {
701
+ const awaiting = res.awaiting;
702
+ log(`gig ${claim.gig_id} awaiting approval` +
703
+ (awaiting ? ` at human chair "${awaiting.role}" (phase "${awaiting.phase}")` : "") +
704
+ ` — ${res.outputs.length} sealed output(s)`);
705
+ try {
706
+ const released = await parkGig(ctx, claim.gig_id);
707
+ if (!released) {
708
+ log(`park not recorded for ${claim.gig_id} (coltrane_mcp_gig_park absent or the row moved) — the drained header carries awaiting_approval`);
709
+ }
710
+ }
711
+ catch (pe) {
712
+ // Same posture as an unrecordable failure: say it, and let the lease expire.
713
+ log(`could not park ${claim.gig_id} (lease will expire): ${pe instanceof Error ? pe.message : String(pe)}`);
714
+ }
715
+ return {
716
+ claimed: true, gig_id: claim.gig_id, status: "awaiting_approval",
717
+ outputs_count: res.outputs.length,
718
+ ...(awaiting ? { awaiting } : {}),
719
+ };
720
+ }
721
+ log(`gig ${claim.gig_id} ${res.status} — ${res.outputs.length} sealed output(s)`);
722
+ return { claimed: true, gig_id: claim.gig_id, status: "complete", outputs_count: res.outputs.length };
723
+ }
724
+ catch (e) {
725
+ const message = e instanceof Error ? e.message : String(e);
726
+ log(`gig ${claim.gig_id} failed: ${message}`);
727
+ try {
728
+ await failGig(ctx, claim.gig_id, message);
729
+ }
730
+ catch (fe) {
731
+ // The failure could not even be recorded — surface both; the lease will expire.
732
+ log(`could not record failure (lease will expire): ${fe instanceof Error ? fe.message : String(fe)}`);
733
+ }
734
+ return { claimed: true, gig_id: claim.gig_id, status: "failed", error: message };
735
+ }
736
+ }
737
+ //# sourceMappingURL=worker.js.map