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