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
@@ -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>;
@@ -0,0 +1,337 @@
1
+ /**
2
+ * THE RUNNING CONSOLE, PROVED LIVE (docs/plans/agents-cockpit.md B1).
3
+ *
4
+ * Bare `toolroll` in a repository attaches to a console already running for this installation instead of starting a
5
+ * second one. Build agents run as the operator's own user, so nothing the filesystem says can tell the operator from
6
+ * an agent: the console's Unix socket is DISCOVERY ONLY. It answers one verb, `hello`, with facts an agent could learn
7
+ * anyway, and changes nothing.
8
+ *
9
+ * What proves the console is real is a fresh random instance secret, minted at each start of `up` and written to the
10
+ * database (behind the agent fence) with its pid, build and port. The CLI reads it from the database it opens itself,
11
+ * then makes the LIVE console answer two fresh challenges with it: over the socket (HMAC of "socket:" + challenge),
12
+ * and over the advertised HTTP listener (HMAC of "http:" + challenge + " " + address + " " + port, where the address and
13
+ * port are what the console itself saw as the receiving end of that very connection). The CLI requires those to be
14
+ * 127.0.0.1 and the recorded port, so a relay on 127.0.0.1 forwarding to the console's other address is refused, as are
15
+ * a replayed hello, a stale record with a recycled pid, and another process now on that port.
16
+ */
17
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
18
+ import { lstatSync, mkdirSync, realpathSync } from "node:fs";
19
+ import { createConnection, createServer } from "node:net";
20
+ import { basename, dirname, join } from "node:path";
21
+ export const CONSOLE_INSTANCE_SCHEMA = `
22
+ CREATE TABLE IF NOT EXISTS console_instance (
23
+ id INTEGER PRIMARY KEY CHECK (id = 1),
24
+ secret TEXT NOT NULL,
25
+ pid INTEGER NOT NULL,
26
+ port INTEGER NOT NULL,
27
+ build TEXT NOT NULL,
28
+ socket TEXT NOT NULL,
29
+ started_at TEXT NOT NULL
30
+ );`;
31
+ /**
32
+ * The socket's own owner-only directory beside the database's REAL file (a symlinked database resolves to the one
33
+ * folder every spelling shares). Each console start binds a socket of its own, under a fresh random name recorded
34
+ * with its secret: no console ever needs to remove — or can remove — another console's socket.
35
+ */
36
+ /** Toolroll's own name, so it never collides with a project or worktree that happens to sit beside the database. */
37
+ export const INSTANCE_DIR = ".toolroll-run";
38
+ export function instanceRunDir(databaseFile) {
39
+ let file = databaseFile;
40
+ try {
41
+ file = realpathSync(databaseFile);
42
+ }
43
+ catch {
44
+ try {
45
+ file = join(realpathSync(dirname(databaseFile)), basename(databaseFile));
46
+ }
47
+ catch { /* not there yet: as given */ }
48
+ }
49
+ return join(dirname(file), INSTANCE_DIR);
50
+ }
51
+ const SOCKET_NAME = /^[0-9a-f]{16}\.sock$/;
52
+ /** The longest Unix socket path the platform binds without truncating (sun_path less its NUL). */
53
+ const SOCKET_PATH_MAX = process.platform === "darwin" ? 103 : 107;
54
+ /** The console's own answer to a challenge over HTTP (an edge route, no sign-in: it reveals nothing). */
55
+ export const INSTANCE_PATH = "/__instance";
56
+ const CHALLENGE = /^[0-9a-f]{32,128}$/;
57
+ const LINE_CAP = 4096;
58
+ /** The record as stored: none, one this build can't read (never taken for none), or the record. */
59
+ export function readInstanceState(store) {
60
+ const row = store.handle.prepare("SELECT secret, pid, port, build, socket, started_at FROM console_instance WHERE id = 1").get();
61
+ if (row === undefined)
62
+ return { state: "none" };
63
+ const secret = String(row["secret"]), pid = Number(row["pid"]), port = Number(row["port"]), socket = String(row["socket"]);
64
+ if (!/^[0-9a-f]{64}$/.test(secret) || !Number.isSafeInteger(pid) || !Number.isInteger(port) || port < 1 || port > 65535 || !SOCKET_NAME.test(socket))
65
+ return { state: "invalid" };
66
+ return { state: "ok", record: { secret, pid, port, build: String(row["build"]), socket, startedAt: String(row["started_at"]) } };
67
+ }
68
+ export function readInstance(store) {
69
+ const read = readInstanceState(store);
70
+ return read.state === "ok" ? read.record : null;
71
+ }
72
+ /** A stopping console forgets itself — only its own record, never a newer start's. */
73
+ export function clearInstance(store, secret) {
74
+ store.handle.prepare("DELETE FROM console_instance WHERE id = 1 AND secret = ?").run(secret);
75
+ }
76
+ export const socketProof = (secret, challenge) => createHmac("sha256", secret).update(`socket:${challenge}`).digest("hex");
77
+ export const httpProof = (secret, challenge, address, port) => createHmac("sha256", secret).update(`http:${challenge} ${address} ${port}`).digest("hex");
78
+ const PROOF = /^[0-9a-f]{64}$/;
79
+ /** Constant-time comparison of two proofs, each first required to be exactly 64 hex characters. */
80
+ const same = (a, b) => typeof a === "string" && PROOF.test(a) && PROOF.test(b) && timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
81
+ /** The HTTP answer: bound to the address and port THIS connection was received on, as the console saw them. */
82
+ export function instanceHttpAnswer(secret, challenge, local) {
83
+ if (challenge === null || !CHALLENGE.test(challenge) || local.address === undefined || local.port === undefined)
84
+ return { status: 400, body: JSON.stringify({ error: "challenge" }) };
85
+ return { status: 200, body: JSON.stringify({ proof: httpProof(secret, challenge, local.address, local.port) }) };
86
+ }
87
+ /**
88
+ * The run directory must be private to this user: a real, canonical directory owned by this user with no access for
89
+ * anyone else (0700), inside a canonical directory owned by this user that nobody else can write. Checked before any
90
+ * change is made, and before connecting or binding.
91
+ */
92
+ export function runDirProblem(dir, uid = process.getuid?.() ?? -1, mustExist = true) {
93
+ const parent = dirname(dir);
94
+ let above;
95
+ try {
96
+ above = lstatSync(parent);
97
+ }
98
+ catch {
99
+ return `${parent} doesn't exist`;
100
+ }
101
+ try {
102
+ if (realpathSync(parent) !== parent)
103
+ return `${parent} isn't its own real path`;
104
+ }
105
+ catch {
106
+ return `${parent} doesn't exist`;
107
+ }
108
+ if (!above.isDirectory())
109
+ return `${parent} isn't a directory`;
110
+ if (uid >= 0 && above.uid !== uid)
111
+ return `${parent} belongs to another user`;
112
+ if ((above.mode & 0o022) !== 0)
113
+ return `${parent} can be written by other users`;
114
+ let info;
115
+ try {
116
+ info = lstatSync(dir);
117
+ }
118
+ catch {
119
+ return mustExist ? `${dir} doesn't exist` : null;
120
+ }
121
+ if (!info.isDirectory())
122
+ return `${dir} isn't a directory`;
123
+ if (uid >= 0 && info.uid !== uid)
124
+ return `${dir} belongs to another user`;
125
+ try {
126
+ lstatSync(join(dir, ".git"));
127
+ return `${dir} holds a repository, not Toolroll's sockets`;
128
+ }
129
+ catch { /* as it should be */ }
130
+ if ((info.mode & 0o077) !== 0)
131
+ return `${dir} is open to other users — Toolroll uses it only when it is yours alone (chmod 700)`;
132
+ return null;
133
+ }
134
+ /** A socket path that binds exactly (no truncation), named as a console names its own. */
135
+ export function socketPathProblem(path) {
136
+ if (!SOCKET_NAME.test(basename(path)))
137
+ return `${path} isn't a console socket`;
138
+ if (Buffer.byteLength(path) > SOCKET_PATH_MAX)
139
+ return `${path} is too long for a socket here`;
140
+ return null;
141
+ }
142
+ /**
143
+ * `up`: the run directory, made 0700 if it isn't there. An existing one is used only as it is (never chmodded): one
144
+ * that isn't private to this user is refused. Everything is checked before anything is created.
145
+ */
146
+ export function prepareRunDir(dir, uid = process.getuid?.() ?? -1) {
147
+ const before = runDirProblem(dir, uid, false);
148
+ if (before !== null)
149
+ return before;
150
+ try {
151
+ mkdirSync(dir, { mode: 0o700 });
152
+ }
153
+ catch (error) {
154
+ if (error.code !== "EEXIST")
155
+ return `${dir} couldn't be made`;
156
+ }
157
+ return runDirProblem(dir, uid, true);
158
+ }
159
+ /** Ask a socket one question; null when nobody is listening (or it said nothing usable in time). */
160
+ export function askSocket(socketPath, request, timeoutMs = 2_000, connect = path => createConnection(path)) {
161
+ return new Promise(resolve => {
162
+ let settled = false, buffer = "";
163
+ const done = (value) => { if (settled)
164
+ return; settled = true; clearTimeout(timer); socket.destroy(); resolve(value); };
165
+ const socket = connect(socketPath);
166
+ const timer = setTimeout(() => done({ none: "no-answer" }), timeoutMs);
167
+ socket.setEncoding("utf8");
168
+ socket.on("error", (error) => done({ none: error.code === "ECONNREFUSED" || error.code === "ENOENT" ? "no-listener" : "no-answer" }));
169
+ socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
170
+ socket.on("data", chunk => {
171
+ buffer += chunk;
172
+ if (buffer.length > LINE_CAP)
173
+ return done({ none: "no-answer" });
174
+ const end = buffer.indexOf("\n");
175
+ if (end < 0)
176
+ return;
177
+ try {
178
+ const parsed = JSON.parse(buffer.slice(0, end));
179
+ done(parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? { answer: parsed } : { none: "no-answer" });
180
+ }
181
+ catch {
182
+ done({ none: "no-answer" });
183
+ }
184
+ });
185
+ socket.on("end", () => done({ none: "no-answer" }));
186
+ });
187
+ }
188
+ /**
189
+ * The console's discovery socket: one line in, one line out. `hello` (with a challenge) answers the public facts and
190
+ * the socket proof; every other verb is refused. It reads nothing else and changes nothing. Bound under a fresh name
191
+ * no other process has, so it never clears or replaces anything; its own listener removes it on close.
192
+ */
193
+ export async function openInstanceSocket(databaseFile, facts, secret) {
194
+ const dir = instanceRunDir(databaseFile);
195
+ const problem = prepareRunDir(dir);
196
+ if (problem !== null)
197
+ return { ok: false, problem };
198
+ const name = `${randomBytes(8).toString("hex")}.sock`, socketPath = join(dir, name);
199
+ const pathProblem = socketPathProblem(socketPath);
200
+ if (pathProblem !== null)
201
+ return { ok: false, problem: pathProblem };
202
+ const server = createServer(socket => {
203
+ let buffer = "";
204
+ socket.setEncoding("utf8");
205
+ socket.setTimeout(5_000, () => socket.destroy());
206
+ const reply = (body) => { socket.end(`${JSON.stringify(body)}\n`); };
207
+ socket.on("error", () => socket.destroy());
208
+ socket.on("data", chunk => {
209
+ buffer += chunk;
210
+ if (buffer.length > LINE_CAP)
211
+ return void socket.destroy();
212
+ const end = buffer.indexOf("\n");
213
+ if (end < 0)
214
+ return;
215
+ let asked;
216
+ try {
217
+ asked = JSON.parse(buffer.slice(0, end));
218
+ }
219
+ catch {
220
+ return reply({ error: "refused" });
221
+ }
222
+ const verb = asked !== null && typeof asked === "object" ? asked.verb : undefined;
223
+ const challenge = asked !== null && typeof asked === "object" ? asked.challenge : undefined;
224
+ const key = secret();
225
+ if (verb !== "hello" || typeof challenge !== "string" || !CHALLENGE.test(challenge) || key === null)
226
+ return reply({ error: "refused" });
227
+ reply({ ...facts, proof: socketProof(key, challenge) });
228
+ });
229
+ });
230
+ // A Unix bind never replaces an existing path (EADDRINUSE).
231
+ const listening = await new Promise(resolve => { server.once("error", () => resolve(false)); server.listen(socketPath, () => resolve(true)); });
232
+ if (!listening)
233
+ return { ok: false, problem: `couldn't listen on ${socketPath}` };
234
+ return { ok: true, name, close: () => { server.close(); } };
235
+ }
236
+ /**
237
+ * A record's console: "live" (its socket answers a fresh challenge with the record's secret and pid), "gone" (nobody
238
+ * is listening on its socket, or the socket isn't there), or "unknown" (anything else: slow, malformed, a wrong proof).
239
+ * Only "gone" lets another start take the record.
240
+ */
241
+ export async function recordLiveness(databaseFile, record, ask = askSocket) {
242
+ const socketPath = join(instanceRunDir(databaseFile), record.socket);
243
+ if (socketPathProblem(socketPath) !== null)
244
+ return "unknown";
245
+ const challenge = randomBytes(32).toString("hex");
246
+ const asked = await ask(socketPath, { verb: "hello", challenge }, 5_000);
247
+ if ("none" in asked)
248
+ return asked.none === "no-listener" ? "gone" : "unknown";
249
+ return asked.answer["pid"] === record.pid && same(asked.answer["proof"], socketProof(record.secret, challenge)) ? "live" : "unknown";
250
+ }
251
+ /**
252
+ * `up` claims the record for its socket — unless a live console already holds it (another start on the same
253
+ * database, by any spelling of its path). The write is a compare-and-set on the record it checked: two starts racing
254
+ * can't both win. Null: not claimed (this console runs, but `toolroll` won't attach to it).
255
+ */
256
+ export async function claimInstance(store, databaseFile, facts, now, liveness = record => recordLiveness(databaseFile, record)) {
257
+ const rawSecret = () => { const row = store.handle.prepare("SELECT secret FROM console_instance WHERE id = 1").get(); return row === undefined ? null : String(row["secret"]); };
258
+ const checked = rawSecret();
259
+ // Taken only from nobody, or from a console confirmed gone: never on a check that merely failed, and never over a
260
+ // record this build can't read.
261
+ const current = readInstanceState(store);
262
+ if (current.state === "invalid")
263
+ return null;
264
+ if (current.state === "ok" && await liveness(current.record) !== "gone")
265
+ return null;
266
+ const secret = randomBytes(32).toString("hex");
267
+ return store.transact(() => {
268
+ // Exactly the record that was checked (or none): anything else is another start's newer claim.
269
+ if (rawSecret() !== checked)
270
+ return null;
271
+ store.handle.prepare("INSERT INTO console_instance (id, secret, pid, port, build, socket, started_at) VALUES (1, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET secret = excluded.secret, pid = excluded.pid, port = excluded.port, build = excluded.build, socket = excluded.socket, started_at = excluded.started_at")
272
+ .run(secret, facts.pid, facts.port, facts.build, facts.socket, now.toISOString());
273
+ return secret;
274
+ });
275
+ }
276
+ /**
277
+ * Prove the console this installation's database names is live, on both channels, before anything is shown or
278
+ * opened. "none": no console runs (no record, or nobody on the socket); "refused": something answers that can't
279
+ * prove it is the console (or a different build) — the person restarts it.
280
+ */
281
+ export async function proveLiveConsole(databaseFile, store, build, seams = {}) {
282
+ const restart = "stop it and run `toolroll up` again";
283
+ // No record: nothing is running here (a console that couldn't claim one isn't offered). One this build can't read
284
+ // isn't "nothing": it is refused.
285
+ const read = readInstanceState(store);
286
+ if (read.state === "none")
287
+ return { ok: false, state: "none", message: "no console is running" };
288
+ if (read.state === "invalid")
289
+ return { ok: false, state: "refused", message: `the console's record can't be read — ${restart}` };
290
+ const record = read.record;
291
+ const dir = instanceRunDir(databaseFile);
292
+ const problem = runDirProblem(dir) ?? socketPathProblem(join(dir, record.socket));
293
+ if (problem !== null)
294
+ return { ok: false, state: "refused", message: `the console's socket can't be trusted: ${problem}` };
295
+ const socketPath = join(dir, record.socket);
296
+ try {
297
+ const socket = lstatSync(socketPath);
298
+ if (!socket.isSocket() || (process.getuid !== undefined && socket.uid !== process.getuid()))
299
+ return { ok: false, state: "refused", message: `the console's socket can't be trusted: ${socketPath} isn't this user's socket` };
300
+ }
301
+ catch (error) {
302
+ // Only "not there" means nothing is running; a socket the system won't show can't be checked.
303
+ if (error.code === "ENOENT")
304
+ return { ok: false, state: "none", message: "no console is running" };
305
+ return { ok: false, state: "refused", message: `the console's socket can't be checked — ${restart}` };
306
+ }
307
+ const challenge = seams.challenge ?? (() => randomBytes(32).toString("hex"));
308
+ const first = challenge();
309
+ const asked = await (seams.ask ?? askSocket)(socketPath, { verb: "hello", challenge: first });
310
+ if ("none" in asked) {
311
+ // Stale (a console that died): nobody is listening, so nothing is running. Discovery removes nothing.
312
+ if (asked.none === "no-listener")
313
+ return { ok: false, state: "none", message: "no console is running" };
314
+ return { ok: false, state: "refused", message: `the console's socket didn't answer — ${restart}` };
315
+ }
316
+ const hello = asked.answer;
317
+ if (hello["build"] !== record.build || hello["build"] !== build) {
318
+ return { ok: false, state: "refused", message: `the running console is a different build (${String(hello["build"] ?? "unknown")}, this is ${build}) — ${restart}` };
319
+ }
320
+ if (hello["pid"] !== record.pid || hello["port"] !== record.port || !same(hello["proof"], socketProof(record.secret, first))) {
321
+ return { ok: false, state: "refused", message: `the console on the socket couldn't prove it's this installation's — ${restart}` };
322
+ }
323
+ const second = challenge();
324
+ let body;
325
+ try {
326
+ const answer = await (seams.fetch ?? fetch)(`http://127.0.0.1:${record.port}${INSTANCE_PATH}?c=${second}`, { redirect: "manual", signal: AbortSignal.timeout(3_000) });
327
+ body = answer.status === 200 ? await answer.json() : null;
328
+ }
329
+ catch {
330
+ body = null;
331
+ }
332
+ const proof = body !== null && typeof body === "object" ? body.proof : undefined;
333
+ if (!same(proof, httpProof(record.secret, second, "127.0.0.1", record.port))) {
334
+ return { ok: false, state: "refused", message: `the address 127.0.0.1:${record.port} couldn't prove it's the console — ${restart}` };
335
+ }
336
+ return { ok: true, url: `http://127.0.0.1:${record.port}`, port: record.port, pid: record.pid, build: record.build };
337
+ }
@@ -85,6 +85,20 @@ export declare const chatActionRequestSchemas: {
85
85
  repo: z.ZodString;
86
86
  starter: z.ZodString;
87
87
  }, z.core.$strict>;
88
+ readonly sweep_cancel_rest: z.ZodObject<{
89
+ sweep: z.ZodInt;
90
+ }, z.core.$strict>;
91
+ readonly sweep_file: z.ZodObject<{
92
+ repo: z.ZodString;
93
+ title: z.ZodString;
94
+ brief: z.ZodString;
95
+ items: z.ZodArray<z.ZodObject<{
96
+ title: z.ZodString;
97
+ item: z.ZodString;
98
+ }, z.core.$strict>>;
99
+ acceptance: z.ZodArray<z.ZodUnknown>;
100
+ maxParallel: z.ZodNumber;
101
+ }, z.core.$strict>;
88
102
  readonly flow_edit: z.ZodObject<{
89
103
  flow: z.ZodInt;
90
104
  name: z.ZodOptional<z.ZodString>;
@@ -309,6 +323,8 @@ export declare const sharedActionSchema: z.ZodObject<{
309
323
  subagent_state: "subagent_state";
310
324
  subagent_tools: "subagent_tools";
311
325
  subagent_undo: "subagent_undo";
326
+ sweep_cancel_rest: "sweep_cancel_rest";
327
+ sweep_file: "sweep_file";
312
328
  task_cancel: "task_cancel";
313
329
  task_resume: "task_resume";
314
330
  tool_add: "tool_add";
@@ -41,6 +41,10 @@ export const chatActionRequestSchemas = {
41
41
  tool_remove: z.strictObject({ repo: words, name: words }),
42
42
  flow_create: z.strictObject({ repo: words, name: words, definition: nested, trigger: nested.optional() }),
43
43
  flow_starter: z.strictObject({ repo: words, starter: words }),
44
+ // Lead fan-out (docs/plans/lead-fan-out.md): N tasks under one brief, one yes.
45
+ sweep_cancel_rest: z.strictObject({ sweep: id }),
46
+ sweep_file: z.strictObject({ repo: words, title: words, brief: words, items: z.array(z.strictObject({ title: words, item: words })).min(2).max(20),
47
+ acceptance: z.array(z.unknown()).min(1), maxParallel: z.number().int().min(1).max(8) }),
44
48
  flow_edit: z.strictObject({ flow: id, name: words.optional(), definition: nested.optional() }),
45
49
  flow_card_add: z.strictObject({ flow: id, title: words, description: z.unknown().optional(), zone: words.optional() }),
46
50
  flow_card_move: z.strictObject({ card: id, zone: words }),
@@ -31,6 +31,15 @@ export declare const chatContentShape: {
31
31
  }, z.core.$strict>>;
32
32
  task: z.ZodOptional<z.ZodString>;
33
33
  run: z.ZodOptional<z.ZodInt>;
34
+ /** A notice its person's watch may hold at the final fence (cockpit §3): set at planning only for a kind a watch
35
+ * can hold. Absent: never held (a security alert, the digest, anything unmarked). */
36
+ watch: z.ZodOptional<z.ZodLiteral<true>>;
37
+ /** Every task (and agent) a `watch` part covers, when it covers more than one: it is held only while its person is
38
+ * watching every one of them. */
39
+ subjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
40
+ ref: z.ZodNullable<z.ZodInt>;
41
+ run: z.ZodNullable<z.ZodInt>;
42
+ }, z.core.$strict>>>;
34
43
  edit: z.ZodOptional<z.ZodString>;
35
44
  phase: z.ZodOptional<z.ZodLiteral<"armed">>;
36
45
  link: z.ZodOptional<z.ZodObject<{
@@ -123,6 +132,11 @@ export declare const chatContentSchema: z.ZodObject<{
123
132
  }, z.core.$strict>>;
124
133
  task: z.ZodOptional<z.ZodString>;
125
134
  run: z.ZodOptional<z.ZodInt>;
135
+ watch: z.ZodOptional<z.ZodLiteral<true>>;
136
+ subjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
137
+ ref: z.ZodNullable<z.ZodInt>;
138
+ run: z.ZodNullable<z.ZodInt>;
139
+ }, z.core.$strict>>>;
126
140
  edit: z.ZodOptional<z.ZodString>;
127
141
  phase: z.ZodOptional<z.ZodLiteral<"armed">>;
128
142
  link: z.ZodOptional<z.ZodObject<{
@@ -205,6 +219,11 @@ export declare const savedChatPartSchema: z.ZodObject<{
205
219
  }, z.core.$strict>>;
206
220
  task: z.ZodOptional<z.ZodString>;
207
221
  run: z.ZodOptional<z.ZodInt>;
222
+ watch: z.ZodOptional<z.ZodLiteral<true>>;
223
+ subjects: z.ZodOptional<z.ZodArray<z.ZodObject<{
224
+ ref: z.ZodNullable<z.ZodInt>;
225
+ run: z.ZodNullable<z.ZodInt>;
226
+ }, z.core.$strict>>>;
208
227
  edit: z.ZodOptional<z.ZodString>;
209
228
  phase: z.ZodOptional<z.ZodLiteral<"armed">>;
210
229
  link: z.ZodOptional<z.ZodObject<{
@@ -26,6 +26,12 @@ export const chatContentShape = {
26
26
  shot: z.strictObject({ follows: z.int().nullable() }).optional(),
27
27
  task: z.string().optional(),
28
28
  run: z.int().optional(),
29
+ /** A notice its person's watch may hold at the final fence (cockpit §3): set at planning only for a kind a watch
30
+ * can hold. Absent: never held (a security alert, the digest, anything unmarked). */
31
+ watch: z.literal(true).optional(),
32
+ /** Every task (and agent) a `watch` part covers, when it covers more than one: it is held only while its person is
33
+ * watching every one of them. */
34
+ subjects: z.array(z.strictObject({ ref: z.int().nullable(), run: z.int().nullable() })).max(100).optional(),
29
35
  edit: z.string().optional(),
30
36
  phase: z.literal("armed").optional(),
31
37
  link: link.optional(),
@@ -647,6 +647,14 @@ export declare const COMMAND_OUTPUTS: {
647
647
  readable: z.ZodBoolean;
648
648
  zones: z.ZodArray<z.ZodUnknown>;
649
649
  triggers: z.ZodArray<z.ZodUnknown>;
650
+ unattended: z.ZodNullable<z.ZodObject<{
651
+ until: z.ZodString;
652
+ signedBy: z.ZodString;
653
+ budgetMicrousd: z.ZodNullable<z.ZodNumber>;
654
+ dailyRunCap: z.ZodNullable<z.ZodNumber>;
655
+ runsToday: z.ZodNumber;
656
+ covers: z.ZodArray<z.ZodString>;
657
+ }, z.core.$loose>>;
650
658
  }, z.core.$loose>;
651
659
  }, z.core.$loose>, z.ZodObject<{
652
660
  envelopeVersion: z.ZodLiteral<1>;
@@ -184,7 +184,9 @@ export const COMMAND_OUTPUTS = {
184
184
  "task unhold": answers("task unhold", { id: str }),
185
185
  "task wait": answers("task wait", { task: str, outcome: str }),
186
186
  "flows list": answers("flows list", { flows: z.array(flowListed) }),
187
- "flows show": answers("flows show", { flow: z.looseObject({ id: int, name: str, repo: str, project: str, readable: z.boolean(), zones: list, triggers: list }) }),
187
+ "flows show": answers("flows show", { flow: z.looseObject({ id: int, name: str, repo: str, project: str, readable: z.boolean(), zones: list, triggers: list,
188
+ // Whether the flow runs on its own, and until when (null: it doesn't).
189
+ unattended: nullable(z.looseObject({ until: str, signedBy: str, budgetMicrousd: nullable(int), dailyRunCap: nullable(int), runsToday: int, covers: z.array(str) })) }) }),
188
190
  grants: answers("grants", { count: int, grants: list }),
189
191
  "runner list": answers("runner list", { count: int, runners: list }),
190
192
  "incident list": answers("incident list", { incidents: list }),