toolroll 0.9.71 → 0.9.72

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 (93) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/agent-fence.d.ts +5 -2
  3. package/dist/agent-fence.js +5 -2
  4. package/dist/approval-authority.d.ts +20 -0
  5. package/dist/approval-authority.js +67 -0
  6. package/dist/assignment.js +26 -4
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/builder.js +48 -6
  9. package/dist/chat-decide.js +17 -2
  10. package/dist/check-access.js +2 -2
  11. package/dist/check-report.d.ts +109 -0
  12. package/dist/check-report.js +204 -0
  13. package/dist/claim.js +1 -1
  14. package/dist/commit-extract.d.ts +94 -0
  15. package/dist/commit-extract.js +436 -0
  16. package/dist/contracts/console-api.d.ts +1 -1
  17. package/dist/contracts/console-api.js +1 -0
  18. package/dist/contracts/flow.d.ts +16 -0
  19. package/dist/contracts/flow.js +3 -0
  20. package/dist/contracts/release-check.d.ts +335 -0
  21. package/dist/contracts/release-check.js +159 -0
  22. package/dist/contracts/release-terms.d.ts +60 -0
  23. package/dist/contracts/release-terms.js +70 -0
  24. package/dist/contracts/verification-receipt.d.ts +42 -8
  25. package/dist/contracts/verification-receipt.js +24 -7
  26. package/dist/contracts/verification-view.d.ts +1 -0
  27. package/dist/deploy-gate.d.ts +17 -0
  28. package/dist/deploy-gate.js +68 -0
  29. package/dist/dispatch.js +3 -10
  30. package/dist/dispose.js +4 -1
  31. package/dist/evidence-pack.d.ts +1 -1
  32. package/dist/evidence-pack.js +20 -2
  33. package/dist/evidence.d.ts +1 -0
  34. package/dist/evidence.js +2 -1
  35. package/dist/flow-engine.js +5 -1
  36. package/dist/flow-pull-request.d.ts +5 -2
  37. package/dist/flow-pull-request.js +14 -5
  38. package/dist/flow-schedule.js +25 -6
  39. package/dist/flows-ui.js +7 -1
  40. package/dist/invoke.d.ts +17 -6
  41. package/dist/invoke.js +48 -18
  42. package/dist/lead-status.js +5 -2
  43. package/dist/live-terminal.d.ts +10 -6
  44. package/dist/live-terminal.js +55 -14
  45. package/dist/nightly-check.d.ts +104 -0
  46. package/dist/nightly-check.js +362 -0
  47. package/dist/operate.js +39 -2
  48. package/dist/person-proof.d.ts +2 -0
  49. package/dist/person-proof.js +14 -0
  50. package/dist/plan-auto.js +4 -4
  51. package/dist/planner-source.d.ts +1 -1
  52. package/dist/project-delete-ui.js +1 -1
  53. package/dist/proof.js +2 -0
  54. package/dist/publish.js +156 -144
  55. package/dist/pull-request-flow.d.ts +2 -0
  56. package/dist/pull-request-flow.js +24 -2
  57. package/dist/release-adapters.d.ts +8 -0
  58. package/dist/release-adapters.js +212 -15
  59. package/dist/release-authority.d.ts +112 -0
  60. package/dist/release-authority.js +126 -0
  61. package/dist/release-carry.d.ts +233 -0
  62. package/dist/release-carry.js +645 -0
  63. package/dist/release-cli.d.ts +3 -0
  64. package/dist/release-cli.js +124 -5
  65. package/dist/release-coverage.js +3 -0
  66. package/dist/release-recheck.d.ts +146 -0
  67. package/dist/release-recheck.js +657 -0
  68. package/dist/release-view.d.ts +34 -0
  69. package/dist/release-view.js +27 -0
  70. package/dist/release.d.ts +131 -1
  71. package/dist/release.js +562 -244
  72. package/dist/remote-main.d.ts +2 -0
  73. package/dist/remote-main.js +1 -1
  74. package/dist/result-completion.d.ts +6 -0
  75. package/dist/result-completion.js +15 -1
  76. package/dist/result-follow-ups.js +29 -1
  77. package/dist/sandbox-runtime.d.ts +16 -5
  78. package/dist/sandbox-runtime.js +19 -6
  79. package/dist/scope.d.ts +115 -5
  80. package/dist/scope.js +219 -7
  81. package/dist/server/render-tasks.d.ts +8 -0
  82. package/dist/server/render-tasks.js +50 -9
  83. package/dist/server/route-table.js +1 -0
  84. package/dist/server/task-views.js +4 -0
  85. package/dist/server/tasks.js +12 -0
  86. package/dist/store.d.ts +227 -5
  87. package/dist/store.js +727 -37
  88. package/dist/task-outcome-cli.js +7 -3
  89. package/dist/toolroll-update.js +2 -2
  90. package/dist/verification-evidence.d.ts +25 -1
  91. package/dist/verification-evidence.js +57 -2
  92. package/dist/work-index.js +3 -6
  93. package/package.json +1 -1
package/dist/invoke.js CHANGED
@@ -24,6 +24,7 @@ import { classifyTerminal } from "./exhaustion.js";
24
24
  import { liftAuthPause } from "./provider-auth.js";
25
25
  import { attestProvider } from "./attest.js";
26
26
  import { run as runCommand, runOwnerTag, streamsInput } from "./exec.js";
27
+ import { spawnSync } from "node:child_process";
27
28
  import { currentContainment } from "./containment.js";
28
29
  import { witnessedRunner } from "./process-custody.js";
29
30
  import { underStopWatch } from "./task-control.js";
@@ -1017,9 +1018,8 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
1017
1018
  ...options,
1018
1019
  fence: fenced.fence,
1019
1020
  // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1020
- // With sign-ins, the keychain too (Claude Code keeps its macOS sign-in there), only while a person still set it.
1021
- fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network,
1022
- ...(signInsKeychain(store, checkout === null ? null : store.getWorktree(checkout)?.repo ?? null) ? { keychain: true } : {}) },
1021
+ // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1022
+ fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
1023
1023
  confinement: "required",
1024
1024
  env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(fenced.cache === undefined || fenced.grants.writes === undefined ? {} : buildCacheEnv(fenced.cache)), ...(signIns?.env ?? {}) },
1025
1025
  ...(signIns === null ? {} : { omitEnv: [...(options?.omitEnv ?? []), ...SIGN_INS_OMITTED_ENV] }),
@@ -1088,9 +1088,8 @@ export function fencedCheckoutCommands(store, repo, checkout, inner, available =
1088
1088
  ...options,
1089
1089
  fence: launch.fence,
1090
1090
  // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1091
- // With sign-ins, the keychain too (Claude Code keeps its macOS sign-in there), only while a person still set it.
1092
- fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network,
1093
- ...(signInsKeychain(store, repo) ? { keychain: true } : {}) },
1091
+ // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1092
+ fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
1094
1093
  confinement: launch.confinement,
1095
1094
  env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(allowed === null ? {} : buildCacheEnv(cache)), ...(signIns?.env ?? {}) },
1096
1095
  ...(signIns === null ? {} : { omitEnv: [...(options?.omitEnv ?? []), ...SIGN_INS_OMITTED_ENV] }),
@@ -1178,19 +1177,50 @@ const SIGN_INS_OMITTED_ENV = ["CLAUDE_CONFIG_DIR"];
1178
1177
  * started unless all of it is.
1179
1178
  */
1180
1179
  /**
1181
- * Whether a check with sign-ins also reaches the keychain (macOS securityd: any item every program may read, not only
1182
- * the sign-ins). Only while the project's sign-ins level stands as a person set it: widening to sign-ins takes an
1183
- * instance operator's password typed now (operate.ts project check-access, Settings → Project → Check access), and the
1184
- * one who set it must still be an approver for the project and an instance operator now. Anything else: no keychain.
1180
+ * macOS: the sign-ins a check with sign-ins needs that live in the keychain, not in files: Claude Code's
1181
+ * ("Claude Code-credentials", its current access token as CLAUDE_CODE_OAUTH_TOKEN) and gh's ("gh:github.com", as
1182
+ * GH_TOKEN). Read here, by Toolroll, outside the sandbox, and handed to the check in its environment, so the check never
1183
+ * reaches securityd (sandbox-runtime.ts SEATBELT_NO_KEYCHAIN) and nothing else in the keychain. The check can't renew
1184
+ * Claude's token: one that has expired fails its sign-in. A value that isn't a plain token is left out.
1185
1185
  */
1186
- export function signInsKeychain(store, repo) {
1187
- if (repo === null || store.projectCheckAccess(repo) !== "sign-ins")
1188
- return false;
1189
- const change = store.projectCheckAccessChange(repo);
1190
- const account = change === null ? null : store.accountOf(change.by);
1191
- return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(change.by, repo) && store.isInstanceOperator(change.by);
1186
+ export const KEYCHAIN_SIGN_INS = [
1187
+ { service: "Claude Code-credentials", env: "CLAUDE_CODE_OAUTH_TOKEN", token: secret => {
1188
+ try {
1189
+ const token = JSON.parse(secret).claudeAiOauth?.accessToken;
1190
+ return typeof token === "string" ? token : null;
1191
+ }
1192
+ catch {
1193
+ return null;
1194
+ }
1195
+ } },
1196
+ // go-keyring (gh's) may keep it base64-encoded behind a prefix.
1197
+ { service: "gh:github.com", env: "GH_TOKEN", token: secret => secret.startsWith("go-keyring-base64:") ? Buffer.from(secret.slice("go-keyring-base64:".length), "base64").toString("utf8") : secret },
1198
+ ];
1199
+ const PLAIN_TOKEN = /^[A-Za-z0-9._~+/=-]{8,4096}$/;
1200
+ /** One keychain item's secret by its service, read with the system's `security` (null: none, or it can't be read). A
1201
+ * keychain file may be named (tests: a throwaway one); never the item's value in an argument. */
1202
+ export function readKeychainSecret(service, keychain) {
1203
+ const read = spawnSync("/usr/bin/security", ["find-generic-password", "-s", service, "-w", ...keychain === undefined ? [] : [keychain]], { encoding: "utf8", timeout: 10_000, env: { PATH: "/usr/bin:/bin", HOME: homedir() }, stdio: ["ignore", "pipe", "ignore"] });
1204
+ return read.status === 0 && typeof read.stdout === "string" ? read.stdout.replace(/\n$/, "") : null;
1205
+ }
1206
+ /** The keychain's sign-ins as a check's environment (macOS only; elsewhere, for anything missing, and where Toolroll
1207
+ * keeps nothing in the keychain (TOOLROLL_SECRET_STORE=files: the test suite), nothing). */
1208
+ export function keychainSignIns(platform = process.platform, read) {
1209
+ if (platform !== "darwin")
1210
+ return {};
1211
+ if (read === undefined && process.env["TOOLROLL_SECRET_STORE"] === "files")
1212
+ return {};
1213
+ read ??= service => readKeychainSecret(service);
1214
+ const env = {};
1215
+ for (const one of KEYCHAIN_SIGN_INS) {
1216
+ const secret = read(one.service);
1217
+ const token = secret === null ? null : one.token(secret);
1218
+ if (token !== null && PLAIN_TOKEN.test(token))
1219
+ env[one.env] = token;
1220
+ }
1221
+ return env;
1192
1222
  }
1193
- export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths, platform = process.platform) {
1223
+ export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths, platform = process.platform, keychain = () => keychainSignIns(platform)) {
1194
1224
  if (!signInsAvailable(platform))
1195
1225
  return { problem: "Sign-ins can't be shared with checks here, so nothing ran." };
1196
1226
  const home = homedir();
@@ -1198,7 +1228,7 @@ export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths
1198
1228
  let egress = null;
1199
1229
  const close = async () => { await egress?.close(); };
1200
1230
  try {
1201
- const env = { CODEX_HOME: join(home, ".codex"), GH_CONFIG_DIR: join(home, ".config", "gh"), XDG_CONFIG_HOME: join(home, ".config") };
1231
+ const env = { CODEX_HOME: join(home, ".codex"), GH_CONFIG_DIR: join(home, ".config", "gh"), XDG_CONFIG_HOME: join(home, ".config"), ...keychain() };
1202
1232
  if (policy.mode === "open")
1203
1233
  return { grants, network: { mode: "open" }, env, close };
1204
1234
  // Exactly the project's network: the services the sign-ins are for are reached only if the operator allowed their
@@ -122,13 +122,16 @@ export function taskWaitSnapshot(store, taskId, now, watchedRun) {
122
122
  const scope = store.handle.prepare(`SELECT digest, approved_digest, profile_state,
123
123
  (approved_at IS NOT NULL AND approved_by IS NOT NULL AND approved_digest = digest) AS approved
124
124
  FROM task_scope WHERE task_id = ?`).get(taskId);
125
- if (task["plan"] === "drafted" && Number(scope?.["approved"]) !== 1)
125
+ // Approved only as dispatch reads it (approval policy): a seal whose authority doesn't stand — an old seal a lead token
126
+ // made, an ended mode — waits for a person, never for a worker.
127
+ const approvedNow = Number(scope?.["approved"]) === 1 && store.modeApprovalLive(taskRef, now);
128
+ if (task["plan"] === "drafted" && !approvedNow)
126
129
  return answer("Needs a person", "Approve plan", true, 1, "needs-person");
127
130
  if (Number(task["strikes"] ?? 0) >= 3)
128
131
  return answer("Needs a person", "Review repeated failures", true, 1, "needs-person");
129
132
  if (scope === undefined)
130
133
  return answer("Needs a person", "Add scope", true, 1, "needs-person");
131
- if (Number(scope["approved"]) !== 1 || scope["profile_state"] !== "resolved")
134
+ if (!approvedNow || scope["profile_state"] !== "resolved")
132
135
  return answer("Needs a person", "Approve task", true, 1, "needs-person");
133
136
  if (state === "running" && attempt === undefined) {
134
137
  const liveClaim = store.handle.prepare(`SELECT 1 AS hit FROM claim INDEXED BY claim_by_task
@@ -202,9 +202,11 @@ export declare class TerminalHost {
202
202
  * End the program and everything that is its own: SIGHUP to every member (an interactive shell gives each job its own
203
203
  * process group, so the leader's group is not enough), then SIGKILL, re-reading the process table each time so a
204
204
  * child started during the close is caught too. Membership is the leader's descendants, every process with this
205
- * terminal as its controlling terminal (started before the program ended), and every one noted while it ran, by pid
206
- * and start time. A process that detached itself from the terminal AND its parents before it was noted is not seen —
207
- * the same as any terminal. Resolves when the sweep is done; never waits without bound.
205
+ * terminal as its controlling terminal while the program runs, and every one noted while it ran, by pid and start
206
+ * time (membersOf). Each signal goes only to a process still that pid and start time; a process on the terminal after
207
+ * the program ended that was never noted, or a pid now someone else's, is never signalled and leaves the close
208
+ * uncertain. A process that detached itself from the terminal AND its parents before it was noted is not seen — the
209
+ * same as any terminal. Resolves when the sweep is done; never waits without bound.
208
210
  */
209
211
  close(id: string, by?: "owner" | "console"): Promise<void>;
210
212
  private sweep;
@@ -231,15 +233,17 @@ export type ProcessTable = Map<number, {
231
233
  export declare function readProcessTable(): ProcessTable | null;
232
234
  /**
233
235
  * A terminal's processes in this table, by pid and start time: what was noted while it ran (still the same process),
234
- * the leader, everything whose controlling terminal is its terminal and that started before it ended (a later process
235
- * on a reused device is someone else's), and every descendant of those. Never this console itself.
236
+ * the leader while it runs, everything whose controlling terminal is its terminal while it runs, and every descendant
237
+ * of those. Never this console itself. Once the program has ended its terminal's name is no one's proof: the device
238
+ * may already be someone else's, so a process on it that wasn't noted is not a member; it goes in `ambiguous` (when
239
+ * given), and the close that meets one says it is uncertain rather than signal it.
236
240
  */
237
241
  export declare function membersOf(table: ProcessTable, session: {
238
242
  leader: number;
239
243
  tty: string | null;
240
244
  known: ReadonlyMap<number, string>;
241
245
  exitedAt: number | null;
242
- }): Map<number, string>;
246
+ }, ambiguous?: Set<number>): Map<number, string>;
243
247
  /**
244
248
  * Each listed process's working directory (Linux: /proc; macOS: lsof), and which ones couldn't be read (a setgid
245
249
  * helper such as ssh-agent is unreadable even to its own user). Null when the lookup itself failed: a timeout, a signal
@@ -388,9 +388,11 @@ export class TerminalHost {
388
388
  * End the program and everything that is its own: SIGHUP to every member (an interactive shell gives each job its own
389
389
  * process group, so the leader's group is not enough), then SIGKILL, re-reading the process table each time so a
390
390
  * child started during the close is caught too. Membership is the leader's descendants, every process with this
391
- * terminal as its controlling terminal (started before the program ended), and every one noted while it ran, by pid
392
- * and start time. A process that detached itself from the terminal AND its parents before it was noted is not seen —
393
- * the same as any terminal. Resolves when the sweep is done; never waits without bound.
391
+ * terminal as its controlling terminal while the program runs, and every one noted while it ran, by pid and start
392
+ * time (membersOf). Each signal goes only to a process still that pid and start time; a process on the terminal after
393
+ * the program ended that was never noted, or a pid now someone else's, is never signalled and leaves the close
394
+ * uncertain. A process that detached itself from the terminal AND its parents before it was noted is not seen — the
395
+ * same as any terminal. Resolves when the sweep is done; never waits without bound.
394
396
  */
395
397
  close(id, by = "owner") {
396
398
  const session = this.sessions.get(id);
@@ -408,19 +410,30 @@ export class TerminalHost {
408
410
  const pause = sleep;
409
411
  const leaderGone = () => session.pty === null;
410
412
  let readable = true;
413
+ // Processes on its terminal after it ended that were never noted: maybe someone else's, never signalled.
414
+ const ambiguous = new Set();
411
415
  const members = () => {
412
416
  const table = this.processes();
413
417
  if (table === null) {
414
418
  readable = false;
415
419
  return new Map();
416
420
  }
417
- const found = membersOf(table, session);
421
+ const found = membersOf(table, session, ambiguous);
418
422
  for (const [pid, started] of found)
419
423
  session.known.set(pid, started);
420
424
  return found;
421
425
  };
426
+ // Each signal goes only to the process it was meant for: the table is read again right before, and a pid whose
427
+ // start time is no longer the one recorded (gone, or a pid used again) is skipped, and the close is uncertain. The
428
+ // leader's group only while it runs: its pid is our unreaped child until the pty reports its exit, and, where the
429
+ // table reads, still the leader that started. A table that can't be read signals the leader alone (the close is
430
+ // uncertain then anyway).
422
431
  const signal = (found, name) => {
423
- if (!leaderGone()) {
432
+ const table = this.processes();
433
+ if (table === null)
434
+ readable = false;
435
+ const leaderStarted = session.known.get(session.leader);
436
+ if (!leaderGone() && (table === null || (leaderStarted !== undefined && table.get(session.leader)?.started === leaderStarted))) {
424
437
  try {
425
438
  process.kill(-session.leader, name);
426
439
  }
@@ -430,7 +443,16 @@ export class TerminalHost {
430
443
  }
431
444
  catch { /* gone */ }
432
445
  }
433
- for (const pid of found.keys()) {
446
+ if (table === null)
447
+ return;
448
+ for (const [pid, started] of found) {
449
+ const now = table.get(pid);
450
+ if (now === undefined)
451
+ continue;
452
+ if (now.started !== started) {
453
+ ambiguous.add(pid);
454
+ continue;
455
+ }
434
456
  try {
435
457
  process.kill(pid, name);
436
458
  }
@@ -457,13 +479,26 @@ export class TerminalHost {
457
479
  }
458
480
  }
459
481
  for (let round = 0; round < 20; round += 1) {
460
- const found = members();
482
+ let found = members();
461
483
  if (!readable)
462
484
  return;
463
485
  if (found.size === 0 && leaderGone()) {
464
486
  cleanup = "clean";
465
487
  return;
466
488
  }
489
+ // Stopped first, then read again until nothing new appears: a stopped process can't fork, so a child one started
490
+ // just before it stopped is still its descendant here and is killed with it. Killed while running, a parent can
491
+ // fork between the read and the kill, and its child goes to launchd unnoted.
492
+ for (let settle = 0; settle < 5; settle += 1) {
493
+ signal(found, "SIGSTOP");
494
+ const again = members();
495
+ if (!readable)
496
+ return;
497
+ const grew = [...again.keys()].some(pid => !found.has(pid));
498
+ found = again;
499
+ if (!grew)
500
+ break;
501
+ }
467
502
  signal(found, "SIGKILL");
468
503
  await pause(100);
469
504
  }
@@ -486,7 +521,7 @@ export class TerminalHost {
486
521
  }
487
522
  }
488
523
  }
489
- session.cleanup = readable && leaderGone() ? cleanup : "uncertain";
524
+ session.cleanup = readable && leaderGone() && ambiguous.size === 0 ? cleanup : "uncertain";
490
525
  if (session.endedAt === null)
491
526
  session.endedAt = this.clock().toISOString();
492
527
  this.ledger.release(session.id, session.cleanup);
@@ -575,19 +610,21 @@ export function readProcessTable() {
575
610
  }
576
611
  /**
577
612
  * A terminal's processes in this table, by pid and start time: what was noted while it ran (still the same process),
578
- * the leader, everything whose controlling terminal is its terminal and that started before it ended (a later process
579
- * on a reused device is someone else's), and every descendant of those. Never this console itself.
613
+ * the leader while it runs, everything whose controlling terminal is its terminal while it runs, and every descendant
614
+ * of those. Never this console itself. Once the program has ended its terminal's name is no one's proof: the device
615
+ * may already be someone else's, so a process on it that wasn't noted is not a member; it goes in `ambiguous` (when
616
+ * given), and the close that meets one says it is uncertain rather than signal it.
580
617
  */
581
- export function membersOf(table, session) {
618
+ export function membersOf(table, session, ambiguous) {
582
619
  const found = new Map();
583
- const startedMs = (started) => Date.parse(started);
620
+ const live = session.exitedAt === null;
621
+ const onTerminal = (tty) => session.tty !== null && tty === session.tty;
584
622
  for (const [pid, row] of table) {
585
623
  if (pid === process.pid || pid <= 1)
586
624
  continue;
587
625
  const noted = session.known.get(pid);
588
- const ownTty = session.tty !== null && row.tty === session.tty && (session.exitedAt === null || startedMs(row.started) <= session.exitedAt + 1_000);
589
626
  // The leader by pid only while it runs: after it exits its pid may be someone else's (it is noted by start time).
590
- if (noted === row.started || (pid === session.leader && session.exitedAt === null) || ownTty)
627
+ if (noted === row.started || (pid === session.leader && live) || (live && onTerminal(row.tty)))
591
628
  found.set(pid, row.started);
592
629
  }
593
630
  for (let grew = true; grew;) {
@@ -598,6 +635,10 @@ export function membersOf(table, session) {
598
635
  grew = true;
599
636
  }
600
637
  }
638
+ if (ambiguous !== undefined && !live)
639
+ for (const [pid, row] of table)
640
+ if (!found.has(pid) && pid !== process.pid && pid > 1 && onTerminal(row.tty))
641
+ ambiguous.add(pid);
601
642
  return found;
602
643
  }
603
644
  /**
@@ -0,0 +1,104 @@
1
+ import { fileIdentity, groupIdentity, journeyIdentity, unitIdentity, versionIdentity, type CheckReport } from "./contracts/release-check.js";
2
+ import type { StandingOrder } from "./contracts/flow.js";
3
+ import { type Store } from "./store.js";
4
+ import type { Runner } from "./worktree.js";
5
+ import type { StandingFiring } from "./flow-schedule.js";
6
+ /** How long a nightly may be missing before its flow says so. */
7
+ export declare const NIGHTLY_MISSED_MS: number;
8
+ export declare const NIGHTLY_STEM = "nightly";
9
+ export type MainCheck = NonNullable<StandingOrder["check"]>;
10
+ /** What every nightly firing files: fixed here, never read from the trigger's settings. */
11
+ export declare const NIGHTLY_GOAL = "Run the full release check on main's head.";
12
+ export declare const NIGHTLY_NOT = "Changing anything: this is a check only.";
13
+ export declare const NIGHTLY_ACCEPTANCE: readonly {
14
+ id: string;
15
+ statement: string;
16
+ how: null;
17
+ evidence: "check"[];
18
+ }[];
19
+ /** The ledger act that records a person's approval of a nightly check, keyed by its trigger. */
20
+ export declare const NIGHTLY_APPROVED = "nightly check approved";
21
+ /** The digest a person's approval of the nightly signs: the trigger, the project, the schedule, the check-only terms
22
+ * and the fixed words each firing files. */
23
+ export declare function nightlyDigestOf(trigger: number, repo: string, schedule: string, check: MainCheck): string;
24
+ /**
25
+ * The approval of a nightly check as the plane recorded it when a person proved themselves (createNightlyCheckFlow):
26
+ * the newest ledger row for this trigger. The trigger's own settings never stand in for it — anyone who can edit them
27
+ * could write one.
28
+ */
29
+ export declare function nightlyApprovalOf(store: Store, trigger: number): {
30
+ digest: string;
31
+ by: string;
32
+ proof: string;
33
+ at: string;
34
+ } | null;
35
+ /** How often the watch loop reads each recorded remote's main again, and how old a read may be when a nightly fires
36
+ * or debt is cleared against it (review round 2: one window, two refreshes wide, for both decisions). */
37
+ export declare const TRUSTED_MAIN_REFRESH_MS: number;
38
+ export declare const TRUSTED_MAIN_FIRE_MS: number;
39
+ export declare const TRUSTED_MAIN_CLEAR_MS: number;
40
+ /**
41
+ * Main's head as Toolroll's own process last read it from the project's RECORDED remote (remote-main.ts: the recorded
42
+ * URL, a private plane-owned Git directory, objects that arrive only by fetch) — never a ref in the project's Git
43
+ * directory, which a build shares and can move (review round 1). Null when there is no read young enough, when the
44
+ * last read failed, or when the read was of another remote than the one recorded now (review round 2).
45
+ */
46
+ export declare function trustedMainOf(store: Store, repo: string, now: Date, maxAgeMs: number): string | null;
47
+ /** The watch loop's read of each project's recorded remote main that the nightly check or main's debt needs, at most
48
+ * every 30 minutes. Each read is a fresh fetch into a private directory. A failed read forgets the last one. A good
49
+ * one hands its verified objects to the project's Git store (hardened git, content-addressed: git checks every object
50
+ * against its hash, and filing verifies the commit again), so the nightly can check a merge the checkout never pulled. */
51
+ export declare function refreshTrustedMains(store: Store, root: string, now: Date, runner?: Runner): Promise<number>;
52
+ /**
53
+ * Set up the nightly full check for a project, approved by a person proved now (their password or a re-check; never a
54
+ * lead or remote token). Its schedule starts paused unless `start`; turning it on is the flow's own switch.
55
+ */
56
+ export declare function createNightlyCheckFlow(store: Store, input: {
57
+ repo: string;
58
+ schedule: string;
59
+ as: string;
60
+ token: string;
61
+ start: boolean;
62
+ }, now: Date): {
63
+ ok: true;
64
+ flow: number;
65
+ trigger: number;
66
+ } | {
67
+ ok: false;
68
+ message: string;
69
+ };
70
+ /** A nightly firing (fileStandingOrder routes here): the approval checked again, main's head read, one check-only task
71
+ * filed and sealed by its standing order. */
72
+ export declare function fileNightlyCheck(store: Store, input: {
73
+ flowId: number;
74
+ repo: string;
75
+ trigger: number;
76
+ schedule: string;
77
+ order: StandingOrder;
78
+ title: string;
79
+ slot: string;
80
+ }, now: Date): StandingFiring;
81
+ /** Whether a task is a nightly check of main (check only: it never deploys, merges or completes). */
82
+ export declare const isNightlyTask: (store: Store, taskId: string) => boolean;
83
+ /** Every item a failed check failed: each failed identity, each failed group or version, and the whole part for a
84
+ * part that couldn't say which items (it stopped, never reported, or crashed). No report: every part. */
85
+ export declare function failedItems(report: CheckReport | null): string[];
86
+ /** The words of one failure notice: "2 unit files, app/pages". */
87
+ export declare function failureSummary(items: readonly string[]): string;
88
+ /**
89
+ * Whether a full run's report shows `item` itself ran and PASSED there (security review): a failed identity has to be
90
+ * reported passed by name — missing, skipped, or a passing part around it never clears it. A part-level item clears
91
+ * when that whole part ran and passed.
92
+ */
93
+ export declare function coversItem(report: CheckReport, item: string): boolean;
94
+ /**
95
+ * The plane's sweep (watch's follow-up checks): settle each finished nightly (failure rows and one attention notice),
96
+ * then let each finished full release check clear the main failures it covers. Returns how many it settled.
97
+ */
98
+ export declare function settleReleaseChecks(store: Store, root: string, now: Date): number;
99
+ /** The nightly's flow health: "the nightly check hasn't run since …" after 26 hours without a firing. Null when it ran
100
+ * recently, or the project has no nightly check. */
101
+ export declare function nightlyHealthWords(store: Store, repo: string, configuredAt: string, now: Date): string | null;
102
+ export { fileIdentity, groupIdentity, journeyIdentity, unitIdentity, versionIdentity };
103
+ /** The items the nightly check of main found failing that are still open: a full check reports each by name. */
104
+ export declare function openMainFailureItems(store: Store, repo: string): string[];