@eir-labs/coltrane 0.10.0 → 0.12.0
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/agents/bandleader.json +54 -0
- package/agents/bill.json +4 -2
- package/agents/change-verifier.json +2 -1
- package/agents/code-implementer.json +7 -4
- package/agents/john.json +5 -3
- package/agents/lineage-scout-external.json +19 -6
- package/agents/lineage-scribe.json +20 -9
- package/agents/lineage-weaver.json +26 -11
- package/agents/miles.json +2 -1
- package/agents/players/bandleader.md +70 -0
- package/agents/pr-publisher.json +4 -2
- package/agents/red-spec-drafter.json +12 -6
- package/agents/room-prober.json +29 -0
- package/agents/source-walker.json +1 -1
- package/charts/room-probe-chart-v1.json +14 -0
- package/charts/worker-contract-docker-v2.json +135 -0
- package/charts/worker-contract-v3.json +206 -0
- package/dist/src/boundary_check.d.ts +18 -0
- package/dist/src/boundary_check.js +70 -0
- package/dist/src/boundary_check.js.map +1 -0
- package/dist/src/boundary_scan.d.ts +62 -0
- package/dist/src/boundary_scan.js +93 -0
- package/dist/src/boundary_scan.js.map +1 -0
- package/dist/src/chart.js +34 -1
- package/dist/src/chart.js.map +1 -1
- package/dist/src/claude_invoker.d.ts +2 -13
- package/dist/src/claude_invoker.js +242 -18
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +115 -27
- package/dist/src/cli.js.map +1 -1
- package/dist/src/drain_preflight.d.ts +33 -0
- package/dist/src/drain_preflight.js +97 -0
- package/dist/src/drain_preflight.js.map +1 -0
- package/dist/src/genome_schema.d.ts +390 -9
- package/dist/src/genome_schema.js +163 -8
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_store.js +9 -0
- package/dist/src/genome_store.js.map +1 -1
- package/dist/src/genome_writer.d.ts +1 -1
- package/dist/src/genome_writer.js +10 -2
- package/dist/src/genome_writer.js.map +1 -1
- package/dist/src/gig_conformance.d.ts +66 -0
- package/dist/src/gig_conformance.js +153 -0
- package/dist/src/gig_conformance.js.map +1 -0
- package/dist/src/gig_tracker.js +8 -0
- package/dist/src/gig_tracker.js.map +1 -1
- package/dist/src/grounding.d.ts +44 -0
- package/dist/src/grounding.js +127 -0
- package/dist/src/grounding.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/institution_loader.d.ts +29 -5
- package/dist/src/institution_loader.js +67 -6
- package/dist/src/institution_loader.js.map +1 -1
- package/dist/src/lineage_adoption.d.ts +82 -0
- package/dist/src/lineage_adoption.js +82 -0
- package/dist/src/lineage_adoption.js.map +1 -0
- package/dist/src/lineage_persist.d.ts +34 -0
- package/dist/src/lineage_persist.js +60 -0
- package/dist/src/lineage_persist.js.map +1 -0
- package/dist/src/loader.d.ts +13 -0
- package/dist/src/loader.js +43 -2
- package/dist/src/loader.js.map +1 -1
- package/dist/src/local_queue.d.ts +105 -0
- package/dist/src/local_queue.js +380 -0
- package/dist/src/local_queue.js.map +1 -0
- package/dist/src/mcp.js +37 -4
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/org_hire.d.ts +23 -0
- package/dist/src/org_hire.js +42 -0
- package/dist/src/org_hire.js.map +1 -0
- package/dist/src/outputs.d.ts +17 -0
- package/dist/src/outputs.js +86 -1
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/player_to_claude_code.js +26 -2
- package/dist/src/player_to_claude_code.js.map +1 -1
- package/dist/src/registry.d.ts +3 -1
- package/dist/src/registry.js +43 -3
- package/dist/src/registry.js.map +1 -1
- package/dist/src/repo_index.d.ts +58 -0
- package/dist/src/repo_index.js +232 -0
- package/dist/src/repo_index.js.map +1 -0
- package/dist/src/residency.d.ts +149 -0
- package/dist/src/residency.js +283 -0
- package/dist/src/residency.js.map +1 -0
- package/dist/src/reuse.d.ts +44 -4
- package/dist/src/reuse.js +0 -0
- package/dist/src/reuse.js.map +1 -1
- package/dist/src/runtime.d.ts +82 -0
- package/dist/src/runtime.js +254 -25
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/server.d.ts +37 -0
- package/dist/src/server.js +422 -11
- package/dist/src/server.js.map +1 -1
- package/dist/src/server_relay.d.ts +40 -2
- package/dist/src/server_relay.js +186 -19
- package/dist/src/server_relay.js.map +1 -1
- package/dist/src/tool_providers.d.ts +27 -0
- package/dist/src/tool_providers.js +48 -0
- package/dist/src/tool_providers.js.map +1 -1
- package/dist/src/venue_credential.d.ts +41 -0
- package/dist/src/venue_credential.js +57 -0
- package/dist/src/venue_credential.js.map +1 -0
- package/dist/src/venue_realize.d.ts +23 -0
- package/dist/src/venue_realize.js +43 -3
- package/dist/src/venue_realize.js.map +1 -1
- package/dist/src/venue_realizer.d.ts +224 -0
- package/dist/src/venue_realizer.js +889 -0
- package/dist/src/venue_realizer.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 +57 -0
- package/dist/src/worker.js +155 -24
- package/dist/src/worker.js.map +1 -1
- package/dist/src/worker_env.d.ts +79 -0
- package/dist/src/worker_env.js +322 -0
- package/dist/src/worker_env.js.map +1 -0
- package/dist/src/workspace.d.ts +12 -1
- package/dist/src/workspace.js +10 -3
- package/dist/src/workspace.js.map +1 -1
- package/domain_types/change-context.json +59 -13
- package/domain_types/lineage-adoption-target.json +22 -0
- package/domain_types/lineage-map.json +7 -2
- package/domain_types/lineage-record.json +55 -5
- package/domain_types/prior-art-hit.json +16 -1
- package/domain_types/red-spec.json +5 -3
- package/domain_types/woodshed-record.json +149 -0
- package/evals/gist-present.json +7 -0
- package/institutions/coltrane.json +178 -6
- package/package.json +6 -3
- package/skills/non-vacuity-probe/fixtures/fixture-001.json +17 -0
- package/skills/non-vacuity-probe/meta.json +12 -0
- package/skills/non-vacuity-probe/skill.md +27 -0
- package/skills/non-vacuity-probe/skill.mjs +81 -0
- package/standards/lineage-adopt-v0.json +37 -0
- package/standards/room-probe-v1.json +28 -0
- package/standards/software-change-pr-v1.json +15 -15
- package/standards/software-change-red-first-v0.json +183 -0
- package/standards/spec-review-and-sequence-v0.json +112 -0
- package/standards/studio-session-v0.json +101 -0
- package/tours/coltrane.json +72 -0
- package/venues/engine-room-v1.json +32 -0
package/dist/src/runtime.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// writes each typed output to the store (validated), links provenance (derived_from),
|
|
4
4
|
// and records one ledger entry with a deterministic genome_hash + a run_fingerprint
|
|
5
5
|
// that carries model_version + (empty, v0) eval_scores — honestly un-tempered.
|
|
6
|
+
import { lineageAdoption } from "./lineage_adoption.js";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
8
|
import { PRIMITIVE_OUTPUT_TYPE, CORE_TYPES } from "./core_types.js";
|
|
8
9
|
import { executeSkillAsync } from "./skill_subprocess.js";
|
|
@@ -21,6 +22,7 @@ import { resolveAndRealize } from "./venue_realize.js";
|
|
|
21
22
|
export const CORE_TO_PRIMITIVE = Object.fromEntries(Object.entries(PRIMITIVE_OUTPUT_TYPE).map(([prim, core]) => [String(core), prim]));
|
|
22
23
|
import { sha256Hex, canonJson, canonStructuralJson, runFingerprint, outputContentHash, CANONICAL_FORM_VERSION } from "./canonical_form.js";
|
|
23
24
|
import { producersSha, reuseCacheKey, checkReuseEntry, runIdentityMismatch, checkpointRoleKey, CHECKPOINT_SCHEMA_VERSION, REUSE_SCHEMA_VERSION, } from "./reuse.js";
|
|
25
|
+
import { checkGigConformance } from "./gig_conformance.js";
|
|
24
26
|
import { drainGigHeader } from "./output_mirror.js";
|
|
25
27
|
import { LEDGER_SCHEMA_VERSION } from "./ledger.js";
|
|
26
28
|
import { COLTRANE_VERSION } from "./version.js";
|
|
@@ -289,11 +291,24 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
289
291
|
// construction, and torn down at each chair's lifecycle end.
|
|
290
292
|
let gigRealization;
|
|
291
293
|
let gigVenue;
|
|
294
|
+
// The SUBSTRATE half of the wire. The policy realization above CONFINES a venue-named gig — it
|
|
295
|
+
// intersects the tool ceiling and refuses a breach — but stands up no room. Without this the
|
|
296
|
+
// container substrate (src/venue_realizer.ts) was unreachable from dispatch: a venue-named gig got
|
|
297
|
+
// NO ROOM, only paper confinement. When the resolved venue declares mcp_servers AND a realizer is
|
|
298
|
+
// wired, the substrate is realized here, ONCE, before any chair — and torn down beside the policy
|
|
299
|
+
// layer in the finally block below.
|
|
300
|
+
let gigSubstrate;
|
|
292
301
|
if (deps.venue !== undefined) {
|
|
293
302
|
const realization = resolveAndRealize(deps.venue, {
|
|
294
303
|
venues: new Map(deps.venues ?? []),
|
|
295
304
|
seats: standard.agents.map((agent) => ({ agent })),
|
|
296
|
-
|
|
305
|
+
// The ambient container environment realize() filters to each seat's allowlisted env
|
|
306
|
+
// (SEAT_ENV_ALLOWLIST — PATH/HOME). Passing `{}` here was the other half of the ENOENT
|
|
307
|
+
// measured on gig 87cffa2c: even a correct allowlist has nothing to admit when the caller
|
|
308
|
+
// feeds it an empty env, so every venue-confined seat spawned with no PATH and died. The
|
|
309
|
+
// allowlist is what makes handing realize() the whole process.env safe — it carries PATH/HOME
|
|
310
|
+
// through and drops every credential (COLTRANE_DRAIN_KEY et al.), never inheriting wholesale.
|
|
311
|
+
ambientEnv: process.env,
|
|
297
312
|
...(deps.credentialsPresent ? { credentialsPresent: deps.credentialsPresent } : {}),
|
|
298
313
|
gigId: gig_id,
|
|
299
314
|
});
|
|
@@ -303,6 +318,33 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
303
318
|
gigRealization = realization;
|
|
304
319
|
// realize() only returns ok once the slug resolved, so the venue is present in the map.
|
|
305
320
|
gigVenue = deps.venues?.get(deps.venue);
|
|
321
|
+
// Realize the SUBSTRATE when the venue declares servers to stand up OR the RUN names a repository
|
|
322
|
+
// to populate the room's tree with — either is a reason to build a real room. A venue with NEITHER
|
|
323
|
+
// has nothing to stand up (the empty room stays free), and an absent realizer is the opt-out that
|
|
324
|
+
// keeps every pre-wire caller byte-identical. realize() tolerates an empty mcp_servers list (its
|
|
325
|
+
// credential/compose steps iterate the servers as no-ops — the existing repo_url-declines law
|
|
326
|
+
// exercises exactly this, mcp_servers: [], and gets a handle back), so a repository-only run
|
|
327
|
+
// reaches it without an empty services list becoming fatal. credentialResolver defaults to an
|
|
328
|
+
// empty async resolver — realize() takes it as a required positional, so undefined would throw at
|
|
329
|
+
// credential-resolution time; the empty default satisfies the interface without a new mandatory
|
|
330
|
+
// field. A bring-up failure propagates like a policy refusal: no chair spawns.
|
|
331
|
+
//
|
|
332
|
+
// The repository is `deps.repoUrl` — named by the RUN, explicit at dispatch. The per-gig git
|
|
333
|
+
// credential plumbing (drainKey/instance/endpoint) is READ FROM THE AMBIENT ENVIRONMENT here, the
|
|
334
|
+
// same discipline src/workspace.ts documents: the SUBJECT of work must be explicit, but the
|
|
335
|
+
// credential that clones it is ambient plumbing, never a per-repository fact. Absent env → the
|
|
336
|
+
// populate declines on its own terms (prepareWorkspace names what it could not obtain).
|
|
337
|
+
if (deps.venueRealizer && gigVenue && (gigVenue.mcp_servers.length > 0 || deps.repoUrl)) {
|
|
338
|
+
gigSubstrate = await deps.venueRealizer.realize(gigVenue, deps.credentialResolver ?? (async () => ({})), {
|
|
339
|
+
gigId: gig_id,
|
|
340
|
+
...(deps.repoUrl ? { repoUrl: deps.repoUrl } : {}),
|
|
341
|
+
...(process.env["COLTRANE_DRAIN_KEY"] ? { drainKey: process.env["COLTRANE_DRAIN_KEY"] } : {}),
|
|
342
|
+
...(process.env["COLTRANE_INSTANCE"] ? { instance: process.env["COLTRANE_INSTANCE"] } : {}),
|
|
343
|
+
...(process.env["COLTRANE_GIT_CREDENTIALS_URL"]
|
|
344
|
+
? { gitCredentialsEndpoint: process.env["COLTRANE_GIT_CREDENTIALS_URL"] }
|
|
345
|
+
: {}),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
306
348
|
}
|
|
307
349
|
// Hash the gig input LAZILY. Three callers want it now (#196's provenance backfill, the
|
|
308
350
|
// resume identity, and the reuse key) but a hostile or circular payload must not be
|
|
@@ -702,23 +744,94 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
702
744
|
// THE GATE. A resume into a moved genome would have chairs from genome B consuming sealed
|
|
703
745
|
// outputs from genome A, and nothing in input_shas / genome_hash / run_fingerprint would
|
|
704
746
|
// record that it happened — the manifest would describe a system that never existed.
|
|
705
|
-
|
|
747
|
+
//
|
|
748
|
+
// SCOPED RELAXATION (gig ce902971): producers_sha — the fold of every producing agent's
|
|
749
|
+
// definition — is NOT a resume gate when every chair still to run is human. Those producers
|
|
750
|
+
// already produced; their sealed, hashed outputs cannot change under a definition edit, and a
|
|
751
|
+
// producing agent's definition cannot change what a PERSON is being asked to decide. Without
|
|
752
|
+
// this, a parked gig becomes permanently unapprovable the moment any agent definition moves
|
|
753
|
+
// after it parks. This is the ONLY relaxation and its ONLY justification is 'this input cannot
|
|
754
|
+
// affect what remains to run': every other identity field still gates, and producers_sha
|
|
755
|
+
// itself still gates the moment ANY remaining chair is a model chair. The remaining-chair set
|
|
756
|
+
// is derived synchronously from data already in scope (cp.roles, standard.phases), so the gate
|
|
757
|
+
// still fires before the first await.
|
|
758
|
+
//
|
|
759
|
+
// The predicate is two-valued — a chair is human or it is a model chair. If a future
|
|
760
|
+
// non-human, agent-less chair type is introduced it MUST be re-examined here: it must not
|
|
761
|
+
// silently inherit this waiver.
|
|
762
|
+
const sealedRoles = new Set(cp.roles.map((r) => r.role));
|
|
763
|
+
const remainingChairs = standard.phases.flatMap((p) => p.chairs).filter((c) => !sealedRoles.has(c.role));
|
|
764
|
+
const allRemainingHuman = remainingChairs.length > 0 &&
|
|
765
|
+
remainingChairs.every((c) => c.human === true && (c.agent_slug ?? "") === "");
|
|
766
|
+
// #19 — an OMITTED --depth is not a DISAGREEMENT. When the operator states no depth,
|
|
767
|
+
// `deps.depth` is undefined and identity() resolves it to "" — collapsing "I did not say"
|
|
768
|
+
// and "I said something different" into one value that then reads as a mismatch against a
|
|
769
|
+
// checkpoint written with a stated depth. The checkpoint already records its depth (a
|
|
770
|
+
// first-class run-identity field), so an unstated depth INHERITS it: substitute the
|
|
771
|
+
// checkpoint's depth as the effective comparison value ONLY when deps.depth is undefined.
|
|
772
|
+
// An EXPLICIT depth still compares as-is, so a skim resume of a deep checkpoint still
|
|
773
|
+
// refuses (#237: depth shapes what the model is asked for; a skim half stitched to a deep
|
|
774
|
+
// half is a real defect). This never MANUFACTURES a false mismatch and it is NOT a waiver
|
|
775
|
+
// of a stated one — see the note beside the 'never waived' rule in src/reuse.ts. identity()
|
|
776
|
+
// stays a pure function of deps, so `cur = identity()` below and every non-comparison use
|
|
777
|
+
// still see the operator's actual (undefined→"") depth.
|
|
778
|
+
// #20 — an OMITTED --input is no more a DISAGREEMENT than an omitted --depth, and the same
|
|
779
|
+
// omission-vs-conflict distinction applies. A checkpoint stores ONLY identity.gig_input_sha —
|
|
780
|
+
// a hash of the canonical dispatch payload — and NEVER the payload itself (see src/reuse.ts,
|
|
781
|
+
// RunIdentity.gig_input_sha), so the omitted payload cannot be "recovered from the checkpoint";
|
|
782
|
+
// it is INHERITED. The producers already consumed the input and sealed their hashed outputs, so
|
|
783
|
+
// when every remaining chair is human — approving already-sealed work it does not read — an
|
|
784
|
+
// unstated --input inherits the checkpoint's recorded gig_input_sha. A SUPPLIED value is never
|
|
785
|
+
// substituted (deps.gig_input_omitted is false), so a disagreeing --input still drives a
|
|
786
|
+
// refusal; and while any remaining chair is a MODEL chair (allRemainingHuman false) the payload
|
|
787
|
+
// is exactly what that chair will consume, so gig_input_sha keeps gating unchanged. identity()
|
|
788
|
+
// itself stays untouched, so `cur = identity()` below and every checkpoint write still see the
|
|
789
|
+
// operator's actual (omitted→{}) payload hash — the substitution is only the comparison value.
|
|
790
|
+
const current = identity();
|
|
791
|
+
const inheritDepth = deps.depth === undefined;
|
|
792
|
+
const inheritGigInput = allRemainingHuman && deps.gig_input_omitted === true;
|
|
793
|
+
const currentForGate = {
|
|
794
|
+
...current,
|
|
795
|
+
...(inheritDepth ? { depth: cp.identity.depth } : {}),
|
|
796
|
+
...(inheritGigInput ? { gig_input_sha: cp.identity.gig_input_sha } : {}),
|
|
797
|
+
};
|
|
798
|
+
const drift = runIdentityMismatch(cp.identity, currentForGate, { waiveProducers: allRemainingHuman });
|
|
706
799
|
if (drift.length > 0) {
|
|
707
800
|
// DIAGNOSTIC HONESTY, not a widened resume. The genome genuinely moved, so the refusal
|
|
708
|
-
// stands — but the
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
801
|
+
// stands — but the refusal must send the operator to the thing that ACTUALLY changed, not
|
|
802
|
+
// to a guess. When the producing and current engine versions DIFFER, "resume from the build
|
|
803
|
+
// that wrote this" is that thing, and `engine_version` names it. When they are IDENTICAL the
|
|
804
|
+
// old prose ("resume from a <version> build") named a version that already matched — sending
|
|
805
|
+
// the reader to verify a correct build before they reached the drift bracket. So when the
|
|
806
|
+
// versions agree, name the fields that drifted instead, drawn from the SAME `drift` list that
|
|
807
|
+
// refused (never a second, re-derived one). The raw before/after hashes always ride in
|
|
712
808
|
// `drift` for a builder who wants them.
|
|
713
809
|
const cur = identity();
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
810
|
+
// Each drift entry reads `<field>: checkpoint="..." current="..."` — the field is the token
|
|
811
|
+
// before the first ':'. This is the authoritative list `runIdentityMismatch` already returned;
|
|
812
|
+
// it is formatted here, never recomputed, so prose and refusal answer to one computation.
|
|
813
|
+
const driftedFields = drift.map((d) => d.slice(0, d.indexOf(":")));
|
|
814
|
+
let why;
|
|
815
|
+
if (cp.engine_version === COLTRANE_VERSION) {
|
|
816
|
+
why =
|
|
817
|
+
`this checkpoint and the current build are both coltrane ${COLTRANE_VERSION}, but the run ` +
|
|
818
|
+
`identity moved under that same version: ${driftedFields.join(", ")} changed since the ` +
|
|
819
|
+
`checkpoint was written (raw before/after values below). Re-dispatch cold, or restore the ` +
|
|
820
|
+
`prior ${driftedFields.join("/")} to resume`;
|
|
821
|
+
}
|
|
822
|
+
else if (cp.engine_version) {
|
|
823
|
+
why =
|
|
824
|
+
`this checkpoint was written by coltrane ${cp.engine_version} (genome_hash ${cp.identity.genome_hash}); ` +
|
|
825
|
+
`the current build is coltrane ${COLTRANE_VERSION} (genome_hash ${cur.genome_hash}). ` +
|
|
826
|
+
`Resume from a ${cp.engine_version} build, or re-dispatch cold`;
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
why =
|
|
830
|
+
`this checkpoint was written by an earlier build (engine version unrecorded) ` +
|
|
831
|
+
`(genome_hash ${cp.identity.genome_hash}); the current build is coltrane ${COLTRANE_VERSION} ` +
|
|
832
|
+
`(genome_hash ${cur.genome_hash}). Resume from the matching build, or re-dispatch cold`;
|
|
833
|
+
}
|
|
834
|
+
throw new ResumeRefused(gig_id, why, drift);
|
|
722
835
|
}
|
|
723
836
|
const rolesInStandard = new Set(standard.phases.flatMap((p) => p.chairs.map((c) => c.role)));
|
|
724
837
|
for (const r of cp.roles) {
|
|
@@ -945,6 +1058,48 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
945
1058
|
type: "chair_complete", phase: phase.name, role: hc.role, producer: deps.approved_by ?? "human",
|
|
946
1059
|
output_types: [domain_type], duration_ms: Date.now() - t0,
|
|
947
1060
|
});
|
|
1061
|
+
// A sealed lineage-verdict either grounds an institution or does not. Decide it here,
|
|
1062
|
+
// where the verdict and the record it approved are both in hand, and report the answer
|
|
1063
|
+
// either way. The record the verdict targets IS the input it approved — the same set
|
|
1064
|
+
// already sealed into this output's input_shas — so no lookup is invented.
|
|
1065
|
+
if (domain_type === "lineage-verdict") {
|
|
1066
|
+
const target = approvalInputs.find((i) => i.domain_type === "lineage-record") ?? approvalInputs[0];
|
|
1067
|
+
// An ENTRY human chair (depends_on []) has no upstream outputs — the record it approves
|
|
1068
|
+
// arrived in the dispatch payload. lineage-adopt-v0 is exactly that shape, and exists to
|
|
1069
|
+
// be: a record composed by any standard can be brought to a seat without re-running the
|
|
1070
|
+
// work that made it. Reading only from approvalInputs made the adoption blind to every
|
|
1071
|
+
// record seeded that way, which is every record that standard will ever see.
|
|
1072
|
+
//
|
|
1073
|
+
// Found by running it, not by testing it: five integration tests passed because all of
|
|
1074
|
+
// them seated the human chair downstream of a compose phase. They encoded the assumption
|
|
1075
|
+
// instead of testing it.
|
|
1076
|
+
//
|
|
1077
|
+
// The payload record carries no content_sha — it has not been sealed by THIS gig — so the
|
|
1078
|
+
// reference falls back to its id. LineageRecordRefSchema admits exactly this: record_ref
|
|
1079
|
+
// is "content_sha (OR SLUG) of the sealed lineage-record". Prefer the sha when a real
|
|
1080
|
+
// upstream output exists; use the slug when the record came in from outside.
|
|
1081
|
+
const seeded = gigInput["lineage-record"];
|
|
1082
|
+
const seededId = typeof seeded?.["id"] === "string" ? seeded["id"] : "";
|
|
1083
|
+
// WHICH institution this grounds. Carried by lineage-adoption-target in the payload,
|
|
1084
|
+
// because the attachment is a property of the adoption ACT — one record, approved once,
|
|
1085
|
+
// may be adopted into two institutions by two separate acts. Neither the record nor the
|
|
1086
|
+
// verdict holds it, and both promise the attachment in prose.
|
|
1087
|
+
const tgt = gigInput["lineage-adoption-target"];
|
|
1088
|
+
const institution_slug = typeof tgt?.["institution_slug"] === "string" ? tgt["institution_slug"] : "";
|
|
1089
|
+
const decision = lineageAdoption({
|
|
1090
|
+
verdict: (approval ?? null),
|
|
1091
|
+
record_ref: target?.content_sha ?? seededId,
|
|
1092
|
+
sealed_at: rec.created_at,
|
|
1093
|
+
institution_slug,
|
|
1094
|
+
});
|
|
1095
|
+
emit({
|
|
1096
|
+
type: "lineage_adoption", phase: phase.name, role: hc.role,
|
|
1097
|
+
adopt: decision.adopt,
|
|
1098
|
+
...(decision.ref ? { record_ref: decision.ref.record_ref, approved_by: decision.ref.approved_by ?? "" } : {}),
|
|
1099
|
+
...(decision.institution_slug ? { institution_slug: decision.institution_slug } : {}),
|
|
1100
|
+
...(decision.refusals.length ? { refusals: decision.refusals.map((r) => r.reason) } : {}),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
948
1103
|
}
|
|
949
1104
|
ready = ready.filter((c) => !(c.human === true && (c.agent_slug ?? "") === ""));
|
|
950
1105
|
if (ready.length === 0)
|
|
@@ -1175,7 +1330,13 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1175
1330
|
const sha = outputContentHash({
|
|
1176
1331
|
core_type: spec.core_type,
|
|
1177
1332
|
domain_type: o.domain_type,
|
|
1178
|
-
|
|
1333
|
+
// The version the loaded genome's type carries now, re-derived through the ONE owner
|
|
1334
|
+
// (typeVersionOf) rather than a constant — so the re-hash reproduces the pre-image the
|
|
1335
|
+
// original seal folded. NOTE (accepted point-in-time exposure, bill-change-plan step 5):
|
|
1336
|
+
// this is the CURRENT version; a cached output whose type was extended since it sealed
|
|
1337
|
+
// would re-hash under the newer version and be refused as stale. Point-in-time version
|
|
1338
|
+
// tracking is out of scope for this change.
|
|
1339
|
+
domain_type_version: deps.outputs.typeVersionOf(o.domain_type),
|
|
1179
1340
|
domain: a.domain,
|
|
1180
1341
|
primitive: spec.primitive,
|
|
1181
1342
|
phase: a.phaseName,
|
|
@@ -1460,11 +1621,18 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1460
1621
|
settleChairCost(p, false);
|
|
1461
1622
|
throw e;
|
|
1462
1623
|
}
|
|
1463
|
-
finally
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1624
|
+
// THE ROOM IS NOT TORN DOWN HERE. It used to be, in this chair-level `finally`, described as
|
|
1625
|
+
// "idempotent and a no-op when no venue was named". Idempotent yes; a no-op no —
|
|
1626
|
+
// `src/venue_realize.ts` sets `torn = true`, and `canReach()` is `!torn && egress.includes(...)`.
|
|
1627
|
+
// So the FIRST chair to finish closed the room's egress probe for every chair after it, and a
|
|
1628
|
+
// multi-chair phase ran the rest of its seats against a room already reported torn down.
|
|
1629
|
+
//
|
|
1630
|
+
// It was invisible because nothing in the run path consults `canReach` today, and because the
|
|
1631
|
+
// policy realization's teardown costs nothing. Neither excuse survives a realizer that does real
|
|
1632
|
+
// work: a container torn down after chair one is gone for chair two.
|
|
1633
|
+
//
|
|
1634
|
+
// A room's lifetime is the GIG's, not a chair's. Teardown now runs once, at the gig boundary,
|
|
1635
|
+
// on both the success and failure paths — see the `finally` on runGig's outer try.
|
|
1468
1636
|
}
|
|
1469
1637
|
async function executeChair(p) {
|
|
1470
1638
|
const { chair, phaseName, inputs, skills, output_specs, producer_slug, domain } = p;
|
|
@@ -1593,6 +1761,15 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1593
1761
|
// venue resolved, so the invoker narrows the spawn by construction; both fields stay
|
|
1594
1762
|
// absent otherwise (the venue-less path is unchanged).
|
|
1595
1763
|
...(gigRealization && gigVenue ? { realization: gigRealization, venue: gigVenue } : {}),
|
|
1764
|
+
// The substrate → spawn wire: when the room was actually stood up, hand the chair the
|
|
1765
|
+
// realized transports (docker-exec stdio configs) so the invoker points the spawn at the
|
|
1766
|
+
// servers INSIDE the room. Absent otherwise — the substrate-less path is unchanged.
|
|
1767
|
+
...(gigSubstrate ? { substrateMcpConfigs: gigSubstrate.mcpServerConfigs } : {}),
|
|
1768
|
+
// The seat → spawn wire: when the substrate stood up a SEAT-BEARING room (a floor image
|
|
1769
|
+
// carrying the toolchain), hand the chair the room's workspace + container so the invoker
|
|
1770
|
+
// runs the seat INSIDE the room with the workspace as cwd. Absent when the room is the
|
|
1771
|
+
// production-only room image (no seat runs there) — the host-spawn path is unchanged.
|
|
1772
|
+
...(gigSubstrate?.seat ? { seatExec: gigSubstrate.seat } : {}),
|
|
1596
1773
|
onEvent: (ev) => {
|
|
1597
1774
|
sink.fold(ev);
|
|
1598
1775
|
emit({ type: "agent_event", phase: phaseName, role: chair.role, event: ev });
|
|
@@ -1765,13 +1942,21 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1765
1942
|
const resolved = [];
|
|
1766
1943
|
for (const spec of output_specs) {
|
|
1767
1944
|
const keyed = data[spec.domain_type];
|
|
1768
|
-
const
|
|
1769
|
-
if (
|
|
1945
|
+
const raw = keyed !== undefined && keyed !== null ? keyed : single ? data : undefined;
|
|
1946
|
+
if (raw === undefined || raw === null)
|
|
1770
1947
|
continue;
|
|
1771
|
-
|
|
1772
|
-
|
|
1948
|
+
// MULTI-RECORD SEAL. captureOutputWrites hands the runtime a LIST of records per declared type
|
|
1949
|
+
// — a chair may seal MANY records of one type (gig 8baced9d's lineage scout made 15 accepted
|
|
1950
|
+
// output_write calls; the old last-wins collapse sealed 1). An ARRAY here is that list, and the
|
|
1951
|
+
// loop seals one record per element. A bare object is one record — the skill-backed path and
|
|
1952
|
+
// the single-output fallback still hand a plain object, so single-seal chairs are untouched.
|
|
1953
|
+
const slices = Array.isArray(raw) ? raw : [raw];
|
|
1954
|
+
for (const slice of slices) {
|
|
1955
|
+
if (typeof slice !== "object" || slice === null || Array.isArray(slice)) {
|
|
1956
|
+
throw new RuntimeError(`chair "${chair.role}" output "${spec.domain_type}" must be a JSON object, got ${Array.isArray(slice) ? "array" : slice === null ? "null" : typeof slice}`);
|
|
1957
|
+
}
|
|
1958
|
+
resolved.push({ spec, slice: slice });
|
|
1773
1959
|
}
|
|
1774
|
-
resolved.push({ spec, slice: slice });
|
|
1775
1960
|
}
|
|
1776
1961
|
// backfillShas refuses an ambiguous provenance field. Run it over EVERY slice up front so
|
|
1777
1962
|
// that throw also lands before the first write, rather than midway through them.
|
|
@@ -1838,6 +2023,10 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1838
2023
|
const rec = deps.outputs.write({
|
|
1839
2024
|
core_type: spec.core_type,
|
|
1840
2025
|
domain_type: spec.domain_type,
|
|
2026
|
+
// Stamp the REAL version the type carries at seal time, read through the ONE owner. Omitting
|
|
2027
|
+
// it let outputs.write default to 1, so a record sealed against a v3 type claimed v1 — inside
|
|
2028
|
+
// its content_sha pre-image, which is the record's identity.
|
|
2029
|
+
domain_type_version: deps.outputs.typeVersionOf(spec.domain_type),
|
|
1841
2030
|
domain,
|
|
1842
2031
|
gig_id,
|
|
1843
2032
|
agent_slug: producer_slug,
|
|
@@ -2024,6 +2213,10 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
2024
2213
|
result.unresolved_evals = unresolved_evals;
|
|
2025
2214
|
if (unfulfilledOutputs.length > 0)
|
|
2026
2215
|
result.unfulfilled_outputs = unfulfilledOutputs;
|
|
2216
|
+
// The gig-close conformance classification — computed over the SETTLED produced[] (every amend
|
|
2217
|
+
// round resolved, every reuse/resume row folded in) so it grades the run as it finally stands.
|
|
2218
|
+
// Attached beside run_fingerprint, never folded into it and never sealed back into the gig.
|
|
2219
|
+
result.conformance = checkGigConformance(standard, produced, skipped, "complete", gig_id);
|
|
2027
2220
|
// Say what was skipped and why. The ABSENCE of these fields is itself a claim — that every
|
|
2028
2221
|
// chair in this manifest ran — so they are present only when there is something to report,
|
|
2029
2222
|
// and `reuse` is present whenever the cache was wired even if nothing hit (a zero-hit run is
|
|
@@ -2075,6 +2268,42 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
2075
2268
|
});
|
|
2076
2269
|
throw e;
|
|
2077
2270
|
}
|
|
2271
|
+
finally {
|
|
2272
|
+
// ONE ROOM, ONE GIG, ONE TEARDOWN — on both paths. A room outlives every chair that sits in it
|
|
2273
|
+
// and dies with the run, which is what `lifecycle: ephemeral` means and what the chair-level
|
|
2274
|
+
// teardown this replaces could not express.
|
|
2275
|
+
//
|
|
2276
|
+
// AWAITED, because a realizer that stands up real resources returns a promise: the interface
|
|
2277
|
+
// already declares `Promise<void> | void`, and an unawaited teardown on the failure path means a
|
|
2278
|
+
// process can exit with a container still running. A leak on the path that is ALREADY going
|
|
2279
|
+
// badly is the worst place to have one.
|
|
2280
|
+
//
|
|
2281
|
+
// Best-effort by construction: a teardown that throws must not replace the gig's own outcome —
|
|
2282
|
+
// a failed teardown after a successful run would turn a good gig into a bad one, and after a
|
|
2283
|
+
// failed run would hide the real error behind a cleanup error. What it must never do is pass
|
|
2284
|
+
// silently, so it is reported on the drain-debug channel like every other best-effort seam here.
|
|
2285
|
+
try {
|
|
2286
|
+
await gigRealization?.teardown();
|
|
2287
|
+
}
|
|
2288
|
+
catch (te) {
|
|
2289
|
+
if (process.env["COLTRANE_DRAIN_DEBUG"]) {
|
|
2290
|
+
console.error(`[venue] teardown failed for gig ${gig_id}: ${te instanceof Error ? te.message : String(te)}`);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
// BOTH LAYERS ARE TORN DOWN, INDEPENDENTLY. The substrate handle (a real compose project, for the
|
|
2294
|
+
// container realizer) gets its OWN try/catch, NOT the policy teardown's: a shared one would let a
|
|
2295
|
+
// throwing substrate teardown skip the policy teardown (or vice versa), and a room that outlives
|
|
2296
|
+
// its gig is the leak this wire exists to close. Best-effort like the policy teardown — a throw
|
|
2297
|
+
// here must not replace the gig's outcome, only surface on the drain-debug channel.
|
|
2298
|
+
try {
|
|
2299
|
+
await gigSubstrate?.teardown();
|
|
2300
|
+
}
|
|
2301
|
+
catch (te) {
|
|
2302
|
+
if (process.env["COLTRANE_DRAIN_DEBUG"]) {
|
|
2303
|
+
console.error(`[venue] substrate teardown failed for gig ${gig_id}: ${te instanceof Error ? te.message : String(te)}`);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2078
2307
|
}
|
|
2079
2308
|
// v0 eval-scorer: a minimal scan over the produced outputs. The named eval is
|
|
2080
2309
|
// looked up by slug (no shared genome handle in the runtime today), so we use
|