toolroll 0.9.70 → 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.
- package/CHANGELOG.md +99 -0
- package/dist/agent-fence.d.ts +7 -0
- package/dist/agent-fence.js +10 -2
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/builder.js +48 -6
- package/dist/chat-decide.js +17 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +204 -0
- package/dist/claim.js +1 -1
- package/dist/commit-extract.d.ts +94 -0
- package/dist/commit-extract.js +436 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +60 -0
- package/dist/contracts/release-terms.js +70 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/flow-engine.js +5 -1
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flows-ui.js +7 -1
- package/dist/invoke.d.ts +19 -1
- package/dist/invoke.js +49 -2
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/proof.js +2 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +8 -0
- package/dist/release-adapters.js +212 -15
- package/dist/release-authority.d.ts +112 -0
- package/dist/release-authority.js +126 -0
- package/dist/release-carry.d.ts +233 -0
- package/dist/release-carry.js +645 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +124 -5
- package/dist/release-coverage.js +3 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +131 -1
- package/dist/release.js +562 -244
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +59 -1
- package/dist/sandbox-runtime.js +159 -5
- package/dist/scope.d.ts +115 -5
- package/dist/scope.js +219 -7
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/store.d.ts +227 -5
- package/dist/store.js +727 -37
- package/dist/task-outcome-cli.js +7 -3
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- 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,6 +1018,7 @@ 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.
|
|
1021
|
+
// Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
|
|
1020
1022
|
fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
|
|
1021
1023
|
confinement: "required",
|
|
1022
1024
|
env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(fenced.cache === undefined || fenced.grants.writes === undefined ? {} : buildCacheEnv(fenced.cache)), ...(signIns?.env ?? {}) },
|
|
@@ -1086,6 +1088,7 @@ export function fencedCheckoutCommands(store, repo, checkout, inner, available =
|
|
|
1086
1088
|
...options,
|
|
1087
1089
|
fence: launch.fence,
|
|
1088
1090
|
// Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
|
|
1091
|
+
// Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
|
|
1089
1092
|
fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
|
|
1090
1093
|
confinement: launch.confinement,
|
|
1091
1094
|
env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(allowed === null ? {} : buildCacheEnv(cache)), ...(signIns?.env ?? {}) },
|
|
@@ -1173,7 +1176,51 @@ const SIGN_INS_OMITTED_ENV = ["CLAUDE_CONFIG_DIR"];
|
|
|
1173
1176
|
* under: open, or its allowlist through a proxy of its own (on Linux, relayed in by the sandbox runtime). Nothing about it is
|
|
1174
1177
|
* started unless all of it is.
|
|
1175
1178
|
*/
|
|
1176
|
-
|
|
1179
|
+
/**
|
|
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
|
+
*/
|
|
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;
|
|
1222
|
+
}
|
|
1223
|
+
export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths, platform = process.platform, keychain = () => keychainSignIns(platform)) {
|
|
1177
1224
|
if (!signInsAvailable(platform))
|
|
1178
1225
|
return { problem: "Sign-ins can't be shared with checks here, so nothing ran." };
|
|
1179
1226
|
const home = homedir();
|
|
@@ -1181,7 +1228,7 @@ export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths
|
|
|
1181
1228
|
let egress = null;
|
|
1182
1229
|
const close = async () => { await egress?.close(); };
|
|
1183
1230
|
try {
|
|
1184
|
-
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() };
|
|
1185
1232
|
if (policy.mode === "open")
|
|
1186
1233
|
return { grants, network: { mode: "open" }, env, close };
|
|
1187
1234
|
// Exactly the project's network: the services the sign-ins are for are reached only if the operator allowed their
|
package/dist/lead-status.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
package/dist/live-terminal.d.ts
CHANGED
|
@@ -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
|
|
206
|
-
*
|
|
207
|
-
* the
|
|
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
|
|
235
|
-
*
|
|
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
|
package/dist/live-terminal.js
CHANGED
|
@@ -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
|
|
392
|
-
*
|
|
393
|
-
* the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
579
|
-
*
|
|
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
|
|
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 &&
|
|
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[];
|