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
@@ -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(),
@@ -66,16 +66,23 @@ export declare const CONSOLE_FORMS: {
66
66
  readonly projectCeiling: FormContract<"repo">;
67
67
  readonly demo: FormContract<"message" | "note">;
68
68
  readonly updatesDismiss: FormContract<"quiet" | "return" | "version">;
69
+ readonly forgetPassword: FormContract<"return">;
70
+ readonly runReveal: FormContract<"return">;
69
71
  readonly updatesChecks: FormContract<"check">;
70
72
  readonly code: FormContract<"acceptance" | "answers" | "base" | "candidate" | "decision" | "goal" | "model" | "password" | "prompt" | "repo" | "requestId" | "title" | "visual" | `question:${string}`>;
73
+ readonly sweep: FormContract<"confirm" | "snapshot">;
74
+ readonly passwordless: FormContract<"action" | "password">;
75
+ readonly term: FormContract<"password" | "program" | "repo">;
71
76
  readonly skillsRevise: FormContract<"feedback" | "nonce" | "repo" | "run">;
72
77
  readonly skillsChange: FormContract<"action" | "content" | "files" | "identity" | "method" | "nonce" | "repo" | "restore" | "revision" | "sample" | "sha" | "url">;
73
78
  readonly kit: FormContract<"repo">;
74
79
  readonly subagents: FormContract<"choice" | "dailyTurns" | "id" | "manager" | "model" | "name" | "note" | "op" | "repo" | "schedule" | "soul" | "state" | "template" | "text" | "tool" | `field.${string}` | `over.${string}` | `undo.${string}` | `use.${string}`>;
75
80
  readonly galleryUse: FormContract<string>;
76
- readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "stage" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
81
+ readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "project" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "sender" | "stage" | "team" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
77
82
  readonly spendBudget: FormContract<"action" | "password" | "stop" | "target" | "usd">;
78
83
  readonly projectConcurrency: FormContract<"concurrency" | "repo">;
84
+ readonly projectLiveView: FormContract<"act" | "nonce" | "password" | "repo">;
85
+ readonly projectNetwork: FormContract<"hosts" | "local" | "mode" | "password" | "repo">;
79
86
  readonly projectDelete: FormContract<"name" | "password" | "repo" | "step">;
80
87
  readonly policy: FormContract<"ceiling" | "models" | "password" | "provider" | "tools">;
81
88
  readonly approval: FormContract<"not_requester" | "password" | "paths" | "protect" | "repo">;
@@ -144,6 +151,7 @@ export declare const CONSOLE_FORMS: {
144
151
  readonly leadFollow: FormContract<"enabled" | "return">;
145
152
  readonly leadEnd: FormContract<"return">;
146
153
  readonly leadStop: FormContract<"return" | "turn">;
154
+ readonly leadQueuedRemove: FormContract<"return">;
147
155
  readonly leadProposal: FormContract<"confirm" | "nonce" | "return" | "token">;
148
156
  readonly coordinatorProposal: FormContract<"confirm" | "return">;
149
157
  readonly chat: FormContract<"message" | "mode" | "project" | "request" | "request-session" | "result" | "task" | "token">;
@@ -156,7 +164,7 @@ export declare const CONSOLE_FORMS: {
156
164
  readonly revise: FormContract<"batch" | "return" | "source">;
157
165
  readonly followUp: FormContract<"level" | "return">;
158
166
  readonly resolveIncident: FormContract<"return">;
159
- readonly taskSteer: FormContract<"note" | "return" | "run">;
167
+ readonly taskSteer: FormContract<"after" | "key" | "note" | "request" | "return" | "run">;
160
168
  readonly taskReply: FormContract<"answer" | "question">;
161
169
  readonly taskHold: FormContract<"reason">;
162
170
  readonly taskBlock: FormContract<"on">;
@@ -170,11 +178,11 @@ export declare const CONSOLE_FORMS: {
170
178
  readonly taskCancel: FormContract<"reason">;
171
179
  readonly taskRoute: FormContract<"agent" | "clear-phase" | "model" | "phase" | "provider" | "return" | "risky" | "sawDigest" | "size">;
172
180
  readonly taskScope: FormContract<"acceptance" | "budget-microusd" | "budget-usd" | "goal" | "goal-brief" | "not" | "permission-mode" | "quality-mode" | "requirement" | "requirement-new" | "sawDigest" | "touches">;
173
- readonly taskApprove: FormContract<"digest" | "nonce" | "return" | "token">;
181
+ readonly taskApprove: FormContract<"digest" | "nonce" | "rebuild" | "return" | "token">;
174
182
  readonly taskAcceptProof: FormContract<"note" | "return" | "run">;
175
183
  readonly taskComplete: FormContract<"accept" | "note" | "publish" | "receipt" | "return" | "run">;
176
184
  readonly taskStop: FormContract<"return" | "run">;
177
- readonly taskPause: FormContract<"mode" | "note" | "request" | "return" | "run">;
185
+ readonly taskPause: FormContract<"key" | "mode" | "note" | "request" | "return" | "run">;
178
186
  readonly taskResume: FormContract<"nonce" | "return" | "run" | "token">;
179
187
  };
180
188
  export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
@@ -75,16 +75,23 @@ export const CONSOLE_FORMS = {
75
75
  projectCeiling: formContract("POST (project ceiling)", ["repo"]),
76
76
  demo: formContract("POST /chat/demo/ask, /chat/demo/<id>/approve|change|revise|complete", ["message", "note"]),
77
77
  updatesDismiss: formContract("POST /settings/updates/dismiss", ["version", "quiet", "return"]),
78
+ forgetPassword: formContract("POST /session/forget-password", ["return"]),
79
+ runReveal: formContract("POST /r/<run>/reveal", ["return"]),
78
80
  updatesChecks: formContract("POST /settings/updates/checks", ["check"]),
79
81
  code: formContract("POST /code, /code/start, /code/<session>/<act>, /code/<anything else>", ["repo", "password", "title", "model", "prompt", "requestId", "base", "candidate", "goal", "acceptance", "visual", "decision", "answers"], { prefixes: ["question:"] }),
82
+ sweep: formContract("POST /sweeps/<sweep>/cancel-rest", ["confirm", "snapshot"]),
83
+ passwordless: formContract("POST /settings/passwordless", ["action", "password"]),
84
+ term: formContract("POST /term/start, /term/<terminal>/close, /term/<terminal>/remove", ["repo", "program", "password"]),
80
85
  skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
81
86
  skillsChange: formContract("POST /settings/skills/import, /settings/skills/change", ["repo", "identity", "revision", "method", "content", "files", "url", "action", "sha", "sample", "nonce", "restore"]),
82
87
  kit: formContract("POST /kits/<kit>/<act>", ["repo"]),
83
88
  subagents: formContract("POST /settings/lead/subagents/new, /settings/lead/subagents/<id>/<part>, /settings/lead/subagents/questions/<id>/answer", ["choice", "text", "repo", "template", "name", "soul", "op", "id", "schedule", "tool", "state", "note", "model", "dailyTurns", "manager"], { prefixes: ["use.", "undo.", "field.", "over."] }),
84
89
  galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed"], { dynamic: true }),
85
- flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules"], { prefixes: ["param."] }),
90
+ flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "sender", "project", "team", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules"], { prefixes: ["param."] }),
86
91
  spendBudget: formContract("POST /spend/budget", ["password", "target", "action", "usd", "stop"]),
87
92
  projectConcurrency: formContract("POST /settings/project/concurrency", ["repo", "concurrency"]),
93
+ projectLiveView: formContract("POST /settings/project/live-view", ["repo", "act", "password", "nonce"]),
94
+ projectNetwork: formContract("POST /settings/project/network", ["repo", "mode", "local", "hosts", "password"]),
88
95
  projectDelete: formContract("POST /settings/project/delete", ["repo", "name", "step", "password"]),
89
96
  policy: formContract("POST /settings/policy", ["password", "provider", "models", "tools", "ceiling"]),
90
97
  approval: formContract("POST /settings/approval", ["repo", "password", "protect", "paths", "not_requester"]),
@@ -153,6 +160,7 @@ export const CONSOLE_FORMS = {
153
160
  leadFollow: formContract("POST /chat/mate/follow", ["enabled", "return"]),
154
161
  leadEnd: formContract("POST /chat/mate/end", ["return"]),
155
162
  leadStop: formContract("POST /chat/mate/stop", ["return", "turn"]),
163
+ leadQueuedRemove: formContract("POST /chat/queued/<message>/remove", ["return"]),
156
164
  leadProposal: formContract("POST /chat/proposal/<id>/confirm|dismiss", ["return", "nonce", "confirm", "token"]),
157
165
  coordinatorProposal: formContract("POST /proposals/<id>/confirm|dismiss", ["return", "confirm"]),
158
166
  chat: formContract("POST /chat", ["task", "project", "result", "mode", "request", "request-session", "message", "token"]),
@@ -167,7 +175,7 @@ export const CONSOLE_FORMS = {
167
175
  followUp: formContract("POST /r/<run>/checks|add-tests", ["return", "level"]),
168
176
  resolveIncident: formContract("POST /i/<id>/resolve", ["return"]),
169
177
  // A task's verbs (/t/<task>/<verb>), one body each.
170
- taskSteer: formContract("POST /t/<task>/steer", ["note", "return", "run"]),
178
+ taskSteer: formContract("POST /t/<task>/steer", ["note", "return", "run", "key", "after", "request"]),
171
179
  taskReply: formContract("POST /t/<task>/reply", ["question", "answer"]),
172
180
  taskHold: formContract("POST /t/<task>/hold", ["reason"]),
173
181
  taskBlock: formContract("POST /t/<task>/block|unblock", ["on"]),
@@ -181,13 +189,13 @@ export const CONSOLE_FORMS = {
181
189
  taskCancel: formContract("POST /t/<task>/cancel", ["reason"]),
182
190
  taskRoute: formContract("POST /t/<task>/route", ["size", "risky", "phase", "clear-phase", "agent", "provider", "model", "sawDigest", "return"]),
183
191
  taskScope: formContract("POST /t/<task>/scope", ["sawDigest", "permission-mode", "quality-mode", "budget-usd", "budget-microusd", "requirement", "requirement-new", "goal-brief", "goal", "not", "touches", "acceptance"]),
184
- taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce"]),
192
+ taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce", "rebuild"]),
185
193
  taskAcceptProof: formContract("POST /t/<task>/accept-proof", ["run", "note", "return"]),
186
194
  taskComplete: formContract("POST /t/<task>/complete", ["receipt", "run", "accept", "note", "publish", "return"]),
187
195
  taskStop: formContract("POST /t/<task>/stop", ["run", "return"]),
188
196
  // Pause, or Pause and steer when a note rides along, or Resume (mode=resume): the exact run shown, and the page's own
189
197
  // request id.
190
- taskPause: formContract("POST /t/<task>/pause", ["run", "request", "note", "mode", "return"]),
198
+ taskPause: formContract("POST /t/<task>/pause", ["run", "request", "note", "mode", "return", "key"]),
191
199
  taskResume: formContract("POST /t/<task>/resume|resume-arm", ["run", "token", "nonce", "return"]),
192
200
  };
193
201
  /** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
@@ -29,6 +29,10 @@ export declare const flowCardSchema: z.ZodObject<{
29
29
  outputs: z.ZodObject<{}, z.core.$catchall<z.ZodString>>;
30
30
  attached: z.ZodObject<{}, z.core.$catchall<z.ZodString>>;
31
31
  createdBy: z.ZodString;
32
+ createdKind: z.ZodEnum<{
33
+ automation: "automation";
34
+ person: "person";
35
+ }>;
32
36
  createdAt: z.ZodString;
33
37
  updatedAt: z.ZodString;
34
38
  owner: z.ZodNullable<z.ZodString>;
@@ -30,6 +30,9 @@ export const flowCardSchema = z.strictObject({
30
30
  outputs: cardOutputsSchema.shape.outputs,
31
31
  attached: cardOutputsSchema.shape.attached,
32
32
  createdBy: z.string(),
33
+ /** v126: whether a person or an automation (a delivery, a poll, a schedule, a public form) made it. Decided where it
34
+ * was made, never read from its label: filing passes it on as the requester's kind. */
35
+ createdKind: z.enum(["person", "automation"]),
33
36
  createdAt: z.string(),
34
37
  updatedAt: z.string(),
35
38
  owner: z.string().nullable(),