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,3 +1,4 @@
1
+ import { credentialProof, freshFacts, proveAccountNow, ssoNowAvailable } from "../scope.js";
1
2
  import { handlersOf } from './handler-registry.js';
2
3
  /** tasks handlers, moved without changing their route bodies. */
3
4
  import {} from "node:http";
@@ -8,7 +9,6 @@ import { createOAuthHttp, resourceMetadataUrl } from "../mcp-oauth.js";
8
9
  import { reproveRemote } from "../operate-remote.js";
9
10
  import { rowVisible } from "../project.js";
10
11
  import { RequestBudget, SourceAdmission } from "../request-budget.js";
11
- import { authenticateAccount } from "../scope.js";
12
12
  import { createSessionRunner } from '../session-remote.js';
13
13
  import {} from "../store.js";
14
14
  import { handleTeamHttp, TEAM_PASSWORD_REVERIFY_MS } from '../team-http.js';
@@ -17,7 +17,7 @@ import { flowHook, telegramHook } from "./remote-hooks.js";
17
17
  import { respond } from "./http.js";
18
18
  import {} from "./session.js";
19
19
  export function createRemoteHandlers(runtime) {
20
- const { identify, store, admitPasswordSource, admitBearer, allowedHost, team, teamBrowserReply, requestBudget, options, coding, codingProjects, liveCeiling, clock, evidenceRoot, consoleOrigin, managedRepos, joinSourceOf, ssoSettings, SSO_FRESH_MS, oauthTokenBudget, hookContext, teamsSourceBudget, teamsTenantBudget } = runtime;
20
+ const { identify, store, admitPasswordSource, admitBearer, allowedHost, team, teamBrowserReply, requestBudget, options, coding, codingProjects, liveCeiling, clock, evidenceRoot, consoleOrigin, managedRepos, joinSourceOf, ssoSettings, oauthTokenBudget, hookContext, teamsSourceBudget, teamsTenantBudget } = runtime;
21
21
  /**
22
22
  * The full principal a live `so_` API token stands for: its person and their generation, the token's own read/act
23
23
  * scope, its project limit narrowed by the person's current access, and its id. Every route that accepts an API token
@@ -119,8 +119,9 @@ export function createRemoteHandlers(runtime) {
119
119
  const who = identify(request, false);
120
120
  if (who === null || who.via !== "cookie")
121
121
  return null;
122
- return { name: who.name, role: who.role, csrf: who.session.csrf,
123
- sso: who.session.sso === undefined ? null : { label: ssoSettings()?.label ?? "your identity provider", fresh: Date.now() - who.session.sso.at < SSO_FRESH_MS } };
122
+ return { name: who.name, role: who.role, csrf: who.session.csrf, generation: who.session.generation,
123
+ sso: who.session.sso === undefined ? null : { label: ssoSettings()?.label ?? "your identity provider",
124
+ fresh: freshFacts.run(runtime.freshFactsOf(request, who), () => ssoNowAvailable(who.name)) } };
124
125
  },
125
126
  sameSite: request => {
126
127
  const origin = request.headers.origin, referer = request.headers.referer;
@@ -129,7 +130,15 @@ export function createRemoteHandlers(runtime) {
129
130
  },
130
131
  // The step-up for a credential: the person's password, or (empty) a fresh identity-provider check. What they may
131
132
  // grant (their role, their projects) is checked beside it.
132
- confirm: (session, typed) => typed === "" ? session.sso?.fresh === true : authenticateAccount(store, session.name, typed).ok,
133
+ // Proved on this request's live session at the moment of consent (signed in here still, same generation), through
134
+ // the one account step-up: a password typed now, or an explicit re-check (spent). Never a remembered password.
135
+ confirm: (request, session, typed) => {
136
+ const who = identify(request, false);
137
+ if (who === null || who.via !== "cookie" || who.name !== session.name || who.session.generation !== session.generation)
138
+ return false;
139
+ // Connecting an outside app is a credential act: never a session (Passwordless) or a remembered password.
140
+ return freshFacts.run(runtime.freshFactsOf(request, who), () => { const proved = proveAccountNow(store, who.name, typed, { credential: true }); return proved.ok && credentialProof(proved.method); });
141
+ },
133
142
  projectsFor: account => store.knownRepos().filter(repo => store.accountCanAccess(account, repo)).sort(),
134
143
  admitToken: request => oauthTokenBudget.admit(joinSourceOf(request)),
135
144
  });
@@ -1,3 +1,5 @@
1
+ /** The conversation: chat and lead pages, proposal cards and the task chat. */
2
+ import type { TaskOrigin } from "../task-source.js";
1
3
  import { type AssignmentSnapshot } from "../assignment.js";
2
4
  import { type BrowserActionCard } from "../browser-workspace.js";
3
5
  import { type ChatDraft } from "../converse.js";
@@ -10,6 +12,7 @@ import { type TaskControlView } from "../task-control.js";
10
12
  import { type TeamChatProviderResolver } from "../team-chat-authorization.js";
11
13
  import { type TeamSnapshot } from "../team-contract.js";
12
14
  import { type DisplayStatus } from "../workspace-ui.js";
15
+ import type { LeadQueueRow } from "../lead-queue.js";
13
16
  import { type Html } from "../html.js";
14
17
  import { type Chrome, type Screen } from "./chrome.js";
15
18
  import { type ProjectPeek } from "./render-pages.js";
@@ -77,6 +80,8 @@ export type TaskChatFocus = {
77
80
  label: string;
78
81
  filedAgo: string | null;
79
82
  } | null;
83
+ /** v126: where the work came from (an outside event's line of context). */
84
+ origin?: TaskOrigin | null;
80
85
  /** The repair chain row when this task is a machine-drafted repair. */
81
86
  repairChain: RepairChainRow | null;
82
87
  } | null;
@@ -214,7 +219,7 @@ export declare function chatPage(chrome: Chrome, data: {
214
219
  /** Chat's first run is on the page: it already says what to do while there is no lead. */
215
220
  firstRunShown?: boolean;
216
221
  }): Screen;
217
- export declare function chatAckPage(chrome: Chrome, turn: ChatTurn, nonce: string): Screen;
222
+ export declare function chatAckPage(chrome: Chrome, turn: ChatTurn, nonce: string, reply?: LeadTurn | null): Screen;
218
223
  /** The card that starts a conversation: the one password ceremony (mate arc §1). */
219
224
  export declare function leadMintCard(enabled: {
220
225
  billing: "metered" | "subscription";
@@ -301,19 +306,23 @@ export type LeadThreadRows = {
301
306
  focusTask: TaskChatFocus | null;
302
307
  /** The lead's open question to this reader, drawn as buttons under its reply. */
303
308
  ask?: LeadAsk | null;
309
+ /** The turn whose question the task has moved past: shown without buttons, marked out of date. */
310
+ staleAsk?: number | null;
304
311
  /** Every question the lead asked in these messages, by turn: answered ones show their words without buttons. */
305
312
  asks?: Map<number, LeadAsk>;
313
+ /** The person's lead queue rows for these messages, by message id (docs/plans/lead-message-queue.md). */
314
+ queue?: Map<number, LeadQueueRow>;
306
315
  };
307
316
  /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
308
317
  * "Something else" moves to the composer. Nothing shows while a reply is running. */
309
318
  export declare function leadAskHtml(ask: LeadAsk, target: {
310
319
  task: string | null;
311
320
  project: string | null;
312
- }, composer: string, open?: boolean): Html;
321
+ }, composer: string, open?: boolean, stale?: boolean): Html;
313
322
  /** The thread's messages as the React conversation renders them, each
314
323
  * with the proposal cards its turn produced; `back` is where a card's
315
324
  * confirm or dismiss returns. */
316
- export declare function leadBrowserMessages(rows: Pick<LeadThreadRows, "messages" | "proposals" | "decisions" | "pending" | "ask" | "asks">, back: string | null, target?: {
325
+ export declare function leadBrowserMessages(rows: Pick<LeadThreadRows, "messages" | "proposals" | "decisions" | "pending" | "ask" | "staleAsk" | "asks" | "queue">, back: string | null, target?: {
317
326
  task: string | null;
318
327
  project: string | null;
319
328
  }): import("../browser-workspace.js").BrowserMessage[];
@@ -1,4 +1,3 @@
1
- /** The conversation: chat and lead pages, proposal cards and the task chat. */
2
1
  import { assignmentSummaryHtml } from "../assignment-ui.js";
3
2
  import {} from "../assignment.js";
4
3
  import {} from "../browser-workspace.js";
@@ -27,7 +26,7 @@ import { assignmentStageOf, stageOfCode } from "../task-status.js";
27
26
  import {} from "../team-chat-authorization.js";
28
27
  import {} from "../team-contract.js";
29
28
  import { buildProgressOf } from "../workspace-ui.js";
30
- import { html, htmlString, isHtml, joinHtml, postForm, replaceMarkup } from "../html.js";
29
+ import { html, htmlString, isHtml, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
31
30
  import { requestContext, withFormToken } from "./request-context.js";
32
31
  import { createHash, randomBytes } from "node:crypto";
33
32
  import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, strokeIcon } from "./chrome.js";
@@ -90,7 +89,9 @@ export function taskChatApproval(focus, csrf) {
90
89
  repairChain: approval.repairChain,
91
90
  route: focus.route,
92
91
  coordinator: approval.coordinator,
92
+ origin: approval.origin ?? null,
93
93
  deliverable: approval.deliverable,
94
+ rebuild: focus.assignment?.need != null && "key" in focus.assignment.need && focus.assignment.need.key === "replan" ? focus.assignment.need.build : null,
94
95
  // Edit plan opens the task page's sheet with its fields open for editing.
95
96
  editHref: `${taskHref(focus.id)}?edit=plan#plan-editor`,
96
97
  notNowHref: "/chat",
@@ -116,12 +117,15 @@ export function taskChatLiveRegion(focus, csrf, fragment = false, inert = false,
116
117
  : fragment && focus.approval !== null && focus.plan !== "requested"
117
118
  ? html `<section class="card chat-action-card chat-refresh-action"><a class="button-link" href="${approvalHref}" data-primary-action>Approve plan</a></section>`
118
119
  : taskChatApproval(focus, csrf));
119
- const approvalCard = focus.approval !== null && focus.dispatch?.action !== "approve-scope"
120
+ // The approval leads when dispatch waits on it or the plan was rewritten after a build (the assignment's one action).
121
+ const approvalLeads = focus.dispatch?.action === "approve-scope" ||
122
+ (focus.assignment?.primaryAction?.code === "approve-scope" && focus.assignment.primaryAction.target.taskId === focus.executionId);
123
+ const approvalCard = focus.approval !== null && !approvalLeads
120
124
  ? html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approvalContent}</details>` : approvalContent;
121
125
  const polling = !inert && ((focus.approval === null || focus.plan === "requested") && focus.state !== "done" && focus.state !== "cancelled" || focus.control.kind === "stopping" || focus.control.kind === "stop");
122
126
  const status = focus.assignment !== null
123
- ? html `${assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) })}${receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : ""}`
124
- : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope");
127
+ ? html `${assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && approvalLeads, sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) })}${receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : ""}`
128
+ : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && approvalLeads);
125
129
  const publication = focus.publication;
126
130
  const prUrl = publication === null ? null : safePrUrl(publication.prUrl);
127
131
  return html `<section id="task-chat-live" aria-live="polite" data-task="${focus.id}" data-execution="${focus.executionId}" data-source="/chat/task-status?task=${encodeURIComponent(focus.id)}" data-poll="${polling ? "1" : "0"}" data-approval="${focus.approval?.digest ?? ""}" data-plan="${focus.plan ?? ""}">${focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : ""}<div class="task-live-summary">${status}${checkProgressHtml(focus.checkProgress)}${
@@ -406,12 +410,12 @@ export function chatPage(chrome, data) {
406
410
  const candidates = data.chat === null ? [] : [...data.chat.candidates.values()];
407
411
  for (const one of candidates) {
408
412
  const draft = one.draft;
409
- parts.push(html `<div class="card"><p><strong>${draft.title}</strong> <span class="badge">Draft ${draft.kind}</span></p><p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p><p style="white-space:pre-wrap">${draft.goal}</p>${draft.outOfScope === null ? "" : html `<p class="meta">Not: ${draft.outOfScope}</p>`}${draft.touches.length > 0 ? html `<p class="meta">Touches: ${draft.touches.join(", ")}</p>` : ""}<p class="meta">Repo: <span class="mono">${projectName(one.repoPath)}</span></p>${postForm(`/chat/file/${one.key}`, html `<input type="password" name="token" placeholder="your password" autocomplete="current-password"><button type="submit">File unapproved</button>`, { attrs: { class: "inline" } })}</div>`);
413
+ parts.push(html `<div class="card"><p><strong>${draft.title}</strong> <span class="badge">Draft ${draft.kind}</span></p><p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p><p style="white-space:pre-wrap">${draft.goal}</p>${draft.outOfScope === null ? "" : html `<p class="meta">Not: ${draft.outOfScope}</p>`}${draft.touches.length > 0 ? html `<p class="meta">Touches: ${draft.touches.join(", ")}</p>` : ""}<p class="meta">Repo: <span class="mono">${projectName(one.repoPath)}</span></p>${postForm(`/chat/file/${one.key}`, html `${reauth(html `<input type="password" name="token" placeholder="your password" autocomplete="current-password">`)}<button type="submit">File unapproved</button>`, { attrs: { class: "inline" } })}</div>`);
410
414
  }
411
415
  if (!subscription && data.canManage && data.focusTask === null) {
412
416
  parts.push(html `<h2>Ask</h2>`, postForm("/chat", joinHtml([
413
417
  html `<label>Message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`,
414
- html `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`,
418
+ html `${reauth(html `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`)}`,
415
419
  html `<button type="submit">Ask</button>`,
416
420
  ], "\n"), { attrs: { class: "card" } }));
417
421
  }
@@ -423,15 +427,20 @@ export function chatPage(chrome, data) {
423
427
  }
424
428
  return screen("chat", chatWorkspace(joinHtml(parts, "\n"), data.projects, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null } });
425
429
  }
426
- export function chatAckPage(chrome, turn, nonce) {
430
+ export function chatAckPage(chrome, turn, nonce, reply = null) {
431
+ // A lead reply's step reserves nothing itself: the reply set the money aside, and a reply whose cost is unknown has
432
+ // already counted that whole amount against its conversation. Say that amount, never the step's zero.
433
+ const replyCharge = reply === null ? null : reply.settledMicrousd ?? reply.reservedMicrousd;
427
434
  return screen("chat", joinHtml([
428
435
  html `<h1>Unknown spend</h1>`,
429
436
  html `<div class="card">`,
430
437
  html `<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${turn.provider} · ${turn.model}</span> may have started before it failed (${turn.failureReason ?? "crashed"}), and its cost could not be measured.</p>`,
431
- html `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
438
+ replyCharge === null
439
+ ? html `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`
440
+ : html `<p><strong>This lead reply's worst case, ${chatMoney(replyCharge)}, has already been counted against its conversation. Acknowledging adds nothing more and re-enables chat on this credential.</strong></p>`,
432
441
  html `<p class="meta">Check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
433
442
  postForm(`/chat/ack/${turn.id}`, joinHtml([
434
- html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
443
+ html `${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`)}`,
435
444
  html `<button type="submit">Accept the charge — re-enable chat</button>`,
436
445
  ], "\n"), { hidden: { nonce } }),
437
446
  html `</div>`,
@@ -752,9 +761,26 @@ export function replacedThreadHtml(previous) {
752
761
  ? html `<div class="msg op" data-previous-message="${one.id}"><p style="white-space:pre-wrap">${one.text}</p></div>`
753
762
  : html `<div class="msg mate" data-previous-message="${one.id}">${renderChatText(one.text)}</div>`)}<p class="chat-previous-divider" role="separator" data-thread-divider>${REPLACED_THREAD_DIVIDER}</p></section>`;
754
763
  }
764
+ /** A queued message's state under its words: waiting (with Remove), or why it was not sent. Nothing for one that ran.
765
+ * A div, not a p: the Remove form can't sit inside a paragraph (the parser would close it first). */
766
+ function queuedStateHtml(row, back) {
767
+ if (row.status === "queued") {
768
+ const waiting = row.error?.startsWith("Waiting: ") === true ? row.error.slice("Waiting: ".length) : "It goes to the lead when it finishes.";
769
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="queued"><span class="so-queued-state">Queued</span> · ${waiting}${postForm(`/chat/queued/${row.message}/remove`, html `<button type="submit" class="so-queued-remove quiet">Remove</button>`, { attrs: { class: "inline" }, ...(back === null ? {} : { returnTo: back }) })}</div>`;
770
+ }
771
+ if (row.status === "running" && row.turnId === null)
772
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="starting"><span class="so-queued-state">Starting</span></div>`;
773
+ if (row.status === "cancelled" || row.status === "failed")
774
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="${row.status}"><span class="so-queued-state">Not sent</span> · ${row.error ?? "It was not sent."}</div>`;
775
+ if (row.status === "uncertain")
776
+ return html `<div class="so-queued" data-queued="${row.message}" data-queue-state="uncertain"><span class="so-queued-state">Not confirmed</span> · ${row.error ?? "Delivery could not be confirmed. Nothing was sent again."}</div>`;
777
+ return html ``;
778
+ }
755
779
  /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
756
780
  * "Something else" moves to the composer. Nothing shows while a reply is running. */
757
- export function leadAskHtml(ask, target, composer, open = true) {
781
+ export function leadAskHtml(ask, target, composer, open = true, stale = false) {
782
+ if (stale)
783
+ return html `<div class="so-owner-ask" data-ask="${ask.turn}" data-ask-stale><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><p class="so-owner-ask-stale">Out of date: the task changed after this was asked. Its status above says what it needs now.</p></div>`;
758
784
  if (!open)
759
785
  return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p></div>`;
760
786
  const hidden = target.task !== null ? { task: target.task } : target.project !== null ? { project: target.project } : {};
@@ -767,7 +793,7 @@ export function leadBrowserMessages(rows, back, target = { task: null, project:
767
793
  const open = rows.pending === null ? rows.ask?.turn ?? null : null;
768
794
  const asked = (turn) => {
769
795
  const ask = turn === null ? undefined : rows.asks?.get(turn);
770
- return ask === undefined ? "" : leadAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
796
+ return ask === undefined ? "" : leadAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open, ask.turn === rows.staleAsk);
771
797
  };
772
798
  // The owner's message each reply answers: when it asked for ids, the reply keeps them.
773
799
  const askedBy = new Map();
@@ -776,7 +802,9 @@ export function leadBrowserMessages(rows, back, target = { task: null, project:
776
802
  askedBy.set(message.id, last); return last; }, undefined);
777
803
  return rows.messages.map(message => ({
778
804
  id: message.id, role: message.role, text: message.text,
779
- html: htmlString(message.role === 'operator' ? html `<p>${message.text}</p>` : html `${renderChatText(message.text, askedBy.get(message.id))}${asked(message.turn)}`),
805
+ html: htmlString(message.role === 'operator'
806
+ ? (() => { const queued = rows.queue?.get(message.id); return html `${message.text === "" ? "" : html `<p>${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, back)}`; })()
807
+ : html `${renderChatText(message.text, askedBy.get(message.id))}${asked(message.turn)}`),
780
808
  activity: message.activity, createdAt: message.createdAt,
781
809
  ...(() => {
782
810
  // Under the lead's message only, as the server-rendered thread does: the person's message shares the turn.
@@ -796,6 +824,7 @@ export function leadChatVersion(rows) {
796
824
  const focus = rows.focusTask;
797
825
  const facts = [
798
826
  rows.messages.map(one => [one.id, one.role, one.turn]),
827
+ rows.queue === undefined ? null : [...rows.queue.values()].map(one => [one.message, one.status, one.revision, one.error]),
799
828
  rows.previous == null ? null : rows.previous.messages.map(one => one.id),
800
829
  rows.proposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
801
830
  rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
@@ -851,7 +880,9 @@ export function leadThreadHtml(data) {
851
880
  for (const message of data.messages) {
852
881
  if (message.role === "operator") {
853
882
  asked = message.text;
854
- thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}"><p style="white-space:pre-wrap">${message.text}</p></div>`);
883
+ // A message that waits its turn (the lead queue) says so, with Remove while it still waits — as in the React thread.
884
+ const queued = data.queue?.get(message.id);
885
+ thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}">${message.text === "" ? "" : html `<p style="white-space:pre-wrap">${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, returnTo)}</div>`);
855
886
  continue;
856
887
  }
857
888
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
@@ -917,7 +948,7 @@ export function leadPage(chrome, data) {
917
948
  // keyboard reaches it. Only this act moves the reader.
918
949
  html `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, postForm("/chat", joinHtml([
919
950
  html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${LEAD_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
920
- html `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : html ` disabled`}>Send</button>`,
951
+ html `<button type="submit" aria-label="${data.pending === null ? "send message" : "send message — it waits for the current reply"}">Send</button>`,
921
952
  ], "\n"), {
922
953
  attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the lead", "data-chat-session": data.session.id, "data-chat-task": data.focusTask?.id ?? "", "data-chat-user": data.session.approver, "data-chat-busy": data.pending === null ? "0" : "1", "data-chat-version": leadChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
923
954
  hidden: { request: randomBytes(16).toString("hex"), "request-session": data.session.id, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
@@ -925,7 +956,7 @@ export function leadPage(chrome, data) {
925
956
  // Concise pass (2026-09-13): the status line speaks only when there is
926
957
  // a state to report — a reply in progress here, the connection from
927
958
  // the continuity script — and the composer carries no second intro.
928
- html `<p class="meta composer-hint" id="chat-connection" role="status" aria-live="polite">${data.pending === null ? "" : "Reply in progress. You can draft your next message or come back later."}</p>`,
959
+ html `<p class="meta composer-hint" id="chat-connection" role="status" aria-live="polite">${data.pending === null ? "" : "Reply in progress. A message you send now waits its turn."}</p>`,
929
960
  // The explicit reconnection (package 2): shown only once the session
930
961
  // or sign-in changed under this page; it reloads on the reader's act.
931
962
  html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`, leadAfterComposerHtml(data), controlsHtml);
@@ -1,4 +1,5 @@
1
1
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
2
+ import { type TaskOrigin } from "../task-source.js";
2
3
  import { type Html } from "../html.js";
3
4
  import { type AssignmentSnapshot } from "../assignment.js";
4
5
  import { type BoardCard } from "../board.js";
@@ -145,14 +146,39 @@ export declare function systemPage(chrome: Chrome, data: {
145
146
  }[];
146
147
  now: Date;
147
148
  }): Screen;
149
+ /**
150
+ * A build's stage as one of three steps — set up, writing, checking and
151
+ * committing — read from the machine's own phase, never a percent. The
152
+ * steps only move forward: evidence and proof checks run both before and
153
+ * after the commit (builder.ts), so everything after the agent's turn is
154
+ * one step, and the stage words say which part. Null for runs that are not
155
+ * builds (planning, review, investigation): those have no honest steps.
156
+ */
157
+ export declare const BUILD_STEPS: readonly ["setting up", "writing", "checking and committing"];
158
+ export declare function buildStepOf(claim: NonNullable<BoardCard["claim"]>): number | null;
159
+ /**
160
+ * The workers a board's strip may show: not retired, bound to at least one
161
+ * project the server's ceiling admits (never a runner serving only projects
162
+ * this server must not speak about), and — on a one-project board — bound to
163
+ * that project.
164
+ */
165
+ export declare function boardWorkers(runners: readonly Runner[], scope: {
166
+ all: boolean;
167
+ project: string | null;
168
+ visible: (repo: string) => boolean;
169
+ }): Runner[];
148
170
  /**
149
171
  * The board: the pipeline as lanes, position as meaning — attention, then
150
- * queued, then waiting, then building, then recently done. Read-only by
151
- * construction (every card is a link, no forms, no nonces), which is what
152
- * makes it safe to re-render itself while somebody watches.
172
+ * queued, then waiting, then building, then recently done — under a strip
173
+ * of the workers doing it. Read-only by construction (every card is a link,
174
+ * no forms, no nonces), which is what makes it safe to re-render itself
175
+ * while somebody watches; a card that changes lane slides there
176
+ * (regionScript's animate swap, keyed by data-vt).
153
177
  */
154
178
  export declare function boardBody(data: {
155
179
  cards: BoardCard[];
180
+ /** The workers this board's work can go to (retired ones left out). */
181
+ workers: readonly Runner[];
156
182
  done: ReturnType<Store["listCompletedWorkScoped"]>;
157
183
  saturated: boolean;
158
184
  now: Date;
@@ -360,7 +386,7 @@ export declare function portfolioOverview(data: {
360
386
  startedAt: string;
361
387
  ranMinutes: number | null;
362
388
  costUsd: number | null;
363
- authMode: "subscription" | "api-key" | null;
389
+ authMode: "subscription" | "api-key" | "agent-login" | null;
364
390
  prNumber: number | null;
365
391
  prUrl: string | null;
366
392
  }[];
@@ -519,6 +545,8 @@ export declare function nextPage(chrome: Chrome, data: {
519
545
  deliverable?: "branch" | "report";
520
546
  /** v48: the agents the yes freezes — the one block every ceremony shows. */
521
547
  route: RouteView | null;
548
+ /** v126: where the work came from (an outside event's line of context, never a term agreed to). */
549
+ origin?: TaskOrigin | null;
522
550
  csrf: string;
523
551
  nonce: string;
524
552
  remaining: number;
@@ -1,5 +1,6 @@
1
1
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
2
- import { html, htmlString, joinHtml, postForm, replaceMarkup } from "../html.js";
2
+ import { originNote } from "../task-source.js";
3
+ import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
3
4
  import {} from "../assignment.js";
4
5
  import {} from "../board.js";
5
6
  import { browserWorkActionHref } from "../browser-workspace.js";
@@ -206,11 +207,74 @@ export function systemPage(chrome, data) {
206
207
  : html `<span class="meta">syncing normally</span>`}</p>`), "\n")}`,
207
208
  ], "\n"), { chrome, refreshSeconds: data.building.length > 0 ? 10 : 60 });
208
209
  }
210
+ /**
211
+ * A build's stage as one of three steps — set up, writing, checking and
212
+ * committing — read from the machine's own phase, never a percent. The
213
+ * steps only move forward: evidence and proof checks run both before and
214
+ * after the commit (builder.ts), so everything after the agent's turn is
215
+ * one step, and the stage words say which part. Null for runs that are not
216
+ * builds (planning, review, investigation): those have no honest steps.
217
+ */
218
+ export const BUILD_STEPS = ["setting up", "writing", "checking and committing"];
219
+ export function buildStepOf(claim) {
220
+ if (claim.role === "planner" || claim.role === "reviewer" || claim.role === "scout")
221
+ return null;
222
+ if (claim.model === null)
223
+ return 0;
224
+ if (claim.phase === null || claim.phase === "agent-running")
225
+ return 1;
226
+ return 2;
227
+ }
228
+ /** A clock that ticks in the page (boardTickScript) from the server's own start time; the text is the no-script answer. */
229
+ function tickingClock(startedIso, now) {
230
+ const minutes = Math.max(0, Math.round((now.getTime() - new Date(startedIso).getTime()) / 60_000));
231
+ return html `<time class="clock" datetime="${startedIso}" data-tick>${minutes}m</time>`;
232
+ }
233
+ /**
234
+ * The workers a board's strip may show: not retired, bound to at least one
235
+ * project the server's ceiling admits (never a runner serving only projects
236
+ * this server must not speak about), and — on a one-project board — bound to
237
+ * that project.
238
+ */
239
+ export function boardWorkers(runners, scope) {
240
+ return runners.filter(runner => runner.retiredAt === null &&
241
+ runner.repos.some(repo => scope.visible(repo)) &&
242
+ (scope.all || scope.project === null || runner.repos.includes(scope.project)));
243
+ }
244
+ /**
245
+ * The worker strip across the top of the board: every worker this board's
246
+ * work can go to, what it is building here, and its stage and clock. Only
247
+ * builds the board already shows are named — a worker busy in a project
248
+ * this reader cannot see reads "no build on this board", never "free".
249
+ */
250
+ function workerStrip(runners, cards, now) {
251
+ if (runners.length === 0)
252
+ return html `<p class="meta crew-empty">No workers yet — work waits in the queue until one joins.</p>`;
253
+ const tiles = runners.map(runner => {
254
+ const builds = cards
255
+ .filter(card => card.lane === "building" && card.claim !== null && card.claim.runner === runner.name)
256
+ .sort((a, b) => a.claim.claimedAt.localeCompare(b.claim.claimedAt));
257
+ const online = runnerAlive(runner, now);
258
+ const state = !online ? "offline" : builds.length > 0 ? "busy" : "idle";
259
+ const dot = state === "busy" ? html `<span class="dot dot-ok pulse"></span>` : html `<span class="dot dot-off"></span>`;
260
+ const jobs = builds.length > 0
261
+ ? builds.map(card => {
262
+ const claim = card.claim;
263
+ const words = claim.model === null ? "preparing workspace" : claim.phase === null ? "the agent is working" : phaseWords(claim.phase);
264
+ return html `<a class="worker-job" href="${card.href}"><span class="t">${card.title}</span><span class="sub"><span>${words}</span>${tickingClock(claim.claimedAt, now)}</span></a>`;
265
+ })
266
+ : html `<p class="worker-job idle">${online ? "no build on this board" : html `offline since ${whenTime(runner.heartbeatAt)}`}</p>`;
267
+ return html `<div class="worker worker-${state}" data-flash="${runner.name}" data-flash-value="${builds.map(card => card.taskId).join(" ")}"><div class="worker-head">${dot}<span class="name">${runner.name}</span><span class="kind">${runner.agents.join(" · ")}</span>${runner.capacity > 1 ? html `<span class="slots">${builds.length}/${runner.capacity}</span>` : null}</div>${jobs}</div>`;
268
+ });
269
+ return html `<section class="crew" aria-label="Workers">${tiles}</section>`;
270
+ }
209
271
  /**
210
272
  * The board: the pipeline as lanes, position as meaning — attention, then
211
- * queued, then waiting, then building, then recently done. Read-only by
212
- * construction (every card is a link, no forms, no nonces), which is what
213
- * makes it safe to re-render itself while somebody watches.
273
+ * queued, then waiting, then building, then recently done — under a strip
274
+ * of the workers doing it. Read-only by construction (every card is a link,
275
+ * no forms, no nonces), which is what makes it safe to re-render itself
276
+ * while somebody watches; a card that changes lane slides there
277
+ * (regionScript's animate swap, keyed by data-vt).
214
278
  */
215
279
  export function boardBody(data, ciRed) {
216
280
  const chip = (repo) => !data.all || repo === null ? null : html `<span class="badge">${projectName(repo)}</span>`;
@@ -260,7 +324,7 @@ export function boardBody(data, ciRed) {
260
324
  const kept = parts.filter((one) => one !== null);
261
325
  return kept.length === 0 ? null : html `<span class="chips">${joinHtml(kept, " ")}</span>`;
262
326
  };
263
- const plain = (card) => html `<a class="lane-card" href="${card.href}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
327
+ const plain = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
264
328
  ...(card.stalledSince === null ? [] : [["waiting", age(card.stalledSince)]]),
265
329
  ])}${chips([
266
330
  chip(card.repo),
@@ -278,7 +342,7 @@ export function boardBody(data, ciRed) {
278
342
  }
279
343
  return heads;
280
344
  })();
281
- const queuedCard = (card) => html `<a class="lane-card" href="${card.href}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
345
+ const queuedCard = (card) => html `<a class="lane-card" href="${card.href}" data-vt="${card.taskId}"><span class="id">${card.taskId}</span><span class="t">${card.title}</span><span class="why">${card.reason}</span>${facts([
282
346
  ["worker", card.assignedRunner === null ? "any free worker" : card.assignedRunner],
283
347
  ])}${chips([
284
348
  queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
@@ -291,14 +355,19 @@ export function boardBody(data, ciRed) {
291
355
  const claim = card.claim;
292
356
  if (claim === null)
293
357
  return plain(card);
294
- const minutes = Math.max(1, Math.round((data.now.getTime() - new Date(claim.claimedAt).getTime()) / 60_000));
295
358
  const workspace = claim.worktree === null ? null : (claim.worktree.split("/").pop() ?? claim.worktree);
296
359
  // Chip copy: unknown phases show nothing rather than raw tokens.
297
360
  const phase = claim.phase === null ? undefined : PHASE_WORDS[claim.phase];
298
361
  // The live strip is the run's own phase and clock — never a percent:
299
362
  // a build has no honest progress figure, only a stage and an elapsed.
300
363
  const live = claim.model === null ? "preparing workspace" : (phase ?? "the agent is working");
301
- return html `<a class="lane-card building" href="${card.href}"><span class="id">${card.taskId}</span><span class="t"><span class="dot dot-ok pulse"></span>${card.title}</span><span class="live-line"><span class="stage">${live}</span><span class="clock">${minutes}m</span></span>${facts([
364
+ // The steps: where the build is among set up → writing → checking →
365
+ // committing, from its phase — a stage, never a percent.
366
+ const step = buildStepOf(claim);
367
+ const steps = step === null
368
+ ? null
369
+ : html `<span class="steps" role="img" aria-label="${`step ${step + 1} of ${BUILD_STEPS.length}: ${BUILD_STEPS[step]}`}">${BUILD_STEPS.map((_, index) => html `<i class="${index < step ? "on" : index === step ? "now" : ""}"></i>`)}</span>`;
370
+ return html `<a class="lane-card building" href="${card.href}" data-vt="${card.taskId}"><span class="id">${card.taskId}</span><span class="t"><span class="dot dot-ok pulse"></span>${card.title}</span>${steps}<span class="live-line"><span class="stage">${live}</span>${tickingClock(claim.claimedAt, data.now)}</span>${facts([
302
371
  ["worker", claim.runner],
303
372
  ...(claim.model === null
304
373
  ? []
@@ -315,7 +384,7 @@ export function boardBody(data, ciRed) {
315
384
  const pr = row.prNumber === null
316
385
  ? null
317
386
  : html `<span class="badge badge-open">PR #${row.prNumber}</span>${ciRed(row.prNumber) ? html ` <span class="badge badge-failed">CI failing</span>` : null}`;
318
- return html `<a class="lane-card" href="${taskHref(row.taskId)}"><span class="id">${row.taskId}</span><span class="t">${row.title}</span>${row.handoff === null ? null : html `<span class="why">${row.handoff.length > 120 ? row.handoff.slice(0, 120) + "…" : row.handoff}</span>`}${facts([
387
+ return html `<a class="lane-card" href="${taskHref(row.taskId)}" data-vt="${row.taskId}"><span class="id">${row.taskId}</span><span class="t">${row.title}</span>${row.handoff === null ? null : html `<span class="why">${row.handoff.length > 120 ? row.handoff.slice(0, 120) + "…" : row.handoff}</span>`}${facts([
319
388
  ...(row.ranMinutes === null ? [] : [["ran", `${row.ranMinutes}m`]]),
320
389
  ["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
321
390
  ])}${chips([
@@ -346,7 +415,8 @@ export function boardBody(data, ciRed) {
346
415
  data.all ? null : html `<p class="meta board-view"><strong>State</strong> · <a href="/board?view=order">order →</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
347
416
  deltaLine,
348
417
  toggle,
349
- html `<div class="board">`,
418
+ workerStrip(data.workers, data.cards, data.now),
419
+ html `<div class="board" data-now="${String(data.now.getTime())}">`,
350
420
  lane("attention", "needs you", "these wait for a person", plain),
351
421
  lane("queued", "queued", "starts when a worker is free", queuedCard),
352
422
  lane("waiting", "waiting", "paused until a time, another task, or a requirement is ready", plain),
@@ -912,7 +982,7 @@ export function projectsPage(chrome, recent, candidates, open, problem, _unscope
912
982
  onboard.record[1].large
913
983
  ? html `<label class="row"><input type="checkbox" name="big-ok" value="1"> this is a large repository (or its size is unknown) — clone it anyway</label>`
914
984
  : null,
915
- html `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
985
+ html `${reauth(html `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`)}`,
916
986
  html `<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
917
987
  ], "\n"), { hidden: { nonce: onboard.record[0] } }),
918
988
  html `</div>`,
@@ -1258,11 +1328,11 @@ export function nextPage(chrome, data) {
1258
1328
  card =
1259
1329
  html `<h1>${item.approval.taskId}</h1><p>${item.approval.title}</p>${data.planDocument === null
1260
1330
  ? null
1261
- : html `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`}${postForm(`${taskHref(item.approval.taskId)}/approve`, html `<p><strong>Approve exactly this:</strong></p>${data.deliverable === "report" ? html `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : null}${scope === null || scope.profileState !== "unresolved" ? null : profileWords(scope)}<p class="meta">Goal</p><p class="recap" style="margin-top:0">${scope?.goal ?? item.approval.goal}</p><p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? html `<em>no exclusions</em>` : scope.outOfScope}</p><p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : joinHtml(scope.touches, ", ")}</p>${scope === null ? null : acceptanceCeremonyHtml(scope.acceptance)}${
1331
+ : html `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`}${postForm(`${taskHref(item.approval.taskId)}/approve`, html `<p><strong>Approve exactly this:</strong></p>${data.deliverable === "report" ? html `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : null}${scope === null || scope.profileState !== "unresolved" ? null : profileWords(scope)}${originNote(data.origin) === null ? null : html `<p class="meta" data-approval-origin>${originNote(data.origin)}</p>`}<p class="meta">Goal</p><p class="recap" style="margin-top:0">${scope?.goal ?? item.approval.goal}</p><p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? html `<em>no exclusions</em>` : scope.outOfScope}</p><p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : joinHtml(scope.touches, ", ")}</p>${scope === null ? null : acceptanceCeremonyHtml(scope.acceptance)}${
1262
1332
  // The AGENTS the yes freezes (v48): the same concise block the task
1263
1333
  // page and chat sign under, before the password — runtime limits one
1264
1334
  // tap away, never in the way.
1265
- agentsCeremonyHtml(data.route)}${scope === null ? null : runtimeDetailsHtml(scope)}<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label><div class="sticky-actions"><button type="submit">Approve this scope</button></div>`, { attrs: { class: "card approve-form" }, returnTo: "next", hidden: { nonce: data.nonce, digest: data.approvalDigest ?? item.approval.digest } })}<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
1335
+ agentsCeremonyHtml(data.route)}${scope === null ? null : runtimeDetailsHtml(scope)}${reauth(html `<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>`)}<div class="sticky-actions"><button type="submit">Approve this scope</button></div>`, { attrs: { class: "card approve-form" }, returnTo: "next", hidden: { nonce: data.nonce, digest: data.approvalDigest ?? item.approval.digest } })}<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
1266
1336
  }
1267
1337
  else if (item.kind === "requeue") {
1268
1338
  card =
@@ -7,8 +7,9 @@ export declare function joinDeadPage(): Html;
7
7
  /**
8
8
  * v100: for someone signed in with the identity provider, every step-up
9
9
  * password field (autocomplete="current-password"; a secret like a bot token
10
- * is never one) becomes that sign-in: confirmed, when the provider checked
11
- * them in the last ten minutes, or a link to be checked again, then back here.
10
+ * is never one) becomes that sign-in: confirmed, when an explicit re-check
11
+ * made on request awaits its one step-up (cockpit B2), or a link to be
12
+ * checked again, then back here. An ordinary sign-in never confirms.
12
13
  */
13
14
  export declare function ssoStepUps(page: Html, sso: {
14
15
  label: string;
@@ -33,8 +33,9 @@ export function joinDeadPage() {
33
33
  /**
34
34
  * v100: for someone signed in with the identity provider, every step-up
35
35
  * password field (autocomplete="current-password"; a secret like a bot token
36
- * is never one) becomes that sign-in: confirmed, when the provider checked
37
- * them in the last ten minutes, or a link to be checked again, then back here.
36
+ * is never one) becomes that sign-in: confirmed, when an explicit re-check
37
+ * made on request awaits its one step-up (cockpit B2), or a link to be
38
+ * checked again, then back here. An ordinary sign-in never confirms.
38
39
  */
39
40
  export function ssoStepUps(page, sso, returnTo) {
40
41
  const stepUp = (attributes) => /\btype="password"/.test(attributes) && /\bautocomplete="current-password"/.test(attributes);
@@ -1,3 +1,5 @@
1
+ /** Results and runs: the review cockpit, evidence, receipts, diffs and the result panel. */
2
+ import type { RunNetworkRecord } from "../egress.js";
1
3
  import { type AssignmentSnapshot } from "../assignment.js";
2
4
  import { type BrowserCheckItem, type BrowserNeedAction, type BrowserResultPanel, type BrowserResultView } from "../browser-workspace.js";
3
5
  import { type ReportShot } from "../evidence.js";
@@ -7,6 +9,7 @@ import { type CriterionMatrixRow, type ProofVerdict } from "../proof.js";
7
9
  import { type PullRequestView } from "../pull-request-flow.js";
8
10
  import { type StructuredHandoffView, type TerminalDiffView } from "../result-evidence-readers.js";
9
11
  import { type FollowUpCheck } from "../result-follow-ups.js";
12
+ import type { RunEfficiency } from "../run-efficiency.js";
10
13
  import { type ResultScreenshot, type ResultTab, type SharedResultFacts } from "../result-review.js";
11
14
  import { type BuildReviewView } from "../review-switch.js";
12
15
  import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
@@ -180,6 +183,10 @@ export type RankedReviewRow = CompletedWorkRow & {
180
183
  * scope, plan, run, artifact, verdict, and publication records,
181
184
  * each read through the verifier the run page already uses. */
182
185
  export type ReviewCockpitView = {
186
+ /** How the result's run went (docs/plans/run-efficiency.md): display only, its sources and their gaps said. */
187
+ efficiency?: RunEfficiency | null;
188
+ /** What the run's network was and what its proxy saw (egress.ts): display only. */
189
+ network?: RunNetworkRecord | null;
183
190
  taskId: string;
184
191
  title: string;
185
192
  repo: string | null;
@@ -285,7 +292,16 @@ export declare function reviewCockpitPage(chrome: Chrome, data: {
285
292
  export declare function reviewCockpitDetail(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview?: boolean, tab?: ResultTab, user?: string): Html;
286
293
  /** The selected result's HTML and, for the rebuilt page, the same result
287
294
  * as data: every form and panel body is the one the HTML carries. */
288
- export declare function reviewCockpitDetailParts(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview: boolean, tab: ResultTab, user: string): {
295
+ /** Where a result is shown: its own page (/review), or the Result tab of its task's page (docs/plans/one-page-task-and-agent.md),
296
+ * whose address is `base` (the task page with tab=result, and its version when not the current one). The result's own
297
+ * view (Summary / Changes / Checks) rides `section=` there, so it never fights the task page's `tab`. */
298
+ export type ResultHost = {
299
+ kind: "review";
300
+ } | {
301
+ kind: "task";
302
+ base: string;
303
+ };
304
+ export declare function reviewCockpitDetailParts(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview: boolean, tab: ResultTab, user: string, host?: ResultHost): {
289
305
  html: Html;
290
306
  selected: NonNullable<BrowserResultView["selected"]>;
291
307
  };
@@ -319,7 +335,7 @@ export declare function pullRequestCardHtml(pr: TaskPullRequest, csrf: string):
319
335
  * bearer session (no token) sees the road named, never a form. */
320
336
  export declare function reviewNextAction(view: ReviewCockpitView, csrf: string, _accepted: boolean, _canAnnotate: boolean): Html;
321
337
  /** The next action for the browser workspace's JSON: its control as markup text. */
322
- export declare function reviewNextActionOf(view: ReviewCockpitView, csrf: string): {
338
+ export declare function reviewNextActionOf(view: ReviewCockpitView, csrf: string, back?: string): {
323
339
  kind: string;
324
340
  title: string;
325
341
  detail: string;