toolroll 0.9.63 → 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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  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 +50 -2
  16. package/dist/agent-fence.js +157 -10
  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 +235 -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 +197 -15
  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/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -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>`,
@@ -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, () => {
@@ -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 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.";
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
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",
@@ -363,6 +364,24 @@ async function dispatch(argv, write, mainOptions) {
363
364
  if (first !== undefined && OPERATE_COMMANDS.has(first)) {
364
365
  return runOperate(first, rest, write, mainOptions.operate ?? {});
365
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
+ }
366
385
  const parsed = parseArgs(argv);
367
386
  if ("error" in parsed) {
368
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 => {
@@ -0,0 +1,124 @@
1
+ import { type Socket } from "node:net";
2
+ import type { Store } from "./store.js";
3
+ export declare const CONSOLE_INSTANCE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS console_instance (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n secret TEXT NOT NULL,\n pid INTEGER NOT NULL,\n port INTEGER NOT NULL,\n build TEXT NOT NULL,\n socket TEXT NOT NULL,\n started_at TEXT NOT NULL\n);";
4
+ /**
5
+ * The socket's own owner-only directory beside the database's REAL file (a symlinked database resolves to the one
6
+ * folder every spelling shares). Each console start binds a socket of its own, under a fresh random name recorded
7
+ * with its secret: no console ever needs to remove — or can remove — another console's socket.
8
+ */
9
+ /** Toolroll's own name, so it never collides with a project or worktree that happens to sit beside the database. */
10
+ export declare const INSTANCE_DIR = ".toolroll-run";
11
+ export declare function instanceRunDir(databaseFile: string): string;
12
+ /** The console's own answer to a challenge over HTTP (an edge route, no sign-in: it reveals nothing). */
13
+ export declare const INSTANCE_PATH = "/__instance";
14
+ export type InstanceRecord = {
15
+ secret: string;
16
+ pid: number;
17
+ port: number;
18
+ build: string;
19
+ socket: string;
20
+ startedAt: string;
21
+ };
22
+ export type HelloFacts = {
23
+ pid: number;
24
+ build: string;
25
+ database: string;
26
+ port: number;
27
+ };
28
+ /** The record as stored: none, one this build can't read (never taken for none), or the record. */
29
+ export declare function readInstanceState(store: Store): {
30
+ state: "none";
31
+ } | {
32
+ state: "invalid";
33
+ } | {
34
+ state: "ok";
35
+ record: InstanceRecord;
36
+ };
37
+ export declare function readInstance(store: Store): InstanceRecord | null;
38
+ /** A stopping console forgets itself — only its own record, never a newer start's. */
39
+ export declare function clearInstance(store: Store, secret: string): void;
40
+ export declare const socketProof: (secret: string, challenge: string) => string;
41
+ export declare const httpProof: (secret: string, challenge: string, address: string, port: number) => string;
42
+ /** The HTTP answer: bound to the address and port THIS connection was received on, as the console saw them. */
43
+ export declare function instanceHttpAnswer(secret: string, challenge: string | null, local: {
44
+ address: string | undefined;
45
+ port: number | undefined;
46
+ }): {
47
+ status: number;
48
+ body: string;
49
+ };
50
+ /**
51
+ * The run directory must be private to this user: a real, canonical directory owned by this user with no access for
52
+ * anyone else (0700), inside a canonical directory owned by this user that nobody else can write. Checked before any
53
+ * change is made, and before connecting or binding.
54
+ */
55
+ export declare function runDirProblem(dir: string, uid?: number, mustExist?: boolean): string | null;
56
+ /** A socket path that binds exactly (no truncation), named as a console names its own. */
57
+ export declare function socketPathProblem(path: string): string | null;
58
+ /**
59
+ * `up`: the run directory, made 0700 if it isn't there. An existing one is used only as it is (never chmodded): one
60
+ * that isn't private to this user is refused. Everything is checked before anything is created.
61
+ */
62
+ export declare function prepareRunDir(dir: string, uid?: number): string | null;
63
+ /** Ask a socket one question; null when nobody is listening (or it said nothing usable in time). */
64
+ export declare function askSocket(socketPath: string, request: object, timeoutMs?: number, connect?: (path: string) => Socket): Promise<{
65
+ answer: Record<string, unknown>;
66
+ } | {
67
+ none: "no-listener" | "no-answer";
68
+ }>;
69
+ /**
70
+ * The console's discovery socket: one line in, one line out. `hello` (with a challenge) answers the public facts and
71
+ * the socket proof; every other verb is refused. It reads nothing else and changes nothing. Bound under a fresh name
72
+ * no other process has, so it never clears or replaces anything; its own listener removes it on close.
73
+ */
74
+ export declare function openInstanceSocket(databaseFile: string, facts: HelloFacts, secret: () => string | null): Promise<{
75
+ ok: true;
76
+ name: string;
77
+ close: () => void;
78
+ } | {
79
+ ok: false;
80
+ problem: string;
81
+ }>;
82
+ /**
83
+ * A record's console: "live" (its socket answers a fresh challenge with the record's secret and pid), "gone" (nobody
84
+ * is listening on its socket, or the socket isn't there), or "unknown" (anything else: slow, malformed, a wrong proof).
85
+ * Only "gone" lets another start take the record.
86
+ */
87
+ export declare function recordLiveness(databaseFile: string, record: InstanceRecord, ask?: typeof askSocket): Promise<"live" | "gone" | "unknown">;
88
+ /**
89
+ * `up` claims the record for its socket — unless a live console already holds it (another start on the same
90
+ * database, by any spelling of its path). The write is a compare-and-set on the record it checked: two starts racing
91
+ * can't both win. Null: not claimed (this console runs, but `toolroll` won't attach to it).
92
+ */
93
+ export declare function claimInstance(store: Store, databaseFile: string, facts: {
94
+ pid: number;
95
+ port: number;
96
+ build: string;
97
+ socket: string;
98
+ }, now: Date, liveness?: (record: InstanceRecord) => Promise<"live" | "gone" | "unknown">): Promise<string | null>;
99
+ export type AttachProof = {
100
+ ok: true;
101
+ url: string;
102
+ port: number;
103
+ pid: number;
104
+ build: string;
105
+ } | {
106
+ ok: false;
107
+ state: "none";
108
+ message: string;
109
+ } | {
110
+ ok: false;
111
+ state: "refused";
112
+ message: string;
113
+ };
114
+ export type AttachSeams = {
115
+ ask?: typeof askSocket;
116
+ fetch?: typeof fetch;
117
+ challenge?: () => string;
118
+ };
119
+ /**
120
+ * Prove the console this installation's database names is live, on both channels, before anything is shown or
121
+ * opened. "none": no console runs (no record, or nobody on the socket); "refused": something answers that can't
122
+ * prove it is the console (or a different build) — the person restarts it.
123
+ */
124
+ export declare function proveLiveConsole(databaseFile: string, store: Store, build: string, seams?: AttachSeams): Promise<AttachProof>;