@eir-labs/coltrane 0.6.2 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) 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/miles.json +44 -0
  7. package/charts/software-delivery-v1.json +9 -0
  8. package/charts/software-delivery-v2.json +39 -0
  9. package/dist/src/canonical_form.d.ts +23 -0
  10. package/dist/src/canonical_form.js +53 -0
  11. package/dist/src/canonical_form.js.map +1 -1
  12. package/dist/src/chart.d.ts +254 -0
  13. package/dist/src/chart.js +897 -0
  14. package/dist/src/chart.js.map +1 -0
  15. package/dist/src/cli.d.ts +19 -4
  16. package/dist/src/cli.js +132 -9
  17. package/dist/src/cli.js.map +1 -1
  18. package/dist/src/composition.d.ts +24 -0
  19. package/dist/src/composition.js +50 -5
  20. package/dist/src/composition.js.map +1 -1
  21. package/dist/src/genome_schema.d.ts +1130 -166
  22. package/dist/src/genome_schema.js +311 -34
  23. package/dist/src/genome_schema.js.map +1 -1
  24. package/dist/src/genome_store.d.ts +53 -3
  25. package/dist/src/genome_store.js +316 -139
  26. package/dist/src/genome_store.js.map +1 -1
  27. package/dist/src/gig_tracker.d.ts +11 -1
  28. package/dist/src/gig_tracker.js +5 -0
  29. package/dist/src/gig_tracker.js.map +1 -1
  30. package/dist/src/index.d.ts +1 -0
  31. package/dist/src/index.js +1 -0
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/ledger.d.ts +22 -0
  34. package/dist/src/ledger.js +4 -0
  35. package/dist/src/ledger.js.map +1 -1
  36. package/dist/src/loader.d.ts +9 -1
  37. package/dist/src/loader.js +105 -5
  38. package/dist/src/loader.js.map +1 -1
  39. package/dist/src/mcp.js +42 -4
  40. package/dist/src/mcp.js.map +1 -1
  41. package/dist/src/output_mirror.d.ts +1 -1
  42. package/dist/src/outputs.d.ts +75 -1
  43. package/dist/src/outputs.js +142 -28
  44. package/dist/src/outputs.js.map +1 -1
  45. package/dist/src/reuse.d.ts +56 -0
  46. package/dist/src/reuse.js +0 -0
  47. package/dist/src/reuse.js.map +1 -1
  48. package/dist/src/runtime.d.ts +91 -2
  49. package/dist/src/runtime.js +217 -17
  50. package/dist/src/runtime.js.map +1 -1
  51. package/dist/src/server.d.ts +11 -0
  52. package/dist/src/server.js +529 -64
  53. package/dist/src/server.js.map +1 -1
  54. package/dist/src/version.d.ts +1 -1
  55. package/dist/src/version.js +1 -1
  56. package/dist/src/worker.d.ts +182 -0
  57. package/dist/src/worker.js +609 -0
  58. package/dist/src/worker.js.map +1 -0
  59. package/domain_types/branch-state.json +21 -0
  60. package/domain_types/change-context.json +39 -0
  61. package/domain_types/change-decision.json +36 -0
  62. package/domain_types/change-plan.json +47 -0
  63. package/domain_types/change-request.json +24 -0
  64. package/domain_types/change-set.json +46 -0
  65. package/domain_types/change-verdict.json +26 -0
  66. package/domain_types/deploy-verdict.json +23 -0
  67. package/domain_types/design-brief.json +37 -0
  68. package/domain_types/design-concept.json +36 -0
  69. package/domain_types/design-definition.json +37 -0
  70. package/domain_types/design-question.json +23 -0
  71. package/domain_types/design-verdict.json +27 -0
  72. package/domain_types/preview-deployment.json +32 -0
  73. package/institutions/quartet.json +344 -0
  74. package/package.json +4 -1
  75. package/skills/vercel-api/fixtures/error.json +10 -0
  76. package/skills/vercel-api/fixtures/ready.json +10 -0
  77. package/skills/vercel-api/fixtures/unsettled.json +10 -0
  78. package/skills/vercel-api/meta.json +10 -0
  79. package/skills/vercel-api/skill.mjs +63 -0
  80. package/standards/preview-deploy-v1.json +89 -0
  81. package/standards/product-design-v1.json +122 -0
  82. package/standards/promote-v1.json +41 -0
  83. package/standards/software-change-v1.json +147 -0
  84. package/venues/ci-deploy-room-v1.json +28 -0
  85. package/venues/empty-room-v1.json +19 -0
@@ -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,9 +11,14 @@ 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";
19
24
  /**
@@ -142,9 +147,16 @@ export function computeAppendCost(ctx, base_cost, k) {
142
147
  // in a canonical (sorted, JCS) form. This is the reproducibility key — same defs,
143
148
  // same genome_hash, regardless of model or run.
144
149
  /**
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).
150
+ * Resolve an agent's skills: the ones it CARRIES on its record, unioned with the repertoire
151
+ * packages its `skill_slugs` name. REPORTS, never decides: it returns both what resolved and what
152
+ * did not, and `prepareChair` decides what a miss means (fatal when the chair declared the skill
153
+ * REQUIRED, reported otherwise).
154
+ *
155
+ * CARRIED-FIRST, and carried wins the slug. An agent's own definition needs no genome lookup (it
156
+ * travels with the player into any institution), and where both a carried definition and a
157
+ * repertoire package answer to one slug the carried one SHADOWS it — the player's own technique is
158
+ * the one that plays, and the same slug never resolves to two skills in one prompt. A slug covered
159
+ * by a carried definition is therefore not missing.
148
160
  *
149
161
  * The boundary (#241): a skill package that LOADS is a legitimate degradation candidate —
150
162
  * it has an identity, a version, a code_hash, and its degradation is already surfaced and
@@ -156,12 +168,15 @@ export function computeAppendCost(ctx, base_cost, k) {
156
168
  * An ABSENT map means resolution was never configured (the documented v0 back-compat path)
157
169
  * — that is not evidence of a dangling binding, so `missing` stays empty.
158
170
  */
159
- function resolveSkills(slugs, map) {
171
+ function resolveSkills(slugs, map, carried) {
172
+ const skills = [...(carried ?? [])];
173
+ const carriedSlugs = new Set(skills.map((s) => s.slug));
160
174
  if (!slugs || slugs.length === 0 || !map)
161
- return { skills: [], missing: [] };
162
- const skills = [];
175
+ return { skills, missing: [] };
163
176
  const missing = [];
164
177
  for (const slug of slugs) {
178
+ if (carriedSlugs.has(slug))
179
+ continue; // the carried definition already answered this slug
165
180
  const rec = map.get(slug);
166
181
  if (rec)
167
182
  skills.push(rec);
@@ -170,7 +185,16 @@ function resolveSkills(slugs, map) {
170
185
  }
171
186
  return { skills, missing };
172
187
  }
173
- function genomeHash(standard) {
188
+ /**
189
+ * The structural identity of a pipeline: the standard's phase graph plus each bound agent's
190
+ * type surface. Deterministic across machines for a given structure.
191
+ *
192
+ * EXPORTED because it is the one identity a DRAINED gig carries. The sink's gig header records
193
+ * `genome_hash` and nothing else about the run's producers, so a worker reconstructing a resume
194
+ * from the sink (src/worker.ts) has to be able to ask "is the standard I just loaded the one
195
+ * those outputs were sealed under" — and it must ask with this function, not a lookalike.
196
+ */
197
+ export function genomeHash(standard) {
174
198
  const agents = [...standard.agents]
175
199
  .map((a) => ({
176
200
  slug: a.slug,
@@ -180,7 +204,13 @@ function genomeHash(standard) {
180
204
  domain: a.domain,
181
205
  }))
182
206
  .sort((x, y) => (x.slug < y.slug ? -1 : 1));
183
- return sha256Hex(canonJson({ standard: { slug: standard.slug, domain: standard.domain, phases: standard.phases }, agents }));
207
+ // canonStructuralJson, not canonJson: a field whose value states NOTHING (an empty array, an
208
+ // absent optional, a null domain) must not be able to move a STRUCTURAL hash. 0.6.6 added two
209
+ // `.default([])` chair fields, no standard's structure changed, and genome_hash moved for the
210
+ // entire genome — re-keying the ledger and refusing resumes for a drift that did not exist.
211
+ // Reaching this canonicalization moved the hash ONE final time; after it, a new schema default
212
+ // is hash-neutral. Pinned by tests/genome_hash_stability.test.ts, which states the bump loudly.
213
+ return sha256Hex(canonStructuralJson({ standard: { slug: standard.slug, domain: standard.domain, phases: standard.phases }, agents }));
184
214
  }
185
215
  /**
186
216
  * Execute one gig: walk phases in order, each phase's agent consumes the prior
@@ -194,7 +224,10 @@ function genomeHash(standard) {
194
224
  // declarations are polymorphic, so a base player written against `Interpretation`
195
225
  // consumes any downstream subtype while domain contracts keep their precision.
196
226
  const CORE_TYPE_SET = new Set(CORE_TYPES);
197
- function outputSatisfiesType(output, declared) {
227
+ /** EXPORTED because the chart layer asks the same question at the movement boundary — which of a
228
+ * source movement's sealed records does an edge of type T carry — and two layers answering "does
229
+ * this record satisfy this declared type" differently is the #263 defect wearing a new hat. */
230
+ export function outputSatisfiesType(output, declared) {
198
231
  if (output.domain_type === declared)
199
232
  return true;
200
233
  if (CORE_TYPE_SET.has(declared) && output.core_type === declared)
@@ -304,6 +337,10 @@ export async function runGig(standard, gigInput, deps) {
304
337
  // any chair"). The payload is validated BEFORE any chair fires — a missing gig input is a
305
338
  // hard stop, so no model tokens are spent on bad input.
306
339
  const standardInputs = new Set(standard.input_types ?? []);
340
+ // Sealed records an earlier MOVEMENT handed to this one over a chart edge (RunDeps.seed_outputs).
341
+ // They are inputs, not products: available to entry chairs, never folded into `produced`.
342
+ const seedRecords = deps.seed_outputs ?? [];
343
+ const seedsConsumed = new Map(); // output_id → record, for the manifest
307
344
  // Keys are the HYPHENATED type slug. `grant_requirements` vs `grant-requirements` is the
308
345
  // single most common dispatch mistake, and the caller's own keys are in scope here.
309
346
  const normalizeKey = (k) => k.toLowerCase().replace(/[_\-\s]/g, "");
@@ -338,6 +375,10 @@ export async function runGig(standard, gigInput, deps) {
338
375
  sealedByRole.set(ch.role, [ch.output_contract[0] ?? "Signal"]);
339
376
  continue;
340
377
  }
378
+ if (ch.human === true && (ch.agent_slug ?? "") === "") {
379
+ sealedByRole.set(ch.role, [ch.output_contract[0] ?? "Judgment"]);
380
+ continue;
381
+ }
341
382
  const ag = standard.agents.find((a) => a.slug === ch.agent_slug);
342
383
  if (!ag)
343
384
  continue; // prepareChair reports an unknown agent_slug precisely; don't pre-empt it
@@ -376,6 +417,10 @@ export async function runGig(standard, gigInput, deps) {
376
417
  continue; // supplied
377
418
  if (reachable.some((t) => mightSatisfy(t, need)))
378
419
  continue; // an upstream can cover it
420
+ // A chart edge satisfies a declared gig input with a SEALED RECORD rather than a payload
421
+ // key. Without this the pre-flight would refuse a correctly-arranged movement at t=0.
422
+ if (seedRecords.some((s) => outputSatisfiesType(s, need)))
423
+ continue;
379
424
  throw missingGigInput(need, ch.role);
380
425
  }
381
426
  sealedThisPhase.push(...(sealedByRole.get(ch.role) ?? []));
@@ -476,7 +521,10 @@ export async function runGig(standard, gigInput, deps) {
476
521
  };
477
522
  const identity = () => ({
478
523
  standard_slug: standard.slug,
479
- genome_hash,
524
+ // A movement's resume gate carries the ARRANGEMENT's identity when it has one: chairs from
525
+ // chart B consuming a movement's sealed outputs from chart A is the same splice as a moved
526
+ // genome, one level up. Byte-identical for a degenerate chart, where chart_hash IS genome_hash.
527
+ genome_hash: deps.chart?.chart_hash ?? genome_hash,
480
528
  // #278 review — genome_hash does NOT see an agent's identity/method/constraints/tools,
481
529
  // nor a skill's code. Those are the producer, and editing one under a stable slug is the
482
530
  // ordinary response to a bad run. Without this the resume gate accepted exactly that.
@@ -524,6 +572,15 @@ export async function runGig(standard, gigInput, deps) {
524
572
  if (!rolesInStandard.has(r.role)) {
525
573
  throw new ResumeRefused(gig_id, `its checkpoint names role "${r.role}", which this standard does not define`);
526
574
  }
575
+ // THE SEAT'S FULL IDENTITY: (chart_slug, movement_id, role). Two movements of one chart may
576
+ // each declare a chair named "reviewer", and restoring one movement's sealed output into the
577
+ // other's seat would be a splice with nothing in the manifest recording it. A legacy row
578
+ // carries no movement_id and defaults to the standard's own slug, so it still restores.
579
+ const seatNow = checkpointRoleKey(deps.chart?.chart_slug ?? standard.slug, deps.chart?.movement_id, r.role);
580
+ const seatThen = checkpointRoleKey(deps.chart?.chart_slug ?? standard.slug, r.movement_id, r.role);
581
+ if (seatNow !== seatThen) {
582
+ 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`);
583
+ }
527
584
  const records = [];
528
585
  for (let i = 0; i < r.output_ids.length; i++) {
529
586
  const id = r.output_ids[i];
@@ -569,6 +626,9 @@ export async function runGig(standard, gigInput, deps) {
569
626
  return;
570
627
  checkpointRoles.set(role, {
571
628
  role, phase: phaseName,
629
+ // WHICH movement's seat this is. Two movements may both declare a chair named "reviewer";
630
+ // the composite (chart_slug, movement_id, role) is what keeps their checkpoints apart.
631
+ ...(deps.chart ? { movement_id: deps.chart.movement_id } : {}),
572
632
  output_ids: records.map((r) => r.id),
573
633
  content_shas: records.map((r) => r.content_sha),
574
634
  domain_types: records.map((r) => r.domain_type),
@@ -597,6 +657,9 @@ export async function runGig(standard, gigInput, deps) {
597
657
  updated_at: new Date().toISOString(),
598
658
  roles: [...checkpointRoles.values()],
599
659
  ...(prior ? { prior_usage: JSON.parse(JSON.stringify(prior)) } : {}),
660
+ // The chart's cumulative spend AT THIS MOVEMENT'S BOUNDARY, so a resumed performance can
661
+ // compare it to the envelope before spawning anything (src/chart.ts, edge case B).
662
+ ...(deps.chart?.prior_budget_state ? { prior_budget_state: deps.chart.prior_budget_state } : {}),
600
663
  });
601
664
  }
602
665
  catch (e) {
@@ -654,6 +717,80 @@ export async function runGig(standard, gigInput, deps) {
654
717
  const stuck = [...remaining.values()].map((c) => c.role).join(", ");
655
718
  throw new RuntimeError(`phase "${phase.name}" cannot advance — chairs [${stuck}] have unresolved depends_on`);
656
719
  }
720
+ // ── THE HUMAN SEAT ─────────────────────────────────────────────────────────────
721
+ // A human chair in the frontier is handled before any model dispatch. With its
722
+ // approval supplied (deps.approvals[role]) the incumbent's verdict seals through the
723
+ // SAME output gate as every record — schema-validated, under the approving
724
+ // principal's name, carrying the input_shas of exactly what was approved. Without
725
+ // it, the gig PARKS: checkpointed, honestly drained as awaiting_approval, nothing
726
+ // hollow sealed. The sketching happens before dispatch; this gate is light.
727
+ const humanReady = ready.filter((c) => c.human === true && (c.agent_slug ?? "") === "");
728
+ for (const hc of humanReady) {
729
+ const approval = deps.approvals?.[hc.role];
730
+ if (!approval) {
731
+ checkpoint();
732
+ emit({ type: "gig_awaiting_approval", phase: phase.name, role: hc.role });
733
+ // AWAITED, unlike the fire-and-forget completion drain: parking is the runtime's
734
+ // last act before the caller (often a CLI) exits, and an in-flight fetch dies with
735
+ // the process — which left the sink's row saying "running" about a gig that was
736
+ // waiting on a person. Parking is not latency-critical; the truth is.
737
+ await drainGigHeader({
738
+ gig_id,
739
+ standard_slug: standard.slug,
740
+ status: "awaiting_approval",
741
+ genome_hash,
742
+ started_at,
743
+ finished_at: new Date().toISOString(),
744
+ outputs_count: produced.length,
745
+ error: `awaiting approval at human chair "${hc.role}" (phase "${phase.name}")`,
746
+ }).catch((de) => {
747
+ if (process.env["COLTRANE_DRAIN_DEBUG"])
748
+ console.error(`[drain] awaiting header ${gig_id}: ${String(de)}`);
749
+ });
750
+ return {
751
+ gig_id,
752
+ standard_slug: standard.slug,
753
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
754
+ genome_hash,
755
+ run_fingerprint: "",
756
+ outputs: produced,
757
+ eval_scores: {},
758
+ status: "awaiting_approval",
759
+ awaiting: { phase: phase.name, role: hc.role },
760
+ };
761
+ }
762
+ remaining.delete(hc.role);
763
+ const domain_type = hc.output_contract[0] ?? "Judgment";
764
+ const core = deps.outputs.coreTypeOf(domain_type) ?? domain_type;
765
+ const primitive = CORE_TO_PRIMITIVE[core] ?? "JUDGE";
766
+ const approvalInputs = hc.depends_on.flatMap((d) => producedByRole.get(d) ?? []);
767
+ const t0 = Date.now();
768
+ emit({ type: "chair_start", phase: phase.name, role: hc.role, producer: deps.approved_by ?? "human" });
769
+ const rec = deps.outputs.write({
770
+ core_type: core,
771
+ domain_type,
772
+ domain: standard.domain,
773
+ gig_id,
774
+ agent_slug: deps.approved_by ?? "human",
775
+ from_role: hc.role,
776
+ phase: phase.name,
777
+ primitive,
778
+ data: approval,
779
+ input_refs: approvalInputs.map((i) => i.id),
780
+ input_shas: approvalInputs.map((i) => i.content_sha),
781
+ });
782
+ for (const i of approvalInputs)
783
+ deps.outputs.addRef(rec.id, i.id, "derived_from", primitive);
784
+ producedByRole.set(hc.role, [rec]);
785
+ produced.push(rec);
786
+ emit({
787
+ type: "chair_complete", phase: phase.name, role: hc.role, producer: deps.approved_by ?? "human",
788
+ output_types: [domain_type], duration_ms: Date.now() - t0,
789
+ });
790
+ }
791
+ ready = ready.filter((c) => !(c.human === true && (c.agent_slug ?? "") === ""));
792
+ if (ready.length === 0)
793
+ continue;
657
794
  // Routing policy: when a selector is injected, it narrows the frontier to the
658
795
  // chairs to dispatch THIS iteration; the rest stay in `remaining` and re-enter
659
796
  // the next frontier. The return is validated strictly — empty or containing a
@@ -747,6 +884,9 @@ export async function runGig(standard, gigInput, deps) {
747
884
  return undefined;
748
885
  const key = reuseCacheKey({
749
886
  standard_slug: standard.slug,
887
+ // A chart may name one standard TWICE. Keyed on movement_id, the two instances occupy
888
+ // separate namespaces even with byte-identical inputs — isolation by default.
889
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
750
890
  phase: a.phaseName,
751
891
  chair: a.chair,
752
892
  agent: a.agent ?? null,
@@ -817,6 +957,25 @@ export async function runGig(standard, gigInput, deps) {
817
957
  }
818
958
  return { key, hit: { cache_key: key, source_gig_id: entry.source_gig_id, outputs: entry.outputs } };
819
959
  }
960
+ /**
961
+ * Offer an ENTRY chair the seeds a chart edge carried in.
962
+ *
963
+ * Scoped to a chair with no `depends_on`: a chair that named its upstream roles asked for those
964
+ * specific seats, and a movement's seed is not one of them. Records enter `inputs` — so what the
965
+ * chair seals carries their content_shas — and are recorded as consumed for the manifest.
966
+ */
967
+ function pullSeeds(chair, inputs, wanted) {
968
+ if (seedRecords.length === 0 || chair.depends_on.length > 0)
969
+ return;
970
+ for (const s of seedRecords) {
971
+ if (inputs.includes(s))
972
+ continue;
973
+ if (!wanted.some((t) => outputSatisfiesType(s, t)))
974
+ continue;
975
+ inputs.push(s);
976
+ seedsConsumed.set(s.id, s);
977
+ }
978
+ }
820
979
  function prepareChair(chair, phaseName) {
821
980
  // A skill-backed chair runs the skill's deterministic code half — no agent, no model.
822
981
  if (chair.skill_slug && (chair.agent_slug ?? "") === "") {
@@ -835,6 +994,7 @@ export async function runGig(standard, gigInput, deps) {
835
994
  throw new RuntimeError(`chair "${chair.role}" depends_on "${dep}" which has not been produced`);
836
995
  inputs.push(...recs);
837
996
  }
997
+ pullSeeds(chair, inputs, chair.input_contract);
838
998
  if (chair.input_contract.length > 0) {
839
999
  for (const need of chair.input_contract) {
840
1000
  // #156: a type satisfied by an upstream record OR by the gig payload (entry-chair seed).
@@ -901,6 +1061,9 @@ export async function runGig(standard, gigInput, deps) {
901
1061
  }
902
1062
  else {
903
1063
  inputs = produced.filter((o) => agent.input_types.some((t) => outputSatisfiesType(o, t)));
1064
+ // A chart edge's carriers are offered to the same chair on the same terms as an in-gig
1065
+ // upstream record — by type, as records, so provenance survives the movement boundary.
1066
+ pullSeeds(chair, inputs, [...chair.input_contract, ...agent.input_types]);
904
1067
  }
905
1068
  // Runtime input_contract check: every type the chair declares it expects
906
1069
  // on input must be satisfied by its actual upstream inputs. Subtype-aware
@@ -956,7 +1119,7 @@ export async function runGig(standard, gigInput, deps) {
956
1119
  // Resolve this agent's skill bindings (slugs) against the genome's skills map.
957
1120
  // resolveSkills REPORTS; this is where the engine DECIDES — and it decides BEFORE the
958
1121
  // budget deduction below, so a dangling binding costs nothing.
959
- const { skills, missing } = resolveSkills(agent.skill_slugs, deps.skills);
1122
+ const { skills, missing } = resolveSkills(agent.skill_slugs, deps.skills, agent.skills);
960
1123
  if (missing.length > 0) {
961
1124
  // #242 — `Chair.required_skills` was validated exactly once, at compose time, as a
962
1125
  // string-subset check against the agent's own declaration. A chair could declare a
@@ -1457,7 +1620,11 @@ export async function runGig(standard, gigInput, deps) {
1457
1620
  eval_scores[slug] = scoreEval(slug, produced, deps.evals);
1458
1621
  }
1459
1622
  const run_fingerprint = runFingerprint({
1460
- genome_hash,
1623
+ // THE CHART SLOT. When this run is a movement of an arrangement, the arrangement's identity
1624
+ // folds in exactly where `genome_hash` folded — because the reproducible thing is no longer
1625
+ // "this standard's structure" but "this movement of this chart". For a degenerate chart
1626
+ // `chart_hash === genomeHash(standard)`, so a single-standard gig's fingerprint is unmoved.
1627
+ genome_hash: deps.chart?.chart_hash ?? genome_hash,
1461
1628
  model_version: deps.model_version ?? "unknown",
1462
1629
  canonical_form_version: CANONICAL_FORM_VERSION,
1463
1630
  eval_scores,
@@ -1469,9 +1636,13 @@ export async function runGig(standard, gigInput, deps) {
1469
1636
  deps.ledger.append({
1470
1637
  kind: "gig",
1471
1638
  schema_version: LEDGER_SCHEMA_VERSION,
1639
+ // One ROW per movement, because a movement runs under its own gig id (src/chart.ts
1640
+ // `movementGigId`) — and for the degenerate chart that id IS the chart's, which is what an
1641
+ // existing single-standard reader looks up.
1472
1642
  entry_id: gig_id,
1473
1643
  gig_id,
1474
1644
  standard_slug: standard.slug,
1645
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
1475
1646
  genome_hash,
1476
1647
  run_fingerprint,
1477
1648
  output_hashes,
@@ -1513,7 +1684,18 @@ export async function runGig(standard, gigInput, deps) {
1513
1684
  deps.checkpoints?.remove(gig_id);
1514
1685
  }
1515
1686
  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" };
1687
+ const result = {
1688
+ gig_id, standard_slug: standard.slug,
1689
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
1690
+ genome_hash, run_fingerprint, outputs: produced, eval_scores, status: "complete",
1691
+ ...(seedsConsumed.size > 0
1692
+ ? {
1693
+ seeded_from: [...seedsConsumed.values()].map((s) => ({
1694
+ gig_id: s.gig_id, output_id: s.id, domain_type: s.domain_type, content_sha: s.content_sha,
1695
+ })),
1696
+ }
1697
+ : {}),
1698
+ };
1517
1699
  if (settledUsage)
1518
1700
  result.usage = settledUsage;
1519
1701
  if (budget)
@@ -1546,13 +1728,31 @@ export async function runGig(standard, gigInput, deps) {
1546
1728
  // not. Attaching the partial accounting to the error is what lets gig_monitor and a
1547
1729
  // synchronous caller report it. This does NOT write a ledger row — absence-of-row remains
1548
1730
  // the honest "un-sealed gig" signal (recorder_durability_mid_crash.spec.ts).
1731
+ let partial;
1549
1732
  if (e && typeof e === "object") {
1550
- const partial = finalizeUsage();
1733
+ partial = finalizeUsage();
1551
1734
  if (partial)
1552
1735
  e["usage"] = partial;
1553
1736
  if (budget)
1554
1737
  e["budget_state"] = budget;
1555
1738
  }
1739
+ // The sink learns the truth either way: a failed run drains a FAILED header (same
1740
+ // fire-and-forget seam as the success path), so the queue row never sits stale on a
1741
+ // local failure. Found live: the worker's first day exposed the success-only drain.
1742
+ void drainGigHeader({
1743
+ gig_id,
1744
+ standard_slug: standard.slug,
1745
+ status: "failed",
1746
+ genome_hash,
1747
+ started_at,
1748
+ finished_at: new Date().toISOString(),
1749
+ outputs_count: produced.length,
1750
+ error: e instanceof Error ? e.message : String(e),
1751
+ ...(partial ? { usage: { total_cost_usd: partial.total_cost_usd, input_tokens: partial.input_tokens, output_tokens: partial.output_tokens } } : {}),
1752
+ }).catch((de) => {
1753
+ if (process.env["COLTRANE_DRAIN_DEBUG"])
1754
+ console.error(`[drain] failed-gig header ${gig_id}: ${String(de)}`);
1755
+ });
1556
1756
  throw e;
1557
1757
  }
1558
1758
  }