toolroll 0.9.65 → 0.9.68

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 (264) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +20 -2
  3. package/dist/acp-client.js +1 -3
  4. package/dist/agent-board.d.ts +0 -1
  5. package/dist/agent-board.js +0 -1
  6. package/dist/agent-fence.d.ts +69 -2
  7. package/dist/agent-fence.js +294 -11
  8. package/dist/agentconfig.js +4 -1
  9. package/dist/batch-checks.js +3 -2
  10. package/dist/browser/THIRD_PARTY_NOTICES.txt +46 -0
  11. package/dist/browser/terminal.js +134 -23
  12. package/dist/browser/workspace.css +2 -2
  13. package/dist/browser/workspace.js +31 -29
  14. package/dist/builder.d.ts +4 -0
  15. package/dist/builder.js +56 -37
  16. package/dist/canonical-json.d.ts +14 -0
  17. package/dist/canonical-json.js +36 -0
  18. package/dist/chat-actions.js +1 -2
  19. package/dist/chat-admission.d.ts +19 -0
  20. package/dist/chat-admission.js +22 -0
  21. package/dist/chat-channel.js +1 -1
  22. package/dist/chat-delivery.js +0 -1
  23. package/dist/chat-follow.d.ts +17 -0
  24. package/dist/chat-follow.js +32 -0
  25. package/dist/chat-messages.js +0 -1
  26. package/dist/chat-settings-page.d.ts +23 -0
  27. package/dist/chat-settings-page.js +41 -0
  28. package/dist/claim.js +3 -6
  29. package/dist/claude-print.d.ts +12 -0
  30. package/dist/claude-print.js +34 -0
  31. package/dist/cli-http.d.ts +0 -7
  32. package/dist/cli-http.js +0 -21
  33. package/dist/cli.d.ts +3 -10
  34. package/dist/cli.js +15 -24
  35. package/dist/coding-provider.js +2 -1
  36. package/dist/coding-workspace.js +1 -1
  37. package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
  38. package/dist/connect-cli.js +331 -0
  39. package/dist/containment.d.ts +0 -2
  40. package/dist/containment.js +2 -6
  41. package/dist/contracts/chat-actions.d.ts +0 -1
  42. package/dist/contracts/chat-content.d.ts +0 -4
  43. package/dist/contracts/chat-content.js +0 -2
  44. package/dist/contracts/chat-tables.d.ts +0 -1
  45. package/dist/contracts/cli.d.ts +0 -62
  46. package/dist/contracts/cli.js +0 -4
  47. package/dist/contracts/coding-workspace.js +1 -1
  48. package/dist/contracts/console-api.d.ts +1 -0
  49. package/dist/contracts/console-api.js +1 -0
  50. package/dist/contracts/contract.js +1 -1
  51. package/dist/contracts/flow-card.d.ts +0 -1
  52. package/dist/contracts/flow-propose.d.ts +0 -1
  53. package/dist/contracts/flow-send.js +1 -1
  54. package/dist/contracts/gateway-tools.d.ts +0 -1
  55. package/dist/contracts/integration-metadata.js +1 -1
  56. package/dist/contracts/lead-tools.d.ts +0 -1
  57. package/dist/contracts/memory-pass.d.ts +0 -1
  58. package/dist/contracts/memory-pass.js +1 -1
  59. package/dist/contracts/plan.js +1 -1
  60. package/dist/contracts/project-knowledge.js +1 -1
  61. package/dist/contracts/project-skills.d.ts +0 -1
  62. package/dist/contracts/proof.js +1 -1
  63. package/dist/contracts/recipes.js +1 -1
  64. package/dist/contracts/scope.js +1 -1
  65. package/dist/contracts/scout-report.js +1 -1
  66. package/dist/contracts/teams-callback.d.ts +0 -1
  67. package/dist/converse.d.ts +0 -6
  68. package/dist/converse.js +0 -19
  69. package/dist/daemon.js +3 -2
  70. package/dist/decision.d.ts +1 -3
  71. package/dist/deprecations.d.ts +12 -4
  72. package/dist/deprecations.js +27 -4
  73. package/dist/desktop-update-recovery.js +1 -1
  74. package/dist/desktop-update.js +3 -2
  75. package/dist/discord-chat.js +4 -15
  76. package/dist/discord-settings.d.ts +2 -6
  77. package/dist/discord-settings.js +10 -38
  78. package/dist/discord.js +12 -23
  79. package/dist/evidence.d.ts +0 -1
  80. package/dist/evidence.js +0 -3
  81. package/dist/exec.d.ts +0 -9
  82. package/dist/exec.js +3 -81
  83. package/dist/flow-actions.js +0 -3
  84. package/dist/flow-draft.d.ts +1 -3
  85. package/dist/flow-draft.js +15 -28
  86. package/dist/flow-scripts.d.ts +1 -8
  87. package/dist/flow-scripts.js +0 -13
  88. package/dist/flow-send.js +1 -1
  89. package/dist/flow-share.d.ts +0 -2
  90. package/dist/flow-share.js +1 -6
  91. package/dist/flow-steps.js +1 -1
  92. package/dist/flow-triggers.d.ts +0 -1
  93. package/dist/flow-triggers.js +1 -2
  94. package/dist/flow-unattended.js +1 -12
  95. package/dist/flows.js +1 -1
  96. package/dist/health-cli.d.ts +2 -2
  97. package/dist/health-cli.js +1 -1
  98. package/dist/invoke.d.ts +18 -3
  99. package/dist/invoke.js +142 -17
  100. package/dist/is-record.d.ts +2 -0
  101. package/dist/is-record.js +2 -0
  102. package/dist/lead-doors.d.ts +1 -5
  103. package/dist/lead-doors.js +0 -4
  104. package/dist/lead-tools.js +3 -6
  105. package/dist/live-detail.d.ts +0 -1
  106. package/dist/live-detail.js +0 -6
  107. package/dist/live-terminal.d.ts +24 -13
  108. package/dist/live-terminal.js +81 -43
  109. package/dist/live.d.ts +0 -1
  110. package/dist/live.js +1 -2
  111. package/dist/mcp-core.js +0 -2
  112. package/dist/memory-pass.js +1 -1
  113. package/dist/model-catalog.js +1 -1
  114. package/dist/modes.js +1 -12
  115. package/dist/operate.d.ts +2 -0
  116. package/dist/operate.js +436 -745
  117. package/dist/password-bearer.d.ts +0 -2
  118. package/dist/password-bearer.js +0 -4
  119. package/dist/peek-cli.js +2 -1
  120. package/dist/peek.d.ts +2 -26
  121. package/dist/peek.js +2 -168
  122. package/dist/phase-routing.d.ts +0 -4
  123. package/dist/phase-routing.js +2 -21
  124. package/dist/planner-source.d.ts +2 -1
  125. package/dist/planner-source.js +2 -12
  126. package/dist/planner.js +2 -14
  127. package/dist/process-tree.d.ts +0 -2
  128. package/dist/process-tree.js +0 -10
  129. package/dist/project-delete-ui.d.ts +6 -0
  130. package/dist/project-delete-ui.js +20 -1
  131. package/dist/project-delete.d.ts +0 -2
  132. package/dist/project-delete.js +1 -3
  133. package/dist/project-learning.d.ts +0 -14
  134. package/dist/project-skills.d.ts +0 -1
  135. package/dist/project-skills.js +0 -14
  136. package/dist/project-tools.js +2 -10
  137. package/dist/provider.d.ts +0 -17
  138. package/dist/provider.js +0 -26
  139. package/dist/pulls.d.ts +0 -2
  140. package/dist/pulls.js +0 -9
  141. package/dist/release-adapters.d.ts +9 -0
  142. package/dist/release-adapters.js +81 -4
  143. package/dist/release.d.ts +7 -1
  144. package/dist/release.js +20 -7
  145. package/dist/remote-exec.d.ts +3 -2
  146. package/dist/remote-exec.js +3 -2
  147. package/dist/repos.d.ts +0 -5
  148. package/dist/repos.js +2 -10
  149. package/dist/repository-context.d.ts +0 -2
  150. package/dist/repository-context.js +0 -4
  151. package/dist/restart-certification.d.ts +0 -2
  152. package/dist/restart-certification.js +1 -5
  153. package/dist/result-follow-ups.d.ts +1 -4
  154. package/dist/result-follow-ups.js +4 -8
  155. package/dist/result-review.d.ts +0 -2
  156. package/dist/result-review.js +0 -2
  157. package/dist/retention.d.ts +0 -1
  158. package/dist/retention.js +0 -1
  159. package/dist/review-context.d.ts +0 -1
  160. package/dist/review-context.js +1 -12
  161. package/dist/reviewer.d.ts +0 -16
  162. package/dist/reviewer.js +0 -18
  163. package/dist/run-closeout.js +2 -1
  164. package/dist/scope.d.ts +13 -0
  165. package/dist/scope.js +11 -18
  166. package/dist/scout-browser.js +2 -1
  167. package/dist/serve.js +3 -2
  168. package/dist/server/chrome.d.ts +10 -1
  169. package/dist/server/chrome.js +45 -4
  170. package/dist/server/console.css +60 -137
  171. package/dist/server/flows.js +2 -2
  172. package/dist/server/http.d.ts +2 -0
  173. package/dist/server/http.js +5 -0
  174. package/dist/server/pages.js +2 -2
  175. package/dist/server/people-tokens.js +3 -4
  176. package/dist/server/remote.js +1 -6
  177. package/dist/server/render-chat.d.ts +9 -2
  178. package/dist/server/render-chat.js +53 -7
  179. package/dist/server/render-pages.d.ts +28 -3
  180. package/dist/server/render-pages.js +78 -9
  181. package/dist/server/render-results.d.ts +0 -6
  182. package/dist/server/render-results.js +3 -9
  183. package/dist/server/render-tasks.js +6 -6
  184. package/dist/server/route-table.d.ts +0 -4
  185. package/dist/server/route-table.js +1 -6
  186. package/dist/server/settings.js +43 -22
  187. package/dist/server/task-views.js +1 -1
  188. package/dist/server/tasks.js +10 -7
  189. package/dist/setup-guide.d.ts +2 -2
  190. package/dist/skills.d.ts +0 -6
  191. package/dist/skills.js +0 -8
  192. package/dist/slack-chat.d.ts +1 -7
  193. package/dist/slack-chat.js +5 -18
  194. package/dist/slack-settings.d.ts +2 -6
  195. package/dist/slack-settings.js +10 -38
  196. package/dist/slack.js +12 -23
  197. package/dist/sleep.d.ts +2 -0
  198. package/dist/sleep.js +2 -0
  199. package/dist/store.d.ts +11 -74
  200. package/dist/store.js +36 -179
  201. package/dist/structured-output.d.ts +0 -1
  202. package/dist/structured-output.js +0 -1
  203. package/dist/subagent-work.d.ts +0 -2
  204. package/dist/subagent-work.js +0 -2
  205. package/dist/subagents.d.ts +1 -3
  206. package/dist/subagents.js +20 -33
  207. package/dist/subscription-chat.d.ts +1 -3
  208. package/dist/subscription-chat.js +2 -3
  209. package/dist/task-sizing.d.ts +1 -4
  210. package/dist/task-sizing.js +3 -10
  211. package/dist/teams-chat.js +4 -11
  212. package/dist/teams.js +12 -19
  213. package/dist/teardown-git.d.ts +22 -0
  214. package/dist/teardown-git.js +136 -2
  215. package/dist/telegram-lead.js +2 -2
  216. package/dist/telegram.js +1 -1
  217. package/dist/token-notices.d.ts +0 -11
  218. package/dist/token-notices.js +1 -5
  219. package/dist/tool-broker.d.ts +12 -0
  220. package/dist/tool-broker.js +66 -2
  221. package/dist/tool-secret-store.d.ts +0 -3
  222. package/dist/tool-secret-store.js +2 -11
  223. package/dist/toolroll-update.d.ts +0 -2
  224. package/dist/toolroll-update.js +2 -2
  225. package/dist/unfenced-test-support.d.ts +2 -0
  226. package/dist/unfenced-test-support.js +11 -0
  227. package/dist/verification-evidence.d.ts +0 -1
  228. package/dist/verification-evidence.js +0 -1
  229. package/dist/webhooks.d.ts +0 -1
  230. package/dist/webhooks.js +0 -1
  231. package/dist/workspace-ui.d.ts +0 -8
  232. package/dist/workspace-ui.js +1 -6
  233. package/dist/writes-mode.d.ts +9 -0
  234. package/dist/writes-mode.js +8 -0
  235. package/package.json +5 -1
  236. package/dist/contracts/native-census.d.ts +0 -42
  237. package/dist/contracts/native-census.js +0 -36
  238. package/dist/process-recovery-anchor.d.ts +0 -75
  239. package/dist/process-recovery-anchor.js +0 -188
  240. package/dist/process-recovery-coalition.d.ts +0 -25
  241. package/dist/process-recovery-coalition.js +0 -105
  242. package/dist/process-recovery-eligibility.d.ts +0 -179
  243. package/dist/process-recovery-eligibility.js +0 -174
  244. package/dist/process-recovery-native.d.ts +0 -161
  245. package/dist/process-recovery-native.js +0 -595
  246. package/dist/process-recovery-provenance.d.ts +0 -163
  247. package/dist/process-recovery-provenance.js +0 -292
  248. package/dist/process-recovery-services.d.ts +0 -56
  249. package/dist/process-recovery-services.js +0 -191
  250. package/dist/process-recovery-settlement.d.ts +0 -32
  251. package/dist/process-recovery-settlement.js +0 -133
  252. package/dist/process-recovery.d.ts +0 -41
  253. package/dist/process-recovery.js +0 -91
  254. package/dist/session-brief.d.ts +0 -8
  255. package/dist/session-brief.js +0 -24
  256. package/dist/session-cli.d.ts +0 -42
  257. package/dist/session-cli.js +0 -271
  258. package/dist/session-contract.d.ts +0 -183
  259. package/dist/session-contract.js +0 -122
  260. package/dist/session-remote.d.ts +0 -19
  261. package/dist/session-remote.js +0 -86
  262. package/dist/session-service.d.ts +0 -22
  263. package/dist/session-service.js +0 -157
  264. package/dist/team-cli.js +0 -692
package/dist/builder.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { WritesMode } from "./writes-mode.js";
1
2
  import type { LaunchNetwork } from "./invoke.js";
2
3
  import type { ProviderId } from "./contracts/provider.js";
3
4
  import { type PreparedEvidence } from "./prepared-evidence.js";
@@ -186,6 +187,9 @@ export declare function redactSecretAssignments(text: string): string;
186
187
  /** A build's network (egress.ts): the one its approval sealed, or — sealed before network policies existed — the
187
188
  * narrowed legacy one (provider hosts and the repository's registries, nothing added since). */
188
189
  export declare function launchNetworkOf(store: Store, taskRef: number, profile: ExecutionProfile): LaunchNetwork;
190
+ /** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
191
+ * its fence allows, as it was approved. */
192
+ export declare function launchWritesOf(profile: ExecutionProfile): WritesMode;
189
193
  export declare function proveApprovedProfile(scope: Scope | null, given: {
190
194
  provider: string;
191
195
  model: string | undefined;
package/dist/builder.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { legacyNetwork, repositoryEcosystems } from "./egress.js";
2
+ import { checkoutGit } from "./teardown-git.js";
2
3
  import { isAcpProvider } from "./acp-agents.js";
3
4
  import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
4
5
  import { checkCommandFor, effectiveCheckLevel, recordRunCheckLevel } from "./check-levels.js";
@@ -41,9 +42,9 @@ import { PREPARED_EVIDENCE_FILE, PREPARED_EVIDENCE_GIT, preparedScreenshotMatche
41
42
  * watchdog and a high runaway-turn breaker still stop a pathological loop;
42
43
  * repair remains narrowly time-bounded because its job is narrowly scoped.
43
44
  */
44
- import { existsSync, unlinkSync, writeFileSync } from "node:fs";
45
+ import { existsSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
45
46
  import { homedir } from "node:os";
46
- import { join } from "node:path";
47
+ import { join, resolve } from "node:path";
47
48
  import { run, runClaudeStreamJsonl, runOwnerTag, streamsInput } from "./exec.js";
48
49
  import { stopRequestedFor, stopWords, underStopWatch } from "./task-control.js";
49
50
  import { witnessedRunner } from "./process-custody.js";
@@ -58,7 +59,7 @@ import { heartbeat as runnerHeartbeat } from "./runner.js";
58
59
  import { MARKER as LEASE_MARKER } from "./worktree.js";
59
60
  import { parseDecision, parseHandoff, repairPrompt, HEADLESS_RULE, HANDOFF_CONCLUSION_CAP, HANDOFF_ITEM_CAP, HANDOFF_LIST_CAP, HANDOFF_PAYLOAD_CAP } from "./decision.js";
60
61
  import { createHash, randomBytes, randomUUID } from "node:crypto";
61
- import { invokeAgent } from "./invoke.js";
62
+ import { fencedCommands, invokeAgent } from "./invoke.js";
62
63
  import { TELEGRAM_TOKEN_ENVS } from "./names.js";
63
64
  import { OPENROUTER_ENV_KEY, auditOf, ALL_CREDENTIAL_ENV } from "./provider.js";
64
65
  import { liveLogContext } from "./live-detail.js";
@@ -73,7 +74,7 @@ import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot
73
74
  import { attemptKeyOf, closeRunQuestions, noteOpenTool, recordAgentTurn } from "./agent-board.js";
74
75
  import { openStatusFile, pulseAgentBoard, removeStatusFile, statusBrief } from "./agent-pulse.js";
75
76
  import { withAgentTurns } from "./agent-turns.js";
76
- import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, artifactManifestOnly, sameDiffStatFacts } from "./proof.js";
77
+ import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, sameDiffStatFacts } from "./proof.js";
77
78
  import { parseHandoffArtifact } from "./contracts/handoff.js";
78
79
  import { captureReviewContext } from "./review-context.js";
79
80
  import { authoritySnapshotDigest, classifyRevisionAuthority, isMilestoneRegression, milestonesOf, parseExecutionPlanDocument, parsePlanRevisionProposal, parseProgressSnapshot, renderExecutionPlanDocument, PROGRESS_LIMITS, REVISION_LIMITS, } from "./plan.js";
@@ -237,6 +238,43 @@ export function launchNetworkOf(store, taskRef, profile) {
237
238
  const repo = store.refForId(taskRef)?.repo ?? null;
238
239
  return { policy: legacyNetwork(profile.provider, repo === null ? [] : repositoryEcosystems(repo)) };
239
240
  }
241
+ /** Asking a checkout which branch it is on: reads no file a build wrote and runs nothing. */
242
+ const BRANCH_QUERIES = [
243
+ ["--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD"], ["--no-optional-locks", "symbolic-ref", "--short", "-q", "HEAD"],
244
+ ];
245
+ /**
246
+ * The plane's git as a build runs it: hardened (teardown-git.ts checkoutGit) for every call, failing closed, but one —
247
+ * asking the project's own main checkout which branch it is on (its config is the operator's, and the query runs
248
+ * nothing). An injected runner (`request.git`: tests only; production always runs git itself) is used as given.
249
+ */
250
+ function buildCheckoutGit(given, worktree, mainCheckout = null) {
251
+ if (given !== run)
252
+ return given;
253
+ const hardened = checkoutGit(given);
254
+ const real = (path) => { try {
255
+ return realpathSync(resolve(path));
256
+ }
257
+ catch {
258
+ return null;
259
+ } };
260
+ const trusted = mainCheckout === null ? null : real(mainCheckout);
261
+ const own = real(worktree);
262
+ return (file, args, options) => {
263
+ const at = options?.cwd === undefined ? null : real(options.cwd);
264
+ const query = BRANCH_QUERIES.some(one => one.length === args.length && one.every((word, index) => args[index] === word));
265
+ return trusted !== null && at === trusted && at !== own && query ? given(file, args, options) : hardened(file, args, options);
266
+ };
267
+ }
268
+ /** A run's setup and check commands, in its build's fence, writing where the approval this build runs under sealed
269
+ * (never the scope as it may since have been rewritten; invoke.ts fencedCommands). */
270
+ function commandRunner(store, runId, profile, inner) {
271
+ return fencedCommands(store, runId, launchWritesOf(profile), inner);
272
+ }
273
+ /** Where a build may write (writes-mode.ts): what its approval sealed, or — sealed before write allowlists — anywhere
274
+ * its fence allows, as it was approved. */
275
+ export function launchWritesOf(profile) {
276
+ return profile.writes ?? "anywhere";
277
+ }
240
278
  export function proveApprovedProfile(scope, given) {
241
279
  // The raw terms first (raw authority repair): a scope whose stored terms
242
280
  // do not read back exactly proves nothing — not the filtered reading.
@@ -331,7 +369,9 @@ export function proveApprovedProfile(scope, given) {
331
369
  * that forgot one is exactly the caller this is protecting against.
332
370
  */
333
371
  export async function build(store, request) {
334
- const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude", model, maxTurns = DEFAULT_MAX_TURNS, timeoutMs = DEFAULT_BUILD_TIMEOUT_MS, agent, git = run, } = request;
372
+ const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude", maxTurns = DEFAULT_MAX_TURNS, timeoutMs = DEFAULT_BUILD_TIMEOUT_MS, agent, git: givenGit = run, } = request;
373
+ // The plane's own git on this build's checkout runs hardened, and never where the checkout's config names a command.
374
+ const git = buildCheckoutGit(givenGit, worktree, store.getWorktree(worktree)?.repo ?? null);
335
375
  const scope = store.getScope(taskId);
336
376
  const approval = approvalOf(scope);
337
377
  if (!approval.approved) {
@@ -625,7 +665,7 @@ export async function build(store, request) {
625
665
  // Setup runs under the stop watch (v52), owned by this run: an
626
666
  // operator's stop ends the setup's process group and the attempt
627
667
  // settles as interrupted below, never as a setup failure.
628
- const made = await underStopWatch(store, request.runId, () => runWithIsolatedDatabase(witnessedRunner(store, request.runId, request.clock ?? (() => now), runSetup), shell.file, shell.args, {
668
+ const made = await underStopWatch(store, request.runId, () => runWithIsolatedDatabase(witnessedRunner(store, request.runId, request.clock ?? (() => now), commandRunner(store, request.runId, effective.profile, runSetup)), shell.file, shell.args, {
629
669
  cwd: worktree,
630
670
  timeoutMs: setupWanted.timeoutMs,
631
671
  processGroup: true,
@@ -1310,6 +1350,7 @@ export async function build(store, request) {
1310
1350
  ...watchdogsOf(effective.profile, effective.timeoutMs),
1311
1351
  omitEnv: AGENT_ENV_DENYLIST,
1312
1352
  network: launchNetworkOf(store, request.taskRef, effective.profile),
1353
+ writes: launchWritesOf(effective.profile),
1313
1354
  ...(agent === undefined ? {} : { runner: agent }),
1314
1355
  onSpawn: (pid) => { spawned = true; request.onProviderSpawn?.(pid); },
1315
1356
  onStreamEvent: (event) => liveLog.observe(event),
@@ -1486,7 +1527,7 @@ export async function settleProviderOutcome(captured, result) {
1486
1527
  }
1487
1528
  catch { /* The commit gate still excludes it. */ }
1488
1529
  }
1489
- const { store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef, runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, proof, clock } = captured;
1530
+ const { store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef, runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, clock } = captured;
1490
1531
  // THE STOP FENCE after the provider (v52): a stop recorded while the
1491
1532
  // agent ran ends the attempt HERE, before any handoff is read, any park
1492
1533
  // sealed, or any commit made — whatever the process wrote is preserved
@@ -1774,7 +1815,7 @@ export async function settleProviderOutcome(captured, result) {
1774
1815
  };
1775
1816
  if (!eligible())
1776
1817
  throw Error("The observation source or execution authority changed.");
1777
- const execute = (file, args, options) => underStopWatch(store, request.runId, () => runWithIsolatedDatabase(witnessedRunner(store, request.runId, clock, request.verify ?? run), file, args, {
1818
+ const execute = (file, args, options) => underStopWatch(store, request.runId, () => runWithIsolatedDatabase(witnessedRunner(store, request.runId, clock, commandRunner(store, request.runId, captured.effective.profile, request.verify ?? run)), file, args, {
1778
1819
  ...options, processGroup: true, owner: runOwnerTag(store, request.runId), beforeSpawn: eligible,
1779
1820
  onSpawn: pid => request.onProviderSpawn?.(pid), envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST,
1780
1821
  }));
@@ -2234,7 +2275,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2234
2275
  const runVerification = async (label) => {
2235
2276
  const streamed = { stdout: false, stderr: false };
2236
2277
  // The check runs under the stop watch (v52), owned by this run.
2237
- const result = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, verifyRunner), verifyShell.file, verifyShell.args, {
2278
+ const result = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, commandRunner(store, runId, captured.effective.profile, verifyRunner)), verifyShell.file, verifyShell.args, {
2238
2279
  cwd: worktree,
2239
2280
  timeoutMs: configured.timeoutMs,
2240
2281
  processGroup: true,
@@ -2276,7 +2317,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2276
2317
  detachShared(worktree, depsRoot);
2277
2318
  const label = "Own install · approved project setup (package.json or lockfile changed)";
2278
2319
  const setupShell = approvedCommandShell(live.command);
2279
- const installed = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, request.setup ?? run), setupShell.file, setupShell.args, {
2320
+ const installed = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, commandRunner(store, runId, captured.effective.profile, request.setup ?? run)), setupShell.file, setupShell.args, {
2280
2321
  cwd: worktree,
2281
2322
  timeoutMs: live.timeoutMs,
2282
2323
  processGroup: true,
@@ -2295,7 +2336,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2295
2336
  checkLog.push(attemptLog(label, live.command, installed));
2296
2337
  if (installed.notFound || installed.timedOut || installed.code !== 0)
2297
2338
  return "own-install-failed";
2298
- return await sealedTreeState(request.git ?? run) === "clean" ? null : "setup-changed-files";
2339
+ return await sealedTreeState(buildCheckoutGit(request.git ?? run, worktree)) === "clean" ? null : "setup-changed-files";
2299
2340
  };
2300
2341
  const ownInstall = await ownInstallIfChanged();
2301
2342
  const first = ownInstall === null ? await runVerification("Project check · attempt 1") : null;
@@ -2339,7 +2380,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2339
2380
  verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "custody-lost" };
2340
2381
  }
2341
2382
  else {
2342
- const gitRunner = request.git ?? run;
2383
+ const gitRunner = buildCheckoutGit(request.git ?? run, worktree);
2343
2384
  const beforeSetup = await sealedTreeState(gitRunner);
2344
2385
  if (beforeSetup === "head-moved") {
2345
2386
  verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "checkout-moved" };
@@ -2366,7 +2407,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2366
2407
  else {
2367
2408
  const setupRunner = request.setup ?? run;
2368
2409
  const setupShell = approvedCommandShell(liveBeforeSetup.command);
2369
- const restored = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, setupRunner), setupShell.file, setupShell.args, {
2410
+ const restored = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, commandRunner(store, runId, captured.effective.profile, setupRunner)), setupShell.file, setupShell.args, {
2370
2411
  cwd: worktree,
2371
2412
  timeoutMs: liveBeforeSetup.timeoutMs,
2372
2413
  processGroup: true,
@@ -2653,6 +2694,7 @@ async function resumeUnhandedWork(captured, sessionId) {
2653
2694
  ...watchdogsOf(effective.profile, effective.timeoutMs),
2654
2695
  omitEnv: AGENT_ENV_DENYLIST,
2655
2696
  network: launchNetworkOf(store, request.taskRef, effective.profile),
2697
+ writes: launchWritesOf(effective.profile),
2656
2698
  ...(agent === undefined ? {} : { runner: agent }),
2657
2699
  ...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
2658
2700
  clock,
@@ -2867,7 +2909,7 @@ async function ingestPark(args) {
2867
2909
  ? watchdogsOf(args.profile, args.profile.repairTimeoutSeconds * 1000, true)
2868
2910
  : { timeoutMs: args.profile !== undefined ? args.profile.repairTimeoutSeconds * 1000 : REPAIR_TIMEOUT_MS }),
2869
2911
  omitEnv: AGENT_ENV_DENYLIST,
2870
- ...(args.profile === undefined ? {} : { network: launchNetworkOf(store, request.taskRef, args.profile) }),
2912
+ ...(args.profile === undefined ? {} : { network: launchNetworkOf(store, request.taskRef, args.profile), writes: launchWritesOf(args.profile) }),
2871
2913
  ...(agent === undefined ? {} : { runner: agent }),
2872
2914
  ...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
2873
2915
  clock,
@@ -3492,29 +3534,6 @@ async function stripEvidenceImages(git, worktree) {
3492
3534
  const rest = await git(GIT, ["--no-optional-locks", "diff", "--cached", "--quiet"], { cwd: worktree });
3493
3535
  return { ok: true, notice: evidenceImagesNotice(images), staged: rest.code !== 0 };
3494
3536
  }
3495
- /**
3496
- * `claude --output-format json` returns an envelope: the result is the
3497
- * summary, and the session id is what lets a malformed park be repaired by
3498
- * resuming the conversation that produced it instead of paying for a new one.
3499
- */
3500
- function envelope(stdout) {
3501
- try {
3502
- const parsed = JSON.parse(stdout);
3503
- return {
3504
- summary: typeof parsed.result === "string" && parsed.result.trim() !== ""
3505
- ? parsed.result.trim()
3506
- : "unattended build",
3507
- ...(typeof parsed.session_id === "string" && parsed.session_id !== ""
3508
- ? { sessionId: parsed.session_id }
3509
- : {}),
3510
- };
3511
- }
3512
- catch {
3513
- // An agent that printed something unparseable still did work — and a
3514
- // session nobody can name simply cannot be resumed.
3515
- return { summary: "unattended build" };
3516
- }
3517
- }
3518
3537
  /**
3519
3538
  * What a non-zero agent exit means, in words a person can act on: the
3520
3539
  * harness's own result line names the ending (a turn ceiling, an error
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Deterministic JSON for digests and equality. These bytes are sealed into approvals and stored digests, so neither
3
+ * function may change its output for any input. The two differ on purpose and must stay distinct:
4
+ *
5
+ * - `canonicalJson` drops object keys whose value is undefined and returns JSON.stringify's own answer for a bare
6
+ * value (an undefined array element therefore renders as nothing: `[,1]`).
7
+ * - `canonicalJsonUndefinedAsNull` keeps every key and renders an undefined (or otherwise unserializable) value as
8
+ * `null` — the planner contract's bytes.
9
+ *
10
+ * Both sort object keys by UTF-16 code units and keep arrays in order.
11
+ */
12
+ export declare function canonicalJson(value: unknown): string;
13
+ /** Stable JSON for hashing and equality only — no filling, no trimming; undefined reads as null. */
14
+ export declare function canonicalJsonUndefinedAsNull(value: unknown): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Deterministic JSON for digests and equality. These bytes are sealed into approvals and stored digests, so neither
3
+ * function may change its output for any input. The two differ on purpose and must stay distinct:
4
+ *
5
+ * - `canonicalJson` drops object keys whose value is undefined and returns JSON.stringify's own answer for a bare
6
+ * value (an undefined array element therefore renders as nothing: `[,1]`).
7
+ * - `canonicalJsonUndefinedAsNull` keeps every key and renders an undefined (or otherwise unserializable) value as
8
+ * `null` — the planner contract's bytes.
9
+ *
10
+ * Both sort object keys by UTF-16 code units and keep arrays in order.
11
+ */
12
+ export function canonicalJson(value) {
13
+ if (Array.isArray(value))
14
+ return `[${value.map(canonicalJson).join(",")}]`;
15
+ if (value !== null && typeof value === "object") {
16
+ const entries = Object.entries(value)
17
+ .filter(([, v]) => v !== undefined)
18
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
19
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
20
+ return `{${entries.join(",")}}`;
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+ /** Stable JSON for hashing and equality only — no filling, no trimming; undefined reads as null. */
25
+ export function canonicalJsonUndefinedAsNull(value) {
26
+ if (Array.isArray(value))
27
+ return `[${value.map(canonicalJsonUndefinedAsNull).join(",")}]`;
28
+ if (value !== null && typeof value === "object") {
29
+ const body = value;
30
+ return `{${Object.keys(body)
31
+ .sort()
32
+ .map(key => `${JSON.stringify(key)}:${canonicalJsonUndefinedAsNull(body[key])}`)
33
+ .join(",")}}`;
34
+ }
35
+ return JSON.stringify(value) ?? "null";
36
+ }
@@ -16,7 +16,6 @@ import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-kno
16
16
  import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
17
17
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
18
18
  import { readAuthModeStrict } from "./keys.js";
19
- import { parseProof } from "./proof.js";
20
19
  import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
21
20
  import { getDecision, recordDecision, retireDecision } from "./project-memory.js";
22
21
  import { resumeTaskStop, taskControlOf } from "./task-control.js";
@@ -26,7 +25,7 @@ import { saveScript, scriptDigest, validateScript } from "./flow-scripts.js";
26
25
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
27
26
  import { chooseFlowCard, flowPersonOf } from "./flow-send.js";
28
27
  import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCard } from "./flow-people.js";
29
- import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, scheduleFromWords, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
28
+ import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
30
29
  import { describeSchedule, parseSchedule } from "./flow-schedule.js";
31
30
  import { dirname } from "node:path";
32
31
  import { handleOf, parseSoul, SOUL_CHARS, SUBAGENT_TEMPLATES, subagentLabel } from "./subagents.js";
@@ -0,0 +1,19 @@
1
+ import type { ChatBinding, ChatState } from "./chat-delivery-state.js";
2
+ import type { ChatEventBody } from "./contracts/chat-content.js";
3
+ import type { ChatApp } from "./contracts/flow.js";
4
+ /**
5
+ * Whether a received message or tap, already proved to come from this app, may be queued — and under which binding.
6
+ * A room (any conversation that isn't a direct message) is taken only while it follows a conversation, for the words
7
+ * that make it follow one, or when it feeds a flow (v89: anyone's message then, as a card with no say over anything).
8
+ * A pairing message comes only from someone unpaired, in a direct message; everything else only from a live paired
9
+ * member in their own conversation or a followed room. Null when it is not taken.
10
+ */
11
+ export declare function admitChatEvent(state: ChatState, app: ChatApp, installation: string, event: {
12
+ kind: "message" | "pair" | "action";
13
+ member: string;
14
+ channel: string;
15
+ isRoom: boolean;
16
+ payload: ChatEventBody;
17
+ }): {
18
+ binding: ChatBinding | null;
19
+ } | null;
@@ -0,0 +1,22 @@
1
+ import { channelInbox } from "./chat-inbox.js";
2
+ import { roomCommand } from "./chat-rooms.js";
3
+ /**
4
+ * Whether a received message or tap, already proved to come from this app, may be queued — and under which binding.
5
+ * A room (any conversation that isn't a direct message) is taken only while it follows a conversation, for the words
6
+ * that make it follow one, or when it feeds a flow (v89: anyone's message then, as a card with no say over anything).
7
+ * A pairing message comes only from someone unpaired, in a direct message; everything else only from a live paired
8
+ * member in their own conversation or a followed room. Null when it is not taken.
9
+ */
10
+ export function admitChatEvent(state, app, installation, event) {
11
+ const { kind, member, channel, isRoom, payload } = event;
12
+ const words = "text" in payload ? payload.text ?? "" : "";
13
+ const inbox = isRoom && kind === "message" ? channelInbox(state.store, app, installation, channel, words) : { watched: false, command: false };
14
+ const roomish = isRoom && (state.room(installation, channel) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
15
+ if (isRoom && !roomish)
16
+ return null;
17
+ const binding = state.bindingFor(installation, member);
18
+ const open = inbox.watched && !inbox.command;
19
+ if (kind === "pair" ? !!binding || isRoom : !open && (!binding || !state.live(binding) || (binding.channel !== channel && !roomish)))
20
+ return null;
21
+ return { binding };
22
+ }
@@ -9,7 +9,7 @@ import { LEAD_TOOL_SCHEMAS } from "./lead-tools.js";
9
9
  import { verifyApproverStanding } from "./principal.js";
10
10
  import { canonicalProject } from "./project.js";
11
11
  import { phoneText, projectLabel } from "./telegram-status.js";
12
- import { CHAT_CONTROLS, chatControlHref, chatResultHref, isChatControl, } from "./chat-controls.js";
12
+ import { CHAT_CONTROLS, chatControlHref, isChatControl, } from "./chat-controls.js";
13
13
  import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
14
14
  export function channelRepos(store, approver, registry) {
15
15
  const seen = new Set();
@@ -23,7 +23,6 @@ import { phoneText, PHONE_HELP, phoneCommand, phoneStatus, phoneTaskView, phoneT
23
23
  import { applyRoomInbound, conversationRow, roomCardApprover, roomCommand, roomGrantAllowed, roomMessagesAfter, roomMessageText, teamDomain } from "./chat-rooms.js";
24
24
  import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
25
25
  import { isShotsKind, resultShotsFor } from "./result-shots.js";
26
- import { CHAT_APP_NAMES } from "./flow-triggers.js";
27
26
  import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
28
27
  import { mergePullRequest } from "./pull-request-flow.js";
29
28
  import { attentionSubjectOf, neverHeld, watchedPing } from "./agent-board.js";
@@ -0,0 +1,17 @@
1
+ /** A pause that ends early, and quietly, when `signal` aborts. */
2
+ export declare const abortablePause: (signal: AbortSignal) => (ms: number) => Promise<void>;
3
+ /**
4
+ * Works the queue while this connection stays the current, leased one: each pass takes in saved events, plans room
5
+ * messages, delivers one part and plans notifications (when they are on), checking it is still current between steps.
6
+ * A pass waits while the connection isn't ready (offline, or the provider asked to retry later).
7
+ */
8
+ export declare function workChatQueue(steps: {
9
+ same: () => boolean;
10
+ ready: () => boolean;
11
+ pause: (ms: number) => Promise<void>;
12
+ notifications: () => boolean;
13
+ process: () => Promise<unknown>;
14
+ rooms: () => Promise<unknown>;
15
+ deliver: () => Promise<unknown>;
16
+ notify: () => Promise<unknown>;
17
+ }): Promise<void>;
@@ -0,0 +1,32 @@
1
+ /** The loop every hosted chat connection (Slack, Discord, Teams) runs over its saved queue. */
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ /** A pause that ends early, and quietly, when `signal` aborts. */
4
+ export const abortablePause = (signal) => async (ms) => {
5
+ try {
6
+ await sleep(ms, undefined, { signal });
7
+ }
8
+ catch {
9
+ /* shutdown */
10
+ }
11
+ };
12
+ /**
13
+ * Works the queue while this connection stays the current, leased one: each pass takes in saved events, plans room
14
+ * messages, delivers one part and plans notifications (when they are on), checking it is still current between steps.
15
+ * A pass waits while the connection isn't ready (offline, or the provider asked to retry later).
16
+ */
17
+ export async function workChatQueue(steps) {
18
+ while (steps.same()) {
19
+ if (!steps.ready()) {
20
+ await steps.pause(1000);
21
+ continue;
22
+ }
23
+ await steps.process();
24
+ if (steps.same())
25
+ await steps.rooms();
26
+ if (steps.same())
27
+ await steps.deliver();
28
+ if (steps.same() && steps.notifications())
29
+ await steps.notify();
30
+ await steps.pause(1000);
31
+ }
32
+ }
@@ -8,7 +8,6 @@
8
8
  *
9
9
  * Imports the store's types only (no runtime cycle): the store builds its chats on it.
10
10
  */
11
- import { join } from "node:path";
12
11
  import { neverHeld, watchedPing } from "./agent-board.js";
13
12
  import { ChatCore } from "./chat-core.js";
14
13
  import { readChatMessageBody, readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
@@ -0,0 +1,23 @@
1
+ import type { Store } from "./store.js";
2
+ import { type Html } from "./html.js";
3
+ import { type ChatIdentity } from "./chat-delivery-state.js";
4
+ export type ChatSettingsOptions = {
5
+ code?: string;
6
+ problem?: string;
7
+ now?: Date;
8
+ who?: string;
9
+ };
10
+ /** What differs between the Slack and Discord settings pages; everything else is one page. */
11
+ export type ChatSettingsPage = {
12
+ provider: "slack" | "discord";
13
+ label: string;
14
+ credentials: ChatIdentity | null;
15
+ /** How to create the app, shown before it is connected. */
16
+ setup: Html;
17
+ /** The connect form's fields around the password. */
18
+ connect: (password: Html) => Html;
19
+ /** The "Access and setup" paragraphs. */
20
+ access: Html;
21
+ };
22
+ /** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
23
+ export declare function chatSettingsHtml(store: Store, dir: string, options: ChatSettingsOptions, page: ChatSettingsPage): Html;
@@ -0,0 +1,41 @@
1
+ import { loadPrimary } from "./webhooks.js";
2
+ import { html, postForm } from "./html.js";
3
+ import { ChatState } from "./chat-delivery-state.js";
4
+ /** Settings → Slack or Discord: connect the app once, then each teammate pairs their own account. */
5
+ export function chatSettingsHtml(store, dir, options, page) {
6
+ const { provider, label, credentials } = page, state = new ChatState(store, provider), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
7
+ const runtime = credentials ? state.runtime(credentials.installation) : null;
8
+ const now = options.now ?? new Date(), live = runtime &&
9
+ runtime.leaseUntil !== null &&
10
+ runtime.leaseUntil > now.toISOString() &&
11
+ runtime.connected;
12
+ const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
13
+ const post = (action, content) => postForm(`/settings/${provider}/${action}`, content, { attrs: { class: "card" } });
14
+ const parts = [];
15
+ if (!credentials) {
16
+ parts.push(html `<p>Manage projects and review results in a private ${label} conversation.</p>`, page.setup, post("connect", page.connect(password)));
17
+ }
18
+ else {
19
+ parts.push(html `<p><strong>${credentials.workspace}</strong> · ${live ? "Connected" : "Waiting for connection"}</p>`);
20
+ if (runtime?.problem)
21
+ parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
22
+ if (!live && !runtime?.problem)
23
+ parts.push(html `<p>Keep the Toolroll worker running to receive ${label} messages.</p>`);
24
+ if (options.code) {
25
+ parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll ${label} app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/${provider}">Check connection</a>`);
26
+ }
27
+ else if (binding && state.live(binding)) {
28
+ const { pending, dropped: failed } = state.partCounts(binding.id);
29
+ parts.push(html `<p>Your ${label} account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === provider
30
+ ? html `<p>Task updates are sent here.</p>`
31
+ : post("alerts", html `<button type="submit">Send task updates here</button>`));
32
+ }
33
+ else {
34
+ parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired ${label} account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
35
+ }
36
+ if (binding && state.live(binding) && !options.code)
37
+ parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
38
+ parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect ${label}</button>`)}</details>`);
39
+ }
40
+ return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>${label}</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}<details><summary>Access and setup</summary>${page.access}</details></section>`;
41
+ }
package/dist/claim.js CHANGED
@@ -749,7 +749,7 @@ export const MAX_STRIKES = 3;
749
749
  function sealInterrupted(store, leaseId, args) {
750
750
  const { runId, taskId, stopRun, now } = args;
751
751
  const db = store.handle;
752
- const run = openAttempt(store, leaseId, runId, "an interruption");
752
+ openAttempt(store, leaseId, runId, "an interruption");
753
753
  // A lease the world moved past still ends its run as interrupted — that IS the honest ending — touching no task state.
754
754
  const fenced = !releaseAs(store, leaseId, "interrupted", now);
755
755
  if (args.message !== undefined && args.message.trim() !== "") {
@@ -797,7 +797,7 @@ function sealInterrupted(store, leaseId, args) {
797
797
  function sealHandBack(store, leaseId, args) {
798
798
  const { runId, taskId, now } = args;
799
799
  const db = store.handle;
800
- const run = openAttempt(store, leaseId, runId, "a hand-back");
800
+ openAttempt(store, leaseId, runId, "a hand-back");
801
801
  // A lease the world moved past still ends its run as interrupted — that IS the honest ending — touching no task state.
802
802
  const fenced = !releaseAs(store, leaseId, "interrupted", now);
803
803
  if (args.message.trim() !== "")
@@ -974,7 +974,6 @@ const PLAN_BACKOFF_MS = [60_000, 2 * 60_000, 4 * 60_000];
974
974
  */
975
975
  function sealPlan(store, leaseId, args) {
976
976
  const { runId, taskId, plan, now } = args;
977
- const db = store.handle;
978
977
  // The ingestion record (contract handoff, task 1) is composed here from
979
978
  // the filed terms and the drafted plan; its file and row land inside the
980
979
  // transaction below, only after the stale-source check admits the draft.
@@ -1199,7 +1198,7 @@ function describeSourceDrift(source, current) {
1199
1198
  */
1200
1199
  function sealRevision(store, leaseId, args) {
1201
1200
  const { runId, taskId, taskRef, revision, now } = args;
1202
- const run = openAttempt(store, leaseId, runId, "a plan revision", "builder");
1201
+ openAttempt(store, leaseId, runId, "a plan revision", "builder");
1203
1202
  const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
1204
1203
  if (stopped !== null)
1205
1204
  return { ok: false, reason: "stopped" };
@@ -1249,7 +1248,6 @@ function sealRevision(store, leaseId, args) {
1249
1248
  */
1250
1249
  function sealPlanFailure(store, leaseId, args) {
1251
1250
  const { runId, taskId, failure: kind, message, now } = args;
1252
- const db = store.handle;
1253
1251
  const run = openAttempt(store, leaseId, runId, "a failure", "planner");
1254
1252
  const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
1255
1253
  if (stopped !== null)
@@ -1331,7 +1329,6 @@ function sealPlanFailure(store, leaseId, args) {
1331
1329
  */
1332
1330
  function sealScout(store, leaseId, args) {
1333
1331
  const { runId, taskId, report, now } = args;
1334
- const db = store.handle;
1335
1332
  const run = openAttempt(store, leaseId, runId, "a report", "scout");
1336
1333
  const stopped = interruptIfStopped(store, { leaseId, runId, taskId, now });
1337
1334
  if (stopped !== null)
@@ -0,0 +1,12 @@
1
+ import type { ExecResult, run } from "./exec.js";
2
+ export type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
3
+ /** No tools, no permission prompts, no MCP servers, nothing kept: a `claude -p` call that can only answer. */
4
+ export declare const CLAUDE_NO_TOOLS_ARGS: readonly string[];
5
+ /** `--model <model>`, or nothing for the CLI's own default. */
6
+ export declare const claudeModelArgs: (model: string) => string[];
7
+ /** What a failed call's output says when this computer's Claude isn't signed in. */
8
+ export declare const CLAUDE_SIGNED_OUT: RegExp;
9
+ /** `claude -p --output-format json` stdout as its one object, or null. */
10
+ export declare function claudeJsonBody(stdout: string, cap: number): Record<string, unknown> | null;
11
+ /** Runs `work` in a fresh temporary folder named `<prefix>…`, removed afterwards whatever happens. */
12
+ export declare function inScratchDir<T>(prefix: string, work: (dir: string) => Promise<T>): Promise<T>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The parts every bare `claude -p` call shares: a throwaway working folder, the flags that leave it with no tools,
3
+ * no MCP servers and no saved session, and reading its one JSON reply. Each caller still builds its own argv around
4
+ * these, in its own order.
5
+ */
6
+ import { Buffer } from "node:buffer";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { strictJsonParse } from "./converse.js";
11
+ /** No tools, no permission prompts, no MCP servers, nothing kept: a `claude -p` call that can only answer. */
12
+ export const CLAUDE_NO_TOOLS_ARGS = [
13
+ "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
14
+ "--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
15
+ ];
16
+ /** `--model <model>`, or nothing for the CLI's own default. */
17
+ export const claudeModelArgs = (model) => (model === "default" ? [] : ["--model", model]);
18
+ /** What a failed call's output says when this computer's Claude isn't signed in. */
19
+ export const CLAUDE_SIGNED_OUT = /not logged in|login|authenticat/i;
20
+ /** `claude -p --output-format json` stdout as its one object, or null. */
21
+ export function claudeJsonBody(stdout, cap) {
22
+ const parsed = strictJsonParse(Buffer.from(stdout, "utf8"), cap, 12);
23
+ return parsed.ok && typeof parsed.value === "object" && parsed.value !== null && !Array.isArray(parsed.value) ? parsed.value : null;
24
+ }
25
+ /** Runs `work` in a fresh temporary folder named `<prefix>…`, removed afterwards whatever happens. */
26
+ export async function inScratchDir(prefix, work) {
27
+ const dir = mkdtempSync(join(tmpdir(), prefix));
28
+ try {
29
+ return await work(dir);
30
+ }
31
+ finally {
32
+ rmSync(dir, { recursive: true, force: true });
33
+ }
34
+ }
@@ -3,7 +3,6 @@ import type { Store } from './store.js';
3
3
  import type { Principal } from './operate.js';
4
4
  import { type RemoteCommandLookup } from './remote-command.js';
5
5
  import { type Admission, type BudgetLimit } from './request-budget.js';
6
- import type { SessionRunner } from './session-remote.js';
7
6
  /**
8
7
  * `POST /api/cli` (remote CLI, Phase 1): a teammate's laptop sends the argv it would have run locally, and the central
9
8
  * server runs that same command as that person. There is no per-resource API here and no second policy: the transport
@@ -12,10 +11,6 @@ import type { SessionRunner } from './session-remote.js';
12
11
  *
13
12
  * Only `Authorization: Bearer so_…` is accepted — never a password, cookie or the owner's saved login — and a request
14
13
  * carrying a browser Origin is refused, so a page can never drive it.
15
- *
16
- * `session …` (D7) is the one command the server answers through its own owner rather than runOperateAs: native
17
- * session operations need the server's live coding workspace (session-remote.ts). They are admitted, budgeted and
18
- * refused here exactly like every other command.
19
14
  */
20
15
  /** Who a remote command runs as, and the runner: operate.ts's own types, so a change there breaks this build instead of
21
16
  * drifting. Type-only: the value is loaded lazily through `run` (operate.ts imports the server that imports this). */
@@ -50,8 +45,6 @@ export type CliHttpOptions = {
50
45
  modeOf?: RemoteCommandLookup | undefined;
51
46
  /** The token's request budget (request-budget.ts), charged once per request before its body is read. */
52
47
  admit?: ((principal: Principal) => Admission) | undefined;
53
- /** Native session operations (`session …`), run by the server's session owner; absent where none is ready. */
54
- session?: SessionRunner | undefined;
55
48
  store: Store;
56
49
  };
57
50
  /** Decode the request's shape and byte limits. The endpoint checks file references against its command policy. */