@eir-labs/coltrane 0.23.1 → 0.24.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/dist/src/cli.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface CliIO {
31
31
  /** Injected for tests. Production reads stdin for `--input -`. */
32
32
  stdin?: () => string;
33
33
  }
34
- export declare const USAGE = "coltrane 0.23.1\n\n coltrane validate load the genome; exit non-zero on load errors\n coltrane dispatch <standard> run a standard\n coltrane monitor <gig-id> report a gig's progress\n coltrane logs <gig-id> per-chair logs for a gig\n coltrane abort <gig-id> stop a running gig\n coltrane trace <output-id> walk an output's provenance\n coltrane simulate <standard> cost/shape a standard without running it\n coltrane health engine + store health\n coltrane serve run the MCP server on stdio\n coltrane enqueue <standard> queue a gig on the LOCAL file queue (no store, no key)\n (env: COLTRANE_QUEUE_DIR \u2014 its absence is the whole\n difference between local and hosted)\n coltrane work claim one queued gig from the org store and run it\n coltrane work --check report whether this box's drain environment is configured\n and exit without claiming (0 ready, 1 not ready)\n (env: COLTRANE_STORE_URL, COLTRANE_STORE_ANON,\n COLTRANE_AGENT_TOKEN; results drain via\n COLTRANE_DRAIN_URL + COLTRANE_DRAIN_KEY;\n checkpoints under COLTRANE_WORKER_CHECKPOINTS,\n default ~/.coltrane/worker-checkpoints;\n exit 0 complete or parked, 1 failed, 3 queue empty)\n\nOptions\n --input <json|@file|-> dispatch payload; @file reads a file, - reads stdin\n --depth <skim|standard|deep> tighten the per-chair turn cap\n --budget <dollars> per-gig ceiling; the run stops when it is gone\n --reuse allow chair-level reuse of prior sealed outputs\n --resume <gig-id> continue a gig that died mid-pipeline\n --approve <role>=<json|@file> a human chair's verdict; repeat once per chair\n --as <name> who is approving; sealed as the verdict's author\n --wait block until the run finishes\n --follow poll until the gig reaches a terminal state\n --direction <upstream|downstream|both> for trace\n --json print the raw result to stdout\n --genome <path> genome root (default: $COLTRANE_GENOME or cwd)\n --help, --version\n\nA dispatch that reaches a chair a HUMAN holds parks: it names the waiting chair and exits 0,\nbecause a gig waiting on a person is not a failed gig. Approve it on the resume \u2014\n\n coltrane dispatch <standard> --resume <gig> \\\n --approve approve=@verdict.json --as eugene\n\n--input is NOT required on an approve-only resume: when every remaining chair is human the\ncheckpoint's recorded payload stands and the omission inherits it. If you DO pass --input it is\nchecked against the checkpoint, and a disagreement still refuses the resume.\n";
34
+ export declare const USAGE = "coltrane 0.24.0\n\n coltrane validate load the genome; exit non-zero on load errors\n coltrane dispatch <standard> run a standard\n coltrane monitor <gig-id> report a gig's progress\n coltrane logs <gig-id> per-chair logs for a gig\n coltrane abort <gig-id> stop a running gig\n coltrane trace <output-id> walk an output's provenance\n coltrane simulate <standard> cost/shape a standard without running it\n coltrane health engine + store health\n coltrane serve run the MCP server on stdio\n coltrane enqueue <standard> queue a gig on the LOCAL file queue (no store, no key)\n (env: COLTRANE_QUEUE_DIR \u2014 its absence is the whole\n difference between local and hosted)\n coltrane work claim one queued gig from the org store and run it\n coltrane work --check report whether this box's drain environment is configured\n and exit without claiming (0 ready, 1 not ready)\n (env: COLTRANE_STORE_URL, COLTRANE_STORE_ANON,\n COLTRANE_AGENT_TOKEN; results drain via\n COLTRANE_DRAIN_URL + COLTRANE_DRAIN_KEY;\n checkpoints under COLTRANE_WORKER_CHECKPOINTS,\n default ~/.coltrane/worker-checkpoints;\n exit 0 complete or parked, 1 failed, 3 queue empty)\n\nOptions\n --input <json|@file|-> dispatch payload; @file reads a file, - reads stdin\n --depth <skim|standard|deep> tighten the per-chair turn cap\n --budget <dollars> per-gig ceiling; the run stops when it is gone\n --reuse allow chair-level reuse of prior sealed outputs\n --resume <gig-id> continue a gig that died mid-pipeline\n --approve <role>=<json|@file> a human chair's verdict; repeat once per chair\n --as <name> who is approving; sealed as the verdict's author\n --wait block until the run finishes\n --follow poll until the gig reaches a terminal state\n --direction <upstream|downstream|both> for trace\n --json print the raw result to stdout\n --genome <path> genome root (default: $COLTRANE_GENOME or cwd)\n --help, --version\n\nA dispatch that reaches a chair a HUMAN holds parks: it names the waiting chair and exits 0,\nbecause a gig waiting on a person is not a failed gig. Approve it on the resume \u2014\n\n coltrane dispatch <standard> --resume <gig> \\\n --approve approve=@verdict.json --as eugene\n\n--input is NOT required on an approve-only resume: when every remaining chair is human the\ncheckpoint's recorded payload stands and the omission inherits it. If you DO pass --input it is\nchecked against the checkpoint, and a disagreement still refuses the resume.\n";
35
35
  interface Parsed {
36
36
  cmd: string | undefined;
37
37
  positional: string[];
@@ -1 +1,44 @@
1
1
  export declare function writeFileAtomic(file: string, text: string): void;
2
+ /** The lock record — names WHO holds the tree, so a refusal can say who to wait on or abort. */
3
+ export interface RepoLockRecord {
4
+ gig_id: string;
5
+ pid: number;
6
+ started_at: string;
7
+ genome_dir: string;
8
+ }
9
+ /** The outcome of a claim: the tree, or the live holder that refused it. */
10
+ export type RepoLockAcquire = {
11
+ ok: true;
12
+ record: RepoLockRecord;
13
+ reacquired: boolean;
14
+ } | {
15
+ ok: false;
16
+ held_by: RepoLockRecord;
17
+ };
18
+ /**
19
+ * The canonical lock path for a genome root: `<genomeDir>/.coltrane/repo-lock-<hex>.json`, where
20
+ * `<hex>` is a stable SHA-256 digest of the ABSOLUTE genome_dir truncated to 16 hex chars. The
21
+ * digest keeps the filename slash/space-free and bounded while the full path is preserved inside
22
+ * the record. `.coltrane/` is gitignored (beside the ledger and the mirror), so the artifact is a
23
+ * runtime lock, never committed.
24
+ */
25
+ export declare function repoLockPath(genomeDir: string): string;
26
+ /**
27
+ * Claim the per-repo lock for `genomeDir`, keyed on the resolved genome root.
28
+ *
29
+ * Atomic and exclusive via O_EXCL. On an existing destination:
30
+ * - the SAME gig_id (a parked gig's own resume) re-acquires the tree it already holds;
31
+ * - a LIVE holder refuses the claim, carrying `held_by`;
32
+ * - a DEAD holder (or a torn file) is broken — recorded, unlinked — and the claim retried.
33
+ */
34
+ export declare function acquireRepoLock(genomeDir: string, holder: {
35
+ gigId: string;
36
+ pid: number;
37
+ startedAt: string;
38
+ }): RepoLockAcquire;
39
+ /**
40
+ * Release the per-repo lock — but ONLY when the recorded holder is `gigId`. A release that finds a
41
+ * different holder never steals it (that lock belongs to another run). Returns whether a lock this
42
+ * gig held was actually removed.
43
+ */
44
+ export declare function releaseRepoLock(genomeDir: string, gigId: string): boolean;
@@ -20,9 +20,9 @@
20
20
  * Two implementations of one concern, one of them documented and one of them absent, is the same
21
21
  * shape as the two identity gates that disagreed in the resume work.
22
22
  */
23
- import { mkdirSync, writeFileSync, renameSync } from "node:fs";
24
- import { dirname } from "node:path";
25
- import { randomUUID } from "node:crypto";
23
+ import { mkdirSync, writeFileSync, renameSync, readFileSync, unlinkSync } from "node:fs";
24
+ import { dirname, join, resolve } from "node:path";
25
+ import { randomUUID, createHash } from "node:crypto";
26
26
  export function writeFileAtomic(file, text) {
27
27
  mkdirSync(dirname(file), { recursive: true });
28
28
  // randomUUID, not pid: two containers sharing a mounted volume routinely both have low pids,
@@ -31,4 +31,136 @@ export function writeFileAtomic(file, text) {
31
31
  writeFileSync(tmp, text, "utf8");
32
32
  renameSync(tmp, file);
33
33
  }
34
+ /**
35
+ * The canonical lock path for a genome root: `<genomeDir>/.coltrane/repo-lock-<hex>.json`, where
36
+ * `<hex>` is a stable SHA-256 digest of the ABSOLUTE genome_dir truncated to 16 hex chars. The
37
+ * digest keeps the filename slash/space-free and bounded while the full path is preserved inside
38
+ * the record. `.coltrane/` is gitignored (beside the ledger and the mirror), so the artifact is a
39
+ * runtime lock, never committed.
40
+ */
41
+ export function repoLockPath(genomeDir) {
42
+ const abs = resolve(genomeDir);
43
+ const hex = createHash("sha256").update(abs).digest("hex").slice(0, 16);
44
+ return join(abs, ".coltrane", `repo-lock-${hex}.json`);
45
+ }
46
+ function isCode(e, code) {
47
+ return typeof e === "object" && e !== null && e.code === code;
48
+ }
49
+ /** A holder pid is alive iff `process.kill(pid, 0)` does not throw ESRCH. EPERM means the process
50
+ * exists but is not ours to signal — still alive. Any other error is treated conservatively as
51
+ * alive so a transient fault never breaks a lock that might still be live. */
52
+ function pidAlive(pid) {
53
+ if (!Number.isInteger(pid) || pid <= 0)
54
+ return false;
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ }
59
+ catch (e) {
60
+ if (isCode(e, "ESRCH"))
61
+ return false;
62
+ return true;
63
+ }
64
+ }
65
+ function readLockRecord(path) {
66
+ try {
67
+ const rec = JSON.parse(readFileSync(path, "utf8"));
68
+ if (rec && typeof rec.gig_id === "string" && typeof rec.pid === "number")
69
+ return rec;
70
+ return undefined;
71
+ }
72
+ catch {
73
+ return undefined; // ENOENT (raced away) or a torn/unparsable file — treat as no readable holder
74
+ }
75
+ }
76
+ /** Record a broken stale lock to stderr — never silent, so an operator can audit which dead gig's
77
+ * grip was released and reclaimed. Shaped as a one-line JSON event like the gig milestone log. */
78
+ function recordStaleBreak(dest, held) {
79
+ const line = JSON.stringify({
80
+ t: new Date().toISOString(),
81
+ ev: "repo_lock_stale_break",
82
+ stale_gig_id: held?.gig_id ?? "(unreadable)",
83
+ stale_pid: held?.pid ?? null,
84
+ reason: "holder pid not alive",
85
+ lock: dest,
86
+ });
87
+ try {
88
+ process.stderr.write(line + "\n");
89
+ }
90
+ catch {
91
+ /* best-effort — the break still happens; only its record is lost */
92
+ }
93
+ }
94
+ /**
95
+ * Claim the per-repo lock for `genomeDir`, keyed on the resolved genome root.
96
+ *
97
+ * Atomic and exclusive via O_EXCL. On an existing destination:
98
+ * - the SAME gig_id (a parked gig's own resume) re-acquires the tree it already holds;
99
+ * - a LIVE holder refuses the claim, carrying `held_by`;
100
+ * - a DEAD holder (or a torn file) is broken — recorded, unlinked — and the claim retried.
101
+ */
102
+ export function acquireRepoLock(genomeDir, holder) {
103
+ const dest = repoLockPath(genomeDir);
104
+ mkdirSync(dirname(dest), { recursive: true });
105
+ const record = {
106
+ gig_id: holder.gigId,
107
+ pid: holder.pid,
108
+ started_at: holder.startedAt,
109
+ genome_dir: resolve(genomeDir),
110
+ };
111
+ const payload = JSON.stringify(record);
112
+ // A small retry budget: each stale break unlinks and loops to re-create. More than one iteration
113
+ // is only reached under an active race (another process re-took the freed slot), which the next
114
+ // read resolves — never an unbounded spin.
115
+ for (let attempt = 0; attempt < 3; attempt++) {
116
+ try {
117
+ writeFileSync(dest, payload, { encoding: "utf8", flag: "wx" });
118
+ return { ok: true, record, reacquired: false };
119
+ }
120
+ catch (e) {
121
+ if (!isCode(e, "EEXIST"))
122
+ throw e;
123
+ }
124
+ const held = readLockRecord(dest);
125
+ if (held && held.gig_id === holder.gigId)
126
+ return { ok: true, record: held, reacquired: true };
127
+ if (held && pidAlive(held.pid))
128
+ return { ok: false, held_by: held };
129
+ // Dead holder or torn file — break it (recorded) and retry the exclusive create.
130
+ recordStaleBreak(dest, held);
131
+ try {
132
+ unlinkSync(dest);
133
+ }
134
+ catch (e) {
135
+ if (!isCode(e, "ENOENT"))
136
+ throw e;
137
+ }
138
+ }
139
+ // Exhausted the budget: another process keeps re-taking the freed slot. Refuse with the best
140
+ // holder we can read rather than clobber a live claim.
141
+ const held = readLockRecord(dest);
142
+ if (held && held.gig_id === holder.gigId)
143
+ return { ok: true, record: held, reacquired: true };
144
+ return { ok: false, held_by: held ?? record };
145
+ }
146
+ /**
147
+ * Release the per-repo lock — but ONLY when the recorded holder is `gigId`. A release that finds a
148
+ * different holder never steals it (that lock belongs to another run). Returns whether a lock this
149
+ * gig held was actually removed.
150
+ */
151
+ export function releaseRepoLock(genomeDir, gigId) {
152
+ const dest = repoLockPath(genomeDir);
153
+ const held = readLockRecord(dest);
154
+ if (!held || held.gig_id !== gigId)
155
+ return false;
156
+ try {
157
+ unlinkSync(dest);
158
+ return true;
159
+ }
160
+ catch (e) {
161
+ if (isCode(e, "ENOENT"))
162
+ return false;
163
+ throw e;
164
+ }
165
+ }
34
166
  //# sourceMappingURL=fs_atomic.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs_atomic.js","sourceRoot":"","sources":["../../src/fs_atomic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IACxD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,6FAA6F;IAC7F,0FAA0F;IAC1F,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAC;IAC1C,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC"}
1
+ {"version":3,"file":"fs_atomic.js","sourceRoot":"","sources":["../../src/fs_atomic.ts"],"names":[],"mappings":"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"}
@@ -75,6 +75,16 @@ export interface GigRunState {
75
75
  /** Why. Surfaced by gig_monitor so `aborted` reads as a decision, not a mystery. */
76
76
  abort_reason?: string;
77
77
  }
78
+ /**
79
+ * Is a gig's status TERMINAL — the run is over and holds the tree no longer?
80
+ *
81
+ * The single-flight per-repo lock (change c1d0c2e0) releases on exactly this set: complete, failed,
82
+ * and aborted. `awaiting_approval` is DELIBERATELY EXCLUDED — a parked gig is not terminal. It holds
83
+ * uncommitted work in the working tree (the chairs before the human gate already ran and left
84
+ * changes on disk) and will continue from that exact tree when approved, so it must RETAIN the lock
85
+ * while parked. Naming the terminal set here keeps the release site from re-deriving it and drifting.
86
+ */
87
+ export declare function isTerminalStatus(status: GigStatus | string): boolean;
78
88
  export declare function newGigRun(gig_id: string, standard_slug: string, phases_total: number, now: string): GigRunState;
79
89
  export declare function applyGigProgress(state: GigRunState, ev: GigProgressEvent): void;
80
90
  /**
@@ -4,6 +4,18 @@
4
4
  // runtime fires GigProgressEvents as it walks the phase graph; this module folds those into
5
5
  // a GigRunState that gig_monitor reads. State is in-memory (per server lifetime): a restart
6
6
  // drops in-flight tracking — acceptable for v0, and a restart mid-gig is its own problem.
7
+ /**
8
+ * Is a gig's status TERMINAL — the run is over and holds the tree no longer?
9
+ *
10
+ * The single-flight per-repo lock (change c1d0c2e0) releases on exactly this set: complete, failed,
11
+ * and aborted. `awaiting_approval` is DELIBERATELY EXCLUDED — a parked gig is not terminal. It holds
12
+ * uncommitted work in the working tree (the chairs before the human gate already ran and left
13
+ * changes on disk) and will continue from that exact tree when approved, so it must RETAIN the lock
14
+ * while parked. Naming the terminal set here keeps the release site from re-deriving it and drifting.
15
+ */
16
+ export function isTerminalStatus(status) {
17
+ return status === "complete" || status === "failed" || status === "aborted";
18
+ }
7
19
  export function newGigRun(gig_id, standard_slug, phases_total, now) {
8
20
  return {
9
21
  gig_id, standard_slug, status: "running", started_at: now,
@@ -1 +1 @@
1
- {"version":3,"file":"gig_tracker.js","sourceRoot":"","sources":["../../src/gig_tracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gig_tracker.js","sourceRoot":"","sources":["../../src/gig_tracker.ts"],"names":[],"mappings":"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"}
@@ -43,7 +43,8 @@ import { readFileSync, existsSync, mkdirSync, appendFileSync, readdirSync, write
43
43
  import { randomUUID, createHash } from "node:crypto";
44
44
  import { join, dirname } from "node:path";
45
45
  import { tmpdir } from "node:os";
46
- import { newGigRun, applyGigProgress, gigEventLogLine, pruneGigRuns } from "./gig_tracker.js";
46
+ import { newGigRun, applyGigProgress, gigEventLogLine, pruneGigRuns, isTerminalStatus } from "./gig_tracker.js";
47
+ import { acquireRepoLock, releaseRepoLock } from "./fs_atomic.js";
47
48
  import { isGig } from "./ledger.js";
48
49
  import { SubthreadRecorder, ApiVersionMismatchError } from "./subthread_recorder.js";
49
50
  import { canonJson, runFingerprint, CANONICAL_FORM_VERSION } from "./canonical_form.js";
@@ -96,6 +97,14 @@ function governanceRow(event, subject_slug, detail, subject_gig_id) {
96
97
  finished_at: now,
97
98
  };
98
99
  }
100
+ /** The prose a single-flight refusal teaches a human: WHO holds the tree, and the two ways out
101
+ * (wait for it to settle, or abort it). The machine-readable holder rides in `data.held_by`. */
102
+ function heldTreeError(held) {
103
+ return (`the working tree at "${held.genome_dir}" is held by a running gig "${held.gig_id}" ` +
104
+ `(pid ${held.pid}, since ${held.started_at}). Single-flight is law: a second dispatch against ` +
105
+ `the same repository is REFUSED — it does not queue and does not wait. Let that gig reach a ` +
106
+ `terminal state, or abort it with gig_abort "${held.gig_id}".`);
107
+ }
99
108
  const KNOWN_SLUGS = new Set(MCP_TOOLS.map((t) => t.slug));
100
109
  // Live registry of admissible tool slugs — the cage gate for agent_define's
101
110
  // allowed_tools. Seeded with the static MCP_TOOLS surface; tool_register grows
@@ -887,9 +896,41 @@ async function runImpl(slug, args, deps, approval) {
887
896
  ...(res.gates_approved ? { gates_approved: res.gates_approved } : {}),
888
897
  ...(res.resumed ? { resumed: res.resumed } : {}),
889
898
  });
899
+ // The gig id the whole performance runs under — minted ONCE for both the wait and async
900
+ // doors, so the single-flight lock names the same holder either way.
901
+ const chartGigId = resumeArg ?? randomUUID();
902
+ // ── single-flight: ONE lock for the CHART's full lifetime, never per-movement ──────────
903
+ // A chart runs its movements sequentially under one promise. The lock is claimed ONCE here
904
+ // (holder = the chart's own gig_id) BEFORE the first movement and released only when the
905
+ // performance settles. A per-movement acquire/release would either open a gap between
906
+ // movements where a concurrent dispatch could slip in, or deadlock movement 2 against the
907
+ // lock movement 1 still holds. Released on every terminal outcome; RETAINED through
908
+ // awaiting_approval (a parked performance holds uncommitted work in the tree).
909
+ let releaseChartLock;
910
+ let chartLockReacquired = false; // re-entered a parked performance's own lock, vs freshly minted
911
+ if (deps.genome_dir) {
912
+ const acq = acquireRepoLock(deps.genome_dir, {
913
+ gigId: chartGigId, pid: process.pid, startedAt: new Date().toISOString(),
914
+ });
915
+ if (!acq.ok) {
916
+ return {
917
+ ok: false, requires_approval: approval, refusal: "repo_locked",
918
+ error: heldTreeError(acq.held_by), data: { held_by: acq.held_by },
919
+ };
920
+ }
921
+ chartLockReacquired = acq.reacquired;
922
+ const genomeDir = deps.genome_dir;
923
+ releaseChartLock = () => { try {
924
+ releaseRepoLock(genomeDir, chartGigId);
925
+ }
926
+ catch { /* best-effort */ } };
927
+ }
890
928
  if (wait) {
891
929
  try {
892
- const res = await runChart(plan, gigInput, chartDeps);
930
+ const res = await runChart(plan, gigInput, { ...chartDeps, gig_id: chartGigId });
931
+ // Terminal (complete / budget-exhausted) frees the tree; a parked chart RETAINS it.
932
+ if (releaseChartLock && res.status !== "awaiting_approval")
933
+ releaseChartLock();
893
934
  return {
894
935
  ok: true, requires_approval: approval,
895
936
  data: {
@@ -902,9 +943,15 @@ async function runImpl(slug, args, deps, approval) {
902
943
  }
903
944
  catch (e) {
904
945
  if (e instanceof ResumeRefused) {
946
+ // A re-entered lock belongs to the parked performance; a fresh one is freed here.
947
+ if (releaseChartLock && !chartLockReacquired)
948
+ releaseChartLock();
905
949
  return { ok: false, requires_approval: approval, error: e.message,
906
950
  data: { resume_refused: true, gig_id: e.gig_id, drift: e.drift } };
907
951
  }
952
+ // Any other terminal throw (budget exhausted, a movement failed) frees the tree.
953
+ if (releaseChartLock)
954
+ releaseChartLock();
908
955
  if (e instanceof BudgetExhausted) {
909
956
  const partial = partialGigUsage(e);
910
957
  return { ok: false, requires_approval: approval, error: e.message,
@@ -917,7 +964,6 @@ async function runImpl(slug, args, deps, approval) {
917
964
  // Async, the default. Same live-state row an async standard dispatch registers, so
918
965
  // gig_monitor and gig_abort reach a performance exactly as they reach a run: the row
919
966
  // names the standard the performance OPENS with, and `chart_slug` names the arrangement.
920
- const chartGigId = resumeArg ?? randomUUID();
921
967
  const chartRuns = deps.gig_runs ?? (deps.gig_runs = new Map());
922
968
  const priorChartState = chartRuns.get(chartGigId);
923
969
  const chartState = newGigRun(chartGigId, plan.movements[0].standard.slug, plan.movements.reduce((n, m) => n + m.standard.phases.length, 0), new Date().toISOString());
@@ -993,7 +1039,14 @@ async function runImpl(slug, args, deps, approval) {
993
1039
  chartState.budget_state = bs;
994
1040
  onChartProgress({ type: "gig_failed", error: chartState.error });
995
1041
  })
996
- .finally(() => { chartState.controller = undefined; });
1042
+ .finally(() => {
1043
+ chartState.controller = undefined; // don't pin a controller past settle
1044
+ // Free the tree on every terminal outcome (complete / failed / aborted); a parked
1045
+ // performance RETAINS it. A refused resume that re-entered a parked holder's lock
1046
+ // leaves it held; one that minted a fresh lock still frees it.
1047
+ if (releaseChartLock && isTerminalStatus(chartState.status) && !(chartRefusal && chartLockReacquired))
1048
+ releaseChartLock();
1049
+ });
997
1050
  if (resumeArg !== undefined) {
998
1051
  await Promise.resolve(); // one turn — see the ordering note on the standard path below
999
1052
  if (chartRefusal) {
@@ -1040,14 +1093,50 @@ async function runImpl(slug, args, deps, approval) {
1040
1093
  venue, venues: deps.venues, venueRealizer: deps.venueRealizer,
1041
1094
  repoUrl: dispatchRepoUrl,
1042
1095
  });
1096
+ // The gig id this run seals under — minted ONCE for both doors so the single-flight lock
1097
+ // names the same holder whether the caller blocked (wait:true) or polled (async default).
1098
+ const gigId = resumeArg ?? randomUUID();
1099
+ // ── single-flight: claim the working tree BEFORE any chair runs ─────────────────────────
1100
+ // Every LOCAL dispatch entry point funnels here (the in-process gig_dispatch tool AND the
1101
+ // CLI, which calls dispatchTool). The lock is per genome ROOT (deps.genome_dir): two gigs
1102
+ // against the same tree each derive their sealed change-set from `git diff` of a tree the
1103
+ // other mutates, so the second is REFUSED — never queued — with a structured error naming
1104
+ // the holder. A parked gig's OWN resume (same gig_id) re-enters the tree it already holds.
1105
+ // The lock activates only when a genome_dir is present: a hosted/bare-deps drain carries no
1106
+ // local tree to lock and is unaffected by construction.
1107
+ let releaseLock;
1108
+ // Did this claim RE-ENTER a lock the same gig already held (a parked gig's own resume), as
1109
+ // opposed to minting a fresh one? A refused resume must NOT free a re-entered lock (the
1110
+ // parked holder keeps its tree), but MUST free a fresh one (nothing else holds it).
1111
+ let lockReacquired = false;
1112
+ if (deps.genome_dir) {
1113
+ const acq = acquireRepoLock(deps.genome_dir, {
1114
+ gigId, pid: process.pid, startedAt: new Date().toISOString(),
1115
+ });
1116
+ if (!acq.ok) {
1117
+ return {
1118
+ ok: false, requires_approval: approval, refusal: "repo_locked",
1119
+ error: heldTreeError(acq.held_by), data: { held_by: acq.held_by },
1120
+ };
1121
+ }
1122
+ lockReacquired = acq.reacquired;
1123
+ const genomeDir = deps.genome_dir;
1124
+ releaseLock = () => { try {
1125
+ releaseRepoLock(genomeDir, gigId);
1126
+ }
1127
+ catch { /* best-effort */ } };
1128
+ }
1043
1129
  // Synchronous mode (opt-in via wait:true) — block, return the manifest. The
1044
1130
  // deterministic test path and any caller that wants the answer in one call.
1045
1131
  if (wait) {
1046
1132
  try {
1047
1133
  const res = await runGig(standard, gigInput, {
1048
- ...dispatchDeps,
1134
+ ...dispatchDeps, gig_id: gigId,
1049
1135
  ...(depth ? { depth } : {}), ...reuseWiring, ...humanWiring,
1050
1136
  });
1137
+ // Terminal (complete) frees the tree; a parked gig (awaiting_approval) RETAINS it.
1138
+ if (releaseLock && res.status !== "awaiting_approval")
1139
+ releaseLock();
1051
1140
  return {
1052
1141
  ok: true, requires_approval: approval,
1053
1142
  data: {
@@ -1069,11 +1158,18 @@ async function runImpl(slug, args, deps, approval) {
1069
1158
  }
1070
1159
  catch (e) {
1071
1160
  // A refused resume is a REFUSAL, not a crash: nothing ran, nothing was spent, and
1072
- // the caller needs the drift list to decide whether to re-dispatch cold.
1161
+ // the caller needs the drift list to decide whether to re-dispatch cold. The holder it
1162
+ // re-entered is unchanged, so the tree is NOT freed here.
1073
1163
  if (e instanceof ResumeRefused) {
1164
+ // Free only a FRESH claim; a re-entered lock belongs to the parked holder, untouched.
1165
+ if (releaseLock && !lockReacquired)
1166
+ releaseLock();
1074
1167
  return { ok: false, requires_approval: approval, error: e.message,
1075
1168
  data: { resume_refused: true, gig_id: e.gig_id, drift: e.drift } };
1076
1169
  }
1170
+ // Every other terminal throw (budget exhausted, a chair failed) frees the tree.
1171
+ if (releaseLock)
1172
+ releaseLock();
1077
1173
  if (e instanceof BudgetExhausted) {
1078
1174
  // #236 — the synchronous half: a depleted gig also burned real dollars before it
1079
1175
  // stopped, and the operator needs them in the same reply as the depletion notice.
@@ -1091,8 +1187,8 @@ async function runImpl(slug, args, deps, approval) {
1091
1187
  // A resumed run CONTINUES the gig it resumes — same id — so the restored outputs stay
1092
1188
  // in-gig and `output_trace` still reaches them. The live-state entry for the earlier
1093
1189
  // attempt is replaced: that gig is running again, and showing its old `failed` state
1094
- // while it runs would be a lie the operator acts on.
1095
- const gigId = resumeArg ?? randomUUID();
1190
+ // while it runs would be a lie the operator acts on. (`gigId` was minted above, before the
1191
+ // wait/async split, so the single-flight lock names this same id.)
1096
1192
  const runs = deps.gig_runs ?? (deps.gig_runs = new Map());
1097
1193
  // #278 review — keep the prior attempt's record so a REFUSED resume can put it back.
1098
1194
  // Overwriting it is right when the resume proceeds (that gig is running again), and
@@ -1203,7 +1299,15 @@ async function runImpl(slug, args, deps, approval) {
1203
1299
  state.budget_state = bs;
1204
1300
  onProgress({ type: "gig_failed", error: state.error });
1205
1301
  })
1206
- .finally(() => { state.controller = undefined; }); // don't pin a controller past settle
1302
+ .finally(() => {
1303
+ state.controller = undefined; // don't pin a controller past settle
1304
+ // Free the tree on every terminal outcome (complete / failed / aborted); a parked gig
1305
+ // (awaiting_approval) RETAINS it — it holds uncommitted work the resume continues from.
1306
+ // A refused resume that RE-ENTERED a parked holder's lock leaves it held; a refused
1307
+ // resume that minted a FRESH lock still frees it (nothing else holds that tree).
1308
+ if (releaseLock && isTerminalStatus(state.status) && !(resumeRefusal && lockReacquired))
1309
+ releaseLock();
1310
+ });
1207
1311
  if (resumeArg !== undefined) {
1208
1312
  await Promise.resolve(); // one turn — see the ordering note above
1209
1313
  if (resumeRefusal) {