acuvo-code 0.2.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 (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
package/lib/lease.mjs ADDED
@@ -0,0 +1,868 @@
1
+ /**
2
+ * ── ⭐⭐ SEVEN TERMINALS, ONE CHECKOUT — A COOPERATIVE FILE LEASE ────────────
3
+ *
4
+ * `lib/parallel.mjs` coordinates tasks inside ONE process: it runs them in a
5
+ * pool and, crucially, DETECTS when two of them wrote the same file. Seven
6
+ * separate `acuvo` processes on one machine share no memory at all, so that
7
+ * detection cannot see across them. Two terminals will eventually write the
8
+ * same file, the second one wins silently, and the first reports success for
9
+ * work that no longer exists.
10
+ *
11
+ * This file is the missing coordination: a lease taken on a PATH, published on
12
+ * disk, visible to every process in the checkout.
13
+ *
14
+ * ── ⚠️ FOUR DESIGN DECISIONS, AND WHY THEY WENT THIS WAY ────────────────────
15
+ *
16
+ * 1. **PATHS, NOT THE REPO.** A repo-wide lock idles six of seven terminals,
17
+ * which deletes the entire point of running seven. A terminal declares the
18
+ * handful of files it intends to write; everything else stays free.
19
+ *
20
+ * 2. **A TTL PLUS A HEARTBEAT, AND A RECLAIM THAT IS DELIBERATELY SLOW.** A
21
+ * crashed terminal must not block the repo for ever, so a lease goes stale.
22
+ * But reclaiming EAGERLY is strictly worse than deadlocking: a deadlock is a
23
+ * refusal a human can read, while an early reclaim hands two live agents the
24
+ * same file and corrupts the work invisibly. So the reclaim boundary is the
25
+ * TTL *plus another whole TTL* (never less than the grace period), and the
26
+ * takeover is written into the record so it can be seen afterwards.
27
+ *
28
+ * 3. **CONSERVATIVE KEYS.** `src/App.ts` and `src/app.ts` are ONE file on
29
+ * Windows and macOS. Keying on the exact spelling would hand two agents the
30
+ * same file on the machine this actually runs on. Over-locking costs one
31
+ * refusal you can read; under-locking costs corruption you cannot see.
32
+ *
33
+ * 4. **EXCLUSIVITY COMES FROM THE FILESYSTEM, NEVER FROM A READ-THEN-WRITE.**
34
+ * Checking "is it free?" and then writing is a race with a window, and eight
35
+ * processes will find that window. Creating a file is the only operation the
36
+ * OS will do exactly once, so every lease is *published* by an exclusive
37
+ * create (hard link, falling back to O_EXCL) and every *replacement* is
38
+ * serialised behind an exclusively-created gate and lands by rename. A
39
+ * reader therefore only ever sees "no file" or "the whole file" — never the
40
+ * half a file a killed process would otherwise leave behind.
41
+ *
42
+ * ⚠️ NO AMBIENT TIME. The wall clock is injected exactly once, as a default.
43
+ * Every TTL decision in here is a pure function of a number that a test can
44
+ * hand over, which is why the whole TTL surface tests in microseconds.
45
+ */
46
+
47
+ import { createHash } from 'node:crypto';
48
+ import {
49
+ linkSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync,
50
+ } from 'node:fs';
51
+ import { isAbsolute, join, resolve, sep } from 'node:path';
52
+
53
+ /** Bump when the on-disk shape changes; an unknown version reads as corrupt. */
54
+ export const LEASE_VERSION = 1;
55
+
56
+ /** Where lease records live, relative to the workspace root. Always '/'-joined. */
57
+ export const LEASE_DIR = '.acuvo/leases';
58
+
59
+ /**
60
+ * ⚠️ THE TTL BAND. A lease shorter than a second cannot be heartbeated
61
+ * reliably, and one longer than an hour is a crashed terminal nobody notices.
62
+ */
63
+ export const MIN_TTL_MS = 1_000;
64
+ export const DEFAULT_TTL_MS = 120_000;
65
+ export const MAX_TTL_MS = 3_600_000;
66
+
67
+ /**
68
+ * ⚠️ THE FLOOR ON PATIENCE. Even a one-second lease gets this long of silence
69
+ * before anyone may take it — long enough that a garbage-collection pause or a
70
+ * slow disk cannot look like a dead terminal.
71
+ */
72
+ export const RECLAIM_GRACE_MS = 30_000;
73
+
74
+ /** A hot path could be taken over for ever; the record must not grow for ever. */
75
+ export const MAX_TAKEOVERS = 8;
76
+
77
+ /**
78
+ * ⚠️ THE ONE AND ONLY READ OF AMBIENT TIME IN THIS MODULE. Everything else
79
+ * takes `clock` as a parameter. A test pins this by counting occurrences, and
80
+ * that test is the reason every TTL case here runs without a single sleep.
81
+ */
82
+ const wallClock = () => Date.now();
83
+
84
+ /** Link failures that mean "this filesystem cannot", not "somebody beat you". */
85
+ const LINK_UNSUPPORTED = new Set(['EPERM', 'EACCES', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV', 'EMLINK']);
86
+
87
+ /** Windows can transiently refuse a rename while another process reads the file. */
88
+ const RENAME_RETRYABLE = new Set(['EPERM', 'EACCES', 'EBUSY']);
89
+ const RENAME_ATTEMPTS = 25;
90
+
91
+ // ═══════════════════════════════════════════════════════════════════════════
92
+ // PURE HELPERS — no disk, no time, no exceptions
93
+ // ═══════════════════════════════════════════════════════════════════════════
94
+
95
+ /**
96
+ * How long a lease must be SILENT before anybody else may take it.
97
+ *
98
+ * ⭐ Doubling rather than adding a fixed grace is the conservative choice: a
99
+ * ten-minute lease is held by something slow and deliberate, and giving it only
100
+ * thirty extra seconds would reclaim it out from under real work. A short lease
101
+ * still gets the full grace period, so nothing is ever taken in under 30s.
102
+ */
103
+ export function reclaimableAfter(ttlMs) {
104
+ const ttl = Number.isFinite(ttlMs) ? Math.max(0, ttlMs) : DEFAULT_TTL_MS;
105
+ return ttl + Math.max(RECLAIM_GRACE_MS, ttl);
106
+ }
107
+
108
+ /** `live` | `expired` (past TTL, still protected) | `reclaimable` (takeable). */
109
+ function stateOf(silentForMs, ttlMs) {
110
+ if (silentForMs > reclaimableAfter(ttlMs)) return 'reclaimable';
111
+ if (silentForMs > ttlMs) return 'expired';
112
+ return 'live';
113
+ }
114
+
115
+ function clampTtl(ttlMs) {
116
+ if (typeof ttlMs !== 'number' || !Number.isFinite(ttlMs)) return DEFAULT_TTL_MS;
117
+ return Math.min(MAX_TTL_MS, Math.max(MIN_TTL_MS, Math.round(ttlMs)));
118
+ }
119
+
120
+ /**
121
+ * ⚠️ A BROKEN CLOCK MUST BE AN ERROR, NEVER A RECORD. A lease whose timestamp
122
+ * is NaN compares false against every boundary, so it would be simultaneously
123
+ * un-expirable and un-reclaimable — a file locked for the life of the checkout.
124
+ */
125
+ function checkClock(clock) {
126
+ if (typeof clock !== 'function') {
127
+ return { ok: false, error: 'clock must be a function returning a millisecond number' };
128
+ }
129
+ let now;
130
+ try {
131
+ now = clock();
132
+ } catch (err) {
133
+ return { ok: false, error: `clock threw: ${err?.message ?? String(err)}` };
134
+ }
135
+ if (typeof now !== 'number' || !Number.isFinite(now)) {
136
+ return { ok: false, error: `clock returned ${JSON.stringify(now) ?? String(now)} — a lease needs a finite millisecond number` };
137
+ }
138
+ return { ok: true, now };
139
+ }
140
+
141
+ /** An anonymous lock cannot be released by its owner or blamed for a stall. */
142
+ function checkHolder(holder) {
143
+ if (typeof holder !== 'string' || !holder.trim()) {
144
+ return { ok: false, error: 'a lease needs a holder name — an anonymous lock cannot be released, renewed or blamed' };
145
+ }
146
+ return { ok: true, holder: holder.trim() };
147
+ }
148
+
149
+ function normalizePid(pid) {
150
+ return Number.isFinite(pid) ? Math.trunc(pid) : process.pid;
151
+ }
152
+
153
+ /**
154
+ * Turn whatever the caller typed into ONE canonical repo-relative path.
155
+ *
156
+ * ⚠️ `./src/app.ts`, `src\app.ts` and `src/./app.ts` are the same file, and a
157
+ * lease keyed on the raw string would let a second terminal walk straight past
158
+ * a held lease by spelling it differently.
159
+ */
160
+ function normalizePath(root, raw) {
161
+ if (typeof root !== 'string' || !root.trim()) {
162
+ return { ok: false, error: 'a workspace root is required' };
163
+ }
164
+ if (typeof raw !== 'string') {
165
+ return { ok: false, error: `a lease path must be a string, got ${raw === null ? 'null' : typeof raw}` };
166
+ }
167
+ const trimmed = raw.trim();
168
+ if (!trimmed) return { ok: false, error: 'a lease path must not be empty' };
169
+
170
+ // ⚠️ A drive-letter path is absolute on Windows and merely odd on Linux —
171
+ // reject it explicitly so the rule is the same on both.
172
+ if (isAbsolute(trimmed) || /^[a-zA-Z]:[\\/]/.test(trimmed)) {
173
+ return { ok: false, error: `a lease path must be relative to the workspace, got "${raw}"` };
174
+ }
175
+
176
+ const rootAbs = resolve(root);
177
+ const absolute = resolve(rootAbs, trimmed);
178
+ if (absolute === rootAbs) {
179
+ return { ok: false, error: 'the workspace root itself cannot be leased — lease the paths you intend to write' };
180
+ }
181
+ if (!absolute.startsWith(rootAbs + sep)) {
182
+ return { ok: false, error: `"${raw}" resolves outside the workspace and cannot be leased` };
183
+ }
184
+
185
+ const path = absolute.slice(rootAbs.length + 1).split(sep).join('/');
186
+ return { ok: true, path, key: path.toLowerCase(), absolute };
187
+ }
188
+
189
+ /**
190
+ * The record filename. A hash keeps nested paths flat and legal on every
191
+ * filesystem, and hashing the CASE-FOLDED key is what makes `App.ts` and
192
+ * `app.ts` collide on purpose.
193
+ */
194
+ function recordName(key) {
195
+ return `${createHash('sha256').update(key).digest('hex').slice(0, 20)}.json`;
196
+ }
197
+
198
+ /**
199
+ * ⭐ A TOKEN IS DERIVED, NOT RANDOM. Proof of ownership has to survive being
200
+ * written to disk and read back by another process, and a reproducible token
201
+ * means a test can assert on it. Holder names collide (every terminal defaults
202
+ * to the same one); holder + pid + path + instant does not.
203
+ */
204
+ function deriveToken({ path, holder, pid, acquiredAt }) {
205
+ return createHash('sha256')
206
+ .update(`${LEASE_VERSION}\u0000${path}\u0000${holder}\u0000${pid}\u0000${acquiredAt}`)
207
+ .digest('hex')
208
+ .slice(0, 24);
209
+ }
210
+
211
+ /** Where a lease for `path` lives. Exported so callers can inspect one file. */
212
+ export function leaseFilePath(root, path) {
213
+ const norm = normalizePath(root, path);
214
+ if (!norm.ok) return norm;
215
+ const dir = join(resolve(root), ...LEASE_DIR.split('/'));
216
+ const name = recordName(norm.key);
217
+ return { ok: true, path: norm.path, key: norm.key, target: norm.absolute, dir, name, absolute: join(dir, name) };
218
+ }
219
+
220
+ // ═══════════════════════════════════════════════════════════════════════════
221
+ // THE RECORD
222
+ // ═══════════════════════════════════════════════════════════════════════════
223
+
224
+ const serialize = (record) => `${JSON.stringify(record, null, 2)}\n`;
225
+
226
+ /**
227
+ * ⚠️ A PARTIAL OR UNKNOWN RECORD IS NOT A LOCK. Every field is checked, because
228
+ * the one thing worse than a corrupt lease file is a corrupt lease file that
229
+ * still answers "held" and blocks the path for ever.
230
+ */
231
+ function parseRecord(raw) {
232
+ if (typeof raw !== 'string' || !raw.trim()) return { ok: false, why: 'the file is empty' };
233
+ let rec;
234
+ try {
235
+ rec = JSON.parse(raw);
236
+ } catch {
237
+ return { ok: false, why: 'the file is not valid JSON' };
238
+ }
239
+ if (!rec || typeof rec !== 'object' || Array.isArray(rec)) return { ok: false, why: 'the file is not a lease object' };
240
+ if (rec.version !== LEASE_VERSION) return { ok: false, why: `unknown lease version ${JSON.stringify(rec.version)}` };
241
+ for (const field of ['path', 'key', 'holder', 'token']) {
242
+ if (typeof rec[field] !== 'string' || !rec[field]) return { ok: false, why: `the "${field}" field is missing` };
243
+ }
244
+ for (const field of ['ttlMs', 'acquiredAt', 'renewedAt']) {
245
+ if (typeof rec[field] !== 'number' || !Number.isFinite(rec[field])) return { ok: false, why: `the "${field}" field is missing or not a number` };
246
+ }
247
+ if (!Number.isFinite(rec.pid)) return { ok: false, why: 'the "pid" field is missing or not a number' };
248
+ if (!Array.isArray(rec.takeovers)) return { ok: false, why: 'the "takeovers" field is missing' };
249
+ return { ok: true, record: rec };
250
+ }
251
+
252
+ /** `absent` | `held` | `corrupt` | `unreadable`. Never throws. */
253
+ function readLeaseFile(absolute) {
254
+ let raw;
255
+ try {
256
+ raw = readFileSync(absolute, 'utf8');
257
+ } catch (err) {
258
+ if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') return { state: 'absent' };
259
+ // ⚠️ Deliberately NOT treated as free. We cannot prove nobody holds it, and
260
+ // guessing "free" is the failure that corrupts work. A human can read this.
261
+ return { state: 'unreadable', why: err?.message ?? String(err) };
262
+ }
263
+ const parsed = parseRecord(raw);
264
+ if (!parsed.ok) return { state: 'corrupt', why: parsed.why };
265
+ return { state: 'held', record: parsed.record };
266
+ }
267
+
268
+ // ═══════════════════════════════════════════════════════════════════════════
269
+ // ATOMIC PUBLISH — the only two ways bytes ever reach a lease file
270
+ // ═══════════════════════════════════════════════════════════════════════════
271
+
272
+ /**
273
+ * Create `target` with `data`, or fail because it already exists. This is the
274
+ * whole mutual-exclusion primitive.
275
+ *
276
+ * ⭐ HARD LINK FIRST, O_EXCL SECOND. `link()` is the classic exclusive create
277
+ * that stays correct on network filesystems where `O_EXCL` historically is not,
278
+ * and it publishes a file that is COMPLETE at the instant it becomes visible —
279
+ * the content is written to the temp name first, so no reader can ever observe
280
+ * a half-written lease. The `O_EXCL` fallback exists for filesystems that
281
+ * refuse links at all, and is still exclusive.
282
+ */
283
+ function exclusiveCreate(target, data, linkImpl, pid) {
284
+ const temp = `${target}.mk-${pid}`;
285
+ try {
286
+ writeFileSync(temp, data, 'utf8');
287
+ } catch (err) {
288
+ return { ok: false, error: `could not stage ${temp}: ${err?.message ?? String(err)}` };
289
+ }
290
+ try {
291
+ linkImpl(temp, target);
292
+ return { ok: true, via: 'link' };
293
+ } catch (err) {
294
+ if (err?.code === 'EEXIST') return { ok: false, taken: true };
295
+ if (!LINK_UNSUPPORTED.has(err?.code)) return { ok: false, error: err?.message ?? String(err) };
296
+ // fall through: this filesystem cannot link, so try an exclusive open
297
+ } finally {
298
+ try { unlinkSync(temp); } catch { /* the link succeeded or never happened */ }
299
+ }
300
+ try {
301
+ writeFileSync(target, data, { encoding: 'utf8', flag: 'wx' });
302
+ return { ok: true, via: 'wx' };
303
+ } catch (err) {
304
+ if (err?.code === 'EEXIST') return { ok: false, taken: true };
305
+ return { ok: false, error: err?.message ?? String(err) };
306
+ }
307
+ }
308
+
309
+ /** Rename, retrying the transient Windows sharing failures. No sleeping. */
310
+ function renameWithRetry(from, to) {
311
+ let last;
312
+ for (let i = 0; i < RENAME_ATTEMPTS; i++) {
313
+ try {
314
+ renameSync(from, to);
315
+ return { ok: true };
316
+ } catch (err) {
317
+ last = err;
318
+ if (!RENAME_RETRYABLE.has(err?.code)) break;
319
+ }
320
+ }
321
+ return { ok: false, error: last?.message ?? String(last) };
322
+ }
323
+
324
+ /**
325
+ * Replace an EXISTING lease record — renew, re-acquire, or take over.
326
+ *
327
+ * ⚠️ THIS IS THE DANGEROUS DIRECTION, so it is serialised. Eight processes that
328
+ * all read the same stale record would all decide "I may take this"; the gate
329
+ * is a shared filename that exactly one of them can create, and the winner
330
+ * re-reads under the gate before committing. The target file is never removed
331
+ * along the way, so a racer can never see the path as momentarily free.
332
+ */
333
+ function replaceRecord(loc, record, pid, linkImpl, confirm) {
334
+ try {
335
+ mkdirSync(loc.dir, { recursive: true });
336
+ } catch (err) {
337
+ return { ok: false, error: `could not create ${loc.dir}: ${err?.message ?? String(err)}` };
338
+ }
339
+ const gate = `${loc.absolute}.claim`;
340
+ const got = exclusiveCreate(gate, `${pid}\n`, linkImpl, pid);
341
+ if (!got.ok) {
342
+ if (got.taken) return { ok: false, busy: true, error: 'another terminal is changing this lease right now' };
343
+ return { ok: false, error: got.error };
344
+ }
345
+ try {
346
+ // ⭐ Re-read UNDER the gate. Between our first look and winning the gate,
347
+ // another terminal may have completed the very takeover we are attempting.
348
+ const verdict = confirm(readLeaseFile(loc.absolute));
349
+ if (!verdict.ok) return verdict;
350
+
351
+ const temp = `${loc.absolute}.tmp-${pid}`;
352
+ try {
353
+ writeFileSync(temp, serialize(record), 'utf8');
354
+ } catch (err) {
355
+ return { ok: false, error: `could not stage the lease: ${err?.message ?? String(err)}` };
356
+ }
357
+ const moved = renameWithRetry(temp, loc.absolute);
358
+ if (!moved.ok) {
359
+ try { unlinkSync(temp); } catch { /* nothing staged */ }
360
+ return { ok: false, error: `could not publish the lease: ${moved.error}` };
361
+ }
362
+ return { ok: true };
363
+ } finally {
364
+ try { unlinkSync(gate); } catch { /* already gone */ }
365
+ }
366
+ }
367
+
368
+ // ═══════════════════════════════════════════════════════════════════════════
369
+ // HANDLES AND REFUSALS
370
+ // ═══════════════════════════════════════════════════════════════════════════
371
+
372
+ function handleFor(root, loc, record) {
373
+ return {
374
+ root: resolve(root),
375
+ path: record.path,
376
+ key: record.key,
377
+ file: loc.absolute,
378
+ dir: loc.dir,
379
+ holder: record.holder,
380
+ pid: record.pid,
381
+ token: record.token,
382
+ ttlMs: record.ttlMs,
383
+ acquiredAt: record.acquiredAt,
384
+ renewedAt: record.renewedAt,
385
+ takeovers: record.takeovers,
386
+ };
387
+ }
388
+
389
+ /**
390
+ * ⚠️ A REFUSAL IS THE PRODUCT HERE, so it has to be readable. Whoever hits this
391
+ * is a person with seven terminals open wondering why one of them stopped, and
392
+ * "lease held" tells them nothing. Name the holder, the pid, the age, and — if
393
+ * it is expired but still protected — say exactly how long the wait is and why.
394
+ */
395
+ function refusal(loc, held, now, holder, pid, warnings) {
396
+ const silentForMs = now - held.renewedAt;
397
+ const reclaimableInMs = Math.max(0, reclaimableAfter(held.ttlMs) - silentForMs);
398
+ const expired = silentForMs > held.ttlMs;
399
+
400
+ let error = `${loc.path} is leased by "${held.holder}" (pid ${held.pid}), taken at ${held.acquiredAt} and last heartbeat ${silentForMs}ms ago`;
401
+ if (expired && reclaimableInMs > 0) {
402
+ error += `; the lease expired ${silentForMs - held.ttlMs}ms ago but cannot be reclaimed for another ${reclaimableInMs}ms — taking it early would hand two live agents the same file`;
403
+ }
404
+ if (held.holder === holder && held.pid !== pid) {
405
+ error += `; the holder name matches yours but the pid does not (${held.pid} vs ${pid}), so that is a second terminal, not you`;
406
+ }
407
+ if (held.path !== loc.path) {
408
+ error += `; the lease is recorded as "${held.path}" and you asked for "${loc.path}" — they differ only in case, and on a case-insensitive filesystem that is ONE file`;
409
+ }
410
+ return {
411
+ ok: false,
412
+ heldBy: held.holder,
413
+ heldByPid: held.pid,
414
+ since: held.acquiredAt,
415
+ path: loc.path,
416
+ state: stateOf(silentForMs, held.ttlMs),
417
+ expired,
418
+ silentForMs,
419
+ reclaimableInMs,
420
+ error,
421
+ warnings,
422
+ };
423
+ }
424
+
425
+ // ═══════════════════════════════════════════════════════════════════════════
426
+ // THE PUBLIC SURFACE
427
+ // ═══════════════════════════════════════════════════════════════════════════
428
+
429
+ /**
430
+ * Take a lease on one path.
431
+ *
432
+ * → `{ ok: true, lease, warnings, reacquired?, takeover? }`
433
+ * → `{ ok: false, heldBy, since, expired, reclaimableInMs, error, warnings }`
434
+ */
435
+ export function acquire(root, options = {}) {
436
+ const opts = options ?? {};
437
+ const { path: rawPath, holder: rawHolder, ttlMs, clock = wallClock, linkImpl = linkSync } = opts;
438
+ const warnings = [];
439
+
440
+ const time = checkClock(clock);
441
+ if (!time.ok) return { ok: false, error: time.error, warnings };
442
+ const now = time.now;
443
+
444
+ const who = checkHolder(rawHolder);
445
+ if (!who.ok) return { ok: false, error: who.error, warnings };
446
+ const holder = who.holder;
447
+
448
+ const loc = leaseFilePath(root, rawPath);
449
+ if (!loc.ok) return { ok: false, error: loc.error, warnings };
450
+
451
+ const pid = normalizePid(opts.pid);
452
+ const ttl = clampTtl(ttlMs);
453
+ const existing = readLeaseFile(loc.absolute);
454
+
455
+ if (existing.state === 'unreadable') {
456
+ return { ok: false, error: `the lease file for ${loc.path} could not be read (${existing.why}) — refusing rather than assuming it is free`, warnings };
457
+ }
458
+
459
+ // ── Somebody (possibly us) already has a readable record ──────────────────
460
+ if (existing.state === 'held') {
461
+ const held = existing.record;
462
+
463
+ // ⭐ RE-ENTRANCY. A terminal that declares the same file twice — or retries
464
+ // after a hiccup — must not deadlock against itself. Same holder AND same
465
+ // pid is the same terminal, so this is a heartbeat that keeps the token.
466
+ if (held.holder === holder && held.pid === pid) {
467
+ const next = { ...held, path: held.path, ttlMs: ttl, renewedAt: now };
468
+ const wrote = replaceRecord(loc, next, pid, linkImpl, (current) => (
469
+ current.state === 'held' && current.record.token === held.token
470
+ ? { ok: true }
471
+ : { ok: false, error: `the lease on ${loc.path} changed hands while re-acquiring it`, warnings }
472
+ ));
473
+ if (!wrote.ok) return { ok: false, error: wrote.error, warnings };
474
+ return { ok: true, reacquired: true, lease: handleFor(root, loc, next), warnings };
475
+ }
476
+
477
+ const silentForMs = now - held.renewedAt;
478
+ if (silentForMs <= reclaimableAfter(held.ttlMs)) {
479
+ return refusal(loc, held, now, holder, pid, warnings);
480
+ }
481
+
482
+ // ── The holder has been silent long past its TTL. Take it, and say so. ──
483
+ const takeover = { from: held.holder, pid: held.pid, at: now, silentForMs, token: held.token };
484
+ const record = {
485
+ version: LEASE_VERSION,
486
+ path: loc.path,
487
+ key: loc.key,
488
+ holder,
489
+ pid,
490
+ token: deriveToken({ path: loc.path, holder, pid, acquiredAt: now }),
491
+ ttlMs: ttl,
492
+ acquiredAt: now,
493
+ renewedAt: now,
494
+ takeovers: [...held.takeovers, takeover].slice(-MAX_TAKEOVERS),
495
+ };
496
+ const wrote = replaceRecord(loc, record, pid, linkImpl, (current) => {
497
+ if (current.state !== 'held') return { ok: false, error: `the lease on ${loc.path} vanished mid-takeover`, warnings };
498
+ if (current.record.token !== held.token) return refusal(loc, current.record, now, holder, pid, warnings);
499
+ if (now - current.record.renewedAt <= reclaimableAfter(current.record.ttlMs)) return refusal(loc, current.record, now, holder, pid, warnings);
500
+ return { ok: true };
501
+ });
502
+ if (!wrote.ok) {
503
+ if (wrote.heldBy) return wrote;
504
+ return { ok: false, heldBy: held.holder, since: held.acquiredAt, error: wrote.error, warnings };
505
+ }
506
+ warnings.push(`took over ${loc.path} from "${held.holder}" (pid ${held.pid}) after ${silentForMs}ms of silence`);
507
+ return { ok: true, takeover, lease: handleFor(root, loc, record), warnings };
508
+ }
509
+
510
+ // ── Corrupt: treat as absent, loudly, and never as a lock ─────────────────
511
+ const record = {
512
+ version: LEASE_VERSION,
513
+ path: loc.path,
514
+ key: loc.key,
515
+ holder,
516
+ pid,
517
+ token: deriveToken({ path: loc.path, holder, pid, acquiredAt: now }),
518
+ ttlMs: ttl,
519
+ acquiredAt: now,
520
+ renewedAt: now,
521
+ takeovers: [],
522
+ };
523
+
524
+ if (existing.state === 'corrupt') {
525
+ warnings.push(`corrupt lease file for ${loc.path} (${existing.why}) — treating it as absent and taking the lease`);
526
+ const wrote = replaceRecord(loc, record, pid, linkImpl, (current) => (
527
+ // ⭐ Still corrupt under the gate? Then it really is debris. If it now
528
+ // parses, a real holder appeared while we looked and it is theirs.
529
+ current.state === 'held'
530
+ ? refusal(loc, current.record, now, holder, pid, warnings)
531
+ : { ok: true }
532
+ ));
533
+ if (!wrote.ok) {
534
+ if (wrote.heldBy) return wrote;
535
+ return { ok: false, error: wrote.error, warnings };
536
+ }
537
+ return { ok: true, lease: handleFor(root, loc, record), warnings };
538
+ }
539
+
540
+ // ── Free: an exclusive create is the whole race ───────────────────────────
541
+ try {
542
+ mkdirSync(loc.dir, { recursive: true });
543
+ } catch (err) {
544
+ return { ok: false, error: `could not create ${loc.dir}: ${err?.message ?? String(err)}`, warnings };
545
+ }
546
+ const made = exclusiveCreate(loc.absolute, serialize(record), linkImpl, pid);
547
+ if (made.ok) return { ok: true, lease: handleFor(root, loc, record), warnings };
548
+ if (!made.taken) return { ok: false, error: made.error, warnings };
549
+
550
+ // We lost the create. Re-read so the refusal names whoever actually won.
551
+ const winner = readLeaseFile(loc.absolute);
552
+ if (winner.state === 'held') return refusal(loc, winner.record, now, holder, pid, warnings);
553
+ return { ok: false, error: `lost a race for ${loc.path} to another terminal`, warnings };
554
+ }
555
+
556
+ /**
557
+ * Heartbeat. → `{ ok: true, lease }` or `{ ok: false, reason, heldBy?, error }`
558
+ * where reason is `gone` | `lost` | `expired` | `corrupt` | `busy` | `invalid`.
559
+ *
560
+ * ⚠️⚠️ THE FAILURES HERE ARE THE POINT. An agent that keeps writing under a
561
+ * lease it no longer holds is the exact corruption this module exists to stop,
562
+ * so a heartbeat that cannot prove ownership must fail loudly instead of
563
+ * quietly re-creating the record.
564
+ */
565
+ export function renew(lease, options = {}) {
566
+ const { clock = wallClock, linkImpl = linkSync } = options ?? {};
567
+
568
+ const time = checkClock(clock);
569
+ if (!time.ok) return { ok: false, reason: 'invalid', error: time.error };
570
+ const now = time.now;
571
+
572
+ if (!lease || typeof lease !== 'object' || typeof lease.file !== 'string' || typeof lease.token !== 'string') {
573
+ return { ok: false, reason: 'invalid', error: 'not a lease handle' };
574
+ }
575
+
576
+ const current = readLeaseFile(lease.file);
577
+ if (current.state === 'absent') {
578
+ return { ok: false, reason: 'gone', error: `the lease on ${lease.path} no longer exists — it was released or cleaned up` };
579
+ }
580
+ if (current.state === 'unreadable') {
581
+ return { ok: false, reason: 'corrupt', error: `the lease file for ${lease.path} could not be read (${current.why})` };
582
+ }
583
+ if (current.state === 'corrupt') {
584
+ return { ok: false, reason: 'corrupt', error: `the lease file for ${lease.path} is corrupt (${current.why}) — re-acquire rather than heartbeat` };
585
+ }
586
+
587
+ const held = current.record;
588
+ if (held.token !== lease.token) {
589
+ return {
590
+ ok: false,
591
+ reason: 'lost',
592
+ heldBy: held.holder,
593
+ heldByPid: held.pid,
594
+ error: `${lease.path} is now leased by "${held.holder}" (pid ${held.pid}) — stop writing to it`,
595
+ };
596
+ }
597
+ if (now - held.renewedAt > held.ttlMs) {
598
+ // ⚠️ Past our own TTL a blind write races whoever is mid-takeover. The
599
+ // honest recovery is one `acquire`, which re-checks under the gate.
600
+ return { ok: false, reason: 'expired', error: `the lease on ${lease.path} lapsed ${now - held.renewedAt - held.ttlMs}ms ago — re-acquire it` };
601
+ }
602
+
603
+ const loc = { absolute: lease.file, dir: lease.dir, path: lease.path };
604
+ const next = { ...held, renewedAt: now };
605
+ const wrote = replaceRecord(loc, next, normalizePid(lease.pid), linkImpl, (under) => (
606
+ under.state === 'held' && under.record.token === lease.token
607
+ ? { ok: true }
608
+ : { ok: false, reason: 'lost', heldBy: under.state === 'held' ? under.record.holder : null, error: `${lease.path} changed hands mid-heartbeat` }
609
+ ));
610
+ if (!wrote.ok) return { ok: false, reason: wrote.reason ?? (wrote.busy ? 'busy' : 'invalid'), heldBy: wrote.heldBy, error: wrote.error };
611
+
612
+ return { ok: true, lease: { ...lease, renewedAt: now, ttlMs: held.ttlMs } };
613
+ }
614
+
615
+ /**
616
+ * Give a lease back. **Always safe** — expired, already released, taken over,
617
+ * a nonsense handle, or a workspace that has been deleted underneath us.
618
+ *
619
+ * ⚠️ NEVER DELETES SOMEBODY ELSE'S LEASE. A terminal that stalled long enough
620
+ * to be taken over would otherwise unlock the file under its new owner on the
621
+ * way out, which is the same corruption arriving by the back door.
622
+ */
623
+ export function release(lease) {
624
+ if (!lease || typeof lease !== 'object' || typeof lease.file !== 'string' || typeof lease.token !== 'string') {
625
+ return { ok: true, released: false, reason: 'not a lease handle' };
626
+ }
627
+ const current = readLeaseFile(lease.file);
628
+ if (current.state === 'absent') return { ok: true, released: false, reason: 'already released' };
629
+ if (current.state === 'unreadable') return { ok: true, released: false, reason: `unreadable (${current.why})` };
630
+ if (current.state === 'corrupt') {
631
+ try { unlinkSync(lease.file); } catch { /* somebody else got there */ }
632
+ return { ok: true, released: true, reason: 'the record was corrupt and has been cleared' };
633
+ }
634
+ if (current.record.token !== lease.token) {
635
+ return { ok: true, released: false, reason: `${lease.path} is now leased by "${current.record.holder}" (pid ${current.record.pid}) — leaving it alone` };
636
+ }
637
+ try {
638
+ unlinkSync(lease.file);
639
+ } catch (err) {
640
+ if (err?.code !== 'ENOENT') return { ok: true, released: false, reason: err?.message ?? String(err) };
641
+ return { ok: true, released: false, reason: 'already released' };
642
+ }
643
+ return { ok: true, released: true };
644
+ }
645
+
646
+ // ═══════════════════════════════════════════════════════════════════════════
647
+ // MANY PATHS AT ONCE
648
+ // ═══════════════════════════════════════════════════════════════════════════
649
+
650
+ /**
651
+ * Take every path or none of them.
652
+ *
653
+ * ⚠️ TWO INVARIANTS AND BOTH ARE ABOUT DEADLOCK. **All-or-nothing**, because a
654
+ * terminal holding three of five files and waiting on the fourth is a stall
655
+ * nobody can diagnose. And a **fixed global order** (case-folded, sorted), so
656
+ * two terminals wanting the same two files always contend in the same
657
+ * direction — lock-ordering deadlock stops being possible rather than becoming
658
+ * unlikely. Everything is validated before a single byte is written.
659
+ */
660
+ export function acquireAll(root, options = {}) {
661
+ const opts = options ?? {};
662
+ const { paths = [], holder, ttlMs, clock = wallClock, linkImpl = linkSync } = opts;
663
+ const warnings = [];
664
+
665
+ if (!Array.isArray(paths)) return { ok: false, error: 'paths must be an array', leases: [], warnings };
666
+
667
+ const located = [];
668
+ for (const raw of paths) {
669
+ const loc = leaseFilePath(root, raw);
670
+ if (!loc.ok) return { ok: false, error: loc.error, path: raw, leases: [], warnings };
671
+ located.push(loc);
672
+ }
673
+
674
+ const ordered = [...new Map(located.map((l) => [l.key, l])).values()]
675
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
676
+
677
+ const taken = [];
678
+ for (const loc of ordered) {
679
+ const got = acquire(root, { path: loc.path, holder, pid: opts.pid, ttlMs, clock, linkImpl });
680
+ if (!got.ok) {
681
+ releaseAll(taken);
682
+ return {
683
+ ok: false,
684
+ error: got.error,
685
+ heldBy: got.heldBy ?? null,
686
+ since: got.since ?? null,
687
+ path: loc.path,
688
+ leases: [],
689
+ warnings: [...warnings, ...(got.warnings ?? [])],
690
+ };
691
+ }
692
+ warnings.push(...got.warnings);
693
+ taken.push(got.lease);
694
+ }
695
+ return { ok: true, leases: taken, warnings };
696
+ }
697
+
698
+ /**
699
+ * Heartbeat a set.
700
+ *
701
+ * ⚠️ REPORTS WHAT IT LOST rather than collapsing to one boolean. "Four of five
702
+ * renewed and here is the one that did not, and who has it now" is actionable;
703
+ * `false` is not.
704
+ */
705
+ export function renewAll(leases = [], options = {}) {
706
+ const renewed = [];
707
+ const lost = [];
708
+ for (const lease of Array.isArray(leases) ? leases : []) {
709
+ const beat = renew(lease, options);
710
+ if (beat.ok) renewed.push(beat.lease);
711
+ else lost.push({ path: lease?.path ?? null, reason: beat.reason, heldBy: beat.heldBy ?? null, error: beat.error });
712
+ }
713
+ return { ok: lost.length === 0, renewed: renewed.length, leases: renewed, lost };
714
+ }
715
+
716
+ /** Give a set back. Never throws, whatever is in the array. */
717
+ export function releaseAll(leases = []) {
718
+ const results = [];
719
+ let released = 0;
720
+ for (const lease of Array.isArray(leases) ? leases : []) {
721
+ const one = release(lease);
722
+ if (one.released) released += 1;
723
+ results.push({ path: lease?.path ?? null, ...one });
724
+ }
725
+ return { ok: true, released, results };
726
+ }
727
+
728
+ // ═══════════════════════════════════════════════════════════════════════════
729
+ // INSPECT — who holds what, and is it stale
730
+ // ═══════════════════════════════════════════════════════════════════════════
731
+
732
+ /**
733
+ * Read every lease in a workspace. **Never throws and never fails on debris** —
734
+ * this is the call a stuck human makes to find out why their terminal stopped,
735
+ * so it has to work on exactly the broken workspace they are staring at.
736
+ */
737
+ export function inspect(root, options = {}) {
738
+ const { clock = wallClock } = options ?? {};
739
+ const time = checkClock(clock);
740
+ if (!time.ok) return { ok: false, error: time.error, leases: [], warnings: [], corrupt: 0 };
741
+ const now = time.now;
742
+
743
+ if (typeof root !== 'string' || !root.trim()) {
744
+ return { ok: false, error: 'a workspace root is required', leases: [], warnings: [], corrupt: 0 };
745
+ }
746
+ const dir = join(resolve(root), ...LEASE_DIR.split('/'));
747
+
748
+ let names;
749
+ try {
750
+ names = readdirSync(dir);
751
+ } catch (err) {
752
+ if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') return { ok: true, dir, leases: [], warnings: [], corrupt: 0 };
753
+ return { ok: false, error: `could not read ${dir}: ${err?.message ?? String(err)}`, leases: [], warnings: [], corrupt: 0 };
754
+ }
755
+
756
+ const leases = [];
757
+ const warnings = [];
758
+ let corrupt = 0;
759
+
760
+ for (const name of names.sort()) {
761
+ // ⚠️ Only records. A `.claim` gate or a `.tmp-` staging file is another
762
+ // terminal mid-write, not debris, and reporting it would be noise.
763
+ if (!name.endsWith('.json')) continue;
764
+ const found = readLeaseFile(join(dir, name));
765
+ if (found.state === 'held') {
766
+ const rec = found.record;
767
+ const silentForMs = now - rec.renewedAt;
768
+ leases.push({
769
+ path: rec.path,
770
+ holder: rec.holder,
771
+ pid: rec.pid,
772
+ token: rec.token,
773
+ state: stateOf(silentForMs, rec.ttlMs),
774
+ ttlMs: rec.ttlMs,
775
+ acquiredAt: rec.acquiredAt,
776
+ renewedAt: rec.renewedAt,
777
+ ageMs: now - rec.acquiredAt,
778
+ silentForMs,
779
+ reclaimableInMs: Math.max(0, reclaimableAfter(rec.ttlMs) - silentForMs),
780
+ takeovers: rec.takeovers,
781
+ file: join(dir, name),
782
+ });
783
+ } else if (found.state === 'corrupt' || found.state === 'unreadable') {
784
+ corrupt += 1;
785
+ warnings.push(`corrupt lease file ${name} (${found.why}) — it holds nothing and can be deleted`);
786
+ }
787
+ }
788
+
789
+ leases.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
790
+ return { ok: true, dir, leases, warnings, corrupt };
791
+ }
792
+
793
+ /** Render `inspect()` for a terminal. Returns lines; never throws. */
794
+ export function formatLeaseSummary(view) {
795
+ if (!view || typeof view !== 'object') return ['no lease information available'];
796
+ if (view.ok === false) return [`could not read leases: ${view.error ?? 'unknown error'}`];
797
+
798
+ const leases = Array.isArray(view.leases) ? view.leases : [];
799
+ const lines = [];
800
+ if (leases.length === 0) {
801
+ lines.push('no file leases are held in this workspace');
802
+ } else {
803
+ lines.push(`${leases.length} file lease${leases.length === 1 ? '' : 's'}:`);
804
+ for (const l of leases) {
805
+ const mark = l.state === 'live' ? '·' : l.state === 'expired' ? '!' : 'x';
806
+ let line = ` ${mark} ${l.path} — ${l.holder} (pid ${l.pid}) ${l.state}, silent ${Math.round(l.silentForMs / 1000)}s`;
807
+ if (l.state === 'expired') line += `, reclaimable in ${Math.round(l.reclaimableInMs / 1000)}s`;
808
+ if (Array.isArray(l.takeovers) && l.takeovers.length) {
809
+ const last = l.takeovers[l.takeovers.length - 1];
810
+ line += ` — took over from ${last.from} (pid ${last.pid})`;
811
+ }
812
+ lines.push(line);
813
+ }
814
+ }
815
+ for (const w of Array.isArray(view.warnings) ? view.warnings : []) lines.push(` ! ${w}`);
816
+ return lines;
817
+ }
818
+
819
+ /**
820
+ * ⭐ THE WIRING, WRITTEN DOWN. This package's repeated failure is a finished
821
+ * module that nothing imports, so the exact lines live next to the code.
822
+ */
823
+ export const REGISTRATION_SNIPPET = `
824
+ // ── WIRING lib/lease.mjs INTO bin/acuvo.mjs ────────────────────────────────
825
+ // The names below already exist in that file: 'root' (line ~281, the resolved
826
+ // workspace), 'opts' (parsed argv), EXIT_OK / EXIT_FAILED (lines 66-67).
827
+
828
+ // 1. one import, beside the parallel.mjs import at the top (line ~34)
829
+ import { acquireAll, renewAll, releaseAll, inspect, formatLeaseSummary } from '../lib/lease.mjs';
830
+
831
+ // 2. a read-only command, before any model work happens.
832
+ // lib/cli-args.mjs must accept 'leases' as a command.
833
+ if (opts.command === 'leases') {
834
+ process.stdout.write(formatLeaseSummary(inspect(root)).join('\\n') + '\\n');
835
+ return EXIT_OK;
836
+ }
837
+
838
+ // 3. take the files this run intends to write, BEFORE the session starts.
839
+ // 'opts.lease' is a new repeatable flag: --lease src/a.ts --lease src/b.ts
840
+ const held = acquireAll(root, {
841
+ paths: opts.lease ?? [],
842
+ holder: opts.holder ?? String(process.pid),
843
+ ttlMs: 120000,
844
+ });
845
+ if (!held.ok) {
846
+ process.stderr.write(held.error + '\\n');
847
+ return EXIT_FAILED;
848
+ }
849
+ held.warnings.forEach((w) => process.stderr.write(' ! ' + w + '\\n'));
850
+ process.on('exit', () => releaseAll(held.leases));
851
+
852
+ // 4. heartbeat between rounds — call this from the runSession onEvent hook.
853
+ // STOP on a lost lease: continuing means writing over another terminal.
854
+ const beat = renewAll(held.leases);
855
+ if (!beat.ok) {
856
+ process.stderr.write('lost a lease mid-run: ' + JSON.stringify(beat.lost) + '\\n');
857
+ return EXIT_FAILED;
858
+ }
859
+
860
+ // ⚠️ THE HONEST LIMIT OF STEP 3: a coding agent does not know which files it
861
+ // will write until it writes them, so an up-front --lease list is a
862
+ // DECLARATION, not a guarantee. The complete fix is to call acquire() inside
863
+ // the executor's write path (lib/workspace.mjs) so every mutation is covered
864
+ // automatically. That is one more call, in one more lane, and this module is
865
+ // ready for it:
866
+ // const got = acquire(root, { path: relPath, holder, ttlMs: 120000 });
867
+ // if (!got.ok) return { ok: false, error: got.error };
868
+ `.trim();