@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.
- 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/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/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/genome_schema.d.ts +1130 -166
- package/dist/src/genome_schema.js +311 -34
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.d.ts +53 -3
- package/dist/src/genome_store.js +316 -139
- package/dist/src/genome_store.js.map +1 -1
- 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/index.d.ts +1 -0
- package/dist/src/index.js +1 -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 +42 -4
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/output_mirror.d.ts +1 -1
- package/dist/src/outputs.d.ts +75 -1
- package/dist/src/outputs.js +142 -28
- package/dist/src/outputs.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 +217 -17
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/server.d.ts +11 -0
- package/dist/src/server.js +529 -64
- package/dist/src/server.js.map +1 -1
- 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 +4 -1
- 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.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Standard, Agent, Chair } from "./composition.js";
|
|
2
|
-
|
|
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>;
|
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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";
|
|
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
|
|
146
|
-
* it returns both what resolved and what
|
|
147
|
-
* 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
}
|