toolroll 0.9.7 → 0.9.9

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 (97) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/dist/assignment-adapters.d.ts +1 -0
  3. package/dist/assignment-adapters.js +2 -0
  4. package/dist/assignment-brief.d.ts +2 -0
  5. package/dist/assignment-brief.js +2 -1
  6. package/dist/assignment-presentation.js +3 -1
  7. package/dist/assignment-status.d.ts +1 -1
  8. package/dist/assignment-ui.js +5 -0
  9. package/dist/assignment.d.ts +15 -0
  10. package/dist/assignment.js +56 -9
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +18 -18
  13. package/dist/browser-crew.js +4 -0
  14. package/dist/browser-workspace.d.ts +40 -0
  15. package/dist/build-review.d.ts +38 -0
  16. package/dist/build-review.js +341 -0
  17. package/dist/builder.d.ts +2 -0
  18. package/dist/builder.js +114 -8
  19. package/dist/chat-controls.d.ts +1 -1
  20. package/dist/chat-controls.js +1 -1
  21. package/dist/check-levels-ui.d.ts +5 -0
  22. package/dist/check-levels-ui.js +16 -2
  23. package/dist/check-progress.js +4 -3
  24. package/dist/checkout-cleanup.d.ts +40 -2
  25. package/dist/checkout-cleanup.js +129 -9
  26. package/dist/cli.js +2 -0
  27. package/dist/desktop-update.d.ts +52 -0
  28. package/dist/desktop-update.js +147 -5
  29. package/dist/dispatch.js +2 -1
  30. package/dist/dispose.d.ts +4 -3
  31. package/dist/dispose.js +12 -4
  32. package/dist/evidence.d.ts +6 -0
  33. package/dist/evidence.js +8 -0
  34. package/dist/exec.d.ts +5 -0
  35. package/dist/exec.js +30 -4
  36. package/dist/first-run.d.ts +1 -1
  37. package/dist/first-run.js +8 -6
  38. package/dist/invoke.js +5 -0
  39. package/dist/lead-context.d.ts +1 -1
  40. package/dist/lead-context.js +4 -2
  41. package/dist/lead-status.d.ts +10 -1
  42. package/dist/lead-status.js +21 -24
  43. package/dist/modes.d.ts +3 -1
  44. package/dist/modes.js +3 -1
  45. package/dist/needs-you.d.ts +55 -0
  46. package/dist/needs-you.js +83 -0
  47. package/dist/operate.d.ts +2 -0
  48. package/dist/operate.js +231 -36
  49. package/dist/plane-review.js +4 -0
  50. package/dist/process-custody.js +8 -0
  51. package/dist/process-recovery-eligibility.d.ts +1 -0
  52. package/dist/project-learning.d.ts +3 -3
  53. package/dist/project-learning.js +28 -17
  54. package/dist/proof.d.ts +3 -1
  55. package/dist/proof.js +4 -2
  56. package/dist/provider.js +16 -0
  57. package/dist/result-completion.d.ts +4 -0
  58. package/dist/result-completion.js +11 -0
  59. package/dist/retention-ui.d.ts +3 -1
  60. package/dist/retention-ui.js +4 -4
  61. package/dist/retention.d.ts +13 -6
  62. package/dist/retention.js +50 -12
  63. package/dist/review-context.d.ts +3 -0
  64. package/dist/review-context.js +45 -5
  65. package/dist/review-switch.d.ts +41 -0
  66. package/dist/review-switch.js +88 -0
  67. package/dist/reviewer.d.ts +24 -2
  68. package/dist/reviewer.js +49 -1
  69. package/dist/run-closeout.d.ts +89 -0
  70. package/dist/run-closeout.js +302 -0
  71. package/dist/serve.d.ts +15 -1
  72. package/dist/serve.js +158 -18
  73. package/dist/setup-guide.d.ts +2 -2
  74. package/dist/shared-deps.d.ts +94 -0
  75. package/dist/shared-deps.js +365 -0
  76. package/dist/storage.js +2 -0
  77. package/dist/store.d.ts +72 -1
  78. package/dist/store.js +258 -31
  79. package/dist/surface.js +5 -1
  80. package/dist/task-status.d.ts +22 -5
  81. package/dist/task-status.js +45 -24
  82. package/dist/telegram-progress.js +2 -1
  83. package/dist/telegram-status.js +3 -3
  84. package/dist/test-temp.d.ts +19 -0
  85. package/dist/test-temp.js +98 -0
  86. package/dist/toolroll-update-ui.js +7 -2
  87. package/dist/toolroll-update.d.ts +21 -1
  88. package/dist/toolroll-update.js +135 -5
  89. package/dist/work-index.d.ts +11 -0
  90. package/dist/work-index.js +103 -16
  91. package/dist/work-summary.d.ts +3 -1
  92. package/dist/work-summary.js +2 -1
  93. package/dist/workspace-ui.d.ts +3 -1
  94. package/dist/workspace-ui.js +10 -9
  95. package/dist/worktree.d.ts +16 -0
  96. package/dist/worktree.js +47 -0
  97. package/package.json +1 -1
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Shared dependencies: one installed node_modules per (project, lockfile, setup, node) under the state folder
3
+ * (`deps/<key>`), linked into each new checkout instead of a fresh `npm ci` there. Measured Oct 2: a checkout of
4
+ * Toolroll is 20,500 files and 18,066 of them were its own node_modules; forty checkouts created and deleted ~800k
5
+ * files, most of the churn behind a bloated fseventsd, and each install cost time.
6
+ *
7
+ * The first checkout with a new key runs the approved setup itself; its node_modules then moves (one rename, same
8
+ * volume) into `deps/<key>`, is made read-only, and is linked back. Later checkouts link it: symlinks, else an
9
+ * APFS/reflink clone, else the plain setup as before. A linked node_modules is a real folder holding one symlink per
10
+ * top-level entry (a few hundred files, not tens of thousands), so caches tools write there stay the checkout's own.
11
+ * Only a setup that installs and does nothing else is shared (`npm ci`, `npm install`): skipping any other command
12
+ * would skip its other effects.
13
+ *
14
+ * The link is read-only to the build. A checkout whose package.json or lockfile no longer matches the copy it links
15
+ * gets its own install (the link goes, the approved setup runs there) and the shared copy is never touched; a copy
16
+ * whose installed tree changed anyway is retired, never linked again. `toolroll storage clean` removes a copy no
17
+ * checkout links or matches.
18
+ */
19
+ import { createHash, randomBytes } from "node:crypto";
20
+ import { spawnSync } from "node:child_process";
21
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, utimesSync, writeFileSync } from "node:fs";
22
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
23
+ export const DEPS_FOLDER = "deps";
24
+ const READY = "ready.json";
25
+ const RETIRED = "retired";
26
+ /** In a linked node_modules: the shared copy it links. */
27
+ const LINK_NOTE = ".toolroll-shared";
28
+ const NODE_MODULES = "node_modules";
29
+ /** A copy linked or made this recently is never removed by a clean-up: a lease may be about to link it. */
30
+ export const SHARED_GRACE_MS = 60 * 60_000;
31
+ /** A half-made copy (a crash mid-promotion) is debris after this long. */
32
+ const PARTIAL_STALE_MS = 24 * 60 * 60_000;
33
+ export function sharedDepsRoot(databaseFile) {
34
+ return join(dirname(databaseFile), DEPS_FOLDER);
35
+ }
36
+ /** A setup whose whole effect is node_modules: `npm ci` or `npm install`, with plain flags only. */
37
+ export function installsOnly(command) {
38
+ return /^\s*npm\s+(?:ci|install|i)(?:\s+--?[A-Za-z][\w-]*(?:=[\w.@/:+-]+)?)*\s*$/.test(command);
39
+ }
40
+ /**
41
+ * The checkout's dependency identity: its npm lockfile and package.json, hashed together. null when there is no
42
+ * lockfile, or the lockfile links local folders (workspaces, `file:`), whose links would point into the shared
43
+ * folder instead of the checkout.
44
+ */
45
+ export function lockDigest(checkout) {
46
+ let lock = null;
47
+ for (const name of ["npm-shrinkwrap.json", "package-lock.json"]) {
48
+ try {
49
+ lock = readFileSync(join(checkout, name));
50
+ break;
51
+ }
52
+ catch {
53
+ lock = null;
54
+ }
55
+ }
56
+ if (lock === null)
57
+ return null;
58
+ let manifest;
59
+ try {
60
+ manifest = readFileSync(join(checkout, "package.json"));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (/"link"\s*:\s*true/.test(lock.toString("utf8")))
66
+ return null;
67
+ return createHash("sha256").update(lock).update("\u0000").update(manifest).digest("hex");
68
+ }
69
+ const nodeSeen = new Map();
70
+ /** The node a setup on this PATH would install with: version, platform and architecture (native modules differ). */
71
+ export function nodeIdentity(path = process.env["PATH"] ?? "") {
72
+ if (nodeSeen.has(path))
73
+ return nodeSeen.get(path);
74
+ const found = spawnSync("node", ["-p", "process.version + ' ' + process.platform + ' ' + process.arch"], {
75
+ encoding: "utf8", timeout: 15_000, env: { PATH: path, HOME: process.env["HOME"] ?? "" }, shell: process.platform === "win32",
76
+ });
77
+ const identity = found.status === 0 && /^v\d+\.\d+\.\d+ \S+ \S+$/.test(found.stdout.trim()) ? found.stdout.trim() : null;
78
+ nodeSeen.set(path, identity);
79
+ return identity;
80
+ }
81
+ export function depsKey(repo, setupDigest, lock, node) {
82
+ return createHash("sha256").update([repo, setupDigest, lock, node].join("\u0000"), "utf8").digest("hex").slice(0, 24);
83
+ }
84
+ /** What a checkout would share: its key and the facts behind it, or null when it can't share. */
85
+ export function sharingFor(args) {
86
+ if (!installsOnly(args.setup.command))
87
+ return null;
88
+ const lock = lockDigest(args.worktree);
89
+ if (lock === null)
90
+ return null;
91
+ const node = nodeIdentity();
92
+ if (node === null)
93
+ return null;
94
+ return { key: depsKey(args.repo, args.setup.digest, lock, node), lock, node };
95
+ }
96
+ function readReady(dir) {
97
+ try {
98
+ const parsed = JSON.parse(readFileSync(join(dir, READY), "utf8"));
99
+ return parsed.version === 1 && typeof parsed.key === "string" && typeof parsed.lock === "string" ? parsed : null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /** npm's record of what it installed: changes when anything installs or removes a package in the tree. */
106
+ function treeDigest(nodeModules) {
107
+ try {
108
+ return createHash("sha256").update(readFileSync(join(nodeModules, ".package-lock.json"))).digest("hex");
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /** The shared node_modules for a key, when one is ready and unchanged since it was made. A changed one is retired. */
115
+ export function readyCopy(root, key) {
116
+ const dir = join(root, key);
117
+ const ready = readReady(dir);
118
+ if (ready === null || ready.key !== key || existsSync(join(dir, RETIRED)) || !existsSync(join(dir, NODE_MODULES)))
119
+ return null;
120
+ if (ready.tree !== treeDigest(join(dir, NODE_MODULES))) {
121
+ retire(dir);
122
+ return null;
123
+ }
124
+ return join(dir, NODE_MODULES);
125
+ }
126
+ /**
127
+ * Take a changed copy out of use. It stays where it is, so a checkout still linking it keeps working; no new
128
+ * checkout links it, and a clean-up removes it once nothing links it.
129
+ */
130
+ function retire(dir) {
131
+ try {
132
+ writeFileSync(join(dir, RETIRED), `${new Date().toISOString()}\n`, "utf8");
133
+ }
134
+ catch { /* the next look retires it */ }
135
+ }
136
+ /**
137
+ * The shared copy a checkout's node_modules links, or null (none, or its own install). A linked node_modules is a
138
+ * small real folder: one link per top-level entry of the shared copy and a note naming it. Tools that write a cache
139
+ * there (`node_modules/.vite`, `.cache`) write into the checkout's own folder, never the shared copy.
140
+ */
141
+ export function linkedKey(worktree, root) {
142
+ let target;
143
+ try {
144
+ target = resolve(readFileSync(join(worktree, NODE_MODULES, LINK_NOTE), "utf8").trim());
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ const inside = relative(resolve(root), target);
150
+ if (inside === "" || inside.startsWith("..") || isAbsolute(inside))
151
+ return null;
152
+ const [key, folder, ...rest] = inside.split(/[\\/]/);
153
+ return key !== undefined && folder === NODE_MODULES && rest.length === 0 ? key : null;
154
+ }
155
+ /** Remove a checkout's linked node_modules (its links and caches, never what they point to). True when there was one. */
156
+ export function detachShared(worktree, root) {
157
+ if (linkedKey(worktree, root) === null)
158
+ return false;
159
+ // rm removes a link itself; it never follows one into the shared copy.
160
+ rmSync(join(worktree, NODE_MODULES), { recursive: true, force: true });
161
+ return true;
162
+ }
163
+ function writable(path, on) {
164
+ if (process.platform === "win32")
165
+ return;
166
+ spawnSync("chmod", ["-R", on ? "u+w" : "a-w", path], { timeout: 120_000 });
167
+ }
168
+ /**
169
+ * Put a shared copy into a checkout that has no node_modules: links (a junction per folder on Windows), else a
170
+ * copy-on-write clone the build owns. null: neither worked, and the caller runs the plain setup.
171
+ */
172
+ export function linkInto(worktree, shared) {
173
+ const at = join(worktree, NODE_MODULES);
174
+ if (existsSync(at) || isLink(at))
175
+ return null;
176
+ try {
177
+ mkdirSync(at);
178
+ for (const entry of readdirSync(shared, { withFileTypes: true })) {
179
+ symlinkSync(join(shared, entry.name), join(at, entry.name), entry.isDirectory() ? (process.platform === "win32" ? "junction" : "dir") : "file");
180
+ }
181
+ writeFileSync(join(at, LINK_NOTE), `${shared}\n`, "utf8");
182
+ touch(dirname(shared));
183
+ return "link";
184
+ }
185
+ catch {
186
+ rmSync(at, { recursive: true, force: true });
187
+ }
188
+ const cloned = process.platform === "darwin" ? spawnSync("cp", ["-c", "-R", shared, at], { timeout: 600_000 })
189
+ : process.platform === "linux" ? spawnSync("cp", ["--reflink=always", "-R", shared, at], { timeout: 600_000 })
190
+ : null;
191
+ if (cloned !== null && cloned.status === 0) {
192
+ writable(at, true);
193
+ touch(dirname(shared));
194
+ return "clone";
195
+ }
196
+ try {
197
+ writable(at, true);
198
+ rmSync(at, { recursive: true, force: true });
199
+ }
200
+ catch { /* a partial clone stays for a person */ }
201
+ return null;
202
+ }
203
+ function isLink(path) {
204
+ try {
205
+ return lstatSync(path).isSymbolicLink();
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ function touch(dir) {
212
+ const now = new Date();
213
+ try {
214
+ utimesSync(join(dir, READY), now, now);
215
+ }
216
+ catch { /* the age only guards a clean-up */ }
217
+ }
218
+ /**
219
+ * After the approved setup succeeded in a checkout with no copy for its key: move its node_modules into
220
+ * `deps/<key>` (one rename), make it read-only and link it back. Returns how the checkout now has it, or null when it
221
+ * kept its own install (a different volume, a setup that rewrote the lockfile, a race lost cleanly).
222
+ */
223
+ export function promoteInstall(args) {
224
+ const own = join(args.worktree, NODE_MODULES);
225
+ try {
226
+ if (!lstatSync(own).isDirectory())
227
+ return null;
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ if (lockDigest(args.worktree) !== args.lock)
233
+ return null;
234
+ const partial = join(args.root, `${args.key}.partial-${process.pid}-${randomBytes(4).toString("hex")}`);
235
+ try {
236
+ mkdirSync(partial, { recursive: true });
237
+ renameSync(own, join(partial, NODE_MODULES));
238
+ }
239
+ catch {
240
+ rmSync(partial, { recursive: true, force: true });
241
+ return null;
242
+ }
243
+ const ready = { version: 1, repo: args.repo, key: args.key, lock: args.lock, node: args.node, setup: args.setupDigest, tree: treeDigest(join(partial, NODE_MODULES)), createdAt: args.now.toISOString() };
244
+ writeFileSync(join(partial, READY), `${JSON.stringify(ready)}\n`, "utf8");
245
+ writable(join(partial, NODE_MODULES), false);
246
+ let shared = null;
247
+ try {
248
+ renameSync(partial, join(args.root, args.key));
249
+ shared = join(args.root, args.key, NODE_MODULES);
250
+ }
251
+ catch {
252
+ // Another checkout made this copy first: use theirs.
253
+ shared = readyCopy(args.root, args.key);
254
+ }
255
+ const linked = shared === null ? null : linkInto(args.worktree, shared);
256
+ if (linked === null && !existsSync(partial) && shared === join(args.root, args.key, NODE_MODULES) && !existsSync(own)) {
257
+ // Ours became the shared copy but couldn't be linked: it goes back to being this checkout's own install.
258
+ writable(shared, true);
259
+ try {
260
+ renameSync(shared, own);
261
+ rmSync(join(args.root, args.key), { recursive: true, force: true });
262
+ }
263
+ catch { /* left for a clean-up */ }
264
+ return null;
265
+ }
266
+ if (existsSync(partial)) {
267
+ // Lost the race: ours goes back if nothing else could be linked, else it's removed.
268
+ if (linked === null && !existsSync(own)) {
269
+ writable(join(partial, NODE_MODULES), true);
270
+ try {
271
+ renameSync(join(partial, NODE_MODULES), own);
272
+ }
273
+ catch { /* left for a clean-up */ }
274
+ }
275
+ removeTree(partial);
276
+ }
277
+ return linked;
278
+ }
279
+ function removeTree(path) {
280
+ writable(path, true);
281
+ rmSync(path, { recursive: true, force: true });
282
+ }
283
+ /** Every shared copy on disk, newest first. */
284
+ export function sharedCopies(root) {
285
+ let names = [];
286
+ try {
287
+ names = readdirSync(root);
288
+ }
289
+ catch {
290
+ names = [];
291
+ }
292
+ const copies = [];
293
+ for (const name of names) {
294
+ if (!/^[0-9a-f]{24}$/.test(name))
295
+ continue;
296
+ const ready = readReady(join(root, name));
297
+ if (ready === null)
298
+ continue;
299
+ let usedAt = ready.createdAt;
300
+ try {
301
+ usedAt = statSync(join(root, name, READY)).mtime.toISOString();
302
+ }
303
+ catch { /* createdAt stands */ }
304
+ copies.push({ key: name, dir: join(root, name), repo: ready.repo, lock: ready.lock, node: ready.node, createdAt: ready.createdAt, usedAt, retired: existsSync(join(root, name, RETIRED)) });
305
+ }
306
+ return copies.sort((a, b) => b.usedAt.localeCompare(a.usedAt));
307
+ }
308
+ /** Which checkouts use each copy: those linking it, and those whose lockfile it was installed from. */
309
+ export function sharedUse(root, checkouts) {
310
+ const copies = sharedCopies(root);
311
+ const users = new Map(copies.map(one => [one.key, new Set()]));
312
+ for (const checkout of checkouts) {
313
+ const linked = linkedKey(checkout.path, root);
314
+ if (linked !== null) {
315
+ users.get(linked)?.add(checkout.path);
316
+ continue;
317
+ }
318
+ if (!existsSync(checkout.path))
319
+ continue;
320
+ const lock = lockDigest(checkout.path);
321
+ if (lock === null)
322
+ continue;
323
+ for (const copy of copies)
324
+ if (!copy.retired && copy.repo === checkout.repo && copy.lock === lock)
325
+ users.get(copy.key).add(checkout.path);
326
+ }
327
+ return copies.map(one => ({ ...one, checkouts: [...users.get(one.key)].sort() }));
328
+ }
329
+ /** Half-made copies under deps/ (a crash mid-promotion), old enough that nothing is still making them. */
330
+ export function sharedDebris(root, now) {
331
+ let names = [];
332
+ try {
333
+ names = readdirSync(root);
334
+ }
335
+ catch {
336
+ return [];
337
+ }
338
+ return names.filter(name => {
339
+ if (!/\.partial-/.test(name))
340
+ return false;
341
+ try {
342
+ return now.getTime() - statSync(join(root, name)).mtime.getTime() > PARTIAL_STALE_MS;
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ }).map(name => join(root, name));
348
+ }
349
+ /** What `storage clean` removes: copies no checkout uses and none linked within the grace period, plus debris. */
350
+ export function unusedShared(root, checkouts, now) {
351
+ const copies = sharedUse(root, checkouts).filter(one => one.checkouts.length === 0 && now.getTime() - Date.parse(one.usedAt) > SHARED_GRACE_MS);
352
+ return { copies, debris: sharedDebris(root, now) };
353
+ }
354
+ /** Remove one copy or debris folder (read-only on disk, so made writable first). */
355
+ export function removeShared(path) {
356
+ if (basename(dirname(path)) !== DEPS_FOLDER)
357
+ return false;
358
+ try {
359
+ removeTree(path);
360
+ return !existsSync(path);
361
+ }
362
+ catch {
363
+ return false;
364
+ }
365
+ }
package/dist/storage.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { lstatSync, readdirSync } from "node:fs";
10
10
  import { basename, dirname, join } from "node:path";
11
+ import { DEPS_FOLDER, sharedCopies } from "./shared-deps.js";
11
12
  /** Settings → Storage: when a finished task's clean checkout is removed. One row, or none (the default). */
12
13
  export const CHECKOUT_CLEANUP_SCHEMA = `
13
14
  CREATE TABLE IF NOT EXISTS checkout_cleanup_setting (
@@ -123,6 +124,7 @@ export function storageReport(store, databaseFile, checkoutNote) {
123
124
  const lines = [
124
125
  { what: "Database", bytes: take(name => name.startsWith(base)) },
125
126
  { what: "Build checkouts", bytes: take(name => name === "worktrees"), count: checkouts.length, ...(checkoutNote === undefined ? {} : { note: checkoutNote }) },
127
+ { what: "Shared dependencies", bytes: take(name => name === DEPS_FOLDER), count: sharedCopies(join(folder, DEPS_FOLDER)).length, note: "one install per lockfile, linked into checkouts" },
126
128
  { what: "Staged releases", bytes: take(name => name === "staged-upgrades"), count: staged, note: "a deploy keeps the release it installed, the one before and the newest few" },
127
129
  { what: "Evidence", bytes: take(name => name === "evidence"), note: "the audit record of every run; kept" },
128
130
  { what: "Backups", bytes: take(name => name === "backups") },
package/dist/store.d.ts CHANGED
@@ -15,12 +15,22 @@ import { type ProjectAccess } from "./project-access.js";
15
15
  import { type LedgerEntry } from "./action-ledger.js";
16
16
  import type { LimitReading, LimitWindow } from "./provider-limits.js";
17
17
  import { underCeiling, type OrgPolicy, type SavedPolicy } from "./policy.js";
18
+ import { type ReviewSwitch } from "./review-switch.js";
18
19
  import { type Billing, type Budget, type BudgetAgent, type BudgetHold, type BudgetScope, type BudgetState, type SpendItem } from "./spend.js";
19
20
  import { type RetentionKind, type RetentionPeriods } from "./retention.js";
20
21
  import { type CheckoutCleanup } from "./storage.js";
21
22
  import { type LedgerChainReport } from "./ledger-chain.js";
22
23
  import { type ApprovalGate, type ApprovalRules, type ApproverKind, type Filer } from "./approval-policy.js";
24
+ /** What stops a finished run reading stopped (Store.stopQuiescenceFact). */
25
+ export type StopFactKind = "open" | "alive" | "elsewhere" | "unprovable" | "unknown";
23
26
  export type TaskAct = "filed" | "approved" | "cancelled" | "completed" | "asked";
27
+ /** Store.stopQuiescenceFact, read safely: a deploy proves completion with this code over the INSTALLED runtime's store,
28
+ * which may predate the method. There the older stopQuiescenceProblem decides, and its words give the kind. */
29
+ export declare function stopFactOf(store: Pick<Store, "stopQuiescenceProblem"> & Partial<Pick<Store, "stopQuiescenceFact">>, runId: number): {
30
+ run: number;
31
+ kind: StopFactKind;
32
+ problem: string;
33
+ } | null;
24
34
  export type TaskActRow = {
25
35
  act: TaskAct;
26
36
  account: string;
@@ -2419,6 +2429,14 @@ export declare class Store {
2419
2429
  };
2420
2430
  /** v99: a policy change, as the ledger keeps it: what changed and from what to what. Nothing is kept when nothing changed. */
2421
2431
  private recordPolicy;
2432
+ /** The project's automatic review switch: its explicit setting, else on
2433
+ * while a hands-off mode is active on it, else off. */
2434
+ reviewSwitch(repo: string, now: Date): ReviewSwitch;
2435
+ /** Turn a project's automatic review on or off; the policy log keeps before → after. */
2436
+ setReviewSwitch(repo: string, on: boolean, by: string, now: Date): {
2437
+ before: ReviewSwitch;
2438
+ after: ReviewSwitch;
2439
+ };
2422
2440
  /** A project deleted (project-delete.ts): who, and what it held → how it went. Returns the entry's id. */
2423
2441
  recordProjectDeleted(by: string, repo: string, held: string, how: string, now: Date): number;
2424
2442
  recordAction(entry: Omit<LedgerEntry, "id" | "detail"> & {
@@ -2702,6 +2720,8 @@ export declare class Store {
2702
2720
  clearWorktreeSetup(repo: string, by: string, now: Date): boolean;
2703
2721
  /** The cache stamp: this checkout completed this setup. Success-only by contract. */
2704
2722
  stampWorktreeSetup(path: string, digest: string): void;
2723
+ /** The checkout no longer holds what its setup made (its dependencies were dropped): the next lease runs setup again. */
2724
+ forgetWorktreeSetup(path: string): void;
2705
2725
  /**
2706
2726
  * Approve one repo's verification command, revoking any predecessor in
2707
2727
  * the same transaction — exactly one live command per repo, enforced by
@@ -3703,6 +3723,10 @@ export declare class Store {
3703
3723
  lastBuilderRun(taskRef: number): Run | null;
3704
3724
  /** How many finished builder attempts of this task ended at `head`. */
3705
3725
  builderAttemptsAt(taskRef: number, head: string): number;
3726
+ /** Why a finished result can't be built again on the same filing: it was
3727
+ * verified, attested or accepted, or its branch was published (revised
3728
+ * through its pull request). Null when `requeueTask` may run it again. */
3729
+ finalResultReason(runId: number): "accepted-result" | "published" | null;
3706
3730
  requeueTask(taskId: string, by: string, now: Date): {
3707
3731
  ok: true;
3708
3732
  resolvedIncidents: number;
@@ -5101,8 +5125,10 @@ export declare class Store {
5101
5125
  hardStop: boolean;
5102
5126
  }, by: string, now: Date): Budget;
5103
5127
  removeBudget(id: number, by: string, now: Date): boolean;
5104
- /** How long each kind of data is kept; null is forever (every kind, until someone chooses). */
5128
+ /** How long each kind of data is kept; null is forever. A kind nobody chose keeps its default (evidence 28 days, the rest forever). */
5105
5129
  retentionPeriods(): RetentionPeriods;
5130
+ /** The periods someone chose: only kinds with a saved row. */
5131
+ retentionChosen(): Partial<RetentionPeriods>;
5106
5132
  /** Set how long one kind is kept (null: forever); the ledger keeps before → after. */
5107
5133
  setRetentionPeriod(kind: RetentionKind, days: number | null, by: string, now: Date): RetentionPeriods;
5108
5134
  /** Settings → Storage: when a finished task's clean checkout is removed (the default until someone chooses). */
@@ -7067,6 +7093,21 @@ export declare class Store {
7067
7093
  /** Every owned run must have ended, and every retained local process
7068
7094
  * witness must be absent. Read-only probes never authorize PID kills. */
7069
7095
  stopQuiescenceProblem(runId: number): string | null;
7096
+ /** The same answer as stopQuiescenceProblem, with which run it names and
7097
+ * what kind of answer it is (needs-you.ts reads it): `alive` while
7098
+ * something of the run may still be running, `elsewhere` when only
7099
+ * another computer can tell, `unprovable` when nothing is known to run
7100
+ * but its exit was never recorded, `unknown` when this computer can't
7101
+ * check at all (both are what `run settle` resolves; only the first may
7102
+ * be called not running). */
7103
+ stopQuiescenceFact(runId: number): {
7104
+ run: number;
7105
+ kind: StopFactKind;
7106
+ problem: string;
7107
+ } | null;
7108
+ /** `run settle` recorded an approver's word that a run whose workspace
7109
+ * note couldn't be read has stopped. */
7110
+ private workspaceConfirmedByApprover;
7070
7111
  reserveRunProcess(runId: number, now: Date, group?: boolean): number;
7071
7112
  /** Save positive OS exit evidence while it is available, before a numeric
7072
7113
  * PID can be reused. Completion, missing ancestry and age prove nothing.
@@ -7077,7 +7118,37 @@ export declare class Store {
7077
7118
  pid: number;
7078
7119
  group: boolean;
7079
7120
  }): number;
7121
+ /** The reconcile road for a finished run whose process outlived its
7122
+ * finish: once each process is provably gone, its exit is recorded, so
7123
+ * the task stops waiting on a person by itself. A run that can't be proven
7124
+ * yet stays open and is passed over, never holding newer runs behind it:
7125
+ * every finished run is visited, a batch at a time. Never signals. */
7126
+ recordFinishedRunExits(now: Date, batch?: number): number;
7080
7127
  finishUnspawnedProcess(witness: number, now: Date): void;
7128
+ /** The reconcile road for a pid-less witness a crash or a thrown spawn
7129
+ * left behind: once its run has finished, no live child is tracked for it
7130
+ * here, every other witness of the run has exited and none of the run's
7131
+ * process groups is alive, it settles as never started, with a ledger
7132
+ * entry. A sole pid-less witness has no group to prove gone and stays for
7133
+ * `run settle`. Native witnesses keep their OS-only road. */
7134
+ settleUnspawnedWitnesses(now: Date): number;
7135
+ /** `run settle` (an approver's last resort): every witness of a finished
7136
+ * run that cannot be proven either way is recorded settled under the
7137
+ * approver's reason, in the ledger. Refuses while anything of the run is
7138
+ * alive or could be checked only from another host. Never signals. */
7139
+ settleRunWitnessesByApprover(args: {
7140
+ runId: number;
7141
+ by: string;
7142
+ why: string;
7143
+ }, now: Date): {
7144
+ ok: true;
7145
+ witnesses: number[];
7146
+ repeated: boolean;
7147
+ } | {
7148
+ ok: false;
7149
+ reason: "unknown-run" | "still-running" | "alive";
7150
+ detail: string;
7151
+ };
7081
7152
  recordRunProcess(runId: number, pid: number, now: Date, group?: boolean, witness?: number): void;
7082
7153
  /** The native OS object a witnessed spawn ran inside (v53) — recorded
7083
7154
  * with the spawn, before the target can execute. */