toolroll 0.9.63 → 0.9.67

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 (302) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +33 -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 +117 -2
  16. package/dist/agent-fence.js +439 -12
  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 +39 -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/batch-checks.js +3 -2
  32. package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
  33. package/dist/browser/terminal.css +37 -0
  34. package/dist/browser/terminal.js +228 -0
  35. package/dist/browser/workspace.css +2 -2
  36. package/dist/browser/workspace.js +42 -38
  37. package/dist/browser-shell.js +6 -3
  38. package/dist/browser-workspace.d.ts +235 -1
  39. package/dist/browser-workspace.js +60 -0
  40. package/dist/builder.d.ts +10 -1
  41. package/dist/builder.js +87 -24
  42. package/dist/chat-actions.d.ts +13 -1
  43. package/dist/chat-actions.js +197 -15
  44. package/dist/chat-ask.d.ts +4 -0
  45. package/dist/chat-ask.js +14 -0
  46. package/dist/chat-channel.js +20 -0
  47. package/dist/chat-continuity.js +5 -4
  48. package/dist/chat-decide.d.ts +0 -14
  49. package/dist/chat-decide.js +4 -1
  50. package/dist/chat-delivery.d.ts +9 -0
  51. package/dist/chat-delivery.js +45 -6
  52. package/dist/chat-messages.d.ts +8 -0
  53. package/dist/chat-messages.js +36 -4
  54. package/dist/check-levels-ui.js +2 -2
  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 +19 -0
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +4 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +21 -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/console-api.d.ts +13 -4
  71. package/dist/contracts/console-api.js +13 -4
  72. package/dist/contracts/flow-card.d.ts +4 -0
  73. package/dist/contracts/flow-card.js +3 -0
  74. package/dist/contracts/flow.d.ts +46 -0
  75. package/dist/contracts/flow.js +11 -2
  76. package/dist/contracts/lead-tools.d.ts +92 -0
  77. package/dist/contracts/lead-tools.js +13 -0
  78. package/dist/contracts/provider.d.ts +11 -1
  79. package/dist/contracts/provider.js +1 -1
  80. package/dist/contracts/route.d.ts +76 -1
  81. package/dist/contracts/route.js +1 -1
  82. package/dist/contracts/store-json.d.ts +2 -0
  83. package/dist/control-setup.d.ts +1 -1
  84. package/dist/control-ui.js +2 -2
  85. package/dist/converse.d.ts +16 -0
  86. package/dist/converse.js +32 -4
  87. package/dist/demo-chat.js +4 -4
  88. package/dist/demo.js +2 -2
  89. package/dist/desktop-update-gate.js +6 -0
  90. package/dist/desktop-update.d.ts +1 -1
  91. package/dist/discord-chat.js +4 -1
  92. package/dist/doctor.js +1 -1
  93. package/dist/egress-bridge.d.ts +17 -0
  94. package/dist/egress-bridge.js +111 -0
  95. package/dist/egress.d.ts +119 -0
  96. package/dist/egress.js +423 -0
  97. package/dist/exec.d.ts +78 -0
  98. package/dist/exec.js +367 -9
  99. package/dist/export-ui.js +2 -2
  100. package/dist/flow-engine.js +24 -5
  101. package/dist/flow-gallery-ui.js +2 -2
  102. package/dist/flow-gallery.js +27 -8
  103. package/dist/flow-share.js +16 -5
  104. package/dist/flow-triggers.d.ts +77 -8
  105. package/dist/flow-triggers.js +517 -70
  106. package/dist/flows-cli.js +8 -2
  107. package/dist/flows-ui.d.ts +0 -2
  108. package/dist/flows-ui.js +52 -4
  109. package/dist/flows.d.ts +4 -0
  110. package/dist/flows.js +9 -1
  111. package/dist/html.d.ts +9 -0
  112. package/dist/html.js +11 -0
  113. package/dist/invoke.d.ts +35 -0
  114. package/dist/invoke.js +347 -28
  115. package/dist/keys.d.ts +18 -14
  116. package/dist/keys.js +5 -0
  117. package/dist/lead-capabilities.js +3 -2
  118. package/dist/lead-contract.js +2 -0
  119. package/dist/lead-doors.d.ts +2 -0
  120. package/dist/lead-doors.js +7 -0
  121. package/dist/lead-follow.d.ts +2 -0
  122. package/dist/lead-follow.js +58 -2
  123. package/dist/lead-progress.js +1 -0
  124. package/dist/lead-queue-runtime.d.ts +46 -0
  125. package/dist/lead-queue-runtime.js +191 -0
  126. package/dist/lead-queue.d.ts +104 -0
  127. package/dist/lead-queue.js +187 -0
  128. package/dist/lead-sweep.d.ts +67 -0
  129. package/dist/lead-sweep.js +99 -0
  130. package/dist/lead-tools.d.ts +22 -0
  131. package/dist/lead-tools.js +152 -1
  132. package/dist/lead.d.ts +13 -5
  133. package/dist/lead.js +89 -29
  134. package/dist/limits-ui.d.ts +8 -1
  135. package/dist/limits-ui.js +26 -0
  136. package/dist/live-bus.d.ts +8 -0
  137. package/dist/live-bus.js +40 -4
  138. package/dist/live-detail.d.ts +27 -0
  139. package/dist/live-detail.js +57 -0
  140. package/dist/live-notes.d.ts +20 -0
  141. package/dist/live-notes.js +47 -0
  142. package/dist/live-records.d.ts +140 -0
  143. package/dist/live-records.js +935 -0
  144. package/dist/live-terminal.d.ts +269 -0
  145. package/dist/live-terminal.js +686 -0
  146. package/dist/live.d.ts +38 -27
  147. package/dist/live.js +142 -124
  148. package/dist/mcp-oauth.d.ts +6 -3
  149. package/dist/mcp-oauth.js +8 -4
  150. package/dist/memory-pass.js +2 -2
  151. package/dist/mobile-viewport.d.ts +5 -1
  152. package/dist/mobile-viewport.js +18 -2
  153. package/dist/model-catalog.js +4 -0
  154. package/dist/monitoring-ui.js +2 -2
  155. package/dist/needs-you.d.ts +1 -1
  156. package/dist/needs-you.js +11 -4
  157. package/dist/operate.d.ts +4 -0
  158. package/dist/operate.js +335 -19
  159. package/dist/passwordless.d.ts +23 -0
  160. package/dist/passwordless.js +26 -0
  161. package/dist/peek-cli.d.ts +12 -4
  162. package/dist/peek-cli.js +19 -11
  163. package/dist/phase-routing.js +10 -3
  164. package/dist/planner-source.d.ts +3 -0
  165. package/dist/planner-source.js +14 -0
  166. package/dist/planner.d.ts +2 -1
  167. package/dist/planner.js +7 -1
  168. package/dist/policy-ui.js +2 -2
  169. package/dist/policy.d.ts +1 -20
  170. package/dist/policy.js +29 -2
  171. package/dist/project-delete-ui.d.ts +18 -0
  172. package/dist/project-delete-ui.js +60 -9
  173. package/dist/project-delete.js +2 -2
  174. package/dist/project-memory.d.ts +3 -1
  175. package/dist/project-memory.js +57 -25
  176. package/dist/project-tools.d.ts +5 -3
  177. package/dist/project-tools.js +12 -9
  178. package/dist/provider-auth.d.ts +4 -3
  179. package/dist/provider-auth.js +5 -4
  180. package/dist/provider-connection.d.ts +2 -1
  181. package/dist/provider-connection.js +5 -0
  182. package/dist/provider.d.ts +25 -2
  183. package/dist/provider.js +63 -9
  184. package/dist/pull-request-ui.js +2 -2
  185. package/dist/push.js +1 -1
  186. package/dist/quick-start.d.ts +24 -0
  187. package/dist/quick-start.js +127 -0
  188. package/dist/release-adapters.d.ts +6 -0
  189. package/dist/release-adapters.js +70 -3
  190. package/dist/release.d.ts +10 -1
  191. package/dist/release.js +34 -15
  192. package/dist/request-budget-ui.js +2 -2
  193. package/dist/result-follow-ups.js +3 -2
  194. package/dist/result-review.d.ts +1 -1
  195. package/dist/result-review.js +9 -3
  196. package/dist/retention-ui.js +2 -2
  197. package/dist/retention.js +10 -1
  198. package/dist/run-efficiency.d.ts +73 -0
  199. package/dist/run-efficiency.js +228 -0
  200. package/dist/runner.d.ts +5 -1
  201. package/dist/runner.js +22 -1
  202. package/dist/scope.d.ts +183 -5
  203. package/dist/scope.js +256 -31
  204. package/dist/scout-net.d.ts +4 -1
  205. package/dist/scout-net.js +37 -12
  206. package/dist/scout.d.ts +20 -6
  207. package/dist/scout.js +27 -3
  208. package/dist/serve.js +248 -40
  209. package/dist/server/chat.d.ts +1 -1
  210. package/dist/server/chat.js +122 -31
  211. package/dist/server/chrome.d.ts +16 -2
  212. package/dist/server/chrome.js +63 -11
  213. package/dist/server/console.css +107 -42
  214. package/dist/server/flows.js +38 -8
  215. package/dist/server/guards.d.ts +1 -0
  216. package/dist/server/guards.js +23 -9
  217. package/dist/server/http.d.ts +7 -0
  218. package/dist/server/http.js +3 -2
  219. package/dist/server/live.d.ts +34 -1
  220. package/dist/server/live.js +39 -17
  221. package/dist/server/pages-assets.js +5 -0
  222. package/dist/server/pages.js +90 -13
  223. package/dist/server/people-tokens.d.ts +3 -0
  224. package/dist/server/people-tokens.js +55 -17
  225. package/dist/server/remote-hooks.js +12 -6
  226. package/dist/server/remote.d.ts +2 -1
  227. package/dist/server/remote.js +14 -5
  228. package/dist/server/render-chat.d.ts +12 -3
  229. package/dist/server/render-chat.js +47 -16
  230. package/dist/server/render-pages.d.ts +32 -4
  231. package/dist/server/render-pages.js +83 -13
  232. package/dist/server/render-people.d.ts +3 -2
  233. package/dist/server/render-people.js +3 -2
  234. package/dist/server/render-results.d.ts +18 -2
  235. package/dist/server/render-results.js +37 -28
  236. package/dist/server/render-settings.js +6 -5
  237. package/dist/server/render-tasks.d.ts +35 -10
  238. package/dist/server/render-tasks.js +111 -27
  239. package/dist/server/request-context.d.ts +2 -0
  240. package/dist/server/request-context.js +2 -1
  241. package/dist/server/route-table.d.ts +3 -0
  242. package/dist/server/route-table.js +47 -5
  243. package/dist/server/runtime.d.ts +33 -2
  244. package/dist/server/session.d.ts +25 -1
  245. package/dist/server/session.js +18 -4
  246. package/dist/server/settings.js +142 -10
  247. package/dist/server/sweeps.d.ts +4 -0
  248. package/dist/server/sweeps.js +58 -0
  249. package/dist/server/task-views.d.ts +2 -1
  250. package/dist/server/task-views.js +96 -6
  251. package/dist/server/tasks.js +327 -78
  252. package/dist/server/terminals.d.ts +31 -0
  253. package/dist/server/terminals.js +365 -0
  254. package/dist/setup-guide.js +3 -2
  255. package/dist/slack-chat.js +4 -1
  256. package/dist/spend-ui.js +3 -3
  257. package/dist/spend.js +2 -1
  258. package/dist/steer-capabilities.d.ts +32 -0
  259. package/dist/steer-capabilities.js +60 -0
  260. package/dist/steer-controls.d.ts +14 -0
  261. package/dist/steer-controls.js +36 -0
  262. package/dist/steer-status.js +2 -2
  263. package/dist/storage-ui.js +4 -4
  264. package/dist/store-baseline.d.ts +1 -1
  265. package/dist/store-baseline.js +1 -1
  266. package/dist/store.d.ts +440 -15
  267. package/dist/store.js +1072 -52
  268. package/dist/subagent-desk.js +1 -1
  269. package/dist/summary.js +3 -0
  270. package/dist/task-source.d.ts +58 -0
  271. package/dist/task-source.js +93 -0
  272. package/dist/team-leads.js +1 -1
  273. package/dist/teams-chat.js +4 -1
  274. package/dist/teardown-git.d.ts +22 -0
  275. package/dist/teardown-git.js +136 -2
  276. package/dist/telegram-lead.d.ts +3 -0
  277. package/dist/telegram-lead.js +44 -7
  278. package/dist/telegram.d.ts +11 -2
  279. package/dist/telegram.js +51 -17
  280. package/dist/terminal-catalog.d.ts +11 -0
  281. package/dist/terminal-catalog.js +129 -0
  282. package/dist/text-limits.d.ts +2 -0
  283. package/dist/text-limits.js +2 -0
  284. package/dist/tool-broker.d.ts +14 -0
  285. package/dist/tool-broker.js +67 -2
  286. package/dist/toolroll-update-ui.js +2 -2
  287. package/dist/toolroll-update.js +13 -3
  288. package/dist/tools-ui.js +2 -2
  289. package/dist/unfenced-test-support.d.ts +2 -0
  290. package/dist/unfenced-test-support.js +11 -0
  291. package/dist/webhook-limits.d.ts +23 -0
  292. package/dist/webhook-limits.js +30 -0
  293. package/dist/webhook-readers.d.ts +56 -0
  294. package/dist/webhook-readers.js +138 -0
  295. package/dist/webhook-signing.d.ts +27 -0
  296. package/dist/webhook-signing.js +119 -0
  297. package/dist/work-index.d.ts +29 -0
  298. package/dist/work-index.js +16 -2
  299. package/dist/workspace-revision.js +3 -1
  300. package/dist/writes-mode.d.ts +9 -0
  301. package/dist/writes-mode.js +8 -0
  302. package/package.json +15 -2
@@ -1,5 +1,8 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
- import { html, htmlString, joinHtml, postForm, replaceMarkup } from "../html.js";
2
+ import { networkWords } from "../egress.js";
3
+ import { writesWords } from "../writes-mode.js";
4
+ import { ACP_AGENTS, isAcpProfile } from "../acp-agents.js";
5
+ import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
3
6
  import { closedQuestionReason, closedQuestionWords } from "../decision.js";
4
7
  import {} from "../activity-line.js";
5
8
  import {} from "../agentconfig.js";
@@ -43,6 +46,7 @@ import { reviewHref, taskChatHref, taskHref } from "./http.js";
43
46
  import { coordinatorProposalsSection } from "./render-chat.js";
44
47
  import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, terminalDiffCard, verificationExplanation, verificationRecovered } from "./render-results.js";
45
48
  import {} from "./session.js";
49
+ import { linkWords, originNote } from "../task-source.js";
46
50
  export function contractChangeHtml(change) {
47
51
  const tag = html `<span class="contract-change-kind contract-change-${change.kind}">${change.kind}</span>`;
48
52
  switch (change.field) {
@@ -147,7 +151,8 @@ export function permissionWordsOf(profile) {
147
151
  ? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
148
152
  : profile.provider === "gemini"
149
153
  ? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
150
- : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
154
+ : isAcpProfile(profile) ? "Full access"
155
+ : profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
151
156
  }
152
157
  /** One plain sentence for a revision (approval critique, Oct 2):
153
158
  * "Fixes what build #9 missed: <criterion>." The notes, paths and lineage
@@ -200,6 +205,28 @@ const fragmentString = (fragment) => fragment === "" ? "" : htmlString(fragment)
200
205
  * Presentation only: the same hidden fields, nonce, digest and password
201
206
  * post to the same route, and the task page and chat card share it.
202
207
  */
208
+ /** v126: a task's origin as a line of words (the identity line). */
209
+ function originWords(origin) {
210
+ const flow = origin.flow === null ? null : origin.flow === "hidden" ? "from a flow" : origin.flow === "removed" ? "from a flow (removed)" : `from flow “${origin.flow.name}”`;
211
+ const source = origin.source === null ? null : `${origin.source.label} (${origin.source.proved})`;
212
+ return [flow, source].filter(one => one !== null).join(" · ");
213
+ }
214
+ /** v126: a task's origin as a fact: the flow, linked for those who may open it, and the outside event, linked to where
215
+ * it lives, with that link's host always shown. */
216
+ function originParts(origin) {
217
+ const parts = [];
218
+ if (origin.flow !== null)
219
+ parts.push(origin.flow === "hidden" ? "A flow" : origin.flow === "removed" ? "A flow (removed)" : { label: `Flow “${origin.flow.name}”`, href: origin.flow.href });
220
+ if (origin.source !== null) {
221
+ if (parts.length > 0)
222
+ parts.push(" · ");
223
+ // The link shows where it goes (host and path), beside what it is.
224
+ parts.push(origin.source.label, ` (${origin.source.proved})`);
225
+ if (origin.source.url !== null)
226
+ parts.push(" · ", { label: linkWords(origin.source.url), href: origin.source.url });
227
+ }
228
+ return parts;
229
+ }
203
230
  export function approvalSheetHtml(input) {
204
231
  const { scope, route } = input;
205
232
  // A machine-drafted repair appends its brief to the signed goal; the row
@@ -261,10 +288,13 @@ export function approvalSheetHtml(input) {
261
288
  allowing.length === 0 ? "" : html `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.join(" · ")}</p>`,
262
289
  yoursToCheck.length === 0 ? "" : html `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.join("; ")}</p>`,
263
290
  ]);
264
- const after = input.deliverable === "report"
265
- ? html `An agent investigates without changing the repository. You'll hear when its report is ready.`
266
- : html `An agent starts in its own branch. You'll hear when it's ready to review.`;
267
- const submit = html `Approve & start`;
291
+ const rebuild = input.rebuild ?? null;
292
+ const after = rebuild !== null
293
+ ? html `Build #${rebuild} was made to the earlier plan. Approving builds it again to this one.`
294
+ : input.deliverable === "report"
295
+ ? html `An agent investigates without changing the repository. You'll hear when its report is ready.`
296
+ : html `An agent starts in its own branch. You'll hear when it's ready to review.`;
297
+ const submit = rebuild !== null ? html `Approve & build again` : html `Approve & start`;
268
298
  // Everything else the digest binds, one tap away.
269
299
  const detail = (title, body, attrs = html ``) => isEmptyHtml(body) ? html `` : html `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
270
300
  const revisionDetail = input.revision === null ? "" : detail("Earlier build", joinHtml([
@@ -366,6 +396,7 @@ export function approvalSheetHtml(input) {
366
396
  sentence === null ? "" : html `<p class="approval-revision">${sentence}</p>`,
367
397
  input.coordinator === null ? "" : html `<p class="approval-note">An agent filed this: <span class="mono">${input.coordinator.label}</span>${input.coordinator.filedAgo === null ? "" : html `, ${input.coordinator.filedAgo}`}. Nothing runs until you approve, and approving runs its request.</p>`,
368
398
  input.deliverable === "report" ? html `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "",
399
+ originNote(input.origin) === null ? "" : html `<p class="approval-note" data-approval-origin>${originNote(input.origin)}</p>`,
369
400
  html `<dl class="approval-rows">`,
370
401
  row("Goal", html `<p class="approval-goal">${goalShown}</p>`),
371
402
  row("Changes", changes),
@@ -378,15 +409,16 @@ export function approvalSheetHtml(input) {
378
409
  who.length === 0 ? "" : html `<p class="approval-who">${who.join(" · ")}</p>`,
379
410
  consent,
380
411
  html `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">`,
381
- html `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password" aria-describedby="${passwordNote}"></label>`,
382
- html `<p class="approval-password-note" id="${passwordNote}">Your password signs this approval.</p>`,
412
+ html `${reauth(html `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password" aria-describedby="${passwordNote}"></label>`)}`,
413
+ input.freshSince == null ? html `<p class="approval-password-note" id="${passwordNote}">Your password signs this approval.</p>`
414
+ : html `<p class="approval-password-note" id="${passwordNote}" data-approval-fresh>Your password from ${whenTime(input.freshSince)} is remembered on this computer — leave this blank to approve with it, or type it again.</p>`,
383
415
  html `<button type="submit" data-primary-action>${submit}</button></div>`,
384
416
  html `<p class="approval-after">${after}</p>`,
385
417
  html `<div class="approval-secondary">${editor ?? html `<a class="approval-link" href="${input.editHref}">Edit plan</a>`}<a class="approval-link" href="${input.notNowHref}">Not now</a></div>`,
386
418
  details,
387
419
  ]), {
388
420
  attrs: { class: "approve-form approval-sheet", id: input.surface === "task" ? "approve" : null, "data-sticky": input.sticky },
389
- hidden: { nonce: input.nonce, digest: input.digest },
421
+ hidden: { nonce: input.nonce, digest: input.digest, ...(rebuild === null ? {} : { rebuild: String(rebuild) }) },
390
422
  returnTo: input.returnTo,
391
423
  }),
392
424
  editorForm,
@@ -413,7 +445,8 @@ export function permissionModeOfProfile(profile) {
413
445
  ? profile.permissionArgv === "bypassPermissions" ? "bypassPermissions" : "auto"
414
446
  : profile.provider === "gemini"
415
447
  ? profile.approvalArgv === "yolo" ? "bypassPermissions" : "auto"
416
- : profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
448
+ : isAcpProfile(profile) ? "bypassPermissions"
449
+ : profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
417
450
  }
418
451
  /** Done when, from the in-place editor: each line keeps its requirement's
419
452
  * id by position, and its evidence and guidance while its words stand; a
@@ -505,8 +538,11 @@ export function profileWords(scope) {
505
538
  : "legacy acceptEdits; edits proceed, commands that ask are denied unattended"}, stops after ${profile.maxTurns} turns, ${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}; repairs on ${repair}, ${profile.repairMaxTurns} turns / ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
506
539
  : profile.provider === "gemini"
507
540
  ? html `<p class="meta">Runs on <span class="mono">gemini · ${profile.model}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
508
- : html `<p class="meta">Runs on <span class="mono">${profile.provider} · ${profile.model}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
509
- return base;
541
+ : isAcpProfile(profile)
542
+ ? html `<p class="meta">Runs on <span class="mono">${ACP_AGENTS[profile.provider].name} (via ACP) · ${profile.model}</span> — full access inside the OS fence; the agent runs any tool it chooses and nothing asks. No turn limit: stops after ${Math.round(profile.timeoutSeconds / 60)} min, or ${Math.round(profile.idleTimeoutSeconds / 60)} min silent; repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min, or ${Math.round(profile.repairIdleTimeoutSeconds / 60)} min silent.</p>`
543
+ : html `<p class="meta">Runs on <span class="mono">${profile.provider} · ${profile.model}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
544
+ // What the build may reach is part of what is approved (egress.ts): said on every approval form.
545
+ return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>`;
510
546
  }
511
547
  export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
512
548
  export function agentsStandingWords(view) {
@@ -706,6 +742,21 @@ export function nonceHashOf(value) {
706
742
  return createHash("sha256").update(value).digest("hex");
707
743
  }
708
744
  /** The approve POST's refusal, in words a person can act on. */
745
+ /** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
746
+ export function approvalProofWords(proof) {
747
+ if (proof == null)
748
+ return "";
749
+ if (proof.method === "fresh-password")
750
+ return ` (password remembered from ${whenText(proof.at)})`;
751
+ if (proof.method === "fresh-sso")
752
+ return ` (sign-in remembered from ${whenText(proof.at)})`;
753
+ if (proof.method === "session")
754
+ return " (passwordless: your signed-in session)";
755
+ if (proof.method === "cli-login")
756
+ return " (passwordless: this computer's remembered login)";
757
+ return "";
758
+ }
759
+ const whenText = (iso) => new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit" }).format(new Date(iso));
709
760
  export function approveRefusalWords(reason) {
710
761
  if (reason === "requester" || reason === "person-required")
711
762
  return gateWords({ verdict: "refuse", reason });
@@ -713,6 +764,8 @@ export function approveRefusalWords(reason) {
713
764
  return "not approved: this scope predates agent routing and its old approval no longer stands — edit and re-file the scope so it is routed under today’s agents, then approve it";
714
765
  if (reason === "profile-unresolved")
715
766
  return "not approved: the scope cannot name an exact agent for every role — fix the agent setup and re-file it";
767
+ if (reason === "password-now")
768
+ return "not approved: this needs your password typed now — protected work, raised permissions, or a plan an agent wrote can't use a remembered one";
716
769
  return `not approved: ${reason}`;
717
770
  }
718
771
  /** The browser's one-click approval binds not only the signed scope but the
@@ -897,7 +950,7 @@ export function resumeCeremonyPage(chrome, data) {
897
950
  data.gate === null ? "" : html `<p class="row"><strong>Before work starts:</strong> ${data.gate.summary} — ${data.gate.detail}</p>`,
898
951
  html `</div>`,
899
952
  postForm(`${taskHref(data.taskId)}/resume`, joinHtml([
900
- html `<label>Confirm with your password<input type="password" name="token" autocomplete="current-password"></label>`,
953
+ html `${reauth(html `<label>Confirm with your password<input type="password" name="token" autocomplete="current-password"></label>`)}`,
901
954
  html `<button type="submit">Resume task</button>`,
902
955
  ], "\n"), { attrs: { class: "card resume-form" }, returnTo: data.returnTo, hidden: { nonce: data.nonceValue, run: control.run } }),
903
956
  back,
@@ -1067,7 +1120,7 @@ export function planRevisionLedgerHtml(ledger, taskId, csrf) {
1067
1120
  : joinHtml([
1068
1121
  postForm(`${taskHref(taskId)}/accept-revision`, joinHtml([
1069
1122
  pendingRevision.authorityKind === "authority-change"
1070
- ? html `<input type="password" name="token" placeholder="approval password" required autocomplete="current-password">`
1123
+ ? html `${reauth(html `<input type="password" name="token" placeholder="approval password" required autocomplete="current-password">`)}`
1071
1124
  : "",
1072
1125
  html `<button type="submit">${pendingRevision.authorityKind === "authority-change" ? html `Approve changes &amp; continue` : html `Use revised plan &amp; continue`}</button>`,
1073
1126
  ]), { attrs: { class: "inline" }, hidden: { "revision-id": pendingRevision.id } }),
@@ -1399,7 +1452,7 @@ export function taskBodyParts(data) {
1399
1452
  const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled"
1400
1453
  ? ""
1401
1454
  : joinHtml([
1402
- postForm(`${taskHref(task.id)}/steer`, html `<input type="text" name="note" placeholder="guidance for the agent" aria-label="steering note" style="width:100%;max-width:28rem"><button type="submit">Steer</button>`, { attrs: { class: "row" } }),
1455
+ postForm(`${taskHref(task.id)}/steer`, html `<input type="text" name="note" placeholder="guidance for the agent" aria-label="steering note" style="width:100%;max-width:28rem">${data.steeringRun == null ? "" : html `<input type="hidden" name="run" value="${String(data.steeringRun)}"><input type="hidden" name="key" value="${randomUUID()}"><input type="hidden" name="return" value="task">`}<button type="submit">${data.steeringRun == null ? "Save for the next attempt" : "Steer"}</button>`, { attrs: { class: "row" } }),
1403
1456
  html `<p class="meta">${steerDestinationWords(steeringLive)} A note can't widen the approved scope.</p>`,
1404
1457
  ]);
1405
1458
  const steeringCard = steerRows === "" && steerForm === "" ? "" : html `<h2>Steering</h2>${steerRows}${steerForm}`;
@@ -1425,7 +1478,7 @@ export function taskBodyParts(data) {
1425
1478
  html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
1426
1479
  acceptanceCeremonyHtml(scope.acceptance),
1427
1480
  approval.approved
1428
- ? html `<p class="meta scope-seal">Approved by ${approval.by} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
1481
+ ? html `<p class="meta scope-seal">Approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
1429
1482
  : html `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p><p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
1430
1483
  html `</div>`,
1431
1484
  ], "\n");
@@ -1533,6 +1586,10 @@ export function taskBodyParts(data) {
1533
1586
  const revisionInApproval = scope !== null && !approval.approved && data.plan !== "requested" &&
1534
1587
  scope.profileState !== "unresolved" && consentDoor.open && data.dispatch?.action !== "repair-dependency" &&
1535
1588
  (data.revision == null || !("problem" in data.revision));
1589
+ // The approval is the next step when dispatch waits on it, or when the plan was rewritten after a build (the
1590
+ // assignment's one action): then it is shown open under the status, never folded.
1591
+ const approvalLeads = data.dispatch?.action === "approve-scope" ||
1592
+ (data.assignment?.primaryAction?.code === "approve-scope" && data.assignment.primaryAction.target.taskId === task.id);
1536
1593
  const approveForm = scope === null || approval.approved || data.plan === "requested"
1537
1594
  ? ""
1538
1595
  : data.revision !== null && data.revision !== undefined && "problem" in data.revision
@@ -1547,6 +1604,7 @@ export function taskBodyParts(data) {
1547
1604
  csrf: data.csrf,
1548
1605
  nonce: data.nonce,
1549
1606
  digest: data.approvalDigest ?? scope.digest,
1607
+ freshSince: data.approvalFresh ?? null,
1550
1608
  returnTo: null,
1551
1609
  scope,
1552
1610
  planDocument: data.plan === "drafted" ? data.planDocument : null,
@@ -1556,10 +1614,12 @@ export function taskBodyParts(data) {
1556
1614
  repairChain: data.repairChain ?? null,
1557
1615
  route: data.route,
1558
1616
  coordinator: data.coordinator ?? null,
1617
+ origin: data.origin ?? null,
1559
1618
  deliverable: data.deliverable ?? "branch",
1619
+ rebuild: data.assignment?.need != null && "key" in data.assignment.need && data.assignment.need.key === "replan" ? data.assignment.need.build : null,
1560
1620
  editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
1561
1621
  notNowHref: "/work",
1562
- sticky: data.dispatch?.action === "approve-scope",
1622
+ sticky: approvalLeads,
1563
1623
  earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
1564
1624
  edit: data.csrf === "" ? null
1565
1625
  : { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
@@ -1711,7 +1771,7 @@ export function taskBodyParts(data) {
1711
1771
  const scopeRow = scope === null
1712
1772
  ? prop("scope", "none yet")
1713
1773
  : approval.approved
1714
- ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by} · ${whenTime(approval.at)}`)
1774
+ ? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)}`)
1715
1775
  : prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
1716
1776
  const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
1717
1777
  const propsCard = html `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
@@ -1727,7 +1787,7 @@ export function taskBodyParts(data) {
1727
1787
  facts.push({ label: "Queue", parts: [`${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : ` in ${data.position.column}'s queue`} · `, { label: "Reorder", href: "/board?view=order" }] });
1728
1788
  }
1729
1789
  facts.push(scope === null ? { label: "Scope", parts: ["none yet"] }
1730
- : approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
1790
+ : approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
1731
1791
  : { label: "Scope", parts: [approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved"] });
1732
1792
  facts.push({ label: "Publishes as", parts: [publishesAs] });
1733
1793
  // v102: who asked for it, and what the project's approval rules need.
@@ -1893,20 +1953,22 @@ export function taskBodyParts(data) {
1893
1953
  // status and property rail, and approval provenance stays in the ceremony.
1894
1954
  const identity = html `<p class="meta task-identity">Task ID <span class="mono">${task.id}</span>${data.rootTitle !== undefined && data.rootTitle !== task.title ? ` · Execution: ${task.title}` : ""}${data.repo === null ? "" : ` · ${projectName(data.repo)}`}${data.coordinator !== null && data.coordinator !== undefined
1895
1955
  ? html ` · filed by <span class="mono">${data.coordinator.label}</span>${data.coordinator.filedAgo === null ? "" : ` ${data.coordinator.filedAgo}`}`
1896
- : data.filedVia === null || data.filedVia === undefined
1897
- ? ""
1898
- : ` · filed via ${data.filedVia}`}${data.deliverable === "report" ? html ` · <span class="badge">Scout</span>` : ""}</p>`;
1956
+ : data.origin != null
1957
+ ? ` · ${originWords(data.origin)}`
1958
+ : data.filedVia === null || data.filedVia === undefined
1959
+ ? ""
1960
+ : ` · filed via ${data.filedVia}`}${data.deliverable === "report" ? html ` · <span class="badge">Scout</span>` : ""}</p>`;
1899
1961
  // A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
1900
1962
  const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
1901
1963
  const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
1902
- const assignmentOptions = data.assignment == null ? null : { workStatus: status, hideAction: (approveForm !== "" && data.dispatch?.action === "approve-scope") || pullRequestActs, problem: status.tone === "problem",
1964
+ const assignmentOptions = data.assignment == null ? null : { workStatus: status, hideAction: (approveForm !== "" && approvalLeads) || pullRequestActs, problem: status.tone === "problem",
1903
1965
  planning: data.dispatch?.code === "running" && data.dispatch.role === "planner", ...(pullRequestFact === undefined ? {} : { pullRequest: pullRequestFact }),
1904
1966
  diagnostics: [...(status.diagnostics ?? []), ...(status.tone === "problem" && status.detail !== data.assignment.detail ? [status] : [])] };
1905
1967
  // Say it once: a pull request that couldn't open, or merged, is the status
1906
1968
  // card's own row (its reason or merge commit under Details), not a second card.
1907
1969
  const pullRequestCard = data.pullRequest == null || (pullRequestFact !== undefined && (data.pullRequest.view?.state === "failed" || data.pullRequest.view?.state === "merged")) ? "" : pullRequestCardHtml(data.pullRequest, data.csrf);
1908
1970
  // The result takes over from the task status as soon as it is ready.
1909
- const statusHtml = html `${data.assignment != null && assignmentOptions !== null ? assignmentSummaryHtml(data.assignment, assignmentOptions) : receiptLeads ? completionReceiptCard(data.completion.receipt, task.id, "task", status) : taskStatusCard(status, task.id, data.dispatch ?? null, liveRunId, approveForm !== "" && data.dispatch?.action === "approve-scope")}${checkProgressHtml(data.checkProgress ?? null)}`;
1971
+ const statusHtml = html `${data.assignment != null && assignmentOptions !== null ? assignmentSummaryHtml(data.assignment, assignmentOptions) : receiptLeads ? completionReceiptCard(data.completion.receipt, task.id, "task", status) : taskStatusCard(status, task.id, data.dispatch ?? null, liveRunId, approveForm !== "" && approvalLeads)}${checkProgressHtml(data.checkProgress ?? null)}`;
1910
1972
  // The exact-run control (v52), directly under the scheduler's answer:
1911
1973
  // the one place a person stops or resumes THIS attempt.
1912
1974
  const controlHtml = taskControlDetailsHtml(data.control ?? { kind: "none" }, task.id, data.csrf, "task", false, false, data.livePause ?? null);
@@ -1939,7 +2001,7 @@ export function taskBodyParts(data) {
1939
2001
  return joinHtml([
1940
2002
  html `<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${state}</span></p>`,
1941
2003
  reopenable
1942
- ? postForm(`${taskHref(task.id)}/reopen`, html `<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password"><button type="submit">Reopen — the approved scope stands</button>`, { attrs: { class: "row" } })
2004
+ ? postForm(`${taskHref(task.id)}/reopen`, html `${reauth(html `<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">`)}<button type="submit">Reopen — the approved scope stands</button>`, { attrs: { class: "row" } })
1943
2005
  : "",
1944
2006
  html `</div>`,
1945
2007
  ]);
@@ -1956,7 +2018,7 @@ export function taskBodyParts(data) {
1956
2018
  ? html `<div class="card"><p><strong>This task is waiting on you: an agent filed it, and it has no scope.</strong></p><p class="meta">Filed by <span class="mono">${data.coordinator.label}</span> — nothing plans, claims, or runs until you write a scope below and sign it. Your signature runs their request.</p></div>`
1957
2019
  : html `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p><p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
1958
2020
  : "";
1959
- const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
2021
+ const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : approvalLeads
1960
2022
  ? html `<section class="task-plan-review" aria-label="Approve the plan">${approveForm}</section>`
1961
2023
  : html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
1962
2024
  const optionsHtml = html `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
@@ -2145,7 +2207,10 @@ export function taskBodyParts(data) {
2145
2207
  review.push({ label: "Publishes as", parts: [publishesAs] });
2146
2208
  const about = [];
2147
2209
  const filedFact = facts.find(one => one.label === "Filed by");
2148
- about.push(filedFact ?? { label: "Filed by", parts: [filedBy ?? (data.filedVia == null ? "Not recorded" : `via ${data.filedVia}`)] });
2210
+ about.push(filedFact ?? { label: "Filed by", parts: [filedBy ?? (data.origin != null ? "An automation" : data.filedVia == null ? "Not recorded" : `via ${data.filedVia}`)] });
2211
+ // v126: where it came from — the flow (linked only for those who can see it) and the outside event, with its host.
2212
+ if (data.origin != null)
2213
+ about.push({ label: "From", parts: originParts(data.origin) });
2149
2214
  about.push({ label: "Filed", parts: [{ at: rootFiled }] });
2150
2215
  about.push({ label: "Updated", parts: [{ at: task.updatedAt }] });
2151
2216
  for (const label of ["Audit", "This attempt", "Task total"]) {
@@ -2204,6 +2269,21 @@ export function taskBodyParts(data) {
2204
2269
  attempts: [...earlierStopped].sort((a, b) => a.id - b.id).map(run => ({ label: `${runNoun(run)[0].toUpperCase()}${runNoun(run).slice(1)} #${run.id}`, href: `/r/${run.id}`,
2205
2270
  text: run.outcome === null ? "Never finished." : run.outcome === "failed" ? failedAttemptSentence(run.reason) : run.reason === null ? null : (words => `${words.charAt(0).toUpperCase()}${words.slice(1)}.`)(reasonWords(run.reason)) })),
2206
2271
  };
2272
+ // ⋯ More actions (docs/plans/one-page-task-and-agent.md): each secondary act once, in one place — Work, Share & reuse,
2273
+ // then Danger last. A ceremony is never cloned here: its item goes to the tab that holds its one form.
2274
+ const sectionIds = new Set(sectionParts.map(one => one.id));
2275
+ const rootOf = data.rootId ?? task.id;
2276
+ const more = [
2277
+ ...(runChecks !== null && data.csrf !== "" ? [{ group: "work", label: "Run checks", kind: "post", action: runChecks.action, fields: { level: runChecks.level, return: runChecks.returnTo } }] : []),
2278
+ ...(sectionIds.has("scope") && data.csrf !== "" ? [{ group: "work", label: approval.approved || finished ? "Plan and agents" : "Edit plan", kind: "goto", tab: "plan", target: "scope" }] : []),
2279
+ ...(sectionIds.has("waits-for") ? [{ group: "work", label: "Dependencies", kind: "goto", tab: "plan", target: "waits-for" }] : []),
2280
+ ...(sectionIds.has("holds") ? [{ group: "work", label: "Holds", kind: "goto", tab: "plan", target: "holds" }] : []),
2281
+ ...(data.csrf !== "" ? [{ group: "work", label: "Task options", kind: "goto", tab: "plan", target: "task-diagnostics" }] : []),
2282
+ { group: "share", label: "Copy link", kind: "copy" },
2283
+ ...(facts.some(one => one.label === "Audit") ? [{ group: "share", label: "Evidence pack", kind: "link", href: `${taskHref(rootOf)}/evidence` }] : []),
2284
+ ...(data.repo !== null && data.scope !== null ? [{ group: "share", label: "Reuse as recipe", kind: "link", href: `/recipes/from-task?task=${encodeURIComponent(task.id)}` }] : []),
2285
+ ...(cancelForm !== "" ? [{ group: "danger", label: "Cancel task", kind: "goto", tab: "plan", target: "cancel-task" }] : []),
2286
+ ];
2207
2287
  const view = {
2208
2288
  kind: "task",
2209
2289
  id: task.id,
@@ -2227,9 +2307,13 @@ export function taskBodyParts(data) {
2227
2307
  activity: liveRun === undefined ? null : data.activity ?? null,
2228
2308
  stop,
2229
2309
  pause,
2310
+ steering: data.csrf !== "" ? data.steerComposer ?? null : null,
2311
+ ...(data.csrf !== "" && (data.steerRecent?.length ?? 0) > 0 ? { steerRecent: data.steerRecent } : {}),
2230
2312
  record,
2231
2313
  earlier,
2232
2314
  approval: fragmentString(approvalHtml),
2315
+ approvalPlacement: approvalLeads ? "lead" : "plan",
2316
+ more,
2233
2317
  confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
2234
2318
  ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
2235
2319
  rebuild,
@@ -33,6 +33,8 @@ export type RequestFacts = {
33
33
  expiresAt: number;
34
34
  etag: string;
35
35
  };
36
+ /** Passwordless is on and this is a signed-in browser: ceremony password fields render as nothing (html.ts reauth). */
37
+ passwordless?: boolean;
36
38
  };
37
39
  /**
38
40
  * The request's own session facts, readable from anywhere below the
@@ -1,6 +1,6 @@
1
1
  /** What one console request knows about itself, readable anywhere below its handler. */
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { provideFormToken } from "../html.js";
3
+ import { provideFormToken, providePasswordless } from "../html.js";
4
4
  /**
5
5
  * The request's own session facts, readable from anywhere below the
6
6
  * dispatcher without threading them through forty call sites: the csrf
@@ -10,6 +10,7 @@ import { provideFormToken } from "../html.js";
10
10
  */
11
11
  export const requestContext = new AsyncLocalStorage();
12
12
  provideFormToken(() => requestContext.getStore()?.csrf);
13
+ providePasswordless(() => requestContext.getStore()?.passwordless === true);
13
14
  /** Run a render outside a live request (a test, a background render) as if a browser session with this token asked. */
14
15
  export function withFormToken(csrf, render) {
15
16
  const facts = requestContext.getStore();
@@ -83,6 +83,9 @@ export interface RouteDeclaration {
83
83
  readonly roleRefusal?: RouteRefusal;
84
84
  /** The largest form body a POST here reads (else the console's default). */
85
85
  readonly bodyCap?: number;
86
+ /** Cockpit B2: whether a password remembered on this session may stand in here (`allowlist`), or never. Only the
87
+ * allowlisted acts run their check inside `withFreshness` (scope.ts); every other step-up is `never`. */
88
+ readonly freshness: "never" | "allowlist";
86
89
  }
87
90
  /** The sole route table, in match order. */
88
91
  export declare const ROUTES: readonly RouteDeclaration[];
@@ -44,6 +44,7 @@ function row(stage, domain, id, method, pattern, options = {}) {
44
44
  roleBrowser: options.roleBrowser ?? false,
45
45
  ...(options.roleRefusal === undefined ? {} : { roleRefusal: options.roleRefusal }),
46
46
  ...(options.bodyCap === undefined ? {} : { bodyCap: options.bodyCap }),
47
+ freshness: options.freshness ?? "never",
47
48
  });
48
49
  }
49
50
  const edge = (domain, id, method, path, options = {}) => row("edge", domain, id, method, path.startsWith("^") ? path : exact(path), options);
@@ -65,6 +66,9 @@ const LARGE_FORM = 1024 * 1024;
65
66
  // ---- edge: answered before the console's sign-in ---------------------------------------------------------------
66
67
  // Ordered exactly as the server checks them: health and webhooks before the Host check, the rest after it.
67
68
  edge("pages", "edge.healthz", "GET,HEAD", "/healthz", { host: "before" });
69
+ // Cockpit B1: the running console proves itself to this installation's CLI (a fresh challenge, answered with the
70
+ // instance secret and the address this connection was received on). It reveals nothing and changes nothing.
71
+ edge("pages", "edge.instance", "GET", "/__instance");
68
72
  edge("remote", "edge.telegram-hook", "POST", "/hooks/telegram", { callers: ["service"], host: "before", proof: "adapter" });
69
73
  edge("remote", "edge.flow-form", "GET,HEAD", "^\\/hooks\\/form\\/[^/]+$", { project: "flow", sample: "/hooks/form/abc", host: "before", proof: "adapter" });
70
74
  edge("remote", "edge.flow-form-send", "POST", "^\\/hooks\\/form\\/[^/]+$", { project: "flow", sample: "/hooks/form/abc", host: "before", proof: "adapter" });
@@ -83,9 +87,9 @@ edge("remote", "edge.team-read", "GET", "/api/team", { callers: ["cookie", "bear
83
87
  edge("remote", "edge.team-send", "POST", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
84
88
  edge("remote", "edge.cli", "POST", "/api/cli", { callers: ["bearer"], scope: "read", project: "adapter", proof: "adapter" });
85
89
  edge("remote", "edge.teams", "POST", "/teams/messages", { callers: ["service"], proof: "adapter" });
86
- edge("pages", "edge.browser-asset", "GET,HEAD", "^\\/assets\\/(workspace\\.js|workspace\\.css|THIRD_PARTY_NOTICES\\.txt)$", { sample: "/assets/workspace.js" });
90
+ edge("pages", "edge.browser-asset", "GET,HEAD", "^\\/assets\\/(workspace\\.js|workspace\\.css|terminal\\.js|terminal\\.css|THIRD_PARTY_NOTICES\\.txt)$", { sample: "/assets/workspace.js" });
87
91
  edge("pages", "edge.workspace-style", "GET,HEAD", "^\\/assets\\/workspace-[0-9a-f]{64}\\.css$", { sample: `/assets/workspace-${"0".repeat(64)}.css` });
88
- edge("pages", "edge.install-asset", "GET", "^\\/(manifest\\.webmanifest|favicon\\.ico|icon\\.svg|icon-192\\.png|icon-512\\.png|apple-touch-icon\\.png|sw\\.js)$", { sample: "/sw.js" });
92
+ edge("pages", "edge.install-asset", "GET", "^\\/(manifest\\.webmanifest|favicon\\.ico|icon\\.svg|icon-attention\\.svg|icon-192\\.png|icon-512\\.png|apple-touch-icon\\.png|sw\\.js)$", { sample: "/sw.js" });
89
93
  edge("pages", "edge.font", "GET", "^\\/fonts\\/geist-(sans|mono)-(400|500|600)\\.woff2$", { sample: "/fonts/geist-sans-400.woff2" });
90
94
  edge("pages", "edge.desktop-health", "GET", "/desktop/health");
91
95
  edge("people", "edge.login-page", "GET", "/login");
@@ -108,6 +112,14 @@ for (const one of ["state", "changes", "ship"])
108
112
  get("tasks", `code.${one}`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING, sample: `/code/${"a".repeat(32)}/${one}` });
109
113
  // Any other coding address answers "not found" after the same access check.
110
114
  get("tasks", "code.other", "^\\/code\\/.*$", { ...CODING, sample: "/code/unknown" });
115
+ // Live terminals (docs/plans/live-terminal.md): an installation-wide operator's own, from a browser. The handlers prove
116
+ // the operator's generation, the terminal's owner and its project; the socket's upgrade is checked in terminals.ts.
117
+ const TERM = { ...free, callers: ["cookie"], project: "none", limited: "deny", ...operator("Terminals are for installation-wide operators.", "/") };
118
+ const TERMINAL = "[a-f0-9]{32}";
119
+ get("tasks", "term.page", "/term", TERM);
120
+ get("tasks", "term.state", "/term/state", TERM);
121
+ get("tasks", "term.session", `^\\/term\\/${TERMINAL}$`, { ...TERM, sample: `/term/${"a".repeat(32)}` });
122
+ get("tasks", "term.socket", `^\\/term\\/${TERMINAL}\\/socket$`, { ...TERM, sample: `/term/${"a".repeat(32)}/socket` });
111
123
  get("pages", "projects.browse", "/projects/browse", free);
112
124
  get("pages", "projects.github", "/projects/github", { ...free, callers: ["cookie"], callerRefusal: { status: 403, type: "text/plain; charset=utf-8", body: "listing repositories feeds a browser session's act" },
113
125
  ...anyApprover("your login can watch — adding projects is an approver's act") });
@@ -211,8 +223,14 @@ for (const one of ["approve", "change", "revise", "complete"])
211
223
  post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
212
224
  post("settings", "settings.updates-dismiss", "/settings/updates/dismiss");
213
225
  post("settings", "settings.updates-checks", "/settings/updates/checks");
226
+ // Cockpit B2: Forget ends this session's remembered password now.
227
+ // Forget is about the caller's own session only: anyone signed in with a browser may use it (a viewer, a project-limited person).
228
+ post("settings", "session.forget-password", "/session/forget-password", { ...free, limited: "self", viewer: true, callers: ["cookie"] });
214
229
  const CODING_ACT = { scope: "step-up", project: "coding", limited: "self" };
215
230
  post("tasks", "code.start", "/code/start", CODING_ACT);
231
+ post("tasks", "term.start", "/term/start", { ...TERM, scope: "step-up" });
232
+ for (const one of ["close", "remove"])
233
+ post("tasks", `term.${one}`, `^\\/term\\/${TERMINAL}\\/${one}$`, { ...TERM, sample: `/term/${"a".repeat(32)}/${one}` });
216
234
  for (const one of ["send", "stop", "resume", "recover", "continue", "ship", "answer"])
217
235
  post("tasks", `code.${one}-send`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING_ACT, sample: `/code/${"a".repeat(32)}/${one}` });
218
236
  // Any other coding action answers "not found" after the same access check.
@@ -252,11 +270,29 @@ post("flows", "flow.trigger.check", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/
252
270
  post("flows", "flow.trigger.press", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/press$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/press", ...EDIT_FLOWS });
253
271
  post("flows", "flow.trigger.renew", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/renew$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/renew", ...EDIT_FLOWS });
254
272
  post("flows", "flow.trigger.secret", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/secret$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/secret", ...EDIT_FLOWS });
273
+ // v126: a signed webhook's set-up (a new address, and a secret made here shown once) and who signs it: behind the password.
274
+ post("flows", "flow.trigger.setup", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/setup$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/setup", ...EDIT_FLOWS });
275
+ // v126: clear a webhook's turned-away count once it has been seen.
276
+ post("flows", "flow.trigger.dropped", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/dropped$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/dropped", ...EDIT_FLOWS });
277
+ // v126: which project a Sentry or Vercel webhook takes deliveries from: behind the password, as its secret is.
278
+ post("flows", "flow.trigger.scope", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/scope$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/scope", ...EDIT_FLOWS });
279
+ post("flows", "flow.trigger.sender", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/sender$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/sender", ...EDIT_FLOWS });
255
280
  post("flows", "flow.trigger.share", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/share$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/share", ...EDIT_FLOWS });
256
281
  post("flows", "flow.trigger.unshare", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/unshare$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/unshare", ...EDIT_FLOWS });
257
282
  post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operator("An instance operator sets budgets.", "/spend") });
258
283
  post("settings", "settings.project-concurrency", "/settings/project/concurrency");
284
+ post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
285
+ post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
286
+ post("settings", "settings.project-writes", "/settings/project/writes", { scope: "step-up", callers: ["cookie"] });
259
287
  post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
288
+ // Lead sweeps (docs/plans/lead-fan-out.md): the table, and cancelling what isn't ready. The handler shows a sweep only in
289
+ // a project the reader can see, and acts only for an approver.
290
+ get("tasks", "sweep.page", "^\\/sweeps\\/[1-9][0-9]{0,9}$", { ...free, limited: "unscoped", sample: "/sweeps/1" });
291
+ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$", { limited: "unscoped", sample: "/sweeps/1/cancel-rest", ...approver("An approver cancels a sweep's work.", "/chat") });
292
+ // Passwordless (docs/plans/passwordless.md): an operator's installation setting. Turning it on takes a typed password
293
+ // (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
294
+ get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
295
+ post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
260
296
  post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
261
297
  const SETTINGS_ACT_ROLES = {
262
298
  policy: operator("An instance operator sets the policy.", "/settings/policy"),
@@ -328,7 +364,7 @@ post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task",
328
364
  post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
329
365
  post("tasks", "task.act.cancel", `^\\/t\\/${TASK}\\/cancel$`, { project: "task", limited: "resource", sample: "/t/one/cancel" });
330
366
  post("tasks", "task.act.scope", `^\\/t\\/${TASK}\\/scope$`, { project: "task", limited: "resource", sample: "/t/one/scope", bodyCap: LARGE_FORM });
331
- post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve" });
367
+ post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve", freshness: "allowlist" });
332
368
  post("tasks", "task.act.plan", `^\\/t\\/${TASK}\\/plan$`, { project: "task", limited: "resource", sample: "/t/one/plan" });
333
369
  post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "task", limited: "resource", sample: "/t/one/plan-edit" });
334
370
  post("tasks", "task.act.next", `^\\/t\\/${TASK}\\/next$`, { project: "task", limited: "resource", sample: "/t/one/next" });
@@ -361,11 +397,15 @@ for (const one of ["identity", "about", "promise/cancel", "on"])
361
397
  post("chat", "chat.config", "/chat/config", { scope: "step-up" });
362
398
  for (const one of ["mint", "follow", "end", "stop"])
363
399
  post("chat", `chat.mate-${one}`, `/chat/mate/${one}`, { callers: ["cookie"], ...(one === "mint" ? { scope: "step-up" } : {}) });
400
+ // Remove a message still waiting in the person's lead queue (docs/plans/lead-message-queue.md): their own, while it waits.
401
+ post("chat", "chat.queued-remove", "^\\/chat\\/queued\\/[1-9][0-9]{0,14}\\/remove$", { callers: ["cookie"], sample: "/chat/queued/1/remove" });
402
+ // Confirming a proposal may approve a scope (scope_approve): that one act takes the same standard-approval freshness as
403
+ // /t/:id/approve (chat-actions.ts scopes it); every other operation it confirms still needs a password typed now.
364
404
  for (const one of ["confirm", "dismiss"])
365
- post("chat", `chat.proposal.${one}`, `^\\/chat\\/proposal\\/${ID}\\/${one}$`, { project: "proposal", limited: "proposal", sample: `/chat/proposal/1/${one}` });
405
+ post("chat", `chat.proposal.${one}`, `^\\/chat\\/proposal\\/${ID}\\/${one}$`, { project: "proposal", limited: "proposal", sample: `/chat/proposal/1/${one}`, ...(one === "confirm" ? { freshness: "allowlist" } : {}) });
366
406
  for (const one of ["confirm", "dismiss"])
367
407
  post("chat", `coordinator.proposal.${one}`, `^\\/proposals\\/${ID}\\/${one}$`, { sample: `/proposals/1/${one}` });
368
- post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation" });
408
+ post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation", freshness: "allowlist" });
369
409
  post("chat", "chat.file", "^\\/chat\\/file\\/[0-9a-f]{32}$", { scope: "step-up", sample: `/chat/file/${"a".repeat(32)}` });
370
410
  post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sample: "/chat/ack/1" });
371
411
  for (const one of ["prepare", "preview", "import", "save"])
@@ -373,6 +413,8 @@ for (const one of ["prepare", "preview", "import", "save"])
373
413
  post("flows", "recipes.launch-send", "/recipes/launch", { limited: "collection" });
374
414
  for (const one of ["note", "comment", "revise", "draft-repair", "checks", "add-tests"])
375
415
  post("tasks", `run.act.${one}`, `^\\/r\\/${ID}\\/${one}$`, { project: "run", limited: "resource", sample: `/r/1/${one}` });
416
+ // Cockpit E: reveal a running attempt's proved checkout in Finder (nothing is launched in it).
417
+ post("tasks", "run.act.reveal", `^\\/r\\/${ID}\\/reveal$`, { project: "run", limited: "resource", callers: ["cookie"], sample: "/r/1/reveal", ...approver("Only an approver can show an agent's files.") });
376
418
  post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
377
419
  post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
378
420
  /** The sole route table, in match order. */