@eir-labs/coltrane 0.5.1 → 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 (115) 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/bifrost_invoker.js +5 -3
  10. package/dist/src/bifrost_invoker.js.map +1 -1
  11. package/dist/src/canonical_form.d.ts +23 -0
  12. package/dist/src/canonical_form.js +53 -0
  13. package/dist/src/canonical_form.js.map +1 -1
  14. package/dist/src/chart.d.ts +254 -0
  15. package/dist/src/chart.js +897 -0
  16. package/dist/src/chart.js.map +1 -0
  17. package/dist/src/claude_invoker.d.ts +20 -3
  18. package/dist/src/claude_invoker.js +30 -5
  19. package/dist/src/claude_invoker.js.map +1 -1
  20. package/dist/src/cli.d.ts +19 -4
  21. package/dist/src/cli.js +132 -9
  22. package/dist/src/cli.js.map +1 -1
  23. package/dist/src/composition.d.ts +24 -0
  24. package/dist/src/composition.js +50 -5
  25. package/dist/src/composition.js.map +1 -1
  26. package/dist/src/conduct_daemon.d.ts +59 -0
  27. package/dist/src/conduct_daemon.js +178 -0
  28. package/dist/src/conduct_daemon.js.map +1 -0
  29. package/dist/src/genome_schema.d.ts +1476 -146
  30. package/dist/src/genome_schema.js +438 -32
  31. package/dist/src/genome_schema.js.map +1 -1
  32. package/dist/src/genome_store.d.ts +79 -0
  33. package/dist/src/genome_store.js +503 -0
  34. package/dist/src/genome_store.js.map +1 -0
  35. package/dist/src/gig_tracker.d.ts +11 -1
  36. package/dist/src/gig_tracker.js +5 -0
  37. package/dist/src/gig_tracker.js.map +1 -1
  38. package/dist/src/hosted_tools.d.ts +23 -0
  39. package/dist/src/hosted_tools.js +170 -0
  40. package/dist/src/hosted_tools.js.map +1 -0
  41. package/dist/src/index.d.ts +4 -0
  42. package/dist/src/index.js +4 -0
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/ledger.d.ts +22 -0
  45. package/dist/src/ledger.js +4 -0
  46. package/dist/src/ledger.js.map +1 -1
  47. package/dist/src/loader.d.ts +9 -1
  48. package/dist/src/loader.js +105 -5
  49. package/dist/src/loader.js.map +1 -1
  50. package/dist/src/mcp.js +52 -5
  51. package/dist/src/mcp.js.map +1 -1
  52. package/dist/src/output_mirror.d.ts +70 -0
  53. package/dist/src/output_mirror.js +381 -0
  54. package/dist/src/output_mirror.js.map +1 -0
  55. package/dist/src/outputs.d.ts +77 -1
  56. package/dist/src/outputs.js +179 -37
  57. package/dist/src/outputs.js.map +1 -1
  58. package/dist/src/play_worker.d.ts +24 -0
  59. package/dist/src/play_worker.js +49 -0
  60. package/dist/src/play_worker.js.map +1 -0
  61. package/dist/src/registry.d.ts +2 -0
  62. package/dist/src/registry.js +63 -33
  63. package/dist/src/registry.js.map +1 -1
  64. package/dist/src/reuse.d.ts +56 -0
  65. package/dist/src/reuse.js +0 -0
  66. package/dist/src/reuse.js.map +1 -1
  67. package/dist/src/runtime.d.ts +91 -2
  68. package/dist/src/runtime.js +236 -18
  69. package/dist/src/runtime.js.map +1 -1
  70. package/dist/src/seal_drill.d.ts +32 -0
  71. package/dist/src/seal_drill.js +138 -0
  72. package/dist/src/seal_drill.js.map +1 -0
  73. package/dist/src/server.d.ts +39 -1
  74. package/dist/src/server.js +760 -68
  75. package/dist/src/server.js.map +1 -1
  76. package/dist/src/skill_subprocess.js +12 -4
  77. package/dist/src/skill_subprocess.js.map +1 -1
  78. package/dist/src/supabase_genome.d.ts +28 -0
  79. package/dist/src/supabase_genome.js +49 -0
  80. package/dist/src/supabase_genome.js.map +1 -0
  81. package/dist/src/tool_surface.d.ts +5 -0
  82. package/dist/src/tool_surface.js +15 -0
  83. package/dist/src/tool_surface.js.map +1 -0
  84. package/dist/src/version.d.ts +1 -1
  85. package/dist/src/version.js +1 -1
  86. package/dist/src/worker.d.ts +182 -0
  87. package/dist/src/worker.js +609 -0
  88. package/dist/src/worker.js.map +1 -0
  89. package/domain_types/branch-state.json +21 -0
  90. package/domain_types/change-context.json +39 -0
  91. package/domain_types/change-decision.json +36 -0
  92. package/domain_types/change-plan.json +47 -0
  93. package/domain_types/change-request.json +24 -0
  94. package/domain_types/change-set.json +46 -0
  95. package/domain_types/change-verdict.json +26 -0
  96. package/domain_types/deploy-verdict.json +23 -0
  97. package/domain_types/design-brief.json +37 -0
  98. package/domain_types/design-concept.json +36 -0
  99. package/domain_types/design-definition.json +37 -0
  100. package/domain_types/design-question.json +23 -0
  101. package/domain_types/design-verdict.json +27 -0
  102. package/domain_types/preview-deployment.json +32 -0
  103. package/institutions/quartet.json +344 -0
  104. package/package.json +21 -3
  105. package/skills/vercel-api/fixtures/error.json +10 -0
  106. package/skills/vercel-api/fixtures/ready.json +10 -0
  107. package/skills/vercel-api/fixtures/unsettled.json +10 -0
  108. package/skills/vercel-api/meta.json +10 -0
  109. package/skills/vercel-api/skill.mjs +63 -0
  110. package/standards/preview-deploy-v1.json +89 -0
  111. package/standards/product-design-v1.json +122 -0
  112. package/standards/promote-v1.json +41 -0
  113. package/standards/software-change-v1.json +147 -0
  114. package/venues/ci-deploy-room-v1.json +28 -0
  115. package/venues/empty-room-v1.json +19 -0
@@ -11,9 +11,15 @@ 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";
22
+ import { drainGigHeader } from "./output_mirror.js";
17
23
  import { LEDGER_SCHEMA_VERSION } from "./ledger.js";
18
24
  /**
19
25
  * #236 — settled spend used to be discarded on every failed gig: `usage` was written only on
@@ -141,9 +147,16 @@ export function computeAppendCost(ctx, base_cost, k) {
141
147
  // in a canonical (sorted, JCS) form. This is the reproducibility key — same defs,
142
148
  // same genome_hash, regardless of model or run.
143
149
  /**
144
- * Resolve a list of skill slugs against the genome's skills map. REPORTS, never decides:
145
- * it returns both what resolved and what did not, and `prepareChair` decides what a miss
146
- * 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.
147
160
  *
148
161
  * The boundary (#241): a skill package that LOADS is a legitimate degradation candidate —
149
162
  * it has an identity, a version, a code_hash, and its degradation is already surfaced and
@@ -155,12 +168,15 @@ export function computeAppendCost(ctx, base_cost, k) {
155
168
  * An ABSENT map means resolution was never configured (the documented v0 back-compat path)
156
169
  * — that is not evidence of a dangling binding, so `missing` stays empty.
157
170
  */
158
- function resolveSkills(slugs, map) {
171
+ function resolveSkills(slugs, map, carried) {
172
+ const skills = [...(carried ?? [])];
173
+ const carriedSlugs = new Set(skills.map((s) => s.slug));
159
174
  if (!slugs || slugs.length === 0 || !map)
160
- return { skills: [], missing: [] };
161
- const skills = [];
175
+ return { skills, missing: [] };
162
176
  const missing = [];
163
177
  for (const slug of slugs) {
178
+ if (carriedSlugs.has(slug))
179
+ continue; // the carried definition already answered this slug
164
180
  const rec = map.get(slug);
165
181
  if (rec)
166
182
  skills.push(rec);
@@ -169,7 +185,16 @@ function resolveSkills(slugs, map) {
169
185
  }
170
186
  return { skills, missing };
171
187
  }
172
- 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) {
173
198
  const agents = [...standard.agents]
174
199
  .map((a) => ({
175
200
  slug: a.slug,
@@ -179,7 +204,13 @@ function genomeHash(standard) {
179
204
  domain: a.domain,
180
205
  }))
181
206
  .sort((x, y) => (x.slug < y.slug ? -1 : 1));
182
- 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 }));
183
214
  }
184
215
  /**
185
216
  * Execute one gig: walk phases in order, each phase's agent consumes the prior
@@ -193,7 +224,10 @@ function genomeHash(standard) {
193
224
  // declarations are polymorphic, so a base player written against `Interpretation`
194
225
  // consumes any downstream subtype while domain contracts keep their precision.
195
226
  const CORE_TYPE_SET = new Set(CORE_TYPES);
196
- 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) {
197
231
  if (output.domain_type === declared)
198
232
  return true;
199
233
  if (CORE_TYPE_SET.has(declared) && output.core_type === declared)
@@ -303,6 +337,10 @@ export async function runGig(standard, gigInput, deps) {
303
337
  // any chair"). The payload is validated BEFORE any chair fires — a missing gig input is a
304
338
  // hard stop, so no model tokens are spent on bad input.
305
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
306
344
  // Keys are the HYPHENATED type slug. `grant_requirements` vs `grant-requirements` is the
307
345
  // single most common dispatch mistake, and the caller's own keys are in scope here.
308
346
  const normalizeKey = (k) => k.toLowerCase().replace(/[_\-\s]/g, "");
@@ -337,6 +375,10 @@ export async function runGig(standard, gigInput, deps) {
337
375
  sealedByRole.set(ch.role, [ch.output_contract[0] ?? "Signal"]);
338
376
  continue;
339
377
  }
378
+ if (ch.human === true && (ch.agent_slug ?? "") === "") {
379
+ sealedByRole.set(ch.role, [ch.output_contract[0] ?? "Judgment"]);
380
+ continue;
381
+ }
340
382
  const ag = standard.agents.find((a) => a.slug === ch.agent_slug);
341
383
  if (!ag)
342
384
  continue; // prepareChair reports an unknown agent_slug precisely; don't pre-empt it
@@ -375,6 +417,10 @@ export async function runGig(standard, gigInput, deps) {
375
417
  continue; // supplied
376
418
  if (reachable.some((t) => mightSatisfy(t, need)))
377
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;
378
424
  throw missingGigInput(need, ch.role);
379
425
  }
380
426
  sealedThisPhase.push(...(sealedByRole.get(ch.role) ?? []));
@@ -475,7 +521,10 @@ export async function runGig(standard, gigInput, deps) {
475
521
  };
476
522
  const identity = () => ({
477
523
  standard_slug: standard.slug,
478
- 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,
479
528
  // #278 review — genome_hash does NOT see an agent's identity/method/constraints/tools,
480
529
  // nor a skill's code. Those are the producer, and editing one under a stable slug is the
481
530
  // ordinary response to a bad run. Without this the resume gate accepted exactly that.
@@ -523,6 +572,15 @@ export async function runGig(standard, gigInput, deps) {
523
572
  if (!rolesInStandard.has(r.role)) {
524
573
  throw new ResumeRefused(gig_id, `its checkpoint names role "${r.role}", which this standard does not define`);
525
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
+ }
526
584
  const records = [];
527
585
  for (let i = 0; i < r.output_ids.length; i++) {
528
586
  const id = r.output_ids[i];
@@ -568,6 +626,9 @@ export async function runGig(standard, gigInput, deps) {
568
626
  return;
569
627
  checkpointRoles.set(role, {
570
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 } : {}),
571
632
  output_ids: records.map((r) => r.id),
572
633
  content_shas: records.map((r) => r.content_sha),
573
634
  domain_types: records.map((r) => r.domain_type),
@@ -596,6 +657,9 @@ export async function runGig(standard, gigInput, deps) {
596
657
  updated_at: new Date().toISOString(),
597
658
  roles: [...checkpointRoles.values()],
598
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 } : {}),
599
663
  });
600
664
  }
601
665
  catch (e) {
@@ -653,6 +717,80 @@ export async function runGig(standard, gigInput, deps) {
653
717
  const stuck = [...remaining.values()].map((c) => c.role).join(", ");
654
718
  throw new RuntimeError(`phase "${phase.name}" cannot advance — chairs [${stuck}] have unresolved depends_on`);
655
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;
656
794
  // Routing policy: when a selector is injected, it narrows the frontier to the
657
795
  // chairs to dispatch THIS iteration; the rest stay in `remaining` and re-enter
658
796
  // the next frontier. The return is validated strictly — empty or containing a
@@ -746,6 +884,9 @@ export async function runGig(standard, gigInput, deps) {
746
884
  return undefined;
747
885
  const key = reuseCacheKey({
748
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 } : {}),
749
890
  phase: a.phaseName,
750
891
  chair: a.chair,
751
892
  agent: a.agent ?? null,
@@ -816,6 +957,25 @@ export async function runGig(standard, gigInput, deps) {
816
957
  }
817
958
  return { key, hit: { cache_key: key, source_gig_id: entry.source_gig_id, outputs: entry.outputs } };
818
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
+ }
819
979
  function prepareChair(chair, phaseName) {
820
980
  // A skill-backed chair runs the skill's deterministic code half — no agent, no model.
821
981
  if (chair.skill_slug && (chair.agent_slug ?? "") === "") {
@@ -834,6 +994,7 @@ export async function runGig(standard, gigInput, deps) {
834
994
  throw new RuntimeError(`chair "${chair.role}" depends_on "${dep}" which has not been produced`);
835
995
  inputs.push(...recs);
836
996
  }
997
+ pullSeeds(chair, inputs, chair.input_contract);
837
998
  if (chair.input_contract.length > 0) {
838
999
  for (const need of chair.input_contract) {
839
1000
  // #156: a type satisfied by an upstream record OR by the gig payload (entry-chair seed).
@@ -900,6 +1061,9 @@ export async function runGig(standard, gigInput, deps) {
900
1061
  }
901
1062
  else {
902
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]);
903
1067
  }
904
1068
  // Runtime input_contract check: every type the chair declares it expects
905
1069
  // on input must be satisfied by its actual upstream inputs. Subtype-aware
@@ -955,7 +1119,7 @@ export async function runGig(standard, gigInput, deps) {
955
1119
  // Resolve this agent's skill bindings (slugs) against the genome's skills map.
956
1120
  // resolveSkills REPORTS; this is where the engine DECIDES — and it decides BEFORE the
957
1121
  // budget deduction below, so a dangling binding costs nothing.
958
- const { skills, missing } = resolveSkills(agent.skill_slugs, deps.skills);
1122
+ const { skills, missing } = resolveSkills(agent.skill_slugs, deps.skills, agent.skills);
959
1123
  if (missing.length > 0) {
960
1124
  // #242 — `Chair.required_skills` was validated exactly once, at compose time, as a
961
1125
  // string-subset check against the agent's own declaration. A chair could declare a
@@ -1456,7 +1620,11 @@ export async function runGig(standard, gigInput, deps) {
1456
1620
  eval_scores[slug] = scoreEval(slug, produced, deps.evals);
1457
1621
  }
1458
1622
  const run_fingerprint = runFingerprint({
1459
- 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,
1460
1628
  model_version: deps.model_version ?? "unknown",
1461
1629
  canonical_form_version: CANONICAL_FORM_VERSION,
1462
1630
  eval_scores,
@@ -1464,22 +1632,43 @@ export async function runGig(standard, gigInput, deps) {
1464
1632
  ...(unresolved_evals.length > 0 ? { unresolved_evals } : {}),
1465
1633
  });
1466
1634
  const settledUsage = finalizeUsage();
1635
+ const gig_finished_at = new Date().toISOString();
1467
1636
  deps.ledger.append({
1468
1637
  kind: "gig",
1469
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.
1470
1642
  entry_id: gig_id,
1471
1643
  gig_id,
1472
1644
  standard_slug: standard.slug,
1645
+ ...(deps.chart ? { chart_slug: deps.chart.chart_slug, movement_id: deps.chart.movement_id } : {}),
1473
1646
  genome_hash,
1474
1647
  run_fingerprint,
1475
1648
  output_hashes,
1476
1649
  started_at,
1477
- finished_at: new Date().toISOString(),
1650
+ finished_at: gig_finished_at,
1478
1651
  // settled model spend (#195) — omitted when nothing was CAPTURED (skill-only gigs, stubbed
1479
1652
  // invokers, or a run whose every invocation reported no usage payload). #235: an absent
1480
1653
  // usage block means "not captured", never "$0.00".
1481
1654
  ...(settledUsage ? { usage: settledUsage } : {}),
1482
1655
  });
1656
+ // Drain the gig HEADER to the sink (fire-and-forget, like every output before it) — the
1657
+ // stub row the drain service fabricated for FK integrity is replaced by the run's own record.
1658
+ void drainGigHeader({
1659
+ gig_id,
1660
+ standard_slug: standard.slug,
1661
+ status: "complete",
1662
+ genome_hash,
1663
+ run_fingerprint,
1664
+ started_at,
1665
+ finished_at: gig_finished_at,
1666
+ outputs_count: produced.length,
1667
+ ...(settledUsage ? { usage: { total_cost_usd: settledUsage.total_cost_usd, input_tokens: settledUsage.input_tokens, output_tokens: settledUsage.output_tokens } } : {}),
1668
+ }).catch((e) => {
1669
+ if (process.env["COLTRANE_DRAIN_DEBUG"])
1670
+ console.error(`[drain] gig header ${gig_id}: ${String(e)}`);
1671
+ });
1483
1672
  // Cycle complete — when a budget was supplied, mark it `settled` and
1484
1673
  // surface the final state in the manifest. `settled` mirrors the
1485
1674
  // budget-state.json cycle terminal-state semantics for a closed cycle.
@@ -1495,7 +1684,18 @@ export async function runGig(standard, gigInput, deps) {
1495
1684
  deps.checkpoints?.remove(gig_id);
1496
1685
  }
1497
1686
  catch { /* reclaiming disk must not fail a run that succeeded */ }
1498
- 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
+ };
1499
1699
  if (settledUsage)
1500
1700
  result.usage = settledUsage;
1501
1701
  if (budget)
@@ -1528,13 +1728,31 @@ export async function runGig(standard, gigInput, deps) {
1528
1728
  // not. Attaching the partial accounting to the error is what lets gig_monitor and a
1529
1729
  // synchronous caller report it. This does NOT write a ledger row — absence-of-row remains
1530
1730
  // the honest "un-sealed gig" signal (recorder_durability_mid_crash.spec.ts).
1731
+ let partial;
1531
1732
  if (e && typeof e === "object") {
1532
- const partial = finalizeUsage();
1733
+ partial = finalizeUsage();
1533
1734
  if (partial)
1534
1735
  e["usage"] = partial;
1535
1736
  if (budget)
1536
1737
  e["budget_state"] = budget;
1537
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
+ });
1538
1756
  throw e;
1539
1757
  }
1540
1758
  }