@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.
Files changed (144) hide show
  1. package/agents/bandleader.json +54 -0
  2. package/agents/bill.json +4 -2
  3. package/agents/change-verifier.json +2 -1
  4. package/agents/code-implementer.json +7 -4
  5. package/agents/john.json +5 -3
  6. package/agents/lineage-scout-external.json +19 -6
  7. package/agents/lineage-scribe.json +20 -9
  8. package/agents/lineage-weaver.json +26 -11
  9. package/agents/miles.json +2 -1
  10. package/agents/players/bandleader.md +70 -0
  11. package/agents/pr-publisher.json +4 -2
  12. package/agents/red-spec-drafter.json +12 -6
  13. package/agents/room-prober.json +29 -0
  14. package/agents/source-walker.json +1 -1
  15. package/charts/room-probe-chart-v1.json +14 -0
  16. package/charts/worker-contract-docker-v2.json +135 -0
  17. package/charts/worker-contract-v3.json +206 -0
  18. package/dist/src/boundary_check.d.ts +18 -0
  19. package/dist/src/boundary_check.js +70 -0
  20. package/dist/src/boundary_check.js.map +1 -0
  21. package/dist/src/boundary_scan.d.ts +62 -0
  22. package/dist/src/boundary_scan.js +93 -0
  23. package/dist/src/boundary_scan.js.map +1 -0
  24. package/dist/src/chart.js +34 -1
  25. package/dist/src/chart.js.map +1 -1
  26. package/dist/src/claude_invoker.d.ts +2 -13
  27. package/dist/src/claude_invoker.js +242 -18
  28. package/dist/src/claude_invoker.js.map +1 -1
  29. package/dist/src/cli.d.ts +1 -1
  30. package/dist/src/cli.js +115 -27
  31. package/dist/src/cli.js.map +1 -1
  32. package/dist/src/drain_preflight.d.ts +33 -0
  33. package/dist/src/drain_preflight.js +97 -0
  34. package/dist/src/drain_preflight.js.map +1 -0
  35. package/dist/src/genome_schema.d.ts +390 -9
  36. package/dist/src/genome_schema.js +163 -8
  37. package/dist/src/genome_schema.js.map +1 -1
  38. package/dist/src/genome_store.js +9 -0
  39. package/dist/src/genome_store.js.map +1 -1
  40. package/dist/src/genome_writer.d.ts +1 -1
  41. package/dist/src/genome_writer.js +10 -2
  42. package/dist/src/genome_writer.js.map +1 -1
  43. package/dist/src/gig_conformance.d.ts +66 -0
  44. package/dist/src/gig_conformance.js +153 -0
  45. package/dist/src/gig_conformance.js.map +1 -0
  46. package/dist/src/gig_tracker.js +8 -0
  47. package/dist/src/gig_tracker.js.map +1 -1
  48. package/dist/src/grounding.d.ts +44 -0
  49. package/dist/src/grounding.js +127 -0
  50. package/dist/src/grounding.js.map +1 -0
  51. package/dist/src/index.d.ts +3 -0
  52. package/dist/src/index.js +9 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/institution_loader.d.ts +29 -5
  55. package/dist/src/institution_loader.js +67 -6
  56. package/dist/src/institution_loader.js.map +1 -1
  57. package/dist/src/lineage_adoption.d.ts +82 -0
  58. package/dist/src/lineage_adoption.js +82 -0
  59. package/dist/src/lineage_adoption.js.map +1 -0
  60. package/dist/src/lineage_persist.d.ts +34 -0
  61. package/dist/src/lineage_persist.js +60 -0
  62. package/dist/src/lineage_persist.js.map +1 -0
  63. package/dist/src/loader.d.ts +13 -0
  64. package/dist/src/loader.js +43 -2
  65. package/dist/src/loader.js.map +1 -1
  66. package/dist/src/local_queue.d.ts +105 -0
  67. package/dist/src/local_queue.js +380 -0
  68. package/dist/src/local_queue.js.map +1 -0
  69. package/dist/src/mcp.js +37 -4
  70. package/dist/src/mcp.js.map +1 -1
  71. package/dist/src/org_hire.d.ts +23 -0
  72. package/dist/src/org_hire.js +42 -0
  73. package/dist/src/org_hire.js.map +1 -0
  74. package/dist/src/outputs.d.ts +17 -0
  75. package/dist/src/outputs.js +86 -1
  76. package/dist/src/outputs.js.map +1 -1
  77. package/dist/src/player_to_claude_code.js +26 -2
  78. package/dist/src/player_to_claude_code.js.map +1 -1
  79. package/dist/src/registry.d.ts +3 -1
  80. package/dist/src/registry.js +43 -3
  81. package/dist/src/registry.js.map +1 -1
  82. package/dist/src/repo_index.d.ts +58 -0
  83. package/dist/src/repo_index.js +232 -0
  84. package/dist/src/repo_index.js.map +1 -0
  85. package/dist/src/residency.d.ts +149 -0
  86. package/dist/src/residency.js +283 -0
  87. package/dist/src/residency.js.map +1 -0
  88. package/dist/src/reuse.d.ts +44 -4
  89. package/dist/src/reuse.js +0 -0
  90. package/dist/src/reuse.js.map +1 -1
  91. package/dist/src/runtime.d.ts +82 -0
  92. package/dist/src/runtime.js +254 -25
  93. package/dist/src/runtime.js.map +1 -1
  94. package/dist/src/server.d.ts +37 -0
  95. package/dist/src/server.js +422 -11
  96. package/dist/src/server.js.map +1 -1
  97. package/dist/src/server_relay.d.ts +40 -2
  98. package/dist/src/server_relay.js +186 -19
  99. package/dist/src/server_relay.js.map +1 -1
  100. package/dist/src/tool_providers.d.ts +27 -0
  101. package/dist/src/tool_providers.js +48 -0
  102. package/dist/src/tool_providers.js.map +1 -1
  103. package/dist/src/venue_credential.d.ts +41 -0
  104. package/dist/src/venue_credential.js +57 -0
  105. package/dist/src/venue_credential.js.map +1 -0
  106. package/dist/src/venue_realize.d.ts +23 -0
  107. package/dist/src/venue_realize.js +43 -3
  108. package/dist/src/venue_realize.js.map +1 -1
  109. package/dist/src/venue_realizer.d.ts +224 -0
  110. package/dist/src/venue_realizer.js +889 -0
  111. package/dist/src/venue_realizer.js.map +1 -0
  112. package/dist/src/version.d.ts +1 -1
  113. package/dist/src/version.js +1 -1
  114. package/dist/src/worker.d.ts +57 -0
  115. package/dist/src/worker.js +155 -24
  116. package/dist/src/worker.js.map +1 -1
  117. package/dist/src/worker_env.d.ts +79 -0
  118. package/dist/src/worker_env.js +322 -0
  119. package/dist/src/worker_env.js.map +1 -0
  120. package/dist/src/workspace.d.ts +12 -1
  121. package/dist/src/workspace.js +10 -3
  122. package/dist/src/workspace.js.map +1 -1
  123. package/domain_types/change-context.json +59 -13
  124. package/domain_types/lineage-adoption-target.json +22 -0
  125. package/domain_types/lineage-map.json +7 -2
  126. package/domain_types/lineage-record.json +55 -5
  127. package/domain_types/prior-art-hit.json +16 -1
  128. package/domain_types/red-spec.json +5 -3
  129. package/domain_types/woodshed-record.json +149 -0
  130. package/evals/gist-present.json +7 -0
  131. package/institutions/coltrane.json +178 -6
  132. package/package.json +6 -3
  133. package/skills/non-vacuity-probe/fixtures/fixture-001.json +17 -0
  134. package/skills/non-vacuity-probe/meta.json +12 -0
  135. package/skills/non-vacuity-probe/skill.md +27 -0
  136. package/skills/non-vacuity-probe/skill.mjs +81 -0
  137. package/standards/lineage-adopt-v0.json +37 -0
  138. package/standards/room-probe-v1.json +28 -0
  139. package/standards/software-change-pr-v1.json +15 -15
  140. package/standards/software-change-red-first-v0.json +183 -0
  141. package/standards/spec-review-and-sequence-v0.json +112 -0
  142. package/standards/studio-session-v0.json +101 -0
  143. package/tours/coltrane.json +72 -0
  144. package/venues/engine-room-v1.json +32 -0
@@ -0,0 +1,105 @@
1
+ /** The lease a claimed gig carries — a continuing renewed fact (monotonic `renewed_at`), not a boot
2
+ * assertion. */
3
+ export interface LeaseState {
4
+ holder: string;
5
+ renewed_at: number;
6
+ expires_at: number;
7
+ }
8
+ /** A claimed local gig — the sibling of worker.ts ClaimedGig, plus the local lease and, on a
9
+ * re-claim, the human seat's role-keyed verdicts. */
10
+ export interface ClaimedLocalGig {
11
+ gig_id: string;
12
+ standard_slug: string;
13
+ standard_version: number | null;
14
+ mode: string;
15
+ input: Record<string, unknown>;
16
+ acting_for: string;
17
+ venue?: string | null;
18
+ worker: string;
19
+ lease: LeaseState;
20
+ approvals?: Record<string, {
21
+ verdict: Record<string, unknown>;
22
+ approved_by?: string;
23
+ }> | null;
24
+ }
25
+ /** One row's observable state — how a reader tells a live claim from an abandoned one. */
26
+ export interface LocalGigView {
27
+ gig_id: string;
28
+ state: "queued" | "claimed" | "awaiting_approval" | "complete" | "failed" | "cancelled";
29
+ holder?: string;
30
+ lease?: LeaseState;
31
+ }
32
+ export interface LocalQueueOptions {
33
+ leaseMs?: number;
34
+ /** Injected clock for deterministic lease/reap laws. Defaults to Date.now(). */
35
+ clock?: () => number;
36
+ }
37
+ /** The local queue port — every verb is offline and needs no credential. */
38
+ export interface LocalQueue {
39
+ enqueue(args: Record<string, unknown>): Promise<{
40
+ gig_id: string;
41
+ status: "queued";
42
+ }>;
43
+ claim(worker: string): Promise<ClaimedLocalGig | null>;
44
+ heartbeat(worker: string, gig_id: string): Promise<boolean>;
45
+ reap(): {
46
+ requeued: string[];
47
+ kept: number;
48
+ errors: string[];
49
+ };
50
+ park(worker: string, gig_id: string): Promise<boolean>;
51
+ approve(gig_id: string, role: string, verdict: Record<string, unknown>, approved_by?: string): Promise<boolean>;
52
+ cancel(args: Record<string, unknown>): Promise<{
53
+ gig_id: string;
54
+ status: "cancelled";
55
+ }>;
56
+ complete(worker: string, gig_id: string, output: Record<string, unknown>): Promise<{
57
+ content_sha: string;
58
+ duplicated: boolean;
59
+ }>;
60
+ list(): LocalGigView[];
61
+ readonly leaseMs: number;
62
+ }
63
+ /** Which backing owns the queue, decided by which environment is present. */
64
+ export type QueueBackingChoice = {
65
+ backing: "file";
66
+ root: string;
67
+ } | {
68
+ backing: "hosted";
69
+ } | {
70
+ backing: "none";
71
+ why: string;
72
+ } | {
73
+ backing: "conflict";
74
+ why: string;
75
+ };
76
+ export interface LocalQueueModule {
77
+ openLocalQueue(root: string, opts?: LocalQueueOptions): LocalQueue;
78
+ fileQueueGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
79
+ fileCancelGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
80
+ selectQueueBacking(env: Record<string, string | undefined>): QueueBackingChoice;
81
+ LOCAL_QUEUE_DIR_VAR: string;
82
+ DRAIN_VARS: readonly string[];
83
+ }
84
+ /** The env var whose presence selects the local backing. */
85
+ export declare const LOCAL_QUEUE_DIR_VAR = "COLTRANE_QUEUE_DIR";
86
+ /** The drain's five-variable contract (drain_preflight.ts:55-61), kept SORTED. The local path must
87
+ * never read a VALUE of any of these; selectQueueBacking only ever inspects key PRESENCE. */
88
+ export declare const DRAIN_VARS: readonly string[];
89
+ /**
90
+ * The single backing selector — file when the local dir is present, hosted for the drain env,
91
+ * conflict when both, none otherwise. It reads the VALUE of exactly one variable, LOCAL_QUEUE_DIR_VAR
92
+ * (not a drain var), and detects hosted presence by KEY PRESENCE via Object.keys — never by reading a
93
+ * drain variable's value. That distinction is load-bearing for F8: the test passes an env Proxy whose
94
+ * `get` trap throws on any drain-var read, so hosted detection must go through `ownKeys` (Object.keys
95
+ * does not trip the `get` trap), letting a local-present env resolve to `file` without touching one of
96
+ * the five. Values of the drain vars are never needed — their mere presence is the whole signal.
97
+ */
98
+ export declare function selectQueueBacking(env: Record<string, string | undefined>): QueueBackingChoice;
99
+ export declare function openLocalQueue(root: string, opts?: LocalQueueOptions): LocalQueue;
100
+ /** The deps.queueGig-shaped enqueue seam — byte-compatible with postgrestQueueGig / rpcQueueGig
101
+ * ({gig_id, status:'queued'}), so a caller cannot tell which backing answered (I1). */
102
+ export declare function fileQueueGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
103
+ /** The deps.cancelGig-shaped seam — sibling of postgrestCancelGig ({gig_id, status:'cancelled'})
104
+ * (I18). */
105
+ export declare function fileCancelGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
@@ -0,0 +1,380 @@
1
+ // The local, file-backed gig queue — the third gig backing, the offline sibling of the two HTTP
2
+ // seams (postgrestQueueGig / rpcQueueGig, src/genome_store.ts:532,:493). It closes the asymmetry the
3
+ // SPEC (docs/specs/SPEC-local-queue-contract.md) names: the genome port already ships a LOCAL sibling
4
+ // (fileGenomeStore, src/genome_store.ts:77) so a genome can be READ from files, but the queue port
5
+ // had none — so an open-source user could run gigs in-process yet never enqueue. This module is that
6
+ // missing sibling: clone → build → enqueue → `coltrane work` in another terminal claims and runs it,
7
+ // with no Supabase, no service origin, no cdk_ key, no minting backend.
8
+ //
9
+ // MUTUAL EXCLUSION WITHOUT A DATABASE OR A LOCK. State is a directory-per-state layout under `root`
10
+ // (queued/, claimed/, parked/, done/, failed/, cancelled/), and every transition is a single POSIX
11
+ // rename(2), which is atomic within a filesystem. A claim renames queued/<id> → claimed/<id>: the
12
+ // winner's rename removes the source, and every loser's rename of the same source fails ENOENT — that
13
+ // is how a loser learns it lost (maildir new/→cur/, cr.yp.to; FSQ tmp/→queue/→done/). No lockfile
14
+ // (which would leave stale residue reintroducing the race), no in-process mutex (which cannot cross
15
+ // the server / `coltrane work` process boundary this queue exists to bridge), no SQLite (a forbidden
16
+ // dependency). node's own fs is sufficient, and the atomicity argument depends on staying on one FS.
17
+ //
18
+ // LEASE, not lock. A claim carries a lease {holder, renewed_at, expires_at} embedded in the moved
19
+ // file, so one atomic rename carries the claim AND its lease together — there is never a window where
20
+ // a claim exists without its lease. The holder renews with heartbeat (strictly advances the lease);
21
+ // a crashed holder's grip falls open by timeout, and reap() returns lapsed claims to queued while
22
+ // never touching a fresh one. A clock may be injected (opts.clock) so lease/reap laws are
23
+ // deterministic and fast; it defaults to Date.now().
24
+ //
25
+ // IDEMPOTENT EFFECT. Because a lapsed lease lets a second worker re-run a gig, the honest guarantee
26
+ // is at-least-once + idempotent-effect: complete() seals the output under content_sha (the same
27
+ // sha256Hex(canonJson(...)) primitive the rest of the codebase uses), a re-run with the SAME output
28
+ // re-seals rather than duplicating (duplicated:true), and a re-run that would seal a DIFFERENT output
29
+ // fails closed rather than forking.
30
+ //
31
+ // This module is NOT wired into any surface (deps.queueGig / deps.cancelGig) — shipping the module
32
+ // and its laws going green is a separate act from selecting it as a backing.
33
+ import * as fs from "node:fs";
34
+ import * as fsp from "node:fs/promises";
35
+ import { join } from "node:path";
36
+ import { randomUUID } from "node:crypto";
37
+ import { sha256Hex, canonJson } from "./canonical_form.js";
38
+ // ── The env-presence contract. ────────────────────────────────────────────────────────────────────
39
+ /** The env var whose presence selects the local backing. */
40
+ export const LOCAL_QUEUE_DIR_VAR = "COLTRANE_QUEUE_DIR";
41
+ /** The drain's five-variable contract (drain_preflight.ts:55-61), kept SORTED. The local path must
42
+ * never read a VALUE of any of these; selectQueueBacking only ever inspects key PRESENCE. */
43
+ export const DRAIN_VARS = [
44
+ "COLTRANE_DRAIN_KEY",
45
+ "COLTRANE_DRAIN_URL",
46
+ "COLTRANE_INSTANCE",
47
+ "COLTRANE_STORE_ANON",
48
+ "COLTRANE_STORE_URL",
49
+ ];
50
+ /**
51
+ * The single backing selector — file when the local dir is present, hosted for the drain env,
52
+ * conflict when both, none otherwise. It reads the VALUE of exactly one variable, LOCAL_QUEUE_DIR_VAR
53
+ * (not a drain var), and detects hosted presence by KEY PRESENCE via Object.keys — never by reading a
54
+ * drain variable's value. That distinction is load-bearing for F8: the test passes an env Proxy whose
55
+ * `get` trap throws on any drain-var read, so hosted detection must go through `ownKeys` (Object.keys
56
+ * does not trip the `get` trap), letting a local-present env resolve to `file` without touching one of
57
+ * the five. Values of the drain vars are never needed — their mere presence is the whole signal.
58
+ */
59
+ export function selectQueueBacking(env) {
60
+ const localRaw = env[LOCAL_QUEUE_DIR_VAR];
61
+ const localPresent = typeof localRaw === "string" && localRaw.length > 0;
62
+ const keys = Object.keys(env);
63
+ const hostedPresent = DRAIN_VARS.some((v) => keys.includes(v));
64
+ if (localPresent && hostedPresent) {
65
+ return {
66
+ backing: "conflict",
67
+ why: `both ${LOCAL_QUEUE_DIR_VAR} and the hosted drain environment are set — refusing to guess which backing owns the gig`,
68
+ };
69
+ }
70
+ if (localPresent)
71
+ return { backing: "file", root: localRaw };
72
+ if (hostedPresent)
73
+ return { backing: "hosted" };
74
+ return {
75
+ backing: "none",
76
+ why: `no queue backing configured — set ${LOCAL_QUEUE_DIR_VAR} for a local file queue, or the drain environment for the hosted queue`,
77
+ };
78
+ }
79
+ function isRecord(v) {
80
+ return typeof v === "object" && v !== null && !Array.isArray(v);
81
+ }
82
+ const DEFAULT_LEASE_MS = 30_000;
83
+ export function openLocalQueue(root, opts) {
84
+ const leaseMs = opts?.leaseMs ?? DEFAULT_LEASE_MS;
85
+ const clock = opts?.clock ?? Date.now;
86
+ const queuedDir = join(root, "queued");
87
+ const claimedDir = join(root, "claimed");
88
+ const parkedDir = join(root, "parked");
89
+ const doneDir = join(root, "done");
90
+ const failedDir = join(root, "failed");
91
+ const cancelledDir = join(root, "cancelled");
92
+ const tmpDir = join(root, "tmp");
93
+ const dirsByState = [
94
+ [queuedDir, "queued"],
95
+ [claimedDir, "claimed"],
96
+ [parkedDir, "awaiting_approval"],
97
+ [doneDir, "complete"],
98
+ [failedDir, "failed"],
99
+ [cancelledDir, "cancelled"],
100
+ ];
101
+ // A tmp+rename atomic write: a reader of the destination sees either the OLD bytes or the whole NEW
102
+ // bytes, never a mixture. tmp lives in a dedicated dir so a claim's readdir of queued/ never lists a
103
+ // half-written temp file (F4).
104
+ async function atomicWrite(dir, name, obj) {
105
+ await fsp.mkdir(tmpDir, { recursive: true });
106
+ const tmp = join(tmpDir, `${randomUUID()}.tmp`);
107
+ await fsp.writeFile(tmp, JSON.stringify(obj), "utf8");
108
+ await fsp.mkdir(dir, { recursive: true });
109
+ await fsp.rename(tmp, join(dir, name));
110
+ }
111
+ async function readGig(path) {
112
+ try {
113
+ return JSON.parse(await fsp.readFile(path, "utf8"));
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ function isEnoent(e) {
120
+ return typeof e === "object" && e !== null && e.code === "ENOENT";
121
+ }
122
+ async function enqueue(args) {
123
+ // F10 — a payload with no standard to run is unrunnable. Refuse at enqueue and persist nothing,
124
+ // rather than enqueue a row that can only fail on a drain thirty minutes later.
125
+ const slug = args["standard_slug"];
126
+ if (typeof slug !== "string" || slug.length === 0) {
127
+ throw new Error("enqueue requires a non-empty standard_slug — nothing to run");
128
+ }
129
+ const gig_id = randomUUID();
130
+ const now = clock();
131
+ const record = {
132
+ gig_id,
133
+ standard_slug: slug,
134
+ standard_version: typeof args["standard_version"] === "number" ? args["standard_version"] : null,
135
+ mode: typeof args["mode"] === "string" ? args["mode"] : "live",
136
+ input: isRecord(args["input"]) ? args["input"] : {},
137
+ acting_for: typeof args["acting_for"] === "string" ? args["acting_for"] : "",
138
+ venue: typeof args["venue"] === "string" ? args["venue"] : null,
139
+ enqueued_at: now,
140
+ };
141
+ // F1/F4 — write then rename over queued/<gig_id>. If the root cannot be persisted to (mkdir/rename
142
+ // fails, e.g. ENOTDIR under a regular file), this throws and NO success is reported.
143
+ await atomicWrite(queuedDir, gig_id, record);
144
+ return { gig_id, status: "queued" };
145
+ }
146
+ async function claim(worker) {
147
+ let candidates;
148
+ try {
149
+ candidates = (await fsp.readdir(queuedDir)).sort();
150
+ }
151
+ catch {
152
+ return null; // no queued/ dir yet ⇒ nothing to claim
153
+ }
154
+ await fsp.mkdir(claimedDir, { recursive: true });
155
+ for (const id of candidates) {
156
+ const src = join(queuedDir, id);
157
+ const dst = join(claimedDir, id);
158
+ try {
159
+ // The arbiter. Exactly one concurrent claimer's rename of a given source succeeds; every other
160
+ // gets ENOENT (I4/I5/F3) and moves on to the next candidate (I19). A live gig is not in
161
+ // queued/ at all, so it is never a candidate here (F7).
162
+ await fsp.rename(src, dst);
163
+ }
164
+ catch {
165
+ // A lost rename race (ENOENT) is how a loser learns it lost; any other per-row fault is
166
+ // likewise "could not take this row". Either way, move to the next candidate — a lost row is
167
+ // the null sentinel, never a thrown claim (I5/F3).
168
+ continue;
169
+ }
170
+ const record = await readGig(dst);
171
+ if (record === null)
172
+ continue;
173
+ const now = clock();
174
+ const lease = { holder: worker, renewed_at: now, expires_at: now + leaseMs };
175
+ record.worker = worker;
176
+ record.lease = lease;
177
+ // Persist the lease alongside the claim (idempotent overwrite of a file only this claimer owns).
178
+ await atomicWrite(claimedDir, id, record);
179
+ const claimed = {
180
+ gig_id: record.gig_id,
181
+ standard_slug: record.standard_slug,
182
+ standard_version: record.standard_version,
183
+ mode: record.mode,
184
+ input: record.input,
185
+ acting_for: record.acting_for,
186
+ venue: record.venue,
187
+ worker,
188
+ lease,
189
+ approvals: record.approvals ?? null,
190
+ };
191
+ return claimed;
192
+ }
193
+ return null;
194
+ }
195
+ async function heartbeat(worker, gig_id) {
196
+ const path = join(claimedDir, gig_id);
197
+ const record = await readGig(path);
198
+ // F5 — the caller must still HOLD the claim. A heartbeat against a lease that was lost (the gig
199
+ // lapsed and someone else took it) or that no longer exists is a loss, never a false "still held".
200
+ if (record === null || record.lease === undefined || record.lease.holder !== worker)
201
+ return false;
202
+ const now = clock();
203
+ record.lease = { holder: worker, renewed_at: now, expires_at: now + leaseMs };
204
+ record.worker = worker;
205
+ await atomicWrite(claimedDir, gig_id, record);
206
+ return true;
207
+ }
208
+ function reap() {
209
+ // SYNCHRONOUS and best-effort (mirrors worker.ts reapWorkerState): a single unreadable claim never
210
+ // aborts the sweep. Only claimed/ is scanned, so a parked or terminal gig is structurally out of
211
+ // reach (I13) rather than guarded by a per-file predicate a future edit could forget.
212
+ const requeued = [];
213
+ const errors = [];
214
+ let kept = 0;
215
+ const now = clock();
216
+ let entries;
217
+ try {
218
+ entries = fs.readdirSync(claimedDir);
219
+ }
220
+ catch {
221
+ return { requeued, kept, errors };
222
+ }
223
+ for (const id of entries) {
224
+ const path = join(claimedDir, id);
225
+ try {
226
+ const record = JSON.parse(fs.readFileSync(path, "utf8"));
227
+ const expires = record.lease?.expires_at;
228
+ if (typeof expires === "number" && expires <= now) {
229
+ // Lapsed (I9/I10/I11) — return it to queued for the next worker. The rename is atomic; the
230
+ // stale lease left in the file is inert (list() reports no lease for a queued row, and the
231
+ // next claim overwrites it).
232
+ fs.mkdirSync(queuedDir, { recursive: true });
233
+ fs.renameSync(path, join(queuedDir, id));
234
+ requeued.push(id);
235
+ }
236
+ else {
237
+ kept++; // fresh (I8/I10) — never touched
238
+ }
239
+ }
240
+ catch (e) {
241
+ errors.push(`${id}: ${e instanceof Error ? e.message : String(e)}`);
242
+ }
243
+ }
244
+ return { requeued, kept, errors };
245
+ }
246
+ async function park(worker, gig_id) {
247
+ const src = join(claimedDir, gig_id);
248
+ const record = await readGig(src);
249
+ if (record === null || record.lease?.holder !== worker)
250
+ return false;
251
+ // Move claimed/ → parked/ first (one atomic rename), THEN clear the lease. The gig is never in two
252
+ // places, and the reaper (which scans only claimed/) can never mistake it for an abandoned claim
253
+ // (I13). awaiting_approval is not a live claim.
254
+ await fsp.mkdir(parkedDir, { recursive: true });
255
+ await fsp.rename(src, join(parkedDir, gig_id));
256
+ delete record.lease;
257
+ delete record.worker;
258
+ await atomicWrite(parkedDir, gig_id, record);
259
+ return true;
260
+ }
261
+ async function approve(gig_id, role, verdict, approved_by) {
262
+ // F6 — an empty verdict is not an approval. Fail closed rather than resume past the human chair on
263
+ // a verdict that says nothing.
264
+ if (!isRecord(verdict) || Object.keys(verdict).length === 0) {
265
+ throw new Error("an empty verdict is not an approval — refusing to resume past the human chair");
266
+ }
267
+ const src = join(parkedDir, gig_id);
268
+ const record = await readGig(src);
269
+ if (record === null)
270
+ return false;
271
+ const entry = approved_by !== undefined ? { verdict, approved_by } : { verdict };
272
+ const approvals = {
273
+ ...(record.approvals ?? {}),
274
+ };
275
+ approvals[role] = entry;
276
+ record.approvals = approvals;
277
+ // Seal the verdict into the parked row, THEN move parked/ → queued/ in one atomic rename, so the
278
+ // re-claimable row appears in queued/ already carrying its verdicts (I12).
279
+ await atomicWrite(parkedDir, gig_id, record);
280
+ await fsp.mkdir(queuedDir, { recursive: true });
281
+ await fsp.rename(join(parkedDir, gig_id), join(queuedDir, gig_id));
282
+ return true;
283
+ }
284
+ async function cancel(args) {
285
+ const gig_id = typeof args["gig_id"] === "string" ? args["gig_id"] : String(args["gig_id"] ?? "");
286
+ // Sibling of postgrestCancelGig: cancels a QUEUED row so it can never be claimed (I18). Moving it
287
+ // out of queued/ is the whole mechanism — a cancelled gig is simply no longer a claim candidate.
288
+ try {
289
+ await fsp.mkdir(cancelledDir, { recursive: true });
290
+ await fsp.rename(join(queuedDir, gig_id), join(cancelledDir, gig_id));
291
+ }
292
+ catch (e) {
293
+ if (!isEnoent(e))
294
+ throw e; // not-queued (already gone) is idempotent; a real IO fault is not
295
+ }
296
+ return { gig_id, status: "cancelled" };
297
+ }
298
+ async function locate(gig_id) {
299
+ for (const [dir] of dirsByState) {
300
+ const path = join(dir, gig_id);
301
+ try {
302
+ await fsp.access(path);
303
+ return { dir, path };
304
+ }
305
+ catch {
306
+ /* not in this state dir */
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+ async function complete(worker, gig_id, output) {
312
+ const loc = await locate(gig_id);
313
+ if (loc === null)
314
+ throw new Error(`cannot complete unknown gig ${gig_id}`);
315
+ const record = await readGig(loc.path);
316
+ if (record === null)
317
+ throw new Error(`cannot complete unreadable gig ${gig_id}`);
318
+ // content_sha is a pure function of the output via the codebase's own canonical hash, so two
319
+ // separate completions of identical output hash identically by construction (I14).
320
+ const content_sha = sha256Hex(canonJson(output));
321
+ if (record.content_sha !== undefined) {
322
+ // I14 — a re-run (after a lapsed lease let a second worker take the gig) re-seals the SAME output
323
+ // rather than duplicating. F9 — a DIFFERENT output for the same gig fails closed, never forks.
324
+ if (record.content_sha === content_sha)
325
+ return { content_sha, duplicated: true };
326
+ throw new Error(`gig ${gig_id} already sealed a different output — refusing to fork the result`);
327
+ }
328
+ // Record the seal in place. The gig stays where it is (typically still claimed): a lapsed lease can
329
+ // then let a second worker re-run it, and that re-run dedups against this recorded content_sha.
330
+ record.content_sha = content_sha;
331
+ record.output = output;
332
+ await atomicWrite(loc.dir, gig_id, record);
333
+ return { content_sha, duplicated: false };
334
+ }
335
+ function list() {
336
+ const views = [];
337
+ for (const [dir, state] of dirsByState) {
338
+ let entries;
339
+ try {
340
+ entries = fs.readdirSync(dir);
341
+ }
342
+ catch {
343
+ continue; // dir not created yet
344
+ }
345
+ for (const id of entries) {
346
+ if (state === "claimed") {
347
+ try {
348
+ const record = JSON.parse(fs.readFileSync(join(dir, id), "utf8"));
349
+ if (record.lease && typeof record.lease.holder === "string") {
350
+ views.push({ gig_id: id, state, holder: record.lease.holder, lease: record.lease });
351
+ continue;
352
+ }
353
+ }
354
+ catch {
355
+ /* fall through to the lease-less view */
356
+ }
357
+ views.push({ gig_id: id, state });
358
+ }
359
+ else {
360
+ views.push({ gig_id: id, state });
361
+ }
362
+ }
363
+ }
364
+ return views;
365
+ }
366
+ return { enqueue, claim, heartbeat, reap, park, approve, cancel, complete, list, leaseMs };
367
+ }
368
+ /** The deps.queueGig-shaped enqueue seam — byte-compatible with postgrestQueueGig / rpcQueueGig
369
+ * ({gig_id, status:'queued'}), so a caller cannot tell which backing answered (I1). */
370
+ export function fileQueueGig(root) {
371
+ const q = openLocalQueue(root);
372
+ return (args) => q.enqueue(args);
373
+ }
374
+ /** The deps.cancelGig-shaped seam — sibling of postgrestCancelGig ({gig_id, status:'cancelled'})
375
+ * (I18). */
376
+ export function fileCancelGig(root) {
377
+ const q = openLocalQueue(root);
378
+ return (args) => q.cancel(args);
379
+ }
380
+ //# sourceMappingURL=local_queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local_queue.js","sourceRoot":"","sources":["../../src/local_queue.ts"],"names":[],"mappings":"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"}
package/dist/src/mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { zodToMcpProps, AgentSchema, StandardSchema, SkillSchema, DomainTypeSchema, ChartSchema, VenueSchema } from "./genome_schema.js";
1
+ import { zodToMcpProps, AgentSchema, StandardSchema, SkillSchema, DomainTypeSchema, ChartSchema, VenueObjectSchema, OrgMemberSchema } from "./genome_schema.js";
2
2
  // A property value is either a bare JSON-schema type name (the common case) or a full schema
3
3
  // fragment — needed for fields that are honestly nullable, e.g. a measurement the engine has no
4
4
  // data to compute (#238). Declaring those as plain "number" is the same lie as returning 1.0.
@@ -71,7 +71,11 @@ export const MCP_TOOLS = [
71
71
  // `validation_result` carries the STRUCTURED violation list (rule + detail + where), not a
72
72
  // boolean: a chart is refused for one named reason at one named place.
73
73
  { slug: "chart_define", category: "build", input_schema: obj(zodToMcpProps(ChartSchema)), output_schema: obj({ chart_id: "string", chart_hash: "string", validation_result: "object" }) },
74
- { slug: "venue_define", category: "build", input_schema: obj(zodToMcpProps(VenueSchema)), output_schema: obj({ venue_id: "string", validation_result: "object" }) },
74
+ // `VenueObjectSchema`, not `VenueSchema`: the advertised surface is generated from the inner
75
+ // `ZodObject`'s `.shape`, which the `.superRefine`-wrapped `VenueSchema` (a `ZodEffects`) no longer
76
+ // exposes. The cross-field rules the refinement adds are enforced at safeParse in the handler, not
77
+ // advertised as arguments — so the two stay one statement of the same fact (#234).
78
+ { slug: "venue_define", category: "build", input_schema: obj(zodToMcpProps(VenueObjectSchema)), output_schema: obj({ venue_id: "string", validation_result: "object" }) },
75
79
  // #239 — `basis`/`sample_size` say WHERE the estimate came from (a measured mean of real runs,
76
80
  // the standard's real structure, or a per-slug guess). estimated_duration_ms is the key the
77
81
  // handler actually returns; the old `estimated_duration` was never present on a response.
@@ -120,7 +124,13 @@ export const MCP_TOOLS = [
120
124
  // (dispatchTarget, src/chart.ts). A chart dispatch's reply carries the ARRANGEMENT's manifest —
121
125
  // chart_hash, the per-movement roll-up, cumulative spend against the envelope — where a standard
122
126
  // dispatch carries the run's.
123
- { slug: "gig_dispatch", category: "run", input_schema: obj({ standard_slug: "string", chart_slug: "string", acting_for: "string", input: "object", depth: "string", wait: "boolean", budget: "object", resume_gig_id: "string", reuse: "boolean", approvals: "object", approved_by: "string" }), output_schema: obj({ gig_id: "string", status: "string", awaiting: "object", depth: "string", manifest: "object", resumed_from: "string", reuse: "boolean", resume_refused: "boolean", drift: "array" }) },
127
+ // #20 `gig_input_omitted` is the caller's statement that it passed NO payload (distinct from an
128
+ // explicit `input: {}`). On an approve-only resume whose every remaining chair is human, an omitted
129
+ // payload inherits the checkpoint's recorded gig_input_sha instead of drifting to sha256('{}') and
130
+ // refusing; a supplied `input` leaves this false, so a disagreeing payload still gates
131
+ // (src/runtime.ts). The handler reads it, so #234 requires it be advertised here — it is a real
132
+ // control, not an internal signal, and a caller must be able to find it.
133
+ { slug: "gig_dispatch", category: "run", input_schema: obj({ standard_slug: "string", chart_slug: "string", venue: "string", repo_url: "string", acting_for: "string", input: "object", gig_input_omitted: "boolean", depth: "string", wait: "boolean", budget: "object", resume_gig_id: "string", reuse: "boolean", approvals: "object", approved_by: "string" }), output_schema: obj({ gig_id: "string", status: "string", awaiting: "object", depth: "string", manifest: "object", resumed_from: "string", reuse: "boolean", resume_refused: "boolean", drift: "array" }) },
124
134
  // `skipped_chairs` / `resumed_from` / `reuse_rejected` are the ASYNC path's only report of a
125
135
  // saving — the manifest never reaches a caller who dispatched without `wait`.
126
136
  // `awaiting` names the human chair a parked run stopped at. On the async path — the DEFAULT
@@ -146,12 +156,35 @@ export const MCP_TOOLS = [
146
156
  // enforcement belongs); hosted routes to deps.approveGig, non-hosted has no local run to
147
157
  // approve (a local run takes its verdicts through gig_dispatch's `approvals`).
148
158
  { slug: "gig_approve", category: "run", input_schema: obj({ gig_id: "string", role: "string", verdict: "object" }), output_schema: obj({ gig_id: "string", role: "string", status: "string", approved: "boolean" }) },
159
+ // venue_credential_mint — the verb that stands up a worker without a browser. `org_slug` scopes
160
+ // the credential and `instance` binds it to one host; a key with an org and no instance is the
161
+ // org's whole authority with nothing to bind it to. The answer carries the COMPLETE worker
162
+ // environment (`env`, canonical names only) — not just the key — so a caller never assembles the
163
+ // rest by hand or infers which URL names which host (that inference is Gap 3). `credential_classes`
164
+ // names what was provisioned in `VenueSchema.credential_surface` vocabulary, so a `realize` room
165
+ // contract can be checked against the grant before a gig is dispatched. The engine ships the schema
166
+ // and its refusals; a deployment wires the minting backend (deps.mintVenueCredential). There is
167
+ // deliberately NO read-back verb (see the venue_credential_* exact-list law) and NO authorization
168
+ // policy in the engine — who may mint lives in the store.
169
+ { slug: "venue_credential_mint", category: "run", input_schema: obj({ org_slug: "string", instance: "string" }), output_schema: obj({ instance: "string", env: "object", credential_classes: "array", expires_at: nullable("string") }) },
170
+ // org_hire — the verb that ADMITS an agent to an org. The org-membership analogue of
171
+ // venue_credential_mint: the engine ships the schema and its refusals, a deployment wires the
172
+ // admission backend (deps.hireMember). `input_schema` is derived from the single Zod source
173
+ // (OrgMemberSchema = {org_slug, agent_slug}) — never a hand-written MCP schema — precisely so no
174
+ // field a capability could travel can appear on it. ADMISSION IS NOT AUTHORITY: membership is
175
+ // BELONGING, seating (a chair, caps, an assignment) is a SEPARATE act with its own gate, and a
176
+ // verb that could admit AND seat in one call would be a path to mint authority. The verb carries
177
+ // exactly the two fields that name the belonging and nothing more. It is intercepted in
178
+ // callSurfaceTool BEFORE the hosted check, so it appears in neither HOSTED_UPSERT (a hire writes
179
+ // no genome file) nor HOSTED_BLOCKED (it is callable in hosted mode); a successful hire is sealed
180
+ // to the ledger as a kind:"genome_mutation" row, so who-hired-whom is auditable, not only a store row.
181
+ { slug: "org_hire", category: "run", input_schema: obj(zodToMcpProps(OrgMemberSchema)), output_schema: obj({ org_slug: "string", agent_slug: "string" }) },
149
182
  // #234 — `gig_id`, `agent_slug` and `phase` were read by the handler and advertised nowhere.
150
183
  // This one had teeth: a skill prompt written against this schema omits `gig_id`, the handler
151
184
  // defaults it to "", and the sealed output lands in the store attached to NO gig. A live run
152
185
  // of the consuming product produced 509 such orphans. The provenance chain is the engine's
153
186
  // core promise, and the field that anchors an output to its run was undiscoverable.
154
- { slug: "output_write", category: "run", input_schema: obj({ core_type: "string", primitive: "string", domain_type: "string", domain_type_version: "number", domain: "string", data: "object", input_refs: "array", refs: "array", gig_id: "string", agent_slug: "string", phase: "string", cost_usd: "number", tokens_used: "number", duration_ms: "number", model: "string", model_tier: "string" }), output_schema: obj({ output_id: "string", primitive: "string", output: "object", validation_result: "object", validated: "boolean" }) },
187
+ { slug: "output_write", category: "run", input_schema: obj({ core_type: "string", primitive: "string", domain_type: "string", domain_type_version: "number", domain: "string", data: "object", input_refs: "array", refs: "array", gig_id: "string", agent_slug: "string", phase: "string", cost_usd: "number", tokens_used: "number", duration_ms: "number", model: "string", model_tier: "string" }), output_schema: obj({ output_id: "string", primitive: "string", output: "object", validation_result: "object", validated: "boolean", sealed: "boolean" }) },
155
188
  { slug: "agent_validate_pipeline", category: "improve", input_schema: obj({ agents: "array", standard_slug: "string", slug: "string", domain: "string", primitives: "array", phases: "array" }), output_schema: obj({ valid: "boolean", graph: "object", unsatisfied_inputs: "array", illegal_progressions: "array" }) },
156
189
  // #238 — success_rate and trend are NULLABLE, because the engine genuinely cannot compute
157
190
  // them (a failed gig writes no ledger row, so the denominator does not exist) and a hardcoded