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.
Files changed (128) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/agent-board.d.ts +208 -0
  3. package/dist/agent-board.js +454 -0
  4. package/dist/agent-fence.d.ts +19 -0
  5. package/dist/agent-fence.js +100 -8
  6. package/dist/agent-pulse.d.ts +43 -0
  7. package/dist/agent-pulse.js +214 -0
  8. package/dist/agent-turns.d.ts +52 -0
  9. package/dist/agent-turns.js +137 -0
  10. package/dist/agents-board.d.ts +101 -0
  11. package/dist/agents-board.js +225 -0
  12. package/dist/assignment-adapters.d.ts +1 -0
  13. package/dist/assignment-status.d.ts +1 -1
  14. package/dist/assignment.d.ts +4 -0
  15. package/dist/assignment.js +8 -6
  16. package/dist/browser/workspace.css +1 -1
  17. package/dist/browser/workspace.js +29 -25
  18. package/dist/browser-crew.js +2 -0
  19. package/dist/browser-workspace.d.ts +79 -1
  20. package/dist/browser-workspace.js +1 -0
  21. package/dist/builder.d.ts +8 -0
  22. package/dist/builder.js +113 -13
  23. package/dist/chat-channel.js +9 -0
  24. package/dist/chat-core.d.ts +6 -0
  25. package/dist/chat-core.js +8 -0
  26. package/dist/chat-delivery.js +22 -0
  27. package/dist/chat-messages.js +8 -1
  28. package/dist/chat-quiet.js +1 -0
  29. package/dist/chat-voice.d.ts +2 -0
  30. package/dist/chat-voice.js +2 -0
  31. package/dist/checkout-cleanup.d.ts +3 -0
  32. package/dist/checkout-cleanup.js +11 -2
  33. package/dist/cli.js +28 -11
  34. package/dist/contracts/console-api.d.ts +3 -1
  35. package/dist/contracts/console-api.js +5 -1
  36. package/dist/contracts/lead-tools.d.ts +16 -2
  37. package/dist/contracts/lead-tools.js +5 -1
  38. package/dist/desktop-update.d.ts +1 -1
  39. package/dist/evidence-pack.js +1 -1
  40. package/dist/evidence.d.ts +6 -0
  41. package/dist/evidence.js +14 -3
  42. package/dist/exec.d.ts +32 -0
  43. package/dist/exec.js +229 -10
  44. package/dist/invoke.js +110 -18
  45. package/dist/landed-cleanup.d.ts +170 -0
  46. package/dist/landed-cleanup.js +814 -0
  47. package/dist/lead-cli.js +22 -19
  48. package/dist/lead-contract.d.ts +1 -1
  49. package/dist/lead-contract.js +4 -4
  50. package/dist/lead-doors.js +45 -1
  51. package/dist/lead-tools.js +40 -7
  52. package/dist/live-input.d.ts +68 -0
  53. package/dist/live-input.js +254 -0
  54. package/dist/live.d.ts +1 -1
  55. package/dist/live.js +4 -2
  56. package/dist/maintenance.d.ts +2 -0
  57. package/dist/maintenance.js +5 -2
  58. package/dist/operate-remote-arguments.js +5 -1
  59. package/dist/operate-remote.js +1 -1
  60. package/dist/operate.d.ts +1 -1
  61. package/dist/operate.js +249 -26
  62. package/dist/orphan-sweep.d.ts +2 -0
  63. package/dist/orphan-sweep.js +5 -3
  64. package/dist/project-admission.d.ts +10 -6
  65. package/dist/project-admission.js +33 -11
  66. package/dist/project-delete-ui.d.ts +12 -0
  67. package/dist/project-delete-ui.js +32 -2
  68. package/dist/project-tools.d.ts +7 -11
  69. package/dist/project-tools.js +13 -37
  70. package/dist/provider.d.ts +65 -8
  71. package/dist/provider.js +80 -21
  72. package/dist/remote-main.d.ts +57 -0
  73. package/dist/remote-main.js +179 -0
  74. package/dist/result-completion.d.ts +7 -0
  75. package/dist/result-completion.js +9 -0
  76. package/dist/runner.d.ts +4 -2
  77. package/dist/runner.js +3 -3
  78. package/dist/serve.js +19 -1
  79. package/dist/server/live.d.ts +3 -0
  80. package/dist/server/live.js +35 -2
  81. package/dist/server/pages.js +59 -35
  82. package/dist/server/render-chat.js +12 -0
  83. package/dist/server/render-tasks.d.ts +15 -2
  84. package/dist/server/render-tasks.js +30 -7
  85. package/dist/server/route-table.js +4 -1
  86. package/dist/server/settings.js +5 -3
  87. package/dist/server/task-views.js +11 -0
  88. package/dist/server/tasks.js +63 -0
  89. package/dist/shared-deps.d.ts +9 -4
  90. package/dist/shared-deps.js +89 -13
  91. package/dist/staged-runtimes.d.ts +16 -4
  92. package/dist/staged-runtimes.js +160 -38
  93. package/dist/steer-live.d.ts +19 -0
  94. package/dist/steer-live.js +93 -0
  95. package/dist/steer-status.d.ts +8 -2
  96. package/dist/steer-status.js +19 -5
  97. package/dist/storage-sweep-child.d.ts +1 -0
  98. package/dist/storage-sweep-child.js +76 -0
  99. package/dist/storage-sweep-process.d.ts +48 -0
  100. package/dist/storage-sweep-process.js +204 -0
  101. package/dist/storage-sweep-schedule.d.ts +30 -0
  102. package/dist/storage-sweep-schedule.js +164 -0
  103. package/dist/storage-sweep.d.ts +31 -6
  104. package/dist/storage-sweep.js +165 -39
  105. package/dist/storage-ui.d.ts +4 -0
  106. package/dist/storage-ui.js +22 -7
  107. package/dist/storage.d.ts +6 -0
  108. package/dist/storage.js +35 -0
  109. package/dist/store.d.ts +200 -3
  110. package/dist/store.js +443 -17
  111. package/dist/surface.js +8 -4
  112. package/dist/sweep-quarantine.d.ts +28 -0
  113. package/dist/sweep-quarantine.js +153 -0
  114. package/dist/task-base.d.ts +35 -0
  115. package/dist/task-base.js +51 -0
  116. package/dist/task-status.d.ts +2 -0
  117. package/dist/task-status.js +5 -1
  118. package/dist/test-temp.d.ts +6 -3
  119. package/dist/test-temp.js +15 -9
  120. package/dist/toolroll-update.js +4 -2
  121. package/dist/work-index.d.ts +1 -0
  122. package/dist/work-index.js +32 -12
  123. package/dist/work-summary.d.ts +2 -2
  124. package/dist/workspace-revision.js +3 -1
  125. package/dist/worktree.d.ts +57 -3
  126. package/dist/worktree.js +201 -23
  127. package/package.json +1 -1
  128. 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
- await cli("create the isolated approver", ["approver", "add", "canary", "--password", password]);
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
- for (const phase of ["plan", "build", "repair"]) {
199
- const provider = options.provider;
200
- const model = options.model;
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
- await cli("approve the repository verification command", [
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
- const title = [
215
- `Add canary.txt: exactly "standing-orders ${options.provider} canary passed" plus newline.`,
216
- "Add canary.test.js using node:test.",
217
- "Only these files; no questions; acceptance evidence: check and changed-path only.",
218
- ].join(" ");
219
- await cli("file the task", ["task", "add", title, "--id", TASK_ID, "--repo", repo]);
220
- let filedDigest = null;
221
- if (options.autoApprove) {
222
- await cli("sign bounded unchanged-plan approval", [
223
- "mode", "set", "--repo", repo, "--name", "standard", "--days", "1",
224
- "--auto-approve", "true", "--plan-auto", "--as", "canary", "--token", password,
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
- await cli("file the fixed contract without approving its scope", [
227
- "task", "scope", TASK_ID, "--goal", title, "--not", "No other files and no publication.",
228
- "--touches", "canary.txt,canary.test.js",
229
- "--acceptance", `canary.txt contains exactly standing-orders ${options.provider} canary passed followed by one 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`,
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
- const filed = await cli("retain the exact filed contract digest", ["task", "show", TASK_ID]);
232
- filedDigest = filed.scope?.digest;
233
- if (typeof filedDigest !== "string" || filed.scope.approvedAt != null) throw new Error("fixed contract was missing or prematurely approved");
234
- }
235
- const requested = await cli("request a repository-aware plan", [
236
- "task", "plan", TASK_ID,
237
- "--provider", options.provider, "--model", options.model,
238
- "--as", "canary", "--token", password,
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
- const afterPlan = await cli("read the drafted scope", ["task", "show", TASK_ID]);
250
- const digest = afterPlan.scope?.digest;
251
- if (typeof digest !== "string" || digest === "") throw new Error("the planning pass produced no approvable scope");
252
- if (!Array.isArray(afterPlan.scope?.acceptance) || afterPlan.scope.acceptance.length === 0) {
253
- throw new Error("the planning pass produced no acceptance criteria");
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
- } else await cli("approve the exact planned scope", [
260
- "task", "approve", TASK_ID, "--yes", "--digest", digest,
261
- "--as", "canary", "--token", password,
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
- const built = await cli("run the build and evidence pass", [
265
- "tick", "--runner", "canary-worker", "--token", runnerToken,
266
- "--repo", repo, "--pool", pool,
267
- ]);
268
- if (!Array.isArray(built.dispatched) || !built.dispatched.some(item => item?.outcome === "built")) {
269
- throw new Error(`builder did not complete the task: ${JSON.stringify(built)}`);
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
- let final = await cli("read the terminal task and proof", ["task", "show", TASK_ID]);
273
- if (final.task?.state !== "done") throw new Error(`task ended ${String(final.task?.state ?? "without a state")}`);
274
- const buildRun = Array.isArray(final.runs)
275
- ? final.runs.find(runRow => runRow?.role === "builder" && runRow?.outcome === "built")
276
- : null;
277
- if (buildRun === null || buildRun === undefined || typeof buildRun.branch !== "string") {
278
- throw new Error("no completed builder run named its branch");
279
- }
280
- const branchSha = await git("rev-parse", buildRun.branch);
281
- if (branchSha === baseSha) throw new Error("the build branch did not advance");
282
- const canaryText = await git("show", `${buildRun.branch}:canary.txt`);
283
- const expectedText = `standing-orders ${options.provider} canary passed`;
284
- if (canaryText !== expectedText) throw new Error(`canary.txt contained ${JSON.stringify(canaryText)}`);
285
- await git("show", `${buildRun.branch}:canary.test.js`);
286
- const changed = (await git("diff", "--name-only", baseSha, branchSha)).split("\n").filter(Boolean).sort();
287
- if (JSON.stringify(changed) !== JSON.stringify(["canary.test.js", "canary.txt"])) {
288
- throw new Error(`the canary changed unexpected paths: ${JSON.stringify(changed)}`);
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
- const leadFile = await createFixtureLead(args => cli("scope the fixture lead", args), {
292
- repo, auth: ["--as", "canary", "--token", password], tokenFile: join(base, "lead.token"),
293
- });
294
- const completion = await completeFixtureAssignment(args => cli("inspect and handle saved result", args), TASK_ID, leadFile, { head: branchSha, runId: buildRun.id });
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
- const duplicate = await cli("prove the completed task cannot dispatch twice", [
297
- "tick", "--runner", "canary-worker", "--token", runnerToken,
298
- "--repo", repo, "--pool", pool,
299
- ], [3]);
300
- if (duplicate.reason !== "empty") throw new Error(`duplicate pass was not empty: ${JSON.stringify(duplicate)}`);
301
- if (await git("rev-parse", "main") !== baseSha) throw new Error("the default branch changed");
302
- if (Number(await git("rev-list", "--count", `${baseSha}..${branchSha}`)) !== 1) throw new Error("expected exactly one work commit");
303
- await assertRuntime();
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
- const providerRun = final.runs.find(runRow => runRow?.provider === options.provider && runRow?.providerStartedAt != null);
306
- certificate = {
307
- version: 3,
308
- passed: true,
309
- provider: options.provider,
310
- model: options.model,
311
- startedAt: startedAt.toISOString(),
312
- finishedAt: new Date().toISOString(),
313
- durationSeconds: Math.round((Date.now() - startedAt.getTime()) / 1000),
314
- platform: process.platform,
315
- architecture: process.arch,
316
- node: process.versions.node,
317
- runtime,
318
- steps,
319
- workflow: {
320
- registration: "passed",
321
- planning: "passed",
322
- approval: options.autoApprove ? "unchanged-plan-under-signed-mode" : "passed",
323
- humanActionsAfterPlanningStarted: options.autoApprove ? 0 : 1,
324
- filedScopeDigest: filedDigest,
325
- worktreeBuild: "passed",
326
- commit: branchSha,
327
- verificationCommand: "node --test",
328
- proofVerdict: final.proofVerdict,
329
- criterionMatrix: final.proofMatrix,
330
- leadCompletion: completion,
331
- duplicateDispatch: "refused-empty",
332
- },
333
- providerRun: providerRun == null ? null : {
334
- run: providerRun.id,
335
- role: providerRun.role,
336
- version: providerRun.providerVersion,
337
- tokensIn: providerRun.tokensIn,
338
- tokensOut: providerRun.tokensOut,
339
- costUsd: providerRun.costUsd,
340
- },
341
- runs: final.runs.map(one => ({ id: one.id, role: one.role, parentRun: one.parentRun,
342
- provider: one.provider, model: one.model, version: one.providerVersion, outcome: one.outcome,
343
- startedAt: one.startedAt, providerStartedAt: one.providerStartedAt, firstByteAt: one.firstByteAt, finishedAt: one.finishedAt,
344
- })),
345
- retainedAt: options.keep ? base : null,
346
- note: "This canary never pushed or opened a pull request.",
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
- passed = true;
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();