toolroll 0.9.62 → 0.9.65

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 (345) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +66 -8
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +203 -10
  16. package/dist/agent-fence.js +623 -41
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +263 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +199 -17
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/check-levels.js +3 -0
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +20 -1
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +5 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +24 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/cli.d.ts +8 -0
  71. package/dist/contracts/cli.js +3 -1
  72. package/dist/contracts/console-api.d.ts +30 -4
  73. package/dist/contracts/console-api.js +13 -4
  74. package/dist/contracts/flow-card.d.ts +4 -0
  75. package/dist/contracts/flow-card.js +3 -0
  76. package/dist/contracts/flow.d.ts +46 -0
  77. package/dist/contracts/flow.js +11 -2
  78. package/dist/contracts/integration-metadata.d.ts +2 -1
  79. package/dist/contracts/integration-metadata.js +2 -1
  80. package/dist/contracts/lead-tools.d.ts +92 -0
  81. package/dist/contracts/lead-tools.js +13 -0
  82. package/dist/contracts/provider.d.ts +11 -1
  83. package/dist/contracts/provider.js +1 -1
  84. package/dist/contracts/route.d.ts +76 -1
  85. package/dist/contracts/route.js +1 -1
  86. package/dist/contracts/store-json.d.ts +2 -0
  87. package/dist/control-setup.d.ts +1 -1
  88. package/dist/control-ui.js +2 -2
  89. package/dist/converse.d.ts +16 -0
  90. package/dist/converse.js +32 -4
  91. package/dist/demo-chat.js +4 -4
  92. package/dist/demo.js +2 -2
  93. package/dist/desktop-update-gate.js +6 -0
  94. package/dist/discord-chat.js +4 -1
  95. package/dist/dispatch.js +7 -4
  96. package/dist/doctor.d.ts +53 -0
  97. package/dist/doctor.js +427 -0
  98. package/dist/egress-bridge.d.ts +17 -0
  99. package/dist/egress-bridge.js +111 -0
  100. package/dist/egress.d.ts +119 -0
  101. package/dist/egress.js +423 -0
  102. package/dist/evidence-pack.d.ts +1 -1
  103. package/dist/evidence-pack.js +2 -2
  104. package/dist/exec.d.ts +81 -0
  105. package/dist/exec.js +368 -9
  106. package/dist/export-ui.js +2 -2
  107. package/dist/flow-code.d.ts +2 -2
  108. package/dist/flow-code.js +4 -15
  109. package/dist/flow-engine.js +35 -11
  110. package/dist/flow-gallery-ui.js +2 -2
  111. package/dist/flow-gallery.js +27 -8
  112. package/dist/flow-share.js +16 -5
  113. package/dist/flow-triggers.d.ts +84 -11
  114. package/dist/flow-triggers.js +554 -87
  115. package/dist/flow-unattended-words.d.ts +28 -0
  116. package/dist/flow-unattended-words.js +50 -0
  117. package/dist/flow-unattended.d.ts +227 -0
  118. package/dist/flow-unattended.js +776 -0
  119. package/dist/flows-cli.d.ts +28 -1
  120. package/dist/flows-cli.js +63 -12
  121. package/dist/flows-ui.d.ts +2 -3
  122. package/dist/flows-ui.js +56 -5
  123. package/dist/flows.d.ts +4 -0
  124. package/dist/flows.js +9 -1
  125. package/dist/google-mail.d.ts +1 -0
  126. package/dist/google-mail.js +61 -18
  127. package/dist/html.d.ts +9 -0
  128. package/dist/html.js +11 -0
  129. package/dist/integrations-ui.d.ts +2 -1
  130. package/dist/integrations-ui.js +6 -3
  131. package/dist/integrations.d.ts +17 -3
  132. package/dist/integrations.js +38 -18
  133. package/dist/invoke.d.ts +17 -0
  134. package/dist/invoke.js +510 -182
  135. package/dist/keys.d.ts +18 -14
  136. package/dist/keys.js +5 -0
  137. package/dist/landed-cleanup.d.ts +3 -2
  138. package/dist/landed-cleanup.js +12 -5
  139. package/dist/lead-capabilities.js +3 -2
  140. package/dist/lead-contract.js +2 -0
  141. package/dist/lead-doors.d.ts +2 -0
  142. package/dist/lead-doors.js +7 -0
  143. package/dist/lead-follow.d.ts +2 -0
  144. package/dist/lead-follow.js +58 -2
  145. package/dist/lead-progress.js +1 -0
  146. package/dist/lead-queue-runtime.d.ts +46 -0
  147. package/dist/lead-queue-runtime.js +191 -0
  148. package/dist/lead-queue.d.ts +104 -0
  149. package/dist/lead-queue.js +187 -0
  150. package/dist/lead-sweep.d.ts +67 -0
  151. package/dist/lead-sweep.js +99 -0
  152. package/dist/lead-tools.d.ts +22 -0
  153. package/dist/lead-tools.js +163 -4
  154. package/dist/lead.d.ts +13 -5
  155. package/dist/lead.js +89 -29
  156. package/dist/limits-ui.d.ts +8 -1
  157. package/dist/limits-ui.js +26 -0
  158. package/dist/live-bus.d.ts +8 -0
  159. package/dist/live-bus.js +40 -4
  160. package/dist/live-detail.d.ts +27 -0
  161. package/dist/live-detail.js +57 -0
  162. package/dist/live-notes.d.ts +20 -0
  163. package/dist/live-notes.js +47 -0
  164. package/dist/live-records.d.ts +140 -0
  165. package/dist/live-records.js +935 -0
  166. package/dist/live-terminal.d.ts +258 -0
  167. package/dist/live-terminal.js +649 -0
  168. package/dist/live.d.ts +38 -27
  169. package/dist/live.js +142 -124
  170. package/dist/mcp-connect.d.ts +8 -7
  171. package/dist/mcp-connect.js +49 -51
  172. package/dist/mcp-oauth.d.ts +6 -3
  173. package/dist/mcp-oauth.js +8 -4
  174. package/dist/memory-pass.js +2 -2
  175. package/dist/mobile-viewport.d.ts +5 -1
  176. package/dist/mobile-viewport.js +18 -2
  177. package/dist/model-catalog.js +4 -0
  178. package/dist/monitoring-ui.js +2 -2
  179. package/dist/needs-you.d.ts +1 -1
  180. package/dist/needs-you.js +11 -4
  181. package/dist/oauth-run-token.d.ts +51 -0
  182. package/dist/oauth-run-token.js +127 -0
  183. package/dist/operate.d.ts +2 -0
  184. package/dist/operate.js +282 -18
  185. package/dist/passwordless.d.ts +23 -0
  186. package/dist/passwordless.js +26 -0
  187. package/dist/peek-cli.d.ts +12 -4
  188. package/dist/peek-cli.js +19 -11
  189. package/dist/phase-routing.js +10 -3
  190. package/dist/plan-auto.d.ts +11 -0
  191. package/dist/plan-auto.js +62 -11
  192. package/dist/planner-source.d.ts +3 -0
  193. package/dist/planner-source.js +14 -0
  194. package/dist/planner.d.ts +2 -1
  195. package/dist/planner.js +7 -1
  196. package/dist/policy-ui.js +2 -2
  197. package/dist/policy.d.ts +1 -20
  198. package/dist/policy.js +29 -2
  199. package/dist/process-recovery-eligibility.d.ts +1 -1
  200. package/dist/project-access.d.ts +3 -0
  201. package/dist/project-access.js +3 -0
  202. package/dist/project-delete-ui.d.ts +12 -0
  203. package/dist/project-delete-ui.js +41 -9
  204. package/dist/project-delete.d.ts +3 -1
  205. package/dist/project-delete.js +12 -6
  206. package/dist/project-file.d.ts +3 -0
  207. package/dist/project-file.js +16 -0
  208. package/dist/project-memory.d.ts +3 -1
  209. package/dist/project-memory.js +57 -25
  210. package/dist/project-tools.d.ts +103 -27
  211. package/dist/project-tools.js +399 -100
  212. package/dist/provider-auth.d.ts +4 -3
  213. package/dist/provider-auth.js +5 -4
  214. package/dist/provider-connection.d.ts +2 -1
  215. package/dist/provider-connection.js +5 -0
  216. package/dist/provider-home.d.ts +22 -0
  217. package/dist/provider-home.js +99 -0
  218. package/dist/provider.d.ts +42 -5
  219. package/dist/provider.js +97 -19
  220. package/dist/pull-request-ui.js +2 -2
  221. package/dist/push.js +1 -1
  222. package/dist/quick-start.d.ts +24 -0
  223. package/dist/quick-start.js +127 -0
  224. package/dist/release.d.ts +3 -0
  225. package/dist/release.js +14 -8
  226. package/dist/remote-main.d.ts +4 -0
  227. package/dist/remote-main.js +3 -3
  228. package/dist/request-budget-ui.js +2 -2
  229. package/dist/result-review.d.ts +1 -1
  230. package/dist/result-review.js +9 -3
  231. package/dist/retention-ui.js +2 -2
  232. package/dist/retention.js +10 -1
  233. package/dist/run-efficiency.d.ts +73 -0
  234. package/dist/run-efficiency.js +228 -0
  235. package/dist/runner.d.ts +5 -1
  236. package/dist/runner.js +22 -1
  237. package/dist/scope.d.ts +171 -6
  238. package/dist/scope.js +251 -31
  239. package/dist/scout-net.d.ts +4 -1
  240. package/dist/scout-net.js +37 -12
  241. package/dist/scout.d.ts +20 -6
  242. package/dist/scout.js +27 -3
  243. package/dist/serve.js +258 -43
  244. package/dist/server/chat.d.ts +1 -1
  245. package/dist/server/chat.js +122 -31
  246. package/dist/server/chrome.d.ts +6 -1
  247. package/dist/server/chrome.js +18 -7
  248. package/dist/server/console.css +52 -38
  249. package/dist/server/flows.js +65 -13
  250. package/dist/server/guards.d.ts +1 -0
  251. package/dist/server/guards.js +23 -9
  252. package/dist/server/http.d.ts +7 -0
  253. package/dist/server/http.js +3 -2
  254. package/dist/server/live.d.ts +34 -1
  255. package/dist/server/live.js +39 -17
  256. package/dist/server/pages-assets.js +5 -0
  257. package/dist/server/pages.js +90 -13
  258. package/dist/server/people-tokens.d.ts +3 -0
  259. package/dist/server/people-tokens.js +55 -17
  260. package/dist/server/remote-hooks.js +12 -6
  261. package/dist/server/remote.d.ts +2 -1
  262. package/dist/server/remote.js +14 -5
  263. package/dist/server/render-chat.d.ts +12 -3
  264. package/dist/server/render-chat.js +47 -16
  265. package/dist/server/render-pages.d.ts +4 -1
  266. package/dist/server/render-pages.js +5 -4
  267. package/dist/server/render-people.d.ts +3 -2
  268. package/dist/server/render-people.js +3 -2
  269. package/dist/server/render-results.d.ts +18 -2
  270. package/dist/server/render-results.js +37 -28
  271. package/dist/server/render-settings.js +6 -5
  272. package/dist/server/render-tasks.d.ts +35 -10
  273. package/dist/server/render-tasks.js +110 -27
  274. package/dist/server/request-context.d.ts +2 -0
  275. package/dist/server/request-context.js +2 -1
  276. package/dist/server/route-table.d.ts +3 -0
  277. package/dist/server/route-table.js +49 -5
  278. package/dist/server/runtime.d.ts +34 -3
  279. package/dist/server/session.d.ts +25 -1
  280. package/dist/server/session.js +18 -4
  281. package/dist/server/settings.js +132 -13
  282. package/dist/server/sweeps.d.ts +4 -0
  283. package/dist/server/sweeps.js +58 -0
  284. package/dist/server/task-views.d.ts +2 -1
  285. package/dist/server/task-views.js +96 -6
  286. package/dist/server/tasks.js +320 -74
  287. package/dist/server/terminals.d.ts +31 -0
  288. package/dist/server/terminals.js +365 -0
  289. package/dist/setup-guide.js +3 -2
  290. package/dist/slack-chat.js +4 -1
  291. package/dist/spend-ui.js +3 -3
  292. package/dist/spend.js +2 -1
  293. package/dist/steer-capabilities.d.ts +32 -0
  294. package/dist/steer-capabilities.js +60 -0
  295. package/dist/steer-controls.d.ts +14 -0
  296. package/dist/steer-controls.js +36 -0
  297. package/dist/steer-status.js +2 -2
  298. package/dist/storage-sweep-child.js +3 -1
  299. package/dist/storage-sweep-process.d.ts +12 -5
  300. package/dist/storage-sweep-process.js +23 -6
  301. package/dist/storage-ui.js +4 -4
  302. package/dist/store-baseline.d.ts +1 -1
  303. package/dist/store-baseline.js +1 -1
  304. package/dist/store.d.ts +466 -19
  305. package/dist/store.js +1185 -63
  306. package/dist/subagent-desk.js +1 -1
  307. package/dist/subagent-tools.d.ts +1 -1
  308. package/dist/summary.js +3 -0
  309. package/dist/surface.js +6 -4
  310. package/dist/task-source.d.ts +58 -0
  311. package/dist/task-source.js +93 -0
  312. package/dist/team-leads.js +1 -1
  313. package/dist/teams-chat.js +4 -1
  314. package/dist/teardown-git.d.ts +14 -0
  315. package/dist/teardown-git.js +119 -0
  316. package/dist/telegram-lead.d.ts +3 -0
  317. package/dist/telegram-lead.js +44 -7
  318. package/dist/telegram.d.ts +11 -2
  319. package/dist/telegram.js +51 -17
  320. package/dist/terminal-catalog.d.ts +11 -0
  321. package/dist/terminal-catalog.js +129 -0
  322. package/dist/text-limits.d.ts +2 -0
  323. package/dist/text-limits.js +2 -0
  324. package/dist/tool-broker.d.ts +39 -0
  325. package/dist/tool-broker.js +370 -0
  326. package/dist/tool-launcher.js +5 -4
  327. package/dist/tool-secret-store.d.ts +170 -0
  328. package/dist/tool-secret-store.js +834 -0
  329. package/dist/toolroll-update-ui.js +2 -2
  330. package/dist/toolroll-update.js +14 -3
  331. package/dist/tools-ui.d.ts +7 -1
  332. package/dist/tools-ui.js +19 -10
  333. package/dist/webhook-limits.d.ts +23 -0
  334. package/dist/webhook-limits.js +30 -0
  335. package/dist/webhook-readers.d.ts +56 -0
  336. package/dist/webhook-readers.js +138 -0
  337. package/dist/webhook-signing.d.ts +27 -0
  338. package/dist/webhook-signing.js +119 -0
  339. package/dist/work-index.d.ts +29 -0
  340. package/dist/work-index.js +21 -5
  341. package/dist/workspace-revision.js +3 -1
  342. package/dist/worktree.d.ts +10 -4
  343. package/dist/worktree.js +70 -29
  344. package/package.json +13 -2
  345. package/scripts/provider-canary.mjs +210 -31
@@ -26,7 +26,33 @@ import { isShotsKind, resultShotsFor } from "./result-shots.js";
26
26
  import { CHAT_APP_NAMES } from "./flow-triggers.js";
27
27
  import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
28
28
  import { mergePullRequest } from "./pull-request-flow.js";
29
- import { watchedPing } from "./agent-board.js";
29
+ import { attentionSubjectOf, neverHeld, watchedPing } from "./agent-board.js";
30
+ /**
31
+ * The watch at a chat app's final fence (cockpit §3, R11), right before the network: a notice about an agent its
32
+ * person has open now waits, as a held part, until the watch ends; then it is delivered as usual. The subject is the
33
+ * part's own run, or its task's live build. Only a part planned with `watch` (never a security alert or the digest)
34
+ * can be held. True: held, so nothing is sent now.
35
+ */
36
+ export function heldForWatch(state, store, part, event, approver, content, now) {
37
+ // Fail closed: only a notice planned as holdable (a kind a watch may hold) is ever held here.
38
+ if (event.kind !== "notice" || content.watch !== true)
39
+ return false;
40
+ // One part may cover several tasks (a grouped card, a batch): it waits only while every one of them is watched,
41
+ // until the first of those watches ends.
42
+ const subjects = content.subjects ?? [{ ref: content.task ? store.lookupRef(content.task)?.id ?? null : null, run: content.run ?? null }];
43
+ if (subjects.length === 0)
44
+ return false;
45
+ let until = null;
46
+ for (const subject of subjects) {
47
+ const decided = watchedPing(store, { kind: "notice", run: subject.run, taskRef: subject.ref }, approver, now);
48
+ if (decided === "send" || decided === "skip")
49
+ return false;
50
+ if (until === null || decided.hold < until)
51
+ until = decided.hold;
52
+ }
53
+ state.holdPart(part, until);
54
+ return true;
55
+ }
30
56
  export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
31
57
  ? v
32
58
  : {};
@@ -397,8 +423,9 @@ export async function processChatEvent(options) {
397
423
  return false;
398
424
  if (!outcome.ok &&
399
425
  "refused" in outcome &&
400
- outcome.refused === "concurrent") {
401
- state.defer(event.id, "Assistant is answering another message", new Date(nowOf(options).getTime() + 5000));
426
+ (outcome.refused === "concurrent" || outcome.refused === "backlog")) {
427
+ // The lead is answering, or messages sent earlier wait in the person's lead queue: this one waits its turn.
428
+ state.defer(event.id, outcome.refused === "backlog" ? "Waiting behind earlier messages" : "Assistant is answering another message", new Date(nowOf(options).getTime() + 5000));
402
429
  return true;
403
430
  }
404
431
  receipt = store.leadRequestReceipt(resolved.session.id, request);
@@ -422,7 +449,7 @@ export async function processChatEvent(options) {
422
449
  if (turn?.state !== "answered") {
423
450
  state.plan(event.id, [
424
451
  {
425
- text: leadFailureText(turn?.failureReason ?? null),
452
+ text: leadFailureText(turn?.failureReason ?? null, turn != null && store.leadTurnCostUnconfirmed(turn.id)),
426
453
  },
427
454
  ], nowOf(options));
428
455
  return true;
@@ -734,7 +761,12 @@ function planQuietCard(options, binding, notification, now) {
734
761
  const view = quietCardView(store, card.tasks, now, options.evidenceRoot, binding.approver);
735
762
  if (view === null)
736
763
  return;
737
- const content = { text: view.text, link: view.link, ...(card.tasks.length === 1 ? { task: notification.taskId } : {}) };
764
+ // A card about one task is that task's: its person's watch may hold it at the final fence (never a security kind).
765
+ // A card about its tasks is theirs: its person's watch may hold it at the final fence while every one of them is
766
+ // watched (never for a security kind).
767
+ const content = { text: view.text, link: view.link, ...(card.tasks.length === 1 ? { task: notification.taskId } : {}),
768
+ ...(card.tasks.length > 0 && !neverHeld(notification.kind) ? { watch: true } : {}),
769
+ ...(card.tasks.length > 1 && !neverHeld(notification.kind) ? { subjects: card.tasks.slice(0, 100).map(ref => ({ ref, run: null })) } : {}) };
738
770
  const shown = chatHash(JSON.stringify(content));
739
771
  if (card.message !== null) {
740
772
  if (card.digest === shown)
@@ -763,7 +795,12 @@ function planFinished(options, binding, taskRef, run, now, notification = null,
763
795
  // A single update keeps its task and run, so a reply to it names that work.
764
796
  const single = batch.items.length === 1 ? batch.items[0] : null;
765
797
  const task = single === null ? null : store.refById(single.taskRef)?.externalId ?? null;
766
- const about = { ...(task === null ? {} : { task }), ...(single?.run == null ? {} : { run: single.run }) };
798
+ // Holdable when its fact is a kind a watch may hold (the batch line for a finished run, or the notification joining
799
+ // it); a batch of several holds only while every one of them is watched.
800
+ const joining = notification === null ? null : options.store.notificationById(notification);
801
+ const holdable = batch.items.length > 0 && (joining === null || !neverHeld(joining.kind));
802
+ const about = { ...(task === null ? {} : { task }), ...(single?.run == null ? {} : { run: single.run }), ...(holdable ? { watch: true } : {}),
803
+ ...(holdable && batch.items.length > 1 ? { subjects: batch.items.slice(0, 100).map(one => ({ ref: one.taskRef, run: one.run })) } : {}) };
767
804
  // A message about one result, plan, failure or pull request acts in place (chat-decide.ts), as on Telegram: its
768
805
  // buttons are minted only when what it offers changed, or its own were spent (Not now), so an unchanged repaint keeps them.
769
806
  const offer = decideOfferFor(options, binding, view.target, repos, now);
@@ -962,6 +999,7 @@ function offerNotice(options, notification, binding, registry) {
962
999
  task: notification.taskId,
963
1000
  run: run.id,
964
1001
  link: card.link,
1002
+ ...(attentionSubjectOf(store, notification) !== null ? { watch: true } : {}),
965
1003
  };
966
1004
  const digest = chatHash(JSON.stringify(content));
967
1005
  const prior = state
@@ -1044,6 +1082,7 @@ function offerNotice(options, notification, binding, registry) {
1044
1082
  text: offer?.text ?? chatText(phoneText(notification.body === "" ? leadSubjectOf(store, notification, binding.approver) : `${leadSubjectOf(store, notification, binding.approver)}\n\n${notification.body}`, 2500), notification.taskId === null ? [] : [{ id: notification.taskId, title: chatTitle(store, notification.taskId) }]),
1045
1083
  ...(notification.taskId ? { task: notification.taskId } : {}),
1046
1084
  ...(run ? { run: run.id } : {}),
1085
+ ...(attentionSubjectOf(store, notification) !== null ? { watch: true } : {}),
1047
1086
  ...(offer !== null ? { decide: mintDecide(options, binding, target, offer, now) }
1048
1087
  : fallback !== null ? { link: fallback }
1049
1088
  : notification.link
@@ -174,6 +174,8 @@ export declare class ChatMessages extends ChatCore {
174
174
  taskId: string | null;
175
175
  sourceRun: number | null;
176
176
  }, now: Date): number;
177
+ /** How many of this chat's messages wait for, or are being answered by, the lead (the lead queue's bound per chat). */
178
+ messagesWaiting(binding: number): number;
177
179
  /** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
178
180
  messageWaitingOn(binding: number, messageId: string): boolean;
179
181
  getMessage(id: number): TelegramConversation | null;
@@ -294,6 +296,12 @@ export declare class ChatMessages extends ChatCore {
294
296
  claimDeliveries(binding: TelegramBinding, owner: string, ttlMs: number, now: Date, only?: "all" | "urgent"): TelegramDelivery[];
295
297
  /** Synchronous final fence, called AFTER each asynchronous enrollment read. */
296
298
  deliveryProblem(row: TelegramDelivery, binding: TelegramBinding, owner: string, projects: readonly string[], now: Date, batch?: readonly number[]): string | null;
299
+ /**
300
+ * The watch at the final fence (cockpit §3, R11): right before the network, a person who has since opened the
301
+ * agent this ping is about holds it — the claim is released with the watch's end as its next attempt, no attempt
302
+ * spent — and news about a finished run they have now seen settles quietly. "send": nothing changed.
303
+ */
304
+ watchFence(row: TelegramDelivery, binding: TelegramBinding, owner: string, now: Date): "send" | "held" | "skipped";
297
305
  claimHeld(row: TelegramDelivery, owner: string, now: Date): boolean;
298
306
  /** A confirmed network message remains history even if authority changed
299
307
  * while it was in flight. It only binds the OLD destination, never new work. */
@@ -9,7 +9,7 @@
9
9
  * Imports the store's types only (no runtime cycle): the store builds its chats on it.
10
10
  */
11
11
  import { join } from "node:path";
12
- import { watchedPing } from "./agent-board.js";
12
+ import { neverHeld, watchedPing } from "./agent-board.js";
13
13
  import { ChatCore } from "./chat-core.js";
14
14
  import { readChatMessageBody, readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
15
15
  import { isTelegramProgressNotification, proposalTaskOf, readNotification, readNotificationReceipt } from "./notification-rows.js";
@@ -381,6 +381,10 @@ export class ChatMessages extends ChatCore {
381
381
  .run(`m${row.updateId}`, row.binding.botId, row.binding.id, row.binding.chatId, row.binding.userId, row.messageId, row.replyTo ?? "", savedChatEventBody(body), now.toISOString());
382
382
  return row.updateId;
383
383
  }
384
+ /** How many of this chat's messages wait for, or are being answered by, the lead (the lead queue's bound per chat). */
385
+ messagesWaiting(binding) {
386
+ return Number(this.prepare("SELECT COUNT(*) AS n FROM chat_event WHERE provider = :provider AND kind = 'message' AND binding = ? AND state IN ('queued', 'running')").get(binding)?.["n"] ?? 0);
387
+ }
384
388
  /** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
385
389
  messageWaitingOn(binding, messageId) {
386
390
  return this.prepare("SELECT 1 AS hit FROM chat_event WHERE provider = :provider AND kind = 'message' AND binding = ? AND ts = ? AND state IN ('queued', 'running') LIMIT 1").get(binding, messageId) !== undefined;
@@ -767,17 +771,45 @@ export class ChatMessages extends ChatCore {
767
771
  return TELEGRAM_HOLD_REASONS.task;
768
772
  }
769
773
  }
770
- if (row.taskRef !== null) {
774
+ // A security alert never waits behind its task's earlier pings (one may be held while its person watches the agent).
775
+ if (row.taskRef !== null && !neverHeld(String(current["kind"]))) {
771
776
  // Skipped history never fences what follows it: it is settled.
772
- const earlier = this.db.prepare(`SELECT n.id FROM notification n
777
+ const earlier = this.db.prepare(`SELECT n.*, d.next_attempt_at AS held_until, d.last_error AS held_error FROM notification n
773
778
  LEFT JOIN notification_delivery d ON d.notification = n.id AND d.destination = ?
774
779
  WHERE n.task_ref = ? AND n.id < ? AND n.resolved_at IS NULL AND ${TELEGRAM_UNSETTLED}`)
775
780
  .all(row.destination, row.taskRef, row.id);
776
- if (earlier.some(one => !batch.includes(Number(one["id"]))))
781
+ // One a watch is holding (cockpit §3) keeps nothing behind it: it goes when the watch ends, and what followed
782
+ // it about another attempt is not that watch's to delay.
783
+ // Held: deferred by a watch (a future next attempt with no failure behind it), or watched right now.
784
+ const held = (one) => (one["held_until"] != null && String(one["held_until"]) > now.toISOString() && one["held_error"] == null)
785
+ || typeof watchedPing(this.store, readNotification(one), binding.approver, now) === "object";
786
+ if (earlier.some(one => !batch.includes(Number(one["id"])) && !held(one)))
777
787
  return TELEGRAM_HOLD_REASONS.order;
778
788
  }
779
789
  return null;
780
790
  }
791
+ /**
792
+ * The watch at the final fence (cockpit §3, R11): right before the network, a person who has since opened the
793
+ * agent this ping is about holds it — the claim is released with the watch's end as its next attempt, no attempt
794
+ * spent — and news about a finished run they have now seen settles quietly. "send": nothing changed.
795
+ */
796
+ watchFence(row, binding, owner, now) {
797
+ const current = this.db.prepare("SELECT * FROM notification WHERE id = ?").get(row.id);
798
+ if (current === undefined)
799
+ return "send";
800
+ const decided = watchedPing(this.store, readNotification(current), binding.approver, now);
801
+ if (decided === "send")
802
+ return "send";
803
+ const where = "WHERE notification = ? AND destination = ? AND claim_owner = ? AND claim_generation = ? AND delivered_at IS NULL";
804
+ if (decided === "skip") {
805
+ this.db.prepare(`UPDATE notification_delivery SET receipt = '${TELEGRAM_SKIPPED_QUIET}', claim_owner = NULL, claim_expires_at = NULL ${where}`)
806
+ .run(row.id, row.destination, owner, row.claimGeneration);
807
+ return "skipped";
808
+ }
809
+ this.db.prepare(`UPDATE notification_delivery SET next_attempt_at = ?, claim_owner = NULL, claim_expires_at = NULL ${where}`)
810
+ .run(decided.hold, row.id, row.destination, owner, row.claimGeneration);
811
+ return "held";
812
+ }
781
813
  claimHeld(row, owner, now) {
782
814
  return this.db.prepare(`SELECT 1 FROM notification_delivery WHERE notification = ? AND destination = ?
783
815
  AND claim_owner = ? AND claim_generation = ? AND claim_expires_at > ? AND delivered_at IS NULL`)
@@ -2,12 +2,12 @@
2
2
  * words. One choice (Quick, Full, Off) and the quick command beside the full
3
3
  * one. Every change takes the password and lands in the ledger; approving a
4
4
  * command restates exactly what will run, unattended, before the yes. */
5
- import { html, postForm } from "./html.js";
5
+ import { html, postForm, reauth } from "./html.js";
6
6
  import { CHECK_LEVELS, CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS } from "./check-levels.js";
7
7
  import { BATCH_HINT } from "./batch-policy.js";
8
8
  export function checkSettingsHtml(view) {
9
9
  const post = (act, body, attrs) => postForm("/settings/checks", body, { attrs, hidden: { repo: view.repo, act } });
10
- const password = html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
10
+ const password = html `${reauth(html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
11
11
  const notes = [
12
12
  view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
13
13
  view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
@@ -153,6 +153,9 @@ export function suggestQuickCommand(repo) {
153
153
  const has = (name) => typeof scripts[name] === "string";
154
154
  const runner = existsSync(join(repo, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(repo, "yarn.lock")) ? "yarn" : "npm";
155
155
  const run = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
156
+ // A project's own quick script knows its tests best (Toolroll's runs the attack suite when a guard changes).
157
+ if (has("check:quick"))
158
+ return run("check:quick");
156
159
  const exec = runner === "npm" ? "npx" : `${runner} exec`;
157
160
  const typecheck = ["typecheck", "type-check", "check-types", "tsc"].find(has) ?? null;
158
161
  const changed = "$(git diff --name-only HEAD~1)";
@@ -0,0 +1,28 @@
1
+ import type { Store } from "./store.js";
2
+ export type CheckoutProof = {
3
+ ok: true;
4
+ path: string;
5
+ } | {
6
+ ok: false;
7
+ reason: string;
8
+ };
9
+ export declare function localCheckoutOf(store: Store, runId: number, local: {
10
+ runner?: string | undefined;
11
+ poolRoot?: string | undefined;
12
+ }, now: Date): CheckoutProof;
13
+ /** How Finder is asked: the system's own `open` by absolute path (never found through PATH), from the root directory,
14
+ * with a minimal environment, one argument. True only when it answered success before the deadline: at the deadline
15
+ * the answer is "not shown", the child is killed outright, and anything it says later is ignored. */
16
+ export type RevealExec = (file: string, args: readonly string[], options: {
17
+ cwd: string;
18
+ env: Record<string, string>;
19
+ }, done: (error: Error | null) => void) => {
20
+ kill: (signal: NodeJS.Signals) => unknown;
21
+ } | undefined;
22
+ export declare const REVEAL_COMMAND = "/usr/bin/open";
23
+ export declare const REVEAL_DEADLINE_MS = 10000;
24
+ export declare function revealInFinder(path: string, run?: RevealExec, deadlineMs?: number, clock?: () => number): Promise<boolean>;
25
+ /** Whether this console can ask Finder at all (a Mac, or a test's own revealer). */
26
+ export declare const canReveal: (injected: boolean) => boolean;
27
+ /** The command to paste into a terminal: `cd` to the path as one single-quoted shell word (a quote inside escaped). */
28
+ export declare function cdCommandOf(path: string): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A running attempt's checkout, proved to be on this machine (docs/plans/agents-cockpit.md phase E): the run is live
3
+ * under the task's current claim, its worktree row is this machine's runner's and this task's, and the path is its own
4
+ * real path, a directory inside this machine's pool. Used to offer "copy the cd command" and "Show in Finder" — and
5
+ * proved again immediately before Finder is asked to reveal it. Nothing is ever launched in it.
6
+ */
7
+ import { execFile } from "node:child_process";
8
+ import { lstatSync, realpathSync } from "node:fs";
9
+ export function localCheckoutOf(store, runId, local, now) {
10
+ if (local.runner === undefined || local.poolRoot === undefined)
11
+ return { ok: false, reason: "this console doesn't run builds on this machine" };
12
+ const run = store.getRun(runId);
13
+ if (run === null || run.outcome !== null || run.worktree === null)
14
+ return { ok: false, reason: "this attempt has no running checkout" };
15
+ if (store.currentLiveLease(run.taskRef, now) !== run.leaseId)
16
+ return { ok: false, reason: "this attempt isn't the one running now" };
17
+ const row = store.getWorktree(run.worktree);
18
+ if (row === null || row.taskRef !== run.taskRef)
19
+ return { ok: false, reason: "the checkout isn't where the record says" };
20
+ // The run, its worktree row and this machine all name the same runner: an inconsistent record fails closed.
21
+ if (run.runner !== local.runner || row.runner !== local.runner)
22
+ return { ok: false, reason: "this attempt runs on another computer" };
23
+ try {
24
+ const real = realpathSync(run.worktree), pool = realpathSync(local.poolRoot);
25
+ // Strictly inside: the pool directory itself is never a checkout.
26
+ if (real !== run.worktree || !real.startsWith(`${pool}/`) || !lstatSync(real).isDirectory())
27
+ return { ok: false, reason: "the checkout isn't inside this machine's pool" };
28
+ return { ok: true, path: real };
29
+ }
30
+ catch {
31
+ return { ok: false, reason: "the checkout couldn't be found on this machine" };
32
+ }
33
+ }
34
+ export const REVEAL_COMMAND = "/usr/bin/open";
35
+ export const REVEAL_DEADLINE_MS = 10_000;
36
+ export function revealInFinder(path, run = (file, args, options, done) => execFile(file, [...args], options, error => done(error)), deadlineMs = REVEAL_DEADLINE_MS, clock = () => performance.now()) {
37
+ return new Promise(resolve => {
38
+ let settled = false;
39
+ let child;
40
+ // The deadline is a moment on a monotonic clock, not the timer having run: a success that arrives after it (even
41
+ // ahead of an overdue timer, in a stalled event loop) is "not shown".
42
+ const due = clock() + deadlineMs;
43
+ const finish = (shown) => { if (settled)
44
+ return; settled = true; clearTimeout(timer); resolve(shown); };
45
+ const timer = setTimeout(() => { finish(false); try {
46
+ child?.kill("SIGKILL");
47
+ }
48
+ catch { /* already gone */ } }, deadlineMs);
49
+ try {
50
+ child = run(REVEAL_COMMAND, ["-R", path], { cwd: "/", env: { PATH: "/usr/bin:/bin" } }, error => finish(error === null && clock() < due));
51
+ }
52
+ catch {
53
+ finish(false);
54
+ }
55
+ });
56
+ }
57
+ /** Whether this console can ask Finder at all (a Mac, or a test's own revealer). */
58
+ export const canReveal = (injected) => injected || process.platform === "darwin";
59
+ /** The command to paste into a terminal: `cd` to the path as one single-quoted shell word (a quote inside escaped). */
60
+ export function cdCommandOf(path) {
61
+ return `cd '${path.replaceAll("'", `'\\''`)}'`;
62
+ }
package/dist/claim.d.ts CHANGED
@@ -70,6 +70,13 @@ export type AcquireResult = {
70
70
  reason: "mode-ended";
71
71
  message: string;
72
72
  }
73
+ /** A lead sweep's limit (docs/plans/lead-fan-out.md): `limit` of its families already hold a live claim. */
74
+ | {
75
+ ok: false;
76
+ reason: "sweep-limit";
77
+ sweep: number;
78
+ limit: number;
79
+ }
73
80
  /** The credential presented with this acquisition did not verify against
74
81
  * the runner row AT CLAIM TIME (MCP spec v6): stale incarnations never
75
82
  * ride a successor's rotation. */
@@ -183,6 +190,11 @@ export { DEFAULT_MAX_OPEN_DECISIONS, missingCapability } from "./dispatch.js";
183
190
  * it, which is a race being won; not-ready means nobody should have it, and
184
191
  * says why.
185
192
  */
193
+ /**
194
+ * A conformance check's claim (acp-check.ts): its task has no scope to approve, so readiness is not asked — but a
195
+ * check runs real agents, so the runner's capacity is counted where the claim lands, as for dispatch.
196
+ */
197
+ export declare function acquireForCheck(store: Store, taskRef: number, runner: string, options: AcquireOptions): AcquireResult | NotReady;
186
198
  export declare function acquireIfReady(store: Store, taskRef: number, runner: string, options: AcquireOptions & {
187
199
  repo?: string;
188
200
  maxOpenDecisions?: number;
package/dist/claim.js CHANGED
@@ -131,6 +131,12 @@ function acquireLocked(store, taskRef, runner, options) {
131
131
  message: "the operating mode that approved this has ended — the approval falls back to a person",
132
132
  };
133
133
  }
134
+ // A LEAD SWEEP'S LIMIT, inside this write transaction so two runners can't both take the last slot: at most
135
+ // max_parallel of its families hold a live claim (claimed-but-not-started counts; a revision counts as its child).
136
+ const sweep = placedRef === null ? null : store.sweepOfTask(placedRef.externalId);
137
+ if (sweep !== null && store.sweepLiveClaims(sweep.id, stamp, taskRef) >= sweep.maxParallel) {
138
+ return { ok: false, reason: "sweep-limit", sweep: sweep.id, limit: sweep.maxParallel };
139
+ }
134
140
  const existing = latest(db, taskRef);
135
141
  if (existing !== undefined && isLive(existing, stamp)) {
136
142
  return {
@@ -199,6 +205,19 @@ export { DEFAULT_MAX_OPEN_DECISIONS, missingCapability } from "./dispatch.js";
199
205
  * it, which is a race being won; not-ready means nobody should have it, and
200
206
  * says why.
201
207
  */
208
+ /**
209
+ * A conformance check's claim (acp-check.ts): its task has no scope to approve, so readiness is not asked — but a
210
+ * check runs real agents, so the runner's capacity is counted where the claim lands, as for dispatch.
211
+ */
212
+ export function acquireForCheck(store, taskRef, runner, options) {
213
+ return inTransaction(store, () => {
214
+ const registered = store.getRunner(runner);
215
+ if (registered !== null && store.liveClaimCount(runner, options.now) >= registered.runner.capacity) {
216
+ return { ok: false, reason: "capacity", message: `${runner} has no free slot for a check` };
217
+ }
218
+ return acquireLocked(store, taskRef, runner, options);
219
+ });
220
+ }
202
221
  export function acquireIfReady(store, taskRef, runner, options) {
203
222
  const role = options.dispatchRole ?? "builder";
204
223
  return inTransaction(store, () => {
@@ -1116,7 +1135,7 @@ function sealPlan(store, leaseId, args) {
1116
1135
  ? ""
1117
1136
  : "The filed goal, exclusions, touches, and acceptance criteria are reproduced exactly.\n"
1118
1137
  : `It AMENDS the filed contract (${oneLine(describeContractChanges(contractChanges).join("; "), 300)})${amendment === null ? "" : ` — because: ${oneLine(amendment, 200)}`}\n`) +
1119
- (autoApproved ? "Approved under the signed operating mode. The next build may proceed with the original terms and required quality checks." : "Review, edit, and approve the scope — nothing builds until you do."),
1138
+ (autoApproved ? `${store.getScope(taskId)?.approvalBasis === "grant" ? "Approved because its flow runs on its own." : "Approved under the signed operating mode."} The next build may proceed with the original terms and required quality checks.` : "Review, edit, and approve the scope — nothing builds until you do."),
1120
1139
  }, now);
1121
1140
  return { ok: true, changes: contractChanges.length, amendment };
1122
1141
  }
@@ -2,4 +2,4 @@
2
2
  * The top-level usage text, on its own so `toolroll --help` can print it
3
3
  * without loading every command family (see cli-fast.ts).
4
4
  */
5
- export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish\n (resumable: rerun to continue; --help)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / worker release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to your lead: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
5
+ export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll in a repository: start Toolroll for it, or open the one already running\n (under an agent or with --json: only says what it would do)\n toolroll scan [path...] report what is in flight (also bare `toolroll` outside a repository)\n toolroll [path...] the same report for the paths given\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish\n (resumable: rerun to continue; --help)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / worker release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to your lead: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll doctor check that Toolroll's service holds no key a build could read\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
package/dist/cli-help.js CHANGED
@@ -6,7 +6,10 @@ import { DEFAULT_MAX_DEPTH } from "./scan.js";
6
6
  export const HELP = `toolroll — a control plane for unattended coding agents
7
7
 
8
8
  Usage
9
- toolroll [path...] report what is in flight
9
+ toolroll in a repository: start Toolroll for it, or open the one already running
10
+ (under an agent or with --json: only says what it would do)
11
+ toolroll scan [path...] report what is in flight (also bare \`toolroll\` outside a repository)
12
+ toolroll [path...] the same report for the paths given
10
13
  toolroll pulls report what is waiting on a person
11
14
  toolroll graph report which work graph is already here
12
15
  toolroll repos list connected repositories, and how to adjust
@@ -63,6 +66,7 @@ and any queue command + --help prints it too
63
66
  (--editor vscode with --runner: file links
64
67
  open in VS Code on the device you browse from)
65
68
  toolroll watch / daemon install advanced: run only the builder
69
+ toolroll doctor check that Toolroll's service holds no key a build could read
66
70
  toolroll reconcile recover what the last stretch left behind
67
71
 
68
72
  With nothing connected it reports everything it can find below the working
package/dist/cli.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  * should not have to parse a column layout.
12
12
  */
13
13
  import { previewGithubRepo, cloneGithubRepo } from "./onboard.js";
14
+ import { type QuickStartSeams } from "./quick-start.js";
14
15
  import { type SessionCliOptions } from "./session-cli.js";
15
16
  import { type TeamCliOptions } from "./team-cli.js";
16
17
  import { type RemoteExecOptions } from "./remote-exec.js";
@@ -69,6 +70,8 @@ export type MainOptions = {
69
70
  };
70
71
  update?: UpdateCliDeps;
71
72
  release?: ReleaseCliDeps;
73
+ /** Tests: bare `toolroll` in a repository (quick-start.ts) — its repository, agent check, proofs and actions. */
74
+ quickStart?: Partial<QuickStartSeams>;
72
75
  };
73
76
  /**
74
77
  * Strip `-o <file>` / `--output <file>` before dispatch. The flag belongs to
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ import { applyClaudeCodeInstall, applyInstall, contextBlock, defaultClaudeCodeSk
24
24
  import { GUIDES, guideNamed } from "./guides.js";
25
25
  import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, isLargeRepo } from "./onboard.js";
26
26
  import { run as execRun } from "./exec.js";
27
+ import { isBareInvocation, runQuickStart } from "./quick-start.js";
27
28
  import { COMMAND_GUIDE, SURFACE_NOTES, SURFACE_SCHEMA_VERSION } from "./surface.js";
28
29
  import { runSessionCommand, SESSION_CLI_ACTIONS } from "./session-cli.js";
29
30
  import { maybeRunTeamCommand, TEAM_CLI_ACTIONS } from "./team-cli.js";
@@ -111,7 +112,7 @@ export function parseArgs(argv) {
111
112
  * exact sets instead of regex-reading source. "" is the no-verb report,
112
113
  * which answers as `scan`. */
113
114
  export const TOP_LEVEL_COMMANDS = [
114
- "", "pulls", "graph", "repos", "repos add", "repos remove", "repos add-from-github",
115
+ "", "scan", "pulls", "graph", "repos", "repos add", "repos remove", "repos add-from-github",
115
116
  "link", "unlink", "update", "release", "contract", "skills list", "skills get", "skills install", "demo",
116
117
  ...SESSION_CLI_ACTIONS.map(action => `session ${action}`),
117
118
  ...TEAM_CLI_ACTIONS,
@@ -172,7 +173,7 @@ export const OPERATE_COMMANDS = new Set([
172
173
  "reconcile",
173
174
  "run",
174
175
  "flows",
175
- "config", "chat", "proposals", "mode", "chat-approval", "people", "tokens", "keys",
176
+ "config", "chat", "proposals", "mode", "chat-approval", "people", "tokens", "keys", "agents",
176
177
  "setup",
177
178
  "verify",
178
179
  "intake",
@@ -313,6 +314,9 @@ async function dispatch(argv, write, mainOptions) {
313
314
  // Command families below load when their command runs, so one command never pays for the others.
314
315
  if (argv[0] === 'health')
315
316
  return (await import("./health-cli.js")).runHealthCommand(argv.slice(1), write, mainOptions.team ?? mainOptions.remote);
317
+ // This computer's own service, read here and never on a central server.
318
+ if (argv[0] === 'doctor')
319
+ return (await import("./doctor.js")).runDoctorCommand(argv.slice(1), write);
316
320
  const central = await maybeRunRemoteCommand(argv, mainOptions.remote ?? mainOptions.team);
317
321
  if (typeof central === "number")
318
322
  return central;
@@ -360,6 +364,24 @@ async function dispatch(argv, write, mainOptions) {
360
364
  if (first !== undefined && OPERATE_COMMANDS.has(first)) {
361
365
  return runOperate(first, rest, write, mainOptions.operate ?? {});
362
366
  }
367
+ // Cockpit B1: the scan by name, and bare `toolroll` in a repository starting (or attaching to) its console.
368
+ if (first === "scan")
369
+ argv = rest;
370
+ else if (isBareInvocation(argv)) {
371
+ const databaseFile = mainOptions.operate?.databaseFile ?? databasePath(process.env, homedir());
372
+ const started = await runQuickStart(argv, write, {
373
+ databaseFile, cwd: process.cwd(), env: process.env, terminal: process.stdout.isTTY === true,
374
+ build: (await import("./monitoring.js")).packageVersion(),
375
+ findRepo: async (cwd) => { const top = await execRun("git", ["rev-parse", "--show-toplevel"], { cwd, timeoutMs: 10_000 }); return top.code === 0 ? top.stdout.trim() : null; },
376
+ enrolled: async (repo) => { const loaded = await loadRepos(registryBeside(databaseFile)); return !("error" in loaded) && loaded.repos.includes(repo); },
377
+ admit: repo => runReposCommand(["add", repo], write, mainOptions.onboard),
378
+ startUp: () => runOperate("up", [], write, mainOptions.operate ?? {}),
379
+ openBrowser: async (url) => { (await import("./operate.js")).openBrowser(url); },
380
+ ...mainOptions.quickStart,
381
+ });
382
+ if (started !== null)
383
+ return started;
384
+ }
363
385
  const parsed = parseArgs(argv);
364
386
  if ("error" in parsed) {
365
387
  write(parsed.error);
package/dist/coding-ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { html, joinHtml, postForm } from './html.js';
2
+ import { html, joinHtml, passwordlessRendering, postForm, reauth } from './html.js';
3
3
  function linkedText(text) {
4
4
  const pattern = /\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|\x60([^\x60\n]+)\x60|\*\*([^*\n]+)\*\*|(https?:\/\/[^\s<>]+)/g;
5
5
  let end = 0;
@@ -42,7 +42,7 @@ function itemHtml(item) {
42
42
  }
43
43
  function requestHtml(request, session) {
44
44
  const questions = request.kind === 'questions';
45
- const body = questions ? request.questions.map((q, index) => html `<label>${q.question}<input type="text" name="question:${q.id}"${q.options.length ? html ` list="coding-options-${request.id}-${index}"` : ''} required autocomplete="off">${q.options.length ? html `<datalist id="coding-options-${request.id}-${index}">${q.options.map(o => html `<option value="${o.label}">${o.description}</option>`)}</datalist>` : ''}</label>`) : html `<pre class="coding-permission-detail">${request.detail}</pre><label>Password to approve this request<input type="password" name="password" autocomplete="current-password" required></label>`;
45
+ const body = questions ? request.questions.map((q, index) => html `<label>${q.question}<input type="text" name="question:${q.id}"${q.options.length ? html ` list="coding-options-${request.id}-${index}"` : ''} required autocomplete="off">${q.options.length ? html `<datalist id="coding-options-${request.id}-${index}">${q.options.map(o => html `<option value="${o.label}">${o.description}</option>`)}</datalist>` : ''}</label>`) : html `<pre class="coding-permission-detail">${request.detail}</pre>${reauth(html `<label>Password to approve this request<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
46
46
  return html `<section class="coding-request" data-coding-request="${request.id}"><h3>${request.title || (questions ? 'Your answer' : 'Allow extra access?')}</h3>${postForm(`${sessionUrl(session)}/answer`, html `${body}<div class="coding-actions"><button type="submit" name="decision" value="accept">${questions ? 'Send answer' : 'Approve access'}</button><button type="submit" name="decision" value="decline" formnovalidate class="coding-secondary">${questions ? 'Cancel question' : 'Decline'}</button></div>`, { attrs: { 'data-coding-form': 'answer' }, hidden: { requestId: request.id } })}</section>`;
47
47
  }
48
48
  function controlsHtml(session) {
@@ -69,7 +69,7 @@ export function codingWorkspaceHtml(input) {
69
69
  let main;
70
70
  if (!selected) {
71
71
  const options = input.projects.map(p => html `<option value="${p.path}"${selectedRepo === p.path ? html ` selected` : ''}>${p.name}</option>`);
72
- main = html `<header class="coding-header"><h1>Build with Codex</h1></header>${warning}${input.projects.length ? postForm('/code/start', html `<label>Project<select name="repo" required>${options}</select></label><label>What would you like to change?<textarea name="prompt" rows="5" required maxlength="24000" placeholder="Describe the result you want…">${input.draft ?? ''}</textarea></label><details class="coding-options"><summary>Session options</summary><label>Title <span class="coding-meta">Optional</span><input name="title" maxlength="160" autocomplete="off"></label><label>Model <span class="coding-meta">Optional; uses the installed default</span><input name="model" maxlength="120" placeholder="Installed default" autocomplete="off"></label></details><p class="coding-terms">Codex uses this installation’s login and edits a separate copy of this project. It can run commands and read files outside the project. Connected tools (MCP servers) keep their own access to this computer and connected services. Additional command and file access requires your approval.</p><label>Password to start<input type="password" name="password" autocomplete="current-password" required></label><button type="submit"${input.available ? '' : html ` disabled`}>Start coding</button>`, { attrs: { 'data-coding-form': 'start', class: 'coding-start' }, hidden: { requestId: randomUUID() } }) : html `<div class="coding-empty"><p>Add a project to start coding.</p><a class="coding-link-button" href="/projects">Open projects</a></div>`}`;
72
+ main = html `<header class="coding-header"><h1>Build with Codex</h1></header>${warning}${input.projects.length ? postForm('/code/start', html `<label>Project<select name="repo" required>${options}</select></label><label>What would you like to change?<textarea name="prompt" rows="5" required maxlength="24000" placeholder="Describe the result you want…">${input.draft ?? ''}</textarea></label><details class="coding-options"><summary>Session options</summary><label>Title <span class="coding-meta">Optional</span><input name="title" maxlength="160" autocomplete="off"></label><label>Model <span class="coding-meta">Optional; uses the installed default</span><input name="model" maxlength="120" placeholder="Installed default" autocomplete="off"></label></details><p class="coding-terms">Codex uses this installation’s login and edits a separate copy of this project. It can run commands and read files outside the project. Connected tools (MCP servers) keep their own access to this computer and connected services. Additional command and file access requires your approval.</p>${reauth(html `<label>Password to start<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit"${input.available ? '' : html ` disabled`}>Start coding</button>`, { attrs: { 'data-coding-form': 'start', class: 'coding-start' }, hidden: { requestId: randomUUID() } }) : html `<div class="coding-empty"><p>Add a project to start coding.</p><a class="coding-link-button" href="/projects">Open projects</a></div>`}`;
73
73
  }
74
74
  else {
75
75
  const s = selected.session;
@@ -94,6 +94,8 @@ export const CODING_CSS = `
94
94
  @media(prefers-reduced-motion:reduce){.coding-workspace *{scroll-behavior:auto!important}}
95
95
  `;
96
96
  export function codingWorkspaceScript() {
97
+ // Passwordless: the request form the script builds leaves its password field out (the server proves the session).
98
+ const approvePassword = passwordlessRendering() ? "" : '<label>Password to approve this request<input type="password" name="password" autocomplete="current-password" required></label>';
97
99
  return String.raw `(() => {
98
100
  const root = document.querySelector('.coding-workspace');
99
101
  if (!root || root.dataset.codingMounted) return;
@@ -196,7 +198,7 @@ export function codingWorkspaceScript() {
196
198
  };
197
199
  const requestHtml = request => {
198
200
  const questions = request.kind === 'questions';
199
- const body = questions ? request.questions.map((q,index) => '<label>' + e(q.question) + '<input type="text" name="question:' + e(q.id) + '"' + (q.options.length ? ' list="coding-options-' + e(request.id) + '-' + index + '"' : '') + ' required autocomplete="off">' + (q.options.length ? '<datalist id="coding-options-' + e(request.id) + '-' + index + '">' + q.options.map(o => '<option value="' + e(o.label) + '">' + e(o.description) + '</option>').join('') + '</datalist>' : '') + '</label>').join('') : '<pre class="coding-permission-detail">' + e(request.detail) + '</pre><label>Password to approve this request<input type="password" name="password" autocomplete="current-password" required></label>';
201
+ const body = questions ? request.questions.map((q,index) => '<label>' + e(q.question) + '<input type="text" name="question:' + e(q.id) + '"' + (q.options.length ? ' list="coding-options-' + e(request.id) + '-' + index + '"' : '') + ' required autocomplete="off">' + (q.options.length ? '<datalist id="coding-options-' + e(request.id) + '-' + index + '">' + q.options.map(o => '<option value="' + e(o.label) + '">' + e(o.description) + '</option>').join('') + '</datalist>' : '') + '</label>').join('') : '<pre class="coding-permission-detail">' + e(request.detail) + '</pre>' + ${JSON.stringify(approvePassword)};
200
202
  return '<section class="coding-request" data-coding-request="' + e(request.id) + '"><h3>' + e(request.title || (questions ? 'Your answer' : 'Allow extra access?')) + '</h3><form method="post" action="' + base + '/answer" data-coding-form="answer">' + h({csrf,requestId:request.id}) + body + '<div class="coding-actions"><button type="submit" name="decision" value="accept">' + (questions ? 'Send answer' : 'Approve access') + '</button><button type="submit" name="decision" value="decline" formnovalidate class="coding-secondary">' + (questions ? 'Cancel question' : 'Decline') + '</button></div></form></section>';
201
203
  };
202
204
  const render = snapshot => {