@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.
- package/README.md +23 -0
- package/agents/bill.json +59 -0
- package/agents/deploy-agent.json +68 -0
- package/agents/deploy-scout.json +40 -0
- package/agents/john.json +42 -0
- package/agents/miles.json +44 -0
- package/charts/software-delivery-v1.json +9 -0
- package/charts/software-delivery-v2.json +39 -0
- package/dist/src/bifrost_invoker.js +5 -3
- package/dist/src/bifrost_invoker.js.map +1 -1
- package/dist/src/canonical_form.d.ts +23 -0
- package/dist/src/canonical_form.js +53 -0
- package/dist/src/canonical_form.js.map +1 -1
- package/dist/src/chart.d.ts +254 -0
- package/dist/src/chart.js +897 -0
- package/dist/src/chart.js.map +1 -0
- package/dist/src/claude_invoker.d.ts +20 -3
- package/dist/src/claude_invoker.js +30 -5
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +19 -4
- package/dist/src/cli.js +132 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/composition.d.ts +24 -0
- package/dist/src/composition.js +50 -5
- package/dist/src/composition.js.map +1 -1
- package/dist/src/conduct_daemon.d.ts +59 -0
- package/dist/src/conduct_daemon.js +178 -0
- package/dist/src/conduct_daemon.js.map +1 -0
- package/dist/src/genome_schema.d.ts +1476 -146
- package/dist/src/genome_schema.js +438 -32
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.d.ts +79 -0
- package/dist/src/genome_store.js +503 -0
- package/dist/src/genome_store.js.map +1 -0
- package/dist/src/gig_tracker.d.ts +11 -1
- package/dist/src/gig_tracker.js +5 -0
- package/dist/src/gig_tracker.js.map +1 -1
- package/dist/src/hosted_tools.d.ts +23 -0
- package/dist/src/hosted_tools.js +170 -0
- package/dist/src/hosted_tools.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/ledger.d.ts +22 -0
- package/dist/src/ledger.js +4 -0
- package/dist/src/ledger.js.map +1 -1
- package/dist/src/loader.d.ts +9 -1
- package/dist/src/loader.js +105 -5
- package/dist/src/loader.js.map +1 -1
- package/dist/src/mcp.js +52 -5
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/output_mirror.d.ts +70 -0
- package/dist/src/output_mirror.js +381 -0
- package/dist/src/output_mirror.js.map +1 -0
- package/dist/src/outputs.d.ts +77 -1
- package/dist/src/outputs.js +179 -37
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/play_worker.d.ts +24 -0
- package/dist/src/play_worker.js +49 -0
- package/dist/src/play_worker.js.map +1 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/registry.js +63 -33
- package/dist/src/registry.js.map +1 -1
- package/dist/src/reuse.d.ts +56 -0
- package/dist/src/reuse.js +0 -0
- package/dist/src/reuse.js.map +1 -1
- package/dist/src/runtime.d.ts +91 -2
- package/dist/src/runtime.js +236 -18
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/seal_drill.d.ts +32 -0
- package/dist/src/seal_drill.js +138 -0
- package/dist/src/seal_drill.js.map +1 -0
- package/dist/src/server.d.ts +39 -1
- package/dist/src/server.js +760 -68
- package/dist/src/server.js.map +1 -1
- package/dist/src/skill_subprocess.js +12 -4
- package/dist/src/skill_subprocess.js.map +1 -1
- package/dist/src/supabase_genome.d.ts +28 -0
- package/dist/src/supabase_genome.js +49 -0
- package/dist/src/supabase_genome.js.map +1 -0
- package/dist/src/tool_surface.d.ts +5 -0
- package/dist/src/tool_surface.js +15 -0
- package/dist/src/tool_surface.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/worker.d.ts +182 -0
- package/dist/src/worker.js +609 -0
- package/dist/src/worker.js.map +1 -0
- package/domain_types/branch-state.json +21 -0
- package/domain_types/change-context.json +39 -0
- package/domain_types/change-decision.json +36 -0
- package/domain_types/change-plan.json +47 -0
- package/domain_types/change-request.json +24 -0
- package/domain_types/change-set.json +46 -0
- package/domain_types/change-verdict.json +26 -0
- package/domain_types/deploy-verdict.json +23 -0
- package/domain_types/design-brief.json +37 -0
- package/domain_types/design-concept.json +36 -0
- package/domain_types/design-definition.json +37 -0
- package/domain_types/design-question.json +23 -0
- package/domain_types/design-verdict.json +27 -0
- package/domain_types/preview-deployment.json +32 -0
- package/institutions/quartet.json +344 -0
- package/package.json +21 -3
- package/skills/vercel-api/fixtures/error.json +10 -0
- package/skills/vercel-api/fixtures/ready.json +10 -0
- package/skills/vercel-api/fixtures/unsettled.json +10 -0
- package/skills/vercel-api/meta.json +10 -0
- package/skills/vercel-api/skill.mjs +63 -0
- package/standards/preview-deploy-v1.json +89 -0
- package/standards/product-design-v1.json +122 -0
- package/standards/promote-v1.json +41 -0
- package/standards/software-change-v1.json +147 -0
- package/venues/ci-deploy-room-v1.json +28 -0
- package/venues/empty-room-v1.json +19 -0
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
145
|
-
* it returns both what resolved and what
|
|
146
|
-
* means (fatal when the chair declared the skill
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 = {
|
|
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
|
-
|
|
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
|
}
|