@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
@@ -1,5 +1,6 @@
1
1
  import type { Standard, Agent, Chair } from "./composition.js";
2
- import { type CheckpointStore, type ReuseStore } from "./reuse.js";
2
+ export declare const CORE_TO_PRIMITIVE: Record<string, Agent["primitives"][number]>;
3
+ import { type CheckpointStore, type ReuseStore, type PriorBudgetState } from "./reuse.js";
3
4
  import type { OutputStore, OutputRecord } from "./outputs.js";
4
5
  import { type Ledger, type GigUsage } from "./ledger.js";
5
6
  import type { Depth } from "./pricing.js";
@@ -89,6 +90,12 @@ export type GigProgressEvent = {
89
90
  } | {
90
91
  type: "gig_complete";
91
92
  outputs: number;
93
+ }
94
+ /** The gig reached a HUMAN chair without an approval — parked, checkpointed, waiting. */
95
+ | {
96
+ type: "gig_awaiting_approval";
97
+ phase: string;
98
+ role: string;
92
99
  } | {
93
100
  type: "gig_failed";
94
101
  error: string;
@@ -135,6 +142,15 @@ export interface RunDeps {
135
142
  budget?: BudgetInput | undefined;
136
143
  onProgress?: ((ev: GigProgressEvent) => void) | undefined;
137
144
  gig_id?: string | undefined;
145
+ /**
146
+ * The human seat's verdicts, keyed by chair role. A gig that reaches a human chair WITH
147
+ * its approval here seals it (through the same output gate, schema-validated) and
148
+ * continues; WITHOUT it, the gig parks as awaiting_approval. Supplied on the approving
149
+ * resume — the sketching happens before dispatch; this is the light gate after.
150
+ */
151
+ approvals?: Record<string, Record<string, unknown>> | undefined;
152
+ /** WHO approved — sealed as the approval output's agent_slug. Defaults to "human". */
153
+ approved_by?: string | undefined;
138
154
  selectChairs?: ChairSelector | undefined;
139
155
  /**
140
156
  * #249/#250 — the cancellation seam. Abort it and the run stops at its next checkpoint
@@ -153,6 +169,25 @@ export interface RunDeps {
153
169
  * the thing that actually spends. Absent = each agent's own `depth_profile` stands.
154
170
  */
155
171
  depth?: Depth | undefined;
172
+ /**
173
+ * Set by `runChart` (src/chart.ts) when this gig is a movement of an arrangement. Absent = a
174
+ * plain single-standard run, byte-identical to every run before charts existed.
175
+ */
176
+ chart?: ChartRunContext | undefined;
177
+ /**
178
+ * Sealed records from an EARLIER MOVEMENT, offered to this run's entry chairs.
179
+ *
180
+ * A chart edge is a provenance edge, not a copy: the sink's entry chair consumes the source
181
+ * movement's real `OutputRecord`s, so what it seals carries their `input_refs`/`input_shas` and
182
+ * the chain reaches back across the movement boundary. Passing the DATA through the gig payload
183
+ * instead would satisfy the type check and produce an output whose provenance says it came from
184
+ * nowhere.
185
+ *
186
+ * Deliberately NOT folded into `produced`: a movement's manifest, ledger row and
187
+ * `run_fingerprint` describe the work THAT MOVEMENT did. Seeds are reported separately, in
188
+ * `GigResult.seeded_from`, so a chair consuming records this run did not produce is never silent.
189
+ */
190
+ seed_outputs?: readonly OutputRecord[] | undefined;
156
191
  /**
157
192
  * Durable per-gig checkpoints. When wired, the runtime records each completed chair's sealed
158
193
  * outputs (id + content_sha + type fingerprint) after every dispatch batch, so a run that
@@ -326,11 +361,46 @@ export interface GigReuseReport {
326
361
  reason: string;
327
362
  }>;
328
363
  }
364
+ /**
365
+ * What a run needs to know about being one MOVEMENT of a chart.
366
+ *
367
+ * Small on purpose: the runtime does not orchestrate arrangements (src/chart.ts does). It only
368
+ * needs to stamp the right identity on what it seals, so a movement's ledger row, checkpoint and
369
+ * fingerprint say which performance they belong to.
370
+ */
371
+ export interface ChartRunContext {
372
+ chart_slug: string;
373
+ movement_id: string;
374
+ /**
375
+ * The arrangement's identity, folded into `run_fingerprint` in the EXACT slot that held
376
+ * `genome_hash`. For a degenerate chart this value IS `genomeHash(standard)`, so a
377
+ * single-standard gig's fingerprint is byte-identical to what it was before charts existed.
378
+ */
379
+ chart_hash: string;
380
+ /** One movement, no edges, no gates — the single-standard gig. Keeps ids and files unchanged. */
381
+ degenerate: boolean;
382
+ /** The cumulative spend at this movement's boundary, recorded on the checkpoint it writes. */
383
+ prior_budget_state?: PriorBudgetState | undefined;
384
+ }
329
385
  export interface GigResult {
330
386
  gig_id: string;
331
387
  standard_slug: string;
388
+ /** Present when this run was a movement of a chart (RunDeps.chart). */
389
+ chart_slug?: string;
390
+ movement_id?: string;
332
391
  genome_hash: string;
333
392
  run_fingerprint: string;
393
+ /**
394
+ * Records this run CONSUMED but did not produce — an earlier movement's sealed outputs, carried
395
+ * in over a chart edge. Present only when seeds were actually read, so its absence is the claim
396
+ * that every input this run consumed was sealed inside it.
397
+ */
398
+ seeded_from?: ReadonlyArray<{
399
+ gig_id: string;
400
+ output_id: string;
401
+ domain_type: string;
402
+ content_sha: string;
403
+ }>;
334
404
  outputs: readonly OutputRecord[];
335
405
  eval_scores: Record<string, number>;
336
406
  /**
@@ -354,7 +424,12 @@ export interface GigResult {
354
424
  phase: string;
355
425
  missing: readonly string[];
356
426
  }>;
357
- status: "complete";
427
+ status: "complete" | "awaiting_approval";
428
+ /** Present iff status is "awaiting_approval": the human chair the run parked at. */
429
+ awaiting?: {
430
+ phase: string;
431
+ role: string;
432
+ };
358
433
  /** Final budget snapshot. Present only when a budget was supplied. */
359
434
  budget_state?: BudgetState;
360
435
  /** Settled model spend (#195). Present when ≥1 real model invocation ran this gig. */
@@ -454,4 +529,18 @@ export declare function computeAppendCost(ctx: {
454
529
  inputs: readonly OutputRecord[];
455
530
  gig_input: Record<string, unknown>;
456
531
  }, base_cost: number, k: number): number;
532
+ /**
533
+ * The structural identity of a pipeline: the standard's phase graph plus each bound agent's
534
+ * type surface. Deterministic across machines for a given structure.
535
+ *
536
+ * EXPORTED because it is the one identity a DRAINED gig carries. The sink's gig header records
537
+ * `genome_hash` and nothing else about the run's producers, so a worker reconstructing a resume
538
+ * from the sink (src/worker.ts) has to be able to ask "is the standard I just loaded the one
539
+ * those outputs were sealed under" — and it must ask with this function, not a lookalike.
540
+ */
541
+ export declare function genomeHash(standard: Standard): string;
542
+ /** EXPORTED because the chart layer asks the same question at the movement boundary — which of a
543
+ * source movement's sealed records does an edge of type T carry — and two layers answering "does
544
+ * this record satisfy this declared type" differently is the #263 defect wearing a new hat. */
545
+ export declare function outputSatisfiesType(output: OutputRecord, declared: string): boolean;
457
546
  export declare function runGig(standard: Standard, gigInput: Record<string, unknown>, deps: RunDeps): Promise<GigResult>;
@@ -11,11 +11,17 @@ import { resolveModel } from "./claude_invoker.js";
11
11
  // core type → the process primitive that produces it (reverse of PRIMITIVE_OUTPUT_TYPE).
12
12
  // A skill-backed chair seals its output as this primitive/core when its output_contract is
13
13
  // a core type.
14
- const CORE_TO_PRIMITIVE = Object.fromEntries(Object.entries(PRIMITIVE_OUTPUT_TYPE).map(([prim, core]) => [String(core), prim]));
15
- import { sha256Hex, canonJson, runFingerprint, outputContentHash, CANONICAL_FORM_VERSION } from "./canonical_form.js";
16
- import { producersSha, reuseCacheKey, checkReuseEntry, runIdentityMismatch, CHECKPOINT_SCHEMA_VERSION, REUSE_SCHEMA_VERSION, } from "./reuse.js";
14
+ //
15
+ // EXPORTED because `primitive` is folded into `content_sha`. Anything that has to re-derive
16
+ // what a chair WOULD have sealed the drain reconstruction in src/worker.ts — has to arrive
17
+ // at the same primitive this seal boundary does, and a second copy of the mapping is exactly
18
+ // the drift that makes two gates on one concern answer differently.
19
+ export const CORE_TO_PRIMITIVE = Object.fromEntries(Object.entries(PRIMITIVE_OUTPUT_TYPE).map(([prim, core]) => [String(core), prim]));
20
+ import { sha256Hex, canonJson, canonStructuralJson, runFingerprint, outputContentHash, CANONICAL_FORM_VERSION } from "./canonical_form.js";
21
+ import { producersSha, reuseCacheKey, checkReuseEntry, runIdentityMismatch, checkpointRoleKey, CHECKPOINT_SCHEMA_VERSION, REUSE_SCHEMA_VERSION, } from "./reuse.js";
17
22
  import { drainGigHeader } from "./output_mirror.js";
18
23
  import { LEDGER_SCHEMA_VERSION } from "./ledger.js";
24
+ import { COLTRANE_VERSION } from "./version.js";
19
25
  /**
20
26
  * #236 — settled spend used to be discarded on every failed gig: `usage` was written only on
21
27
  * the success path, and the async dispatcher's `.catch` set status/error and nothing else. A
@@ -142,9 +148,16 @@ export function computeAppendCost(ctx, base_cost, k) {
142
148
  // in a canonical (sorted, JCS) form. This is the reproducibility key — same defs,
143
149
  // same genome_hash, regardless of model or run.
144
150
  /**
145
- * Resolve a list of skill slugs against the genome's skills map. REPORTS, never decides:
146
- * it returns both what resolved and what did not, and `prepareChair` decides what a miss
147
- * means (fatal when the chair declared the skill REQUIRED, reported otherwise).
151
+ * Resolve an agent's skills: the ones it CARRIES on its record, unioned with the repertoire
152
+ * packages its `skill_slugs` name. REPORTS, never decides: it returns both what resolved and what
153
+ * did not, and `prepareChair` decides what a miss means (fatal when the chair declared the skill
154
+ * REQUIRED, reported otherwise).
155
+ *
156
+ * CARRIED-FIRST, and carried wins the slug. An agent's own definition needs no genome lookup (it
157
+ * travels with the player into any institution), and where both a carried definition and a
158
+ * repertoire package answer to one slug the carried one SHADOWS it — the player's own technique is
159
+ * the one that plays, and the same slug never resolves to two skills in one prompt. A slug covered
160
+ * by a carried definition is therefore not missing.
148
161
  *
149
162
  * The boundary (#241): a skill package that LOADS is a legitimate degradation candidate —
150
163
  * it has an identity, a version, a code_hash, and its degradation is already surfaced and
@@ -156,12 +169,15 @@ export function computeAppendCost(ctx, base_cost, k) {
156
169
  * An ABSENT map means resolution was never configured (the documented v0 back-compat path)
157
170
  * — that is not evidence of a dangling binding, so `missing` stays empty.
158
171
  */
159
- function resolveSkills(slugs, map) {
172
+ function resolveSkills(slugs, map, carried) {
173
+ const skills = [...(carried ?? [])];
174
+ const carriedSlugs = new Set(skills.map((s) => s.slug));
160
175
  if (!slugs || slugs.length === 0 || !map)
161
- return { skills: [], missing: [] };
162
- const skills = [];
176
+ return { skills, missing: [] };
163
177
  const missing = [];
164
178
  for (const slug of slugs) {
179
+ if (carriedSlugs.has(slug))
180
+ continue; // the carried definition already answered this slug
165
181
  const rec = map.get(slug);
166
182
  if (rec)
167
183
  skills.push(rec);
@@ -170,7 +186,16 @@ function resolveSkills(slugs, map) {
170
186
  }
171
187
  return { skills, missing };
172
188
  }
173
- function genomeHash(standard) {
189
+ /**
190
+ * The structural identity of a pipeline: the standard's phase graph plus each bound agent's
191
+ * type surface. Deterministic across machines for a given structure.
192
+ *
193
+ * EXPORTED because it is the one identity a DRAINED gig carries. The sink's gig header records
194
+ * `genome_hash` and nothing else about the run's producers, so a worker reconstructing a resume
195
+ * from the sink (src/worker.ts) has to be able to ask "is the standard I just loaded the one
196
+ * those outputs were sealed under" — and it must ask with this function, not a lookalike.
197
+ */
198
+ export function genomeHash(standard) {
174
199
  const agents = [...standard.agents]
175
200
  .map((a) => ({
176
201
  slug: a.slug,
@@ -180,7 +205,13 @@ function genomeHash(standard) {
180
205
  domain: a.domain,
181
206
  }))
182
207
  .sort((x, y) => (x.slug < y.slug ? -1 : 1));
183
- return sha256Hex(canonJson({ standard: { slug: standard.slug, domain: standard.domain, phases: standard.phases }, agents }));
208
+ // canonStructuralJson, not canonJson: a field whose value states NOTHING (an empty array, an
209
+ // absent optional, a null domain) must not be able to move a STRUCTURAL hash. 0.6.6 added two
210
+ // `.default([])` chair fields, no standard's structure changed, and genome_hash moved for the
211
+ // entire genome — re-keying the ledger and refusing resumes for a drift that did not exist.
212
+ // Reaching this canonicalization moved the hash ONE final time; after it, a new schema default
213
+ // is hash-neutral. Pinned by tests/genome_hash_stability.test.ts, which states the bump loudly.
214
+ return sha256Hex(canonStructuralJson({ standard: { slug: standard.slug, domain: standard.domain, phases: standard.phases }, agents }));
184
215
  }
185
216
  /**
186
217
  * Execute one gig: walk phases in order, each phase's agent consumes the prior
@@ -194,7 +225,10 @@ function genomeHash(standard) {
194
225
  // declarations are polymorphic, so a base player written against `Interpretation`
195
226
  // consumes any downstream subtype while domain contracts keep their precision.
196
227
  const CORE_TYPE_SET = new Set(CORE_TYPES);
197
- function outputSatisfiesType(output, declared) {
228
+ /** EXPORTED because the chart layer asks the same question at the movement boundary — which of a
229
+ * source movement's sealed records does an edge of type T carry — and two layers answering "does
230
+ * this record satisfy this declared type" differently is the #263 defect wearing a new hat. */
231
+ export function outputSatisfiesType(output, declared) {
198
232
  if (output.domain_type === declared)
199
233
  return true;
200
234
  if (CORE_TYPE_SET.has(declared) && output.core_type === declared)
@@ -304,6 +338,10 @@ export async function runGig(standard, gigInput, deps) {
304
338
  // any chair"). The payload is validated BEFORE any chair fires — a missing gig input is a
305
339
  // hard stop, so no model tokens are spent on bad input.
306
340
  const standardInputs = new Set(standard.input_types ?? []);
341
+ // Sealed records an earlier MOVEMENT handed to this one over a chart edge (RunDeps.seed_outputs).
342
+ // They are inputs, not products: available to entry chairs, never folded into `produced`.
343
+ const seedRecords = deps.seed_outputs ?? [];
344
+ const seedsConsumed = new Map(); // output_id → record, for the manifest
307
345
  // Keys are the HYPHENATED type slug. `grant_requirements` vs `grant-requirements` is the
308
346
  // single most common dispatch mistake, and the caller's own keys are in scope here.
309
347
  const normalizeKey = (k) => k.toLowerCase().replace(/[_\-\s]/g, "");
@@ -338,6 +376,10 @@ export async function runGig(standard, gigInput, deps) {
338
376
  sealedByRole.set(ch.role, [ch.output_contract[0] ?? "Signal"]);
339
377
  continue;
340
378
  }
379
+ if (ch.human === true && (ch.agent_slug ?? "") === "") {
380
+ sealedByRole.set(ch.role, [ch.output_contract[0] ?? "Judgment"]);
381
+ continue;
382
+ }
341
383
  const ag = standard.agents.find((a) => a.slug === ch.agent_slug);
342
384
  if (!ag)
343
385
  continue; // prepareChair reports an unknown agent_slug precisely; don't pre-empt it
@@ -376,6 +418,10 @@ export async function runGig(standard, gigInput, deps) {
376
418
  continue; // supplied
377
419
  if (reachable.some((t) => mightSatisfy(t, need)))
378
420
  continue; // an upstream can cover it
421
+ // A chart edge satisfies a declared gig input with a SEALED RECORD rather than a payload
422
+ // key. Without this the pre-flight would refuse a correctly-arranged movement at t=0.
423
+ if (seedRecords.some((s) => outputSatisfiesType(s, need)))
424
+ continue;
379
425
  throw missingGigInput(need, ch.role);
380
426
  }
381
427
  sealedThisPhase.push(...(sealedByRole.get(ch.role) ?? []));
@@ -476,7 +522,10 @@ export async function runGig(standard, gigInput, deps) {
476
522
  };
477
523
  const identity = () => ({
478
524
  standard_slug: standard.slug,
479
- genome_hash,
525
+ // A movement's resume gate carries the ARRANGEMENT's identity when it has one: chairs from
526
+ // chart B consuming a movement's sealed outputs from chart A is the same splice as a moved
527
+ // genome, one level up. Byte-identical for a degenerate chart, where chart_hash IS genome_hash.
528
+ genome_hash: deps.chart?.chart_hash ?? genome_hash,
480
529
  // #278 review — genome_hash does NOT see an agent's identity/method/constraints/tools,
481
530
  // nor a skill's code. Those are the producer, and editing one under a stable slug is the
482
531
  // ordinary response to a bad run. Without this the resume gate accepted exactly that.
@@ -515,7 +564,21 @@ export async function runGig(standard, gigInput, deps) {
515
564
  // record that it happened — the manifest would describe a system that never existed.
516
565
  const drift = runIdentityMismatch(cp.identity, identity());
517
566
  if (drift.length > 0) {
518
- throw new ResumeRefused(gig_id, "it was checkpointed under a different run identity", drift);
567
+ // DIAGNOSTIC HONESTY, not a widened resume. The genome genuinely moved, so the refusal
568
+ // stands — but the operator's real fix is "resume from the build that wrote this", and
569
+ // nothing told them which build that was. `engine_version` (stamped by every build since
570
+ // this landed) names it; a checkpoint from before this field says so plainly rather than
571
+ // crash. Lead with the version + the action; the raw before/after hashes still ride in
572
+ // `drift` for a builder who wants them.
573
+ const cur = identity();
574
+ const wroteBy = cp.engine_version
575
+ ? `coltrane ${cp.engine_version}`
576
+ : "an earlier build (engine version unrecorded)";
577
+ const resumeAction = cp.engine_version
578
+ ? `Resume from a ${cp.engine_version} build, or re-dispatch cold`
579
+ : "Resume from the matching build, or re-dispatch cold";
580
+ throw new ResumeRefused(gig_id, `this checkpoint was written by ${wroteBy} (genome_hash ${cp.identity.genome_hash}); ` +
581
+ `the current build is coltrane ${COLTRANE_VERSION} (genome_hash ${cur.genome_hash}). ${resumeAction}`, drift);
519
582
  }
520
583
  const rolesInStandard = new Set(standard.phases.flatMap((p) => p.chairs.map((c) => c.role)));
521
584
  for (const r of cp.roles) {
@@ -524,6 +587,15 @@ export async function runGig(standard, gigInput, deps) {
524
587
  if (!rolesInStandard.has(r.role)) {
525
588
  throw new ResumeRefused(gig_id, `its checkpoint names role "${r.role}", which this standard does not define`);
526
589
  }
590
+ // THE SEAT'S FULL IDENTITY: (chart_slug, movement_id, role). Two movements of one chart may
591
+ // each declare a chair named "reviewer", and restoring one movement's sealed output into the
592
+ // other's seat would be a splice with nothing in the manifest recording it. A legacy row
593
+ // carries no movement_id and defaults to the standard's own slug, so it still restores.
594
+ const seatNow = checkpointRoleKey(deps.chart?.chart_slug ?? standard.slug, deps.chart?.movement_id, r.role);
595
+ const seatThen = checkpointRoleKey(deps.chart?.chart_slug ?? standard.slug, r.movement_id, r.role);
596
+ if (seatNow !== seatThen) {
597
+ throw new ResumeRefused(gig_id, `its checkpoint names seat "${seatThen}" and this run is seat "${seatNow}" — a movement does not restore another movement's chair`);
598
+ }
527
599
  const records = [];
528
600
  for (let i = 0; i < r.output_ids.length; i++) {
529
601
  const id = r.output_ids[i];
@@ -569,6 +641,9 @@ export async function runGig(standard, gigInput, deps) {
569
641
  return;
570
642
  checkpointRoles.set(role, {
571
643
  role, phase: phaseName,
644
+ // WHICH movement's seat this is. Two movements may both declare a chair named "reviewer";
645
+ // the composite (chart_slug, movement_id, role) is what keeps their checkpoints apart.
646
+ ...(deps.chart ? { movement_id: deps.chart.movement_id } : {}),
572
647
  output_ids: records.map((r) => r.id),
573
648
  content_shas: records.map((r) => r.content_sha),
574
649
  domain_types: records.map((r) => r.domain_type),
@@ -593,10 +668,16 @@ export async function runGig(standard, gigInput, deps) {
593
668
  schema_version: CHECKPOINT_SCHEMA_VERSION,
594
669
  gig_id,
595
670
  identity: identity(),
671
+ // The build that wrote this. When a later build's evolved schema drifts the identity,
672
+ // this is what turns the refusal's two raw hashes into "resume from a <version> build".
673
+ engine_version: COLTRANE_VERSION,
596
674
  started_at: checkpointStartedAt,
597
675
  updated_at: new Date().toISOString(),
598
676
  roles: [...checkpointRoles.values()],
599
677
  ...(prior ? { prior_usage: JSON.parse(JSON.stringify(prior)) } : {}),
678
+ // The chart's cumulative spend AT THIS MOVEMENT'S BOUNDARY, so a resumed performance can
679
+ // compare it to the envelope before spawning anything (src/chart.ts, edge case B).
680
+ ...(deps.chart?.prior_budget_state ? { prior_budget_state: deps.chart.prior_budget_state } : {}),
600
681
  });
601
682
  }
602
683
  catch (e) {
@@ -654,6 +735,80 @@ export async function runGig(standard, gigInput, deps) {
654
735
  const stuck = [...remaining.values()].map((c) => c.role).join(", ");
655
736
  throw new RuntimeError(`phase "${phase.name}" cannot advance — chairs [${stuck}] have unresolved depends_on`);
656
737
  }
738
+ // ── THE HUMAN SEAT ─────────────────────────────────────────────────────────────
739
+ // A human chair in the frontier is handled before any model dispatch. With its
740
+ // approval supplied (deps.approvals[role]) the incumbent's verdict seals through the
741
+ // SAME output gate as every record — schema-validated, under the approving
742
+ // principal's name, carrying the input_shas of exactly what was approved. Without
743
+ // it, the gig PARKS: checkpointed, honestly drained as awaiting_approval, nothing
744
+ // hollow sealed. The sketching happens before dispatch; this gate is light.
745
+ const humanReady = ready.filter((c) => c.human === true && (c.agent_slug ?? "") === "");
746
+ for (const hc of humanReady) {
747
+ const approval = deps.approvals?.[hc.role];
748
+ if (!approval) {
749
+ checkpoint();
750
+ emit({ type: "gig_awaiting_approval", phase: phase.name, role: hc.role });
751
+ // AWAITED, unlike the fire-and-forget completion drain: parking is the runtime's
752
+ // last act before the caller (often a CLI) exits, and an in-flight fetch dies with
753
+ // the process — which left the sink's row saying "running" about a gig that was
754
+ // waiting on a person. Parking is not latency-critical; the truth is.
755
+ await drainGigHeader({
756
+ gig_id,
757
+ standard_slug: standard.slug,
758
+ status: "awaiting_approval",
759
+ genome_hash,
760
+ started_at,
761
+ finished_at: new Date().toISOString(),
762
+ outputs_count: produced.length,
763
+ error: `awaiting approval at human chair "${hc.role}" (phase "${phase.name}")`,
764
+ }).catch((de) => {
765
+ if (process.env["COLTRANE_DRAIN_DEBUG"])
766
+ console.error(`[drain] awaiting header ${gig_id}: ${String(de)}`);
767
+ });
768
+ return {
769
+ gig_id,
770
+ standard_slug: standard.slug,
771
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
772
+ genome_hash,
773
+ run_fingerprint: "",
774
+ outputs: produced,
775
+ eval_scores: {},
776
+ status: "awaiting_approval",
777
+ awaiting: { phase: phase.name, role: hc.role },
778
+ };
779
+ }
780
+ remaining.delete(hc.role);
781
+ const domain_type = hc.output_contract[0] ?? "Judgment";
782
+ const core = deps.outputs.coreTypeOf(domain_type) ?? domain_type;
783
+ const primitive = CORE_TO_PRIMITIVE[core] ?? "JUDGE";
784
+ const approvalInputs = hc.depends_on.flatMap((d) => producedByRole.get(d) ?? []);
785
+ const t0 = Date.now();
786
+ emit({ type: "chair_start", phase: phase.name, role: hc.role, producer: deps.approved_by ?? "human" });
787
+ const rec = deps.outputs.write({
788
+ core_type: core,
789
+ domain_type,
790
+ domain: standard.domain,
791
+ gig_id,
792
+ agent_slug: deps.approved_by ?? "human",
793
+ from_role: hc.role,
794
+ phase: phase.name,
795
+ primitive,
796
+ data: approval,
797
+ input_refs: approvalInputs.map((i) => i.id),
798
+ input_shas: approvalInputs.map((i) => i.content_sha),
799
+ });
800
+ for (const i of approvalInputs)
801
+ deps.outputs.addRef(rec.id, i.id, "derived_from", primitive);
802
+ producedByRole.set(hc.role, [rec]);
803
+ produced.push(rec);
804
+ emit({
805
+ type: "chair_complete", phase: phase.name, role: hc.role, producer: deps.approved_by ?? "human",
806
+ output_types: [domain_type], duration_ms: Date.now() - t0,
807
+ });
808
+ }
809
+ ready = ready.filter((c) => !(c.human === true && (c.agent_slug ?? "") === ""));
810
+ if (ready.length === 0)
811
+ continue;
657
812
  // Routing policy: when a selector is injected, it narrows the frontier to the
658
813
  // chairs to dispatch THIS iteration; the rest stay in `remaining` and re-enter
659
814
  // the next frontier. The return is validated strictly — empty or containing a
@@ -747,6 +902,9 @@ export async function runGig(standard, gigInput, deps) {
747
902
  return undefined;
748
903
  const key = reuseCacheKey({
749
904
  standard_slug: standard.slug,
905
+ // A chart may name one standard TWICE. Keyed on movement_id, the two instances occupy
906
+ // separate namespaces even with byte-identical inputs — isolation by default.
907
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
750
908
  phase: a.phaseName,
751
909
  chair: a.chair,
752
910
  agent: a.agent ?? null,
@@ -817,6 +975,25 @@ export async function runGig(standard, gigInput, deps) {
817
975
  }
818
976
  return { key, hit: { cache_key: key, source_gig_id: entry.source_gig_id, outputs: entry.outputs } };
819
977
  }
978
+ /**
979
+ * Offer an ENTRY chair the seeds a chart edge carried in.
980
+ *
981
+ * Scoped to a chair with no `depends_on`: a chair that named its upstream roles asked for those
982
+ * specific seats, and a movement's seed is not one of them. Records enter `inputs` — so what the
983
+ * chair seals carries their content_shas — and are recorded as consumed for the manifest.
984
+ */
985
+ function pullSeeds(chair, inputs, wanted) {
986
+ if (seedRecords.length === 0 || chair.depends_on.length > 0)
987
+ return;
988
+ for (const s of seedRecords) {
989
+ if (inputs.includes(s))
990
+ continue;
991
+ if (!wanted.some((t) => outputSatisfiesType(s, t)))
992
+ continue;
993
+ inputs.push(s);
994
+ seedsConsumed.set(s.id, s);
995
+ }
996
+ }
820
997
  function prepareChair(chair, phaseName) {
821
998
  // A skill-backed chair runs the skill's deterministic code half — no agent, no model.
822
999
  if (chair.skill_slug && (chair.agent_slug ?? "") === "") {
@@ -835,6 +1012,7 @@ export async function runGig(standard, gigInput, deps) {
835
1012
  throw new RuntimeError(`chair "${chair.role}" depends_on "${dep}" which has not been produced`);
836
1013
  inputs.push(...recs);
837
1014
  }
1015
+ pullSeeds(chair, inputs, chair.input_contract);
838
1016
  if (chair.input_contract.length > 0) {
839
1017
  for (const need of chair.input_contract) {
840
1018
  // #156: a type satisfied by an upstream record OR by the gig payload (entry-chair seed).
@@ -901,6 +1079,9 @@ export async function runGig(standard, gigInput, deps) {
901
1079
  }
902
1080
  else {
903
1081
  inputs = produced.filter((o) => agent.input_types.some((t) => outputSatisfiesType(o, t)));
1082
+ // A chart edge's carriers are offered to the same chair on the same terms as an in-gig
1083
+ // upstream record — by type, as records, so provenance survives the movement boundary.
1084
+ pullSeeds(chair, inputs, [...chair.input_contract, ...agent.input_types]);
904
1085
  }
905
1086
  // Runtime input_contract check: every type the chair declares it expects
906
1087
  // on input must be satisfied by its actual upstream inputs. Subtype-aware
@@ -956,7 +1137,7 @@ export async function runGig(standard, gigInput, deps) {
956
1137
  // Resolve this agent's skill bindings (slugs) against the genome's skills map.
957
1138
  // resolveSkills REPORTS; this is where the engine DECIDES — and it decides BEFORE the
958
1139
  // budget deduction below, so a dangling binding costs nothing.
959
- const { skills, missing } = resolveSkills(agent.skill_slugs, deps.skills);
1140
+ const { skills, missing } = resolveSkills(agent.skill_slugs, deps.skills, agent.skills);
960
1141
  if (missing.length > 0) {
961
1142
  // #242 — `Chair.required_skills` was validated exactly once, at compose time, as a
962
1143
  // string-subset check against the agent's own declaration. A chair could declare a
@@ -1457,7 +1638,11 @@ export async function runGig(standard, gigInput, deps) {
1457
1638
  eval_scores[slug] = scoreEval(slug, produced, deps.evals);
1458
1639
  }
1459
1640
  const run_fingerprint = runFingerprint({
1460
- genome_hash,
1641
+ // THE CHART SLOT. When this run is a movement of an arrangement, the arrangement's identity
1642
+ // folds in exactly where `genome_hash` folded — because the reproducible thing is no longer
1643
+ // "this standard's structure" but "this movement of this chart". For a degenerate chart
1644
+ // `chart_hash === genomeHash(standard)`, so a single-standard gig's fingerprint is unmoved.
1645
+ genome_hash: deps.chart?.chart_hash ?? genome_hash,
1461
1646
  model_version: deps.model_version ?? "unknown",
1462
1647
  canonical_form_version: CANONICAL_FORM_VERSION,
1463
1648
  eval_scores,
@@ -1469,9 +1654,13 @@ export async function runGig(standard, gigInput, deps) {
1469
1654
  deps.ledger.append({
1470
1655
  kind: "gig",
1471
1656
  schema_version: LEDGER_SCHEMA_VERSION,
1657
+ // One ROW per movement, because a movement runs under its own gig id (src/chart.ts
1658
+ // `movementGigId`) — and for the degenerate chart that id IS the chart's, which is what an
1659
+ // existing single-standard reader looks up.
1472
1660
  entry_id: gig_id,
1473
1661
  gig_id,
1474
1662
  standard_slug: standard.slug,
1663
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
1475
1664
  genome_hash,
1476
1665
  run_fingerprint,
1477
1666
  output_hashes,
@@ -1513,7 +1702,18 @@ export async function runGig(standard, gigInput, deps) {
1513
1702
  deps.checkpoints?.remove(gig_id);
1514
1703
  }
1515
1704
  catch { /* reclaiming disk must not fail a run that succeeded */ }
1516
- const result = { gig_id, standard_slug: standard.slug, genome_hash, run_fingerprint, outputs: produced, eval_scores, status: "complete" };
1705
+ const result = {
1706
+ gig_id, standard_slug: standard.slug,
1707
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
1708
+ genome_hash, run_fingerprint, outputs: produced, eval_scores, status: "complete",
1709
+ ...(seedsConsumed.size > 0
1710
+ ? {
1711
+ seeded_from: [...seedsConsumed.values()].map((s) => ({
1712
+ gig_id: s.gig_id, output_id: s.id, domain_type: s.domain_type, content_sha: s.content_sha,
1713
+ })),
1714
+ }
1715
+ : {}),
1716
+ };
1517
1717
  if (settledUsage)
1518
1718
  result.usage = settledUsage;
1519
1719
  if (budget)
@@ -1546,13 +1746,31 @@ export async function runGig(standard, gigInput, deps) {
1546
1746
  // not. Attaching the partial accounting to the error is what lets gig_monitor and a
1547
1747
  // synchronous caller report it. This does NOT write a ledger row — absence-of-row remains
1548
1748
  // the honest "un-sealed gig" signal (recorder_durability_mid_crash.spec.ts).
1749
+ let partial;
1549
1750
  if (e && typeof e === "object") {
1550
- const partial = finalizeUsage();
1751
+ partial = finalizeUsage();
1551
1752
  if (partial)
1552
1753
  e["usage"] = partial;
1553
1754
  if (budget)
1554
1755
  e["budget_state"] = budget;
1555
1756
  }
1757
+ // The sink learns the truth either way: a failed run drains a FAILED header (same
1758
+ // fire-and-forget seam as the success path), so the queue row never sits stale on a
1759
+ // local failure. Found live: the worker's first day exposed the success-only drain.
1760
+ void drainGigHeader({
1761
+ gig_id,
1762
+ standard_slug: standard.slug,
1763
+ status: "failed",
1764
+ genome_hash,
1765
+ started_at,
1766
+ finished_at: new Date().toISOString(),
1767
+ outputs_count: produced.length,
1768
+ error: e instanceof Error ? e.message : String(e),
1769
+ ...(partial ? { usage: { total_cost_usd: partial.total_cost_usd, input_tokens: partial.input_tokens, output_tokens: partial.output_tokens } } : {}),
1770
+ }).catch((de) => {
1771
+ if (process.env["COLTRANE_DRAIN_DEBUG"])
1772
+ console.error(`[drain] failed-gig header ${gig_id}: ${String(de)}`);
1773
+ });
1556
1774
  throw e;
1557
1775
  }
1558
1776
  }