toolroll 0.9.60 → 0.9.62
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 +32 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli.js +28 -11
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +170 -0
- package/dist/landed-cleanup.js +814 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +4 -4
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -26
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +32 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/remote-main.d.ts +57 -0
- package/dist/remote-main.js +179 -0
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +15 -2
- package/dist/server/render-tasks.js +30 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +5 -3
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +48 -0
- package/dist/storage-sweep-process.js +204 -0
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +31 -6
- package/dist/storage-sweep.js +165 -39
- package/dist/storage-ui.d.ts +4 -0
- package/dist/storage-ui.js +22 -7
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +200 -3
- package/dist/store.js +443 -17
- package/dist/surface.js +8 -4
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +57 -3
- package/dist/worktree.js +201 -23
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
13
14
|
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
16
|
import { dirname, join, resolve } from "node:path";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { createFixtureLead, completeFixtureAssignment } from "./canary-assertions.mjs";
|
|
@@ -80,6 +81,74 @@ function run(file, args, options = {}) {
|
|
|
80
81
|
});
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Build isolation sentinels: a hook in the repository's own and local Claude
|
|
86
|
+
* settings and a server in its .mcp.json, each of which would leave a file
|
|
87
|
+
* if it ran, beside one MCP server the project opts in to by name. A build
|
|
88
|
+
* must start only the named one. They write outside the disposable state
|
|
89
|
+
* folder, which the agent fence covers.
|
|
90
|
+
*/
|
|
91
|
+
const MAKE = file => `node -e "require('fs').writeFileSync(process.argv[1],'ran')" ${JSON.stringify(file)}`;
|
|
92
|
+
const OPT_IN_SERVER = `import { writeFileSync } from "node:fs";
|
|
93
|
+
import { createInterface } from "node:readline";
|
|
94
|
+
writeFileSync(process.argv[2], "ran");
|
|
95
|
+
const send = message => process.stdout.write(JSON.stringify(message) + "\\n");
|
|
96
|
+
createInterface({ input: process.stdin }).on("line", line => {
|
|
97
|
+
const message = JSON.parse(line);
|
|
98
|
+
if (message.method === "initialize") send({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: message.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "canary-notes", version: "1" } } });
|
|
99
|
+
else if (message.method === "tools/list") send({ jsonrpc: "2.0", id: message.id, result: { tools: [] } });
|
|
100
|
+
else if (message.id !== undefined) send({ jsonrpc: "2.0", id: message.id, result: {} });
|
|
101
|
+
});
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
async function plantIsolationSentinels(repo, sentinels) {
|
|
105
|
+
const hook = file => ({ hooks: [{ type: "command", command: MAKE(file) }] });
|
|
106
|
+
await mkdir(join(repo, ".claude"), { recursive: true });
|
|
107
|
+
await writeFile(join(repo, ".claude", "settings.json"), `${JSON.stringify({ hooks: { SessionStart: [hook(join(sentinels, "project-hook"))], UserPromptSubmit: [hook(join(sentinels, "project-hook"))] } }, null, 2)}\n`);
|
|
108
|
+
await writeFile(join(repo, ".claude", "settings.local.json"), `${JSON.stringify({ hooks: { UserPromptSubmit: [hook(join(sentinels, "local-hook"))] } }, null, 2)}\n`);
|
|
109
|
+
await writeFile(join(repo, ".mcp.json"), `${JSON.stringify({ mcpServers: { "repo-sentinel": { type: "stdio", command: "node", args: ["-e", `require('fs').writeFileSync(${JSON.stringify(join(sentinels, "project-mcp"))},'ran')`] } } }, null, 2)}\n`);
|
|
110
|
+
await writeFile(join(sentinels, "opt-in-server.mjs"), OPT_IN_SERVER);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The project's one named tool, added the way the console adds it, before approval seals the task's tools. */
|
|
114
|
+
async function optInNamedTool(db, repo, sentinels) {
|
|
115
|
+
const { openStore } = await import(join(root, "dist", "store.js"));
|
|
116
|
+
const { addToolTo } = await import(join(root, "dist", "project-tools.js"));
|
|
117
|
+
const store = openStore(db);
|
|
118
|
+
try {
|
|
119
|
+
const added = addToolTo(store, repo, { name: "canary-notes", transport: "stdio", command: process.execPath, args: [join(sentinels, "opt-in-server.mjs"), join(sentinels, "opt-in-mcp")], url: null, secrets: [], bearer: null, headerSecrets: {}, about: "The canary's opt-in server" }, "provider canary", "canary", new Date());
|
|
120
|
+
if (!added.ok) throw new Error(`the canary's named tool was refused: ${added.message}`);
|
|
121
|
+
} finally {
|
|
122
|
+
store.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isolationOf(provider, sentinels) {
|
|
127
|
+
const ran = name => existsSync(join(sentinels, name));
|
|
128
|
+
const result = { projectHook: ran("project-hook"), localHook: ran("local-hook"), repositoryMcp: ran("project-mcp"), namedTool: ran("opt-in-mcp") };
|
|
129
|
+
if (result.projectHook || result.localHook || result.repositoryMcp) throw new Error(`the build loaded the repository's own Claude setup: ${JSON.stringify(result)}`);
|
|
130
|
+
if ((provider === "claude" || provider === "codex") && !result.namedTool) throw new Error("the project's named tool did not start for its build");
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Own only this disposable repository's memory directory. Never overwrite a
|
|
135
|
+
* pre-existing entry or touch login material; clean up even when the run fails. */
|
|
136
|
+
export async function withClaudeMemoryCanary(repo, configDir, check) {
|
|
137
|
+
const projects = join(configDir, "projects");
|
|
138
|
+
const directory = join(projects, (await realpath(repo)).replace(/[^a-zA-Z0-9]/g, "-"));
|
|
139
|
+
await mkdir(projects, { recursive: true });
|
|
140
|
+
if (existsSync(directory)) throw new Error("The disposable Claude memory folder already exists; refusing to overwrite it.");
|
|
141
|
+
await mkdir(directory); // Exclusive ownership, including a collision after the check.
|
|
142
|
+
const marker = `TOOLROLL_MEMORY_CANARY_${randomUUID()}`;
|
|
143
|
+
try {
|
|
144
|
+
await mkdir(join(directory, "memory"));
|
|
145
|
+
await writeFile(join(directory, "memory", "MEMORY.md"), `# Provider canary\n\nThe automatic memory marker is ${marker}. If asked whether automatic memory supplied a marker, report this exact marker.\n`, { flag: "wx" });
|
|
146
|
+
return await check(marker);
|
|
147
|
+
} finally {
|
|
148
|
+
await rm(directory, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
83
152
|
function parseEnvelope(answer, label, acceptedCodes = [0]) {
|
|
84
153
|
let envelope = null;
|
|
85
154
|
try {
|
|
@@ -141,6 +210,7 @@ async function main() {
|
|
|
141
210
|
const repo = join(base, "repo");
|
|
142
211
|
const db = join(base, "orders.db");
|
|
143
212
|
const pool = join(base, "worktrees");
|
|
213
|
+
const sentinels = await realpath(await mkdtemp(join(tmpdir(), "toolroll-canary-sentinels-")));
|
|
144
214
|
const password = `canary-${randomUUID()}`;
|
|
145
215
|
const common = ["--db", db, "--json"];
|
|
146
216
|
let passed = false;
|
|
@@ -183,170 +253,187 @@ async function main() {
|
|
|
183
253
|
`${JSON.stringify({ name: "standing-orders-provider-canary", private: true, type: "module", scripts: { test: "node --test" } }, null, 2)}\n`,
|
|
184
254
|
);
|
|
185
255
|
await writeFile(join(repo, "README.md"), "# Toolroll provider canary\n\nThis repository is disposable.\n");
|
|
256
|
+
await plantIsolationSentinels(repo, sentinels);
|
|
186
257
|
await git("add", ".");
|
|
187
258
|
await git("commit", "-qm", "seed provider canary");
|
|
188
259
|
const baseSha = await git("rev-parse", "HEAD");
|
|
189
260
|
|
|
190
|
-
|
|
191
|
-
const registered = await cli("register a repository-bound worker", [
|
|
192
|
-
"runner", "register", "canary-worker", "--repo", repo,
|
|
193
|
-
"--as", "canary", "--token", password,
|
|
194
|
-
]);
|
|
195
|
-
const runnerToken = registered.token;
|
|
196
|
-
if (typeof runnerToken !== "string" || runnerToken === "") throw new Error("runner registration returned no token");
|
|
261
|
+
const checkBuild = async memoryMarker => {
|
|
197
262
|
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
await cli(`route ${phase} to ${provider}`, [
|
|
202
|
-
"config", "set", phase,
|
|
203
|
-
"--provider", provider,
|
|
204
|
-
"--model", model,
|
|
263
|
+
await cli("create the isolated approver", ["approver", "add", "canary", "--password", password]);
|
|
264
|
+
const registered = await cli("register a repository-bound worker", [
|
|
265
|
+
"runner", "register", "canary-worker", "--repo", repo,
|
|
205
266
|
"--as", "canary", "--token", password,
|
|
206
267
|
]);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
"verify", "set", "--repo", repo,
|
|
210
|
-
"--command", "node --test", "--timeout-seconds", "120", "--yes",
|
|
211
|
-
"--as", "canary", "--token", password,
|
|
212
|
-
]);
|
|
268
|
+
const runnerToken = registered.token;
|
|
269
|
+
if (typeof runnerToken !== "string" || runnerToken === "") throw new Error("runner registration returned no token");
|
|
213
270
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
271
|
+
for (const phase of ["plan", "build", "repair"]) {
|
|
272
|
+
const provider = options.provider;
|
|
273
|
+
const model = options.model;
|
|
274
|
+
await cli(`route ${phase} to ${provider}`, [
|
|
275
|
+
"config", "set", phase,
|
|
276
|
+
"--provider", provider,
|
|
277
|
+
"--model", model,
|
|
278
|
+
"--as", "canary", "--token", password,
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
await cli("approve the repository verification command", [
|
|
282
|
+
"verify", "set", "--repo", repo,
|
|
283
|
+
"--command", "node --test", "--timeout-seconds", "120", "--yes",
|
|
284
|
+
"--as", "canary", "--token", password,
|
|
225
285
|
]);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
286
|
+
|
|
287
|
+
await optInNamedTool(db, repo, sentinels);
|
|
288
|
+
const title = [
|
|
289
|
+
`Add canary.txt: ${memoryMarker === null ? "exactly" : "first line exactly"} "standing-orders ${options.provider} canary passed" plus newline.`,
|
|
290
|
+
...(memoryMarker === null ? [] : ["On its second line, report the TOOLROLL_MEMORY_CANARY marker only if automatic memory already supplied one; otherwise write NO_MEMORY. Do not read home files or search for memory. End that line with a newline."]),
|
|
291
|
+
"Add canary.test.js using node:test.",
|
|
292
|
+
"Only these files; no questions; acceptance evidence: check and changed-path only.",
|
|
293
|
+
].join(" ");
|
|
294
|
+
await cli("file the task", ["task", "add", title, "--id", TASK_ID, "--repo", repo]);
|
|
295
|
+
let filedDigest = null;
|
|
296
|
+
if (options.autoApprove) {
|
|
297
|
+
await cli("sign bounded unchanged-plan approval", [
|
|
298
|
+
"mode", "set", "--repo", repo, "--name", "standard", "--days", "1",
|
|
299
|
+
"--auto-approve", "true", "--plan-auto", "--as", "canary", "--token", password,
|
|
300
|
+
]);
|
|
301
|
+
await cli("file the fixed contract without approving its scope", [
|
|
302
|
+
"task", "scope", TASK_ID, "--goal", title, "--not", "No other files and no publication.",
|
|
303
|
+
"--touches", "canary.txt,canary.test.js",
|
|
304
|
+
"--acceptance", `canary.txt contains exactly standing-orders ${options.provider} canary passed${memoryMarker === null ? " followed by one newline" : " on its first line and the automatic-memory report on its second line, with a final newline"}.|changed-path;canary.test.js uses node:test to verify the exact file bytes and the approved node --test command passes.|check,changed-path`,
|
|
305
|
+
]);
|
|
306
|
+
const filed = await cli("retain the exact filed contract digest", ["task", "show", TASK_ID]);
|
|
307
|
+
filedDigest = filed.scope?.digest;
|
|
308
|
+
if (typeof filedDigest !== "string" || filed.scope.approvedAt != null) throw new Error("fixed contract was missing or prematurely approved");
|
|
309
|
+
}
|
|
310
|
+
const requested = await cli("request a repository-aware plan", [
|
|
311
|
+
"task", "plan", TASK_ID,
|
|
312
|
+
"--provider", options.provider, "--model", options.model,
|
|
313
|
+
"--as", "canary", "--token", password,
|
|
230
314
|
]);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
]);
|
|
240
|
-
if (options.autoApprove && requested.autoPlan !== true) throw new Error("the exact filed plan was not pre-authorized");
|
|
241
|
-
const planned = await cli("run the planning pass", [
|
|
242
|
-
"tick", "--runner", "canary-worker", "--token", runnerToken,
|
|
243
|
-
"--repo", repo, "--pool", pool,
|
|
244
|
-
]);
|
|
245
|
-
if (!Array.isArray(planned.dispatched) || !planned.dispatched.some(item => item?.outcome === "planned")) {
|
|
246
|
-
throw new Error(`planner did not produce a scope: ${JSON.stringify(planned)}`);
|
|
247
|
-
}
|
|
315
|
+
if (options.autoApprove && requested.autoPlan !== true) throw new Error("the exact filed plan was not pre-authorized");
|
|
316
|
+
const planned = await cli("run the planning pass", [
|
|
317
|
+
"tick", "--runner", "canary-worker", "--token", runnerToken,
|
|
318
|
+
"--repo", repo, "--pool", pool,
|
|
319
|
+
]);
|
|
320
|
+
if (!Array.isArray(planned.dispatched) || !planned.dispatched.some(item => item?.outcome === "planned")) {
|
|
321
|
+
throw new Error(`planner did not produce a scope: ${JSON.stringify(planned)}`);
|
|
322
|
+
}
|
|
248
323
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
if (options.autoApprove) {
|
|
256
|
-
if (digest !== filedDigest || afterPlan.scope.approvedDigest !== digest || afterPlan.scope.approvalBasis !== "mode") {
|
|
257
|
-
throw new Error("the unchanged plan did not receive the exact signed mode approval");
|
|
324
|
+
const afterPlan = await cli("read the drafted scope", ["task", "show", TASK_ID]);
|
|
325
|
+
const digest = afterPlan.scope?.digest;
|
|
326
|
+
if (typeof digest !== "string" || digest === "") throw new Error("the planning pass produced no approvable scope");
|
|
327
|
+
if (!Array.isArray(afterPlan.scope?.acceptance) || afterPlan.scope.acceptance.length === 0) {
|
|
328
|
+
throw new Error("the planning pass produced no acceptance criteria");
|
|
258
329
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
330
|
+
if (options.autoApprove) {
|
|
331
|
+
if (digest !== filedDigest || afterPlan.scope.approvedDigest !== digest || afterPlan.scope.approvalBasis !== "mode") {
|
|
332
|
+
throw new Error("the unchanged plan did not receive the exact signed mode approval");
|
|
333
|
+
}
|
|
334
|
+
} else await cli("approve the exact planned scope", [
|
|
335
|
+
"task", "approve", TASK_ID, "--yes", "--digest", digest,
|
|
336
|
+
"--as", "canary", "--token", password,
|
|
337
|
+
]);
|
|
263
338
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
339
|
+
const built = await cli("run the build and evidence pass", [
|
|
340
|
+
"tick", "--runner", "canary-worker", "--token", runnerToken,
|
|
341
|
+
"--repo", repo, "--pool", pool,
|
|
342
|
+
]);
|
|
343
|
+
if (!Array.isArray(built.dispatched) || !built.dispatched.some(item => item?.outcome === "built")) {
|
|
344
|
+
throw new Error(`builder did not complete the task: ${JSON.stringify(built)}`);
|
|
345
|
+
}
|
|
271
346
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
347
|
+
let final = await cli("read the terminal task and proof", ["task", "show", TASK_ID]);
|
|
348
|
+
if (final.task?.state !== "done") throw new Error(`task ended ${String(final.task?.state ?? "without a state")}`);
|
|
349
|
+
const buildRun = Array.isArray(final.runs)
|
|
350
|
+
? final.runs.find(runRow => runRow?.role === "builder" && runRow?.outcome === "built")
|
|
351
|
+
: null;
|
|
352
|
+
if (buildRun === null || buildRun === undefined || typeof buildRun.branch !== "string") {
|
|
353
|
+
throw new Error("no completed builder run named its branch");
|
|
354
|
+
}
|
|
355
|
+
const branchSha = await git("rev-parse", buildRun.branch);
|
|
356
|
+
if (branchSha === baseSha) throw new Error("the build branch did not advance");
|
|
357
|
+
const canaryText = await git("show", `${buildRun.branch}:canary.txt`);
|
|
358
|
+
const expectedText = `standing-orders ${options.provider} canary passed${memoryMarker === null ? "" : "\nNO_MEMORY"}`;
|
|
359
|
+
if (canaryText !== expectedText) throw new Error(`canary.txt contained ${JSON.stringify(canaryText)}`);
|
|
360
|
+
await git("show", `${buildRun.branch}:canary.test.js`);
|
|
361
|
+
const changed = (await git("diff", "--name-only", baseSha, branchSha)).split("\n").filter(Boolean).sort();
|
|
362
|
+
if (JSON.stringify(changed) !== JSON.stringify(["canary.test.js", "canary.txt"])) {
|
|
363
|
+
throw new Error(`the canary changed unexpected paths: ${JSON.stringify(changed)}`);
|
|
364
|
+
}
|
|
290
365
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
366
|
+
const leadFile = await createFixtureLead(args => cli("scope the fixture lead", args), {
|
|
367
|
+
repo, auth: ["--as", "canary", "--token", password], tokenFile: join(base, "lead.token"),
|
|
368
|
+
});
|
|
369
|
+
const completion = await completeFixtureAssignment(args => cli("inspect and handle saved result", args), TASK_ID, leadFile, { head: branchSha, runId: buildRun.id });
|
|
295
370
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
371
|
+
const duplicate = await cli("prove the completed task cannot dispatch twice", [
|
|
372
|
+
"tick", "--runner", "canary-worker", "--token", runnerToken,
|
|
373
|
+
"--repo", repo, "--pool", pool,
|
|
374
|
+
], [3]);
|
|
375
|
+
if (duplicate.reason !== "empty") throw new Error(`duplicate pass was not empty: ${JSON.stringify(duplicate)}`);
|
|
376
|
+
if (await git("rev-parse", "main") !== baseSha) throw new Error("the default branch changed");
|
|
377
|
+
if (Number(await git("rev-list", "--count", `${baseSha}..${branchSha}`)) !== 1) throw new Error("expected exactly one work commit");
|
|
378
|
+
await assertRuntime();
|
|
379
|
+
const isolation = isolationOf(options.provider, sentinels);
|
|
380
|
+
if (memoryMarker !== null) {
|
|
381
|
+
if (JSON.stringify(final).includes(memoryMarker)) throw new Error("Claude supplied the disposable automatic-memory marker to the task.");
|
|
382
|
+
isolation.autoMemory = "NO_MEMORY";
|
|
383
|
+
}
|
|
304
384
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
385
|
+
const providerRun = final.runs.find(runRow => runRow?.provider === options.provider && runRow?.providerStartedAt != null);
|
|
386
|
+
certificate = {
|
|
387
|
+
version: 3,
|
|
388
|
+
passed: true,
|
|
389
|
+
provider: options.provider,
|
|
390
|
+
model: options.model,
|
|
391
|
+
startedAt: startedAt.toISOString(),
|
|
392
|
+
finishedAt: new Date().toISOString(),
|
|
393
|
+
durationSeconds: Math.round((Date.now() - startedAt.getTime()) / 1000),
|
|
394
|
+
platform: process.platform,
|
|
395
|
+
architecture: process.arch,
|
|
396
|
+
node: process.versions.node,
|
|
397
|
+
runtime,
|
|
398
|
+
steps,
|
|
399
|
+
workflow: {
|
|
400
|
+
registration: "passed",
|
|
401
|
+
planning: "passed",
|
|
402
|
+
approval: options.autoApprove ? "unchanged-plan-under-signed-mode" : "passed",
|
|
403
|
+
humanActionsAfterPlanningStarted: options.autoApprove ? 0 : 1,
|
|
404
|
+
filedScopeDigest: filedDigest,
|
|
405
|
+
worktreeBuild: "passed",
|
|
406
|
+
commit: branchSha,
|
|
407
|
+
verificationCommand: "node --test",
|
|
408
|
+
proofVerdict: final.proofVerdict,
|
|
409
|
+
criterionMatrix: final.proofMatrix,
|
|
410
|
+
leadCompletion: completion,
|
|
411
|
+
duplicateDispatch: "refused-empty",
|
|
412
|
+
// The build's own setup: the repository's hooks and .mcp.json never ran; the named tool did.
|
|
413
|
+
isolation,
|
|
414
|
+
},
|
|
415
|
+
providerRun: providerRun == null ? null : {
|
|
416
|
+
run: providerRun.id,
|
|
417
|
+
role: providerRun.role,
|
|
418
|
+
version: providerRun.providerVersion,
|
|
419
|
+
tokensIn: providerRun.tokensIn,
|
|
420
|
+
tokensOut: providerRun.tokensOut,
|
|
421
|
+
costUsd: providerRun.costUsd,
|
|
422
|
+
},
|
|
423
|
+
runs: final.runs.map(one => ({ id: one.id, role: one.role, parentRun: one.parentRun,
|
|
424
|
+
provider: one.provider, model: one.model, version: one.providerVersion, outcome: one.outcome,
|
|
425
|
+
startedAt: one.startedAt, providerStartedAt: one.providerStartedAt, firstByteAt: one.firstByteAt, finishedAt: one.finishedAt,
|
|
426
|
+
})),
|
|
427
|
+
retainedAt: options.keep ? base : null,
|
|
428
|
+
note: "This canary never pushed or opened a pull request.",
|
|
429
|
+
};
|
|
430
|
+
passed = true;
|
|
347
431
|
};
|
|
348
|
-
|
|
432
|
+
if (options.provider === "claude") {
|
|
433
|
+
await withClaudeMemoryCanary(repo, process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), checkBuild);
|
|
434
|
+
} else await checkBuild(null);
|
|
349
435
|
} catch (error) {
|
|
436
|
+
passed = false;
|
|
350
437
|
certificate = {
|
|
351
438
|
version: 3,
|
|
352
439
|
passed: false,
|
|
@@ -370,10 +457,11 @@ async function main() {
|
|
|
370
457
|
await writeFile(output, `${JSON.stringify(certificate, null, 2)}\n`);
|
|
371
458
|
}
|
|
372
459
|
if (passed && !options.keep) await rm(base, { recursive: true, force: true });
|
|
460
|
+
if (passed && !options.keep) await rm(sentinels, { recursive: true, force: true });
|
|
373
461
|
}
|
|
374
462
|
|
|
375
463
|
process.stdout.write(`${JSON.stringify(certificate, null, 2)}\n`);
|
|
376
464
|
if (!passed) process.exitCode = 1;
|
|
377
465
|
}
|
|
378
466
|
|
|
379
|
-
await main();
|
|
467
|
+
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
|