toolroll 0.9.76 → 0.9.78
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 +273 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +46 -10
- package/dist/assignment.js +3 -0
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +18 -3
- package/dist/builder.js +152 -47
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +295 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +67 -6
- package/dist/check-report.js +254 -9
- package/dist/checkout-cleanup.js +2 -2
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-service.js +4 -1
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.js +5 -0
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +9 -5
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +19 -4
- package/dist/exec.d.ts +9 -0
- package/dist/exec.js +22 -2
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/grant.js +5 -1
- package/dist/guides.js +22 -11
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +969 -328
- package/dist/planner.js +2 -2
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +2 -2
- package/dist/proof.js +6 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +80 -2
- package/dist/release-adapters.js +441 -16
- package/dist/release-authority.js +6 -2
- package/dist/release-auto.d.ts +38 -7
- package/dist/release-auto.js +131 -15
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +95 -17
- package/dist/release-coverage.js +5 -2
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +8 -2
- package/dist/release.d.ts +144 -14
- package/dist/release.js +421 -81
- package/dist/review-context.js +8 -6
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +73 -9
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +26 -3
- package/dist/scout.js +2 -2
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +112 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/task-views.js +7 -2
- package/dist/server/tasks.js +4 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage-sweep.js +1 -1
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +135 -2
- package/dist/store.js +275 -13
- package/dist/surface.js +3 -1
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +52 -3
- package/dist/teardown-git.js +145 -33
- package/dist/test-infra.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +81 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/workspace-revision.js +3 -1
- package/package.json +2 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toolroll up` as two processes (docs/plans/isolated-checks.md, stage 1): the console (HTTP: serving, reading and the
|
|
3
|
+
* person's ceremonies) and the worker (the watch loops: builds, checks, batch checks, follow-ups, reconcilers, the
|
|
4
|
+
* storage sweep and checkout sizing). `up` itself keeps the identities it minted (the approver's login, the runner and
|
|
5
|
+
* its token), supervises both with controller-supervisor, and relays between them. Nothing here grants authority:
|
|
6
|
+
* each role is handed only what it needs, by message, never on a command line or in the environment.
|
|
7
|
+
*
|
|
8
|
+
* What each holds:
|
|
9
|
+
* console the database, the console's own session and sign-in secrets, the attach (instance) secret, push keys, the
|
|
10
|
+
* port. Never the runner token: it can't claim, lease or build.
|
|
11
|
+
* worker the database and the runner name and token, for the watch leases and claims. It opens no port and mints
|
|
12
|
+
* no console session or attach secret.
|
|
13
|
+
* Both are the same user on the same database folder, so files beside the database (the saved login, tool secrets)
|
|
14
|
+
* are reachable by either: the split keeps secrets out of each other's memory and messages, not out of the folder.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
const MAX_TEXT = 16_384;
|
|
19
|
+
const MAX_LIST = 4_096;
|
|
20
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
const text = (value, max = MAX_TEXT) => typeof value === "string" && value.length <= max;
|
|
22
|
+
const nonEmpty = (value, max = MAX_TEXT) => text(value, max) && value !== "" && !value.includes("\0");
|
|
23
|
+
const texts = (value) => Array.isArray(value) && value.length <= MAX_LIST && value.every(one => nonEmpty(one));
|
|
24
|
+
const whole = (value, min = 0, max = 2_147_483_647) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
|
|
25
|
+
const orNull = (value, check) => value === null || check(value);
|
|
26
|
+
/** The parent's word to a role, checked before anything acts on it. */
|
|
27
|
+
export function parentMessage(value) {
|
|
28
|
+
if (!isObject(value))
|
|
29
|
+
return null;
|
|
30
|
+
switch (value["type"]) {
|
|
31
|
+
case "repos": return texts(value["repos"]) ? { type: "repos", repos: [...value["repos"]] } : null;
|
|
32
|
+
case "stopping": return { type: "stopping" };
|
|
33
|
+
case "hard-stop": return { type: "hard-stop" };
|
|
34
|
+
case "sign-in-link": return whole(value["id"]) && nonEmpty(value["account"], 256) ? { type: "sign-in-link", id: value["id"], account: value["account"] } : null;
|
|
35
|
+
default: return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** A role's word to the parent, checked before the parent relays or acts on it. */
|
|
39
|
+
export function roleMessage(value, role) {
|
|
40
|
+
if (!isObject(value))
|
|
41
|
+
return null;
|
|
42
|
+
switch (value["type"]) {
|
|
43
|
+
case "ready":
|
|
44
|
+
if (value["role"] !== role || !nonEmpty(value["build"], 512) || !whole(value["pid"], 1))
|
|
45
|
+
return null;
|
|
46
|
+
if (!orNull(value["url"], (one) => nonEmpty(one, 2_048)) || !orNull(value["port"], (one) => whole(one, 1, 65_535)))
|
|
47
|
+
return null;
|
|
48
|
+
// Only the console serves: a worker that names an address is not a worker.
|
|
49
|
+
if (role === "worker" && (value["url"] !== null || value["port"] !== null))
|
|
50
|
+
return null;
|
|
51
|
+
if (role === "console" && (value["url"] === null || value["port"] === null))
|
|
52
|
+
return null;
|
|
53
|
+
return { type: "ready", role, build: value["build"], pid: value["pid"], url: value["url"], port: value["port"] };
|
|
54
|
+
case "log": return text(value["line"]) ? { type: "log", line: value["line"] } : null;
|
|
55
|
+
case "repos": return role === "worker" && texts(value["repos"]) ? { type: "repos", repos: [...value["repos"]] } : null;
|
|
56
|
+
case "failed": return (value["reason"] === "port-busy" || value["reason"] === "start") && text(value["detail"]) ? { type: "failed", reason: value["reason"], detail: value["detail"] } : null;
|
|
57
|
+
case "fatal": return text(value["detail"]) ? { type: "fatal", detail: value["detail"] } : null;
|
|
58
|
+
case "sign-in-link": return role === "console" && whole(value["id"]) && orNull(value["link"], (one) => nonEmpty(one, 2_048)) ? { type: "sign-in-link", id: value["id"], link: value["link"] } : null;
|
|
59
|
+
case "closed": return orNull(value["problem"], (one) => text(one)) ? { type: "closed", problem: value["problem"] } : null;
|
|
60
|
+
default: return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The start message a role child waits for. The console's config carries no runner token; anything else refuses. */
|
|
64
|
+
export function startMessage(value) {
|
|
65
|
+
if (!isObject(value) || value["type"] !== "start" || !nonEmpty(value["databaseFile"], 4_096) || !orNull(value["containment"], (one) => nonEmpty(one, 64)) || !nonEmpty(value["build"], 512))
|
|
66
|
+
return null;
|
|
67
|
+
const config = value["config"];
|
|
68
|
+
if (!isObject(config))
|
|
69
|
+
return null;
|
|
70
|
+
const base = { type: "start", databaseFile: value["databaseFile"], containment: value["containment"], build: value["build"] };
|
|
71
|
+
if (value["role"] === "console") {
|
|
72
|
+
const keys = ["host", "port", "allowedHosts", "publicUrl", "localRunner", "poolRoot", "repos", "projectRoots", "registryPath", "editor"];
|
|
73
|
+
if (Object.keys(config).some(key => !keys.includes(key)))
|
|
74
|
+
return null;
|
|
75
|
+
if (!nonEmpty(config["host"], 253) || !whole(config["port"], 0, 65_535) || !orNull(config["allowedHosts"], texts) || !orNull(config["publicUrl"], (one) => nonEmpty(one, 2_048)) ||
|
|
76
|
+
!nonEmpty(config["localRunner"], 256) || !nonEmpty(config["poolRoot"], 4_096) || !texts(config["repos"]) || !texts(config["projectRoots"]) || !nonEmpty(config["registryPath"], 4_096) || typeof config["editor"] !== "boolean")
|
|
77
|
+
return null;
|
|
78
|
+
return { ...base, role: "console", config: config };
|
|
79
|
+
}
|
|
80
|
+
if (value["role"] === "worker") {
|
|
81
|
+
const keys = ["runner", "token", "repos", "pinnedRepos", "projectRoots", "registryPath", "pool", "forMs", "publicUrl", "stopGraceMs", "verbose", "terminal", "leaseWaitMs"];
|
|
82
|
+
if (Object.keys(config).some(key => !keys.includes(key)))
|
|
83
|
+
return null;
|
|
84
|
+
if (!nonEmpty(config["runner"], 256) || !nonEmpty(config["token"], 1_024) || !texts(config["repos"]) || !texts(config["pinnedRepos"]) || !texts(config["projectRoots"]) ||
|
|
85
|
+
!nonEmpty(config["registryPath"], 4_096) || !nonEmpty(config["pool"], 4_096) || !orNull(config["forMs"], (one) => whole(one, 1)) ||
|
|
86
|
+
!orNull(config["publicUrl"], (one) => nonEmpty(one, 2_048)) || !whole(config["stopGraceMs"], 0) || typeof config["verbose"] !== "boolean" || typeof config["terminal"] !== "boolean" || !whole(config["leaseWaitMs"], 0))
|
|
87
|
+
return null;
|
|
88
|
+
return { ...base, role: "worker", config: config };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/** Two ends of an in-memory channel: one process composing both roles (tests and the desktop host), same messages. */
|
|
93
|
+
export function memoryChannel(role) {
|
|
94
|
+
const toParent = [];
|
|
95
|
+
const toRole = [];
|
|
96
|
+
const stops = [];
|
|
97
|
+
// Delivered on a later turn, as IPC would be, and through the same checks: a role can't lean on synchronous replies.
|
|
98
|
+
const later = (fn) => { queueMicrotask(fn); };
|
|
99
|
+
return {
|
|
100
|
+
port: {
|
|
101
|
+
send: message => { const checked = roleMessage(JSON.parse(JSON.stringify(message)), role); if (checked !== null)
|
|
102
|
+
later(() => { for (const one of toParent)
|
|
103
|
+
one(checked); }); },
|
|
104
|
+
onMessage: handler => { toRole.push(handler); },
|
|
105
|
+
onStop: handler => { stops.push(handler); },
|
|
106
|
+
},
|
|
107
|
+
parent: {
|
|
108
|
+
send: message => { const checked = parentMessage(JSON.parse(JSON.stringify(message))); if (checked !== null)
|
|
109
|
+
later(() => { for (const one of toRole)
|
|
110
|
+
one(checked); }); },
|
|
111
|
+
onMessage: handler => { toParent.push(handler); },
|
|
112
|
+
stop: hard => { for (const one of stops)
|
|
113
|
+
one(hard); },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** A build identity: the version and the exact code of the module that composes `up`. Both roles must report the
|
|
118
|
+
* parent's own, or the parent stops rather than run a console and a worker from different builds. */
|
|
119
|
+
export function buildIdentity(version, moduleFile) {
|
|
120
|
+
let digest = `unreadable-${randomUUID()}`;
|
|
121
|
+
try {
|
|
122
|
+
digest = createHash("sha256").update(readFileSync(moduleFile)).digest("hex").slice(0, 16);
|
|
123
|
+
}
|
|
124
|
+
catch { /* unique: never equal to any other identity */ }
|
|
125
|
+
return `${version}+${digest}`;
|
|
126
|
+
}
|
|
127
|
+
/** How many replacements in a row may end before becoming ready, with no refusal said, before `up` stops. */
|
|
128
|
+
export const ROLE_NOT_READY_REPLACEMENTS = 3;
|
|
129
|
+
/**
|
|
130
|
+
* One supervised role's generations (Codex review round 1, finding 5): readiness is per generation, not for the role's
|
|
131
|
+
* whole life. When a generation ends, the verdict is:
|
|
132
|
+
* - `fail-start`: the role never became ready at all; the start fails as one, as before the split;
|
|
133
|
+
* - `restart`: this generation was ready and crashed afterwards (the supervisor starts it again);
|
|
134
|
+
* - `end`: a replacement refused to start (another build, another database, a start refused) — permanent, so the whole
|
|
135
|
+
* `up` stops rather than loop — or ROLE_NOT_READY_REPLACEMENTS replacements in a row ended before readiness.
|
|
136
|
+
*/
|
|
137
|
+
export class RoleGenerations {
|
|
138
|
+
everReady = false;
|
|
139
|
+
generationReady = false;
|
|
140
|
+
refusal = null;
|
|
141
|
+
notReadyStreak = 0;
|
|
142
|
+
/** A new generation was spawned: nothing about it is known yet. */
|
|
143
|
+
spawned() { this.generationReady = false; this.refusal = null; }
|
|
144
|
+
/** This generation became ready; true when it is the role's first readiness ever. */
|
|
145
|
+
ready() { const first = !this.everReady; this.everReady = true; this.generationReady = true; return first; }
|
|
146
|
+
/** This generation refused its start for good (it said why, or its build is another). */
|
|
147
|
+
refused(detail) { this.refusal = detail; }
|
|
148
|
+
/** What this generation said when it refused, if it did. */
|
|
149
|
+
refusedWith() { return this.refusal; }
|
|
150
|
+
/** Whether any generation ever became ready. */
|
|
151
|
+
wasReady() { return this.everReady; }
|
|
152
|
+
/** This generation ended (not stopped by `up`): what happens next. */
|
|
153
|
+
ended() {
|
|
154
|
+
if (!this.everReady)
|
|
155
|
+
return "fail-start";
|
|
156
|
+
if (this.refusal !== null)
|
|
157
|
+
return "end";
|
|
158
|
+
if (this.generationReady) {
|
|
159
|
+
this.notReadyStreak = 0;
|
|
160
|
+
return "restart";
|
|
161
|
+
}
|
|
162
|
+
this.notReadyStreak += 1;
|
|
163
|
+
return this.notReadyStreak >= ROLE_NOT_READY_REPLACEMENTS ? "end" : "restart";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Started only by `toolroll up` (up-processes.ts): one of its two roles, the console or the worker, in a process of
|
|
3
|
+
* its own. It takes its whole start from the parent over the IPC channel (never from arguments or the environment, so
|
|
4
|
+
* the runner token is in no process listing), and stops when the parent says so or goes away.
|
|
5
|
+
*/
|
|
6
|
+
import { parentMessage, startMessage } from "./up-processes.js";
|
|
7
|
+
if (process.send === undefined || !process.connected) {
|
|
8
|
+
process.stderr.write("This process is started by `toolroll up`; run `toolroll up` instead.\n");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
// node:sqlite still says it is experimental; the CLI hushes exactly that notice, and so does a role.
|
|
12
|
+
const emitWarning = process.emitWarning.bind(process);
|
|
13
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
14
|
+
const type = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
|
|
15
|
+
if (type === "ExperimentalWarning" && (warning instanceof Error ? warning.message : String(warning)).includes("SQLite"))
|
|
16
|
+
return;
|
|
17
|
+
emitWarning(warning, ...rest);
|
|
18
|
+
});
|
|
19
|
+
const handlers = [];
|
|
20
|
+
const stops = [];
|
|
21
|
+
let stopRequested = false;
|
|
22
|
+
const stop = (hard) => {
|
|
23
|
+
stopRequested = true;
|
|
24
|
+
for (const one of stops)
|
|
25
|
+
one(hard);
|
|
26
|
+
};
|
|
27
|
+
const port = {
|
|
28
|
+
send: (message) => { if (process.connected) {
|
|
29
|
+
try {
|
|
30
|
+
process.send?.(message);
|
|
31
|
+
}
|
|
32
|
+
catch { /* the parent is gone; its loss stops this role */ }
|
|
33
|
+
} },
|
|
34
|
+
onMessage: handler => { handlers.push(handler); },
|
|
35
|
+
onStop: handler => { stops.push(handler); if (stopRequested)
|
|
36
|
+
handler(false); },
|
|
37
|
+
};
|
|
38
|
+
// A terminal's Ctrl-C reaches the whole process group: the parent decides what a signal means (drain, then a hard
|
|
39
|
+
// stop on the second), so a role never acts on SIGINT itself. SIGTERM (the supervisor's stop) drains, once.
|
|
40
|
+
process.on("SIGINT", () => { });
|
|
41
|
+
process.on("SIGTERM", () => stop(false));
|
|
42
|
+
// The parent gone (its channel closed): drain, as a stop would. The worker's own grace timer bounds it; past a minute
|
|
43
|
+
// with nobody left to stop it, the role stops its work outright and ends, never running on as an orphan.
|
|
44
|
+
process.once("disconnect", () => {
|
|
45
|
+
stop(false);
|
|
46
|
+
setTimeout(() => { stop(true); setTimeout(() => process.exit(1), 5_000).unref(); }, 60_000).unref();
|
|
47
|
+
});
|
|
48
|
+
const start = await new Promise(resolve => {
|
|
49
|
+
const timer = setTimeout(() => resolve(null), 60_000);
|
|
50
|
+
process.once("message", value => { clearTimeout(timer); resolve(value); });
|
|
51
|
+
});
|
|
52
|
+
const checked = startMessage(start);
|
|
53
|
+
// The role named on the command line (for a process listing) and the one started must be the same.
|
|
54
|
+
if (checked === null || checked.role !== process.argv[2]) {
|
|
55
|
+
process.stderr.write("The start message from `toolroll up` was not understood.\n");
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
process.on("message", value => {
|
|
59
|
+
const message = parentMessage(value);
|
|
60
|
+
if (message === null)
|
|
61
|
+
return;
|
|
62
|
+
if (message.type === "hard-stop") {
|
|
63
|
+
stop(true);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const one of handlers)
|
|
67
|
+
one(message);
|
|
68
|
+
});
|
|
69
|
+
const { runUpRole } = await import("./operate.js");
|
|
70
|
+
let code = 1;
|
|
71
|
+
try {
|
|
72
|
+
code = await runUpRole(checked, port);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
port.send({ type: "fatal", detail: error instanceof Error ? error.message : String(error) });
|
|
76
|
+
}
|
|
77
|
+
// Everything this role owned is closed; leave promptly even if a stray handle lingers.
|
|
78
|
+
if (process.connected)
|
|
79
|
+
process.disconnect?.();
|
|
80
|
+
setTimeout(() => process.exit(code), 200).unref();
|
|
81
|
+
process.exitCode = code;
|
|
@@ -29,7 +29,10 @@ export function effectiveCheckFacts(raw, facts) {
|
|
|
29
29
|
if (facts === null || coveragePasses(facts.coverage))
|
|
30
30
|
return { result: raw, facts };
|
|
31
31
|
const exitCode = raw.configured && raw.ran ? raw.exitCode : null;
|
|
32
|
-
|
|
32
|
+
// A check that stopped before it could report keeps its own failure (it timed out, Toolroll stopped it): that is why.
|
|
33
|
+
if (facts.coverage === "no-result" && raw.configured && !raw.ran)
|
|
34
|
+
return { result: raw, facts };
|
|
35
|
+
return { result: { configured: true, ran: false, attemptFailed: true, failure: facts.coverage === "no-result" ? "no-result" : "coverage-incomplete" }, facts: { ...facts, check: { ...facts.check, exitCode } } };
|
|
33
36
|
}
|
|
34
37
|
export function sealVerificationReceipt(store, root, runId, head, command, result, now, reusedFrom, release) {
|
|
35
38
|
const source = store.getRun(runId);
|
|
@@ -142,6 +145,15 @@ export function verificationEvidence(store, root, runId) {
|
|
|
142
145
|
const sha = (capture) => { const found = artifacts.filter(a => a.kind === "structured-output" && a.capture === capture); return found.length === 1 ? found[0].sha256 : null; };
|
|
143
146
|
if (sha(CHECK_REPORT_CAPTURE) !== checked.check.report || sha(CHECK_INVENTORY_CAPTURE) !== checked.check.inventory)
|
|
144
147
|
return fail("The release check's sealed report or inventory changed.");
|
|
148
|
+
// Their bytes too, not only their records (eval-harness review 1, finding 3): a report or inventory the receipt
|
|
149
|
+
// names that is missing, cut short or changed on disk no longer verifies.
|
|
150
|
+
for (const capture of [CHECK_REPORT_CAPTURE, CHECK_INVENTORY_CAPTURE]) {
|
|
151
|
+
const one = artifacts.find(a => a.kind === "structured-output" && a.capture === capture);
|
|
152
|
+
if (one === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
if (one.truncated || one.redacted || one.captureStatus !== "ok" || !readVerifiedArtifact(root, one).ok)
|
|
155
|
+
return fail("The release check's sealed report or inventory no longer verifies.");
|
|
156
|
+
}
|
|
145
157
|
if ((checked.coverage === "composed") !== (checked.composed !== undefined))
|
|
146
158
|
return fail("The release check's coverage and its links disagree.");
|
|
147
159
|
// A check that didn't cover what it owed can never read as a pass.
|
|
@@ -8,7 +8,9 @@ const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_
|
|
|
8
8
|
// Who is watching an agent right now: renewed by every open page's keep-alive, never news.
|
|
9
9
|
'agent_view',
|
|
10
10
|
// A run's How it ran, derived from its own records on first view (run-efficiency.ts): a cache, never news.
|
|
11
|
-
'run_efficiency'
|
|
11
|
+
'run_efficiency',
|
|
12
|
+
// v137: lifecycle heartbeats, latency timelines and the continuous copy's record are bookkeeping, never news.
|
|
13
|
+
'service_lifecycle', 'latency_timeline', 'live_backup']);
|
|
12
14
|
const QUIET = {
|
|
13
15
|
runner: ['heartbeat_at'], claim: ['heartbeat_at', 'expires_at'], watch_lease: ['heartbeat_at', 'expires_at'],
|
|
14
16
|
watch_episode: ['ticks'], provider_readiness: ['observed_at'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.78",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"test:slow": "vitest run .slow.test",
|
|
12
12
|
"test:all": "vitest run",
|
|
13
13
|
"test:attacks": "vitest run --config test/attacks/vitest.config.ts",
|
|
14
|
+
"test:git": "vitest run test/attacks/checkout-git- test/attacks/fence-git-views test/attacks/repository-context-git test/attacks/discover-dirty src/teardown-git.test.ts src/worktree.slow.test.ts",
|
|
14
15
|
"check:quick": "node scripts/quick-check.mjs",
|
|
15
16
|
"dev": "tsx src/cli.ts",
|
|
16
17
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.browser.json",
|