toolroll 0.9.8 → 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 (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/assignment-brief.d.ts +2 -0
  3. package/dist/assignment-brief.js +2 -1
  4. package/dist/assignment-presentation.js +3 -1
  5. package/dist/assignment-status.d.ts +1 -1
  6. package/dist/assignment-ui.js +5 -0
  7. package/dist/assignment.d.ts +9 -0
  8. package/dist/assignment.js +39 -7
  9. package/dist/browser/workspace.css +1 -1
  10. package/dist/browser/workspace.js +18 -18
  11. package/dist/browser-crew.js +4 -0
  12. package/dist/browser-workspace.d.ts +40 -0
  13. package/dist/builder.d.ts +2 -0
  14. package/dist/builder.js +114 -8
  15. package/dist/chat-controls.d.ts +1 -1
  16. package/dist/chat-controls.js +1 -1
  17. package/dist/check-progress.js +4 -3
  18. package/dist/checkout-cleanup.d.ts +40 -2
  19. package/dist/checkout-cleanup.js +129 -9
  20. package/dist/desktop-update.d.ts +52 -0
  21. package/dist/desktop-update.js +147 -5
  22. package/dist/evidence.d.ts +6 -0
  23. package/dist/evidence.js +8 -0
  24. package/dist/first-run.d.ts +1 -1
  25. package/dist/first-run.js +7 -5
  26. package/dist/lead-context.d.ts +1 -1
  27. package/dist/lead-context.js +4 -2
  28. package/dist/lead-status.d.ts +10 -1
  29. package/dist/lead-status.js +21 -24
  30. package/dist/needs-you.d.ts +55 -0
  31. package/dist/needs-you.js +83 -0
  32. package/dist/operate.d.ts +2 -0
  33. package/dist/operate.js +112 -14
  34. package/dist/plane-review.js +4 -0
  35. package/dist/process-recovery-eligibility.d.ts +1 -0
  36. package/dist/project-learning.d.ts +3 -3
  37. package/dist/project-learning.js +28 -17
  38. package/dist/proof.d.ts +3 -1
  39. package/dist/proof.js +4 -2
  40. package/dist/result-completion.d.ts +4 -0
  41. package/dist/result-completion.js +11 -0
  42. package/dist/review-context.d.ts +3 -0
  43. package/dist/review-context.js +45 -5
  44. package/dist/run-closeout.d.ts +89 -0
  45. package/dist/run-closeout.js +302 -0
  46. package/dist/serve.d.ts +15 -1
  47. package/dist/serve.js +129 -14
  48. package/dist/setup-guide.d.ts +2 -2
  49. package/dist/shared-deps.d.ts +94 -0
  50. package/dist/shared-deps.js +365 -0
  51. package/dist/storage.js +2 -0
  52. package/dist/store.d.ts +36 -0
  53. package/dist/store.js +125 -29
  54. package/dist/task-status.d.ts +18 -4
  55. package/dist/task-status.js +35 -18
  56. package/dist/telegram-progress.js +2 -1
  57. package/dist/telegram-status.js +3 -3
  58. package/dist/test-temp.d.ts +19 -0
  59. package/dist/test-temp.js +98 -0
  60. package/dist/toolroll-update-ui.js +7 -2
  61. package/dist/toolroll-update.d.ts +21 -1
  62. package/dist/toolroll-update.js +135 -5
  63. package/dist/work-index.d.ts +11 -0
  64. package/dist/work-index.js +103 -16
  65. package/dist/work-summary.d.ts +3 -1
  66. package/dist/workspace-ui.js +1 -1
  67. package/dist/worktree.d.ts +16 -0
  68. package/dist/worktree.js +47 -0
  69. 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
@@ -21,7 +21,16 @@ import { type RetentionKind, type RetentionPeriods } from "./retention.js";
21
21
  import { type CheckoutCleanup } from "./storage.js";
22
22
  import { type LedgerChainReport } from "./ledger-chain.js";
23
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";
24
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;
25
34
  export type TaskActRow = {
26
35
  act: TaskAct;
27
36
  account: string;
@@ -2711,6 +2720,8 @@ export declare class Store {
2711
2720
  clearWorktreeSetup(repo: string, by: string, now: Date): boolean;
2712
2721
  /** The cache stamp: this checkout completed this setup. Success-only by contract. */
2713
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;
2714
2725
  /**
2715
2726
  * Approve one repo's verification command, revoking any predecessor in
2716
2727
  * the same transaction — exactly one live command per repo, enforced by
@@ -3712,6 +3723,10 @@ export declare class Store {
3712
3723
  lastBuilderRun(taskRef: number): Run | null;
3713
3724
  /** How many finished builder attempts of this task ended at `head`. */
3714
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;
3715
3730
  requeueTask(taskId: string, by: string, now: Date): {
3716
3731
  ok: true;
3717
3732
  resolvedIncidents: number;
@@ -7078,6 +7093,21 @@ export declare class Store {
7078
7093
  /** Every owned run must have ended, and every retained local process
7079
7094
  * witness must be absent. Read-only probes never authorize PID kills. */
7080
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;
7081
7111
  reserveRunProcess(runId: number, now: Date, group?: boolean): number;
7082
7112
  /** Save positive OS exit evidence while it is available, before a numeric
7083
7113
  * PID can be reused. Completion, missing ancestry and age prove nothing.
@@ -7088,6 +7118,12 @@ export declare class Store {
7088
7118
  pid: number;
7089
7119
  group: boolean;
7090
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;
7091
7127
  finishUnspawnedProcess(witness: number, now: Date): void;
7092
7128
  /** The reconcile road for a pid-less witness a crash or a thrown spawn
7093
7129
  * left behind: once its run has finished, no live child is tracked for it