toolroll 0.9.72 → 0.9.74

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 (137) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/dist/agent-fence.d.ts +5 -0
  4. package/dist/agent-fence.js +42 -7
  5. package/dist/agent-turns.js +5 -0
  6. package/dist/agents-live.d.ts +31 -0
  7. package/dist/agents-live.js +43 -0
  8. package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
  9. package/dist/browser/workspace.css +2 -2
  10. package/dist/browser/workspace.js +31 -30
  11. package/dist/browser-workspace.d.ts +6 -0
  12. package/dist/builder.js +1 -1
  13. package/dist/chat-actions.js +13 -8
  14. package/dist/chat-continuity.js +10 -0
  15. package/dist/chat-decide.d.ts +47 -1
  16. package/dist/chat-decide.js +170 -4
  17. package/dist/chat-quiet.js +3 -3
  18. package/dist/check-report.js +2 -1
  19. package/dist/coding-provider.js +4 -2
  20. package/dist/commit-extract.d.ts +17 -0
  21. package/dist/commit-extract.js +82 -4
  22. package/dist/contracts/console-api.d.ts +3 -3
  23. package/dist/contracts/console-api.js +4 -4
  24. package/dist/contracts/release-terms.d.ts +7 -1
  25. package/dist/contracts/release-terms.js +6 -2
  26. package/dist/deploy-gate.js +5 -2
  27. package/dist/desktop-update.d.ts +1 -1
  28. package/dist/dispose.js +35 -2
  29. package/dist/exec.d.ts +1 -1
  30. package/dist/exec.js +20 -19
  31. package/dist/first-run.d.ts +17 -1
  32. package/dist/first-run.js +38 -0
  33. package/dist/flow-gallery-ui.js +4 -2
  34. package/dist/flow-gallery.d.ts +3 -0
  35. package/dist/flow-gallery.js +6 -2
  36. package/dist/flow-triggers.d.ts +8 -0
  37. package/dist/flow-triggers.js +15 -0
  38. package/dist/flows-cli.js +13 -3
  39. package/dist/guides.js +10 -1
  40. package/dist/invoke.d.ts +13 -0
  41. package/dist/invoke.js +21 -12
  42. package/dist/kits-ui.js +4 -2
  43. package/dist/kits.d.ts +6 -4
  44. package/dist/kits.js +9 -11
  45. package/dist/knowledge-ui.d.ts +4 -1
  46. package/dist/knowledge-ui.js +10 -5
  47. package/dist/lead-progress.d.ts +4 -1
  48. package/dist/lead-progress.js +3 -1
  49. package/dist/lead.js +4 -0
  50. package/dist/live-bus.d.ts +2 -0
  51. package/dist/live-bus.js +1 -0
  52. package/dist/live-children-read.d.ts +52 -0
  53. package/dist/live-children-read.js +333 -0
  54. package/dist/live-children.d.ts +73 -0
  55. package/dist/live-children.js +318 -0
  56. package/dist/live-detail.d.ts +4 -3
  57. package/dist/live-detail.js +9 -5
  58. package/dist/live-input.js +7 -8
  59. package/dist/live-main-read.d.ts +19 -0
  60. package/dist/live-main-read.js +91 -0
  61. package/dist/live-open.d.ts +28 -0
  62. package/dist/live-open.js +125 -0
  63. package/dist/live-records.d.ts +10 -1
  64. package/dist/live-records.js +16 -5
  65. package/dist/live.d.ts +54 -22
  66. package/dist/live.js +218 -69
  67. package/dist/mcp-connect.d.ts +10 -0
  68. package/dist/mcp-connect.js +2 -0
  69. package/dist/operate.js +3 -2
  70. package/dist/peek-cli.d.ts +10 -3
  71. package/dist/peek-cli.js +17 -8
  72. package/dist/planner.js +1 -1
  73. package/dist/project-tools.js +3 -1
  74. package/dist/protected-storage.d.ts +67 -0
  75. package/dist/protected-storage.js +398 -0
  76. package/dist/provider-limits.d.ts +3 -1
  77. package/dist/provider-limits.js +5 -2
  78. package/dist/provider.js +6 -12
  79. package/dist/release-adapters.d.ts +8 -1
  80. package/dist/release-adapters.js +84 -14
  81. package/dist/release-authority.d.ts +6 -0
  82. package/dist/release-authority.js +9 -0
  83. package/dist/release-card.d.ts +76 -0
  84. package/dist/release-card.js +252 -0
  85. package/dist/release-carry.d.ts +3 -0
  86. package/dist/release-carry.js +23 -4
  87. package/dist/release-cli.js +36 -3
  88. package/dist/release-git.d.ts +43 -0
  89. package/dist/release-git.js +106 -0
  90. package/dist/release.d.ts +11 -2
  91. package/dist/release.js +82 -6
  92. package/dist/repository-context-ui.js +2 -1
  93. package/dist/run-efficiency.d.ts +3 -3
  94. package/dist/run-efficiency.js +20 -8
  95. package/dist/scope.d.ts +7 -0
  96. package/dist/scope.js +22 -5
  97. package/dist/scout.d.ts +3 -0
  98. package/dist/scout.js +5 -3
  99. package/dist/serve.js +34 -23
  100. package/dist/server/agents-live-lead.d.ts +47 -0
  101. package/dist/server/agents-live-lead.js +116 -0
  102. package/dist/server/agents-live-room.d.ts +23 -0
  103. package/dist/server/agents-live-room.js +88 -0
  104. package/dist/server/chat.js +84 -30
  105. package/dist/server/chrome.js +2 -2
  106. package/dist/server/console.css +68 -40
  107. package/dist/server/flows.js +40 -10
  108. package/dist/server/live.d.ts +4 -0
  109. package/dist/server/live.js +42 -2
  110. package/dist/server/pages.js +81 -1
  111. package/dist/server/render-chat.d.ts +15 -1
  112. package/dist/server/render-chat.js +14 -7
  113. package/dist/server/render-pages.d.ts +8 -4
  114. package/dist/server/render-pages.js +55 -50
  115. package/dist/server/render-settings.js +1 -1
  116. package/dist/server/render-tasks.js +10 -3
  117. package/dist/server/route-table.js +5 -1
  118. package/dist/server/runtime.d.ts +1 -1
  119. package/dist/server/settings.js +29 -8
  120. package/dist/server/task-views.js +1 -1
  121. package/dist/server/tasks.js +59 -41
  122. package/dist/setup-guide.d.ts +2 -2
  123. package/dist/store.d.ts +34 -1
  124. package/dist/store.js +81 -2
  125. package/dist/stream-scope.d.ts +18 -0
  126. package/dist/stream-scope.js +28 -0
  127. package/dist/subagent-memory.d.ts +1 -0
  128. package/dist/subagent-memory.js +1 -1
  129. package/dist/subagent-tools.d.ts +22 -6
  130. package/dist/subagent-tools.js +44 -8
  131. package/dist/subagent-work.d.ts +2 -0
  132. package/dist/subagent-work.js +5 -2
  133. package/dist/subagents-ui.js +6 -4
  134. package/dist/subscription-chat.js +5 -3
  135. package/dist/telegram.d.ts +6 -1
  136. package/dist/telegram.js +58 -6
  137. package/package.json +1 -1
@@ -39,7 +39,7 @@ import { html, htmlString, postForm, reauth } from "../html.js";
39
39
  import { requestContext } from "./request-context.js";
40
40
  import { refuse, screen, whenTime } from "./chrome.js";
41
41
  import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
42
- import { chatAckPage, chatPage, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
42
+ import { chatAckPage, chatPage, NOT_STARTED_SESSION, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
43
43
  import {} from "./render-pages.js";
44
44
  import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
45
45
  import { browserCaller, oneLineUa } from "./session.js";
@@ -54,25 +54,31 @@ export function createChatHandlers(runtime) {
54
54
  async function mateStatus(ctx) {
55
55
  const { url, response, now } = ctx;
56
56
  const who = browserCaller(ctx.who);
57
- const session = store.activeLeadSession(who.name);
57
+ const found = store.activeLeadSession(who.name);
58
58
  const principal = leadPrincipal(who);
59
- if (session === null || principal === null || session.ceilingDigest !== principal.ceilingDigest || session.approverGeneration !== principal.generation) {
59
+ // Quiet page visits: a membership conversation not started yet is still a page to keep current (its task's
60
+ // progress and approval); it reads as session 0, and nothing here starts it.
61
+ const enabled = chatEnablement();
62
+ const notStarted = principal !== null && enabled.ok && enabled.billing === "subscription" && (found === null || found.ceilingDigest !== principal.ceilingDigest);
63
+ const session = notStarted ? null : found;
64
+ if (principal === null || (!notStarted && (session === null || session.ceilingDigest !== principal.ceilingDigest || session.approverGeneration !== principal.generation))) {
60
65
  return respond(response, 200, "application/json", JSON.stringify({ session: null }));
61
66
  }
67
+ const sessionId = session?.id ?? NOT_STARTED_SESSION;
62
68
  store.sweepStaleLeadTurns(now);
63
69
  const request = url.searchParams.get("request") ?? "";
64
70
  // Received: the engine took it, or the lead queue holds it (a queued acknowledgement lost on the way still frees the draft).
65
- const received = /^[a-f0-9]{32}$/.test(request) && store.leadRequestReceived(session.id, request);
71
+ const received = session !== null && /^[a-f0-9]{32}$/.test(request) && store.leadRequestReceived(session.id, request);
66
72
  const requestedTask = url.searchParams.get("task");
67
73
  const focusTask = requestedTask === null || requestedTask === "" ? null : taskChatFocus(requestedTask, now, who, { mintNonce: false });
68
74
  if (requestedTask !== null && requestedTask !== "" && focusTask === null) {
69
75
  // The lens's task is gone or no longer admitted: the page keeps its
70
76
  // draft and says so; it never falls back to the unified thread.
71
- return respond(response, 200, "application/json", JSON.stringify({ session: session.id, task: requestedTask, unavailable: true, received }));
77
+ return respond(response, 200, "application/json", JSON.stringify({ session: sessionId, task: requestedTask, unavailable: true, received }));
72
78
  }
73
79
  const chatProject = focusTask !== null ? null : chatProjectOf(url.searchParams.getAll("project").filter(one => one !== ""));
74
80
  if (chatProject === undefined)
75
- return respond(response, 200, "application/json", JSON.stringify({ session: session.id, unavailable: true, received }));
81
+ return respond(response, 200, "application/json", JSON.stringify({ session: sessionId, unavailable: true, received }));
76
82
  const rows = leadConversationRows(who, principal, focusTask, now, chatProject);
77
83
  const version = leadChatVersion({ ...rows, focusTask });
78
84
  const known = url.searchParams.get("version") ?? "";
@@ -80,12 +86,12 @@ export function createChatHandlers(runtime) {
80
86
  const fragments = known === version
81
87
  ? null
82
88
  : {
83
- thread: htmlString(leadThreadHtml({ ...rows, focusTask, csrf, now, problem: takeLeadNote(csrf, session.id), chatProject })),
89
+ thread: htmlString(leadThreadHtml({ ...rows, focusTask, csrf, now, problem: takeLeadNote(csrf, session?.id ?? null), chatProject })),
84
90
  after: htmlString(leadAfterComposerHtml({ messages: rows.messages, pending: rows.pending, focusTask })),
85
91
  live: focusTask === null ? null : htmlString(taskChatLiveRegion(focusTask, csrf, true, rows.pending !== null)),
86
92
  };
87
93
  return respond(response, 200, "application/json", JSON.stringify({
88
- session: session.id,
94
+ session: sessionId,
89
95
  task: focusTask?.id ?? "",
90
96
  version,
91
97
  pending: rows.pending !== null,
@@ -237,24 +243,22 @@ export function createChatHandlers(runtime) {
237
243
  // The mate (mate arc §5): while a mate session is live, /chat IS the
238
244
  // thread — the same rows the CLI reads. Without one, fleet chat as
239
245
  // before, plus the card that mints a session.
240
- let leadSession = enabled.ok && who.role === "approver" ? store.activeLeadSession(who.name) : null;
246
+ const leadSession = enabled.ok && who.role === "approver" ? store.activeLeadSession(who.name) : null;
241
247
  const principal = enabled.ok && who.role === "approver" ? leadPrincipal(who) : null;
242
248
  // Chat opens ready to talk (2026-09-23): an approver in good standing
243
- // needs no second password to start their own conversation. The one
244
- // exception to "a GET writes nothing": the strict same-site session
245
- // cookie keeps other sites out, and starting spends nothing. Actions
246
- // the lead proposes still go through their own confirmation cards.
247
- // Membership chat only: it spends no dollars. Direct-API chat keeps a
248
- // one-tap Start with its visible spending limit (no password).
249
+ // needs no second password to start their own conversation. Quiet page
250
+ // visits (2026-10-27): a visit only shows it. With no membership
251
+ // conversation yet (or only one under another ceiling), the page shows
252
+ // the composer and the first message starts one (chatSend); nothing
253
+ // here writes. Direct-API chat keeps a one-tap Start with its visible
254
+ // spending limit (no password).
249
255
  const settingsView = url.searchParams.get("settings") === "1";
250
- if (!settingsView && enabled.ok && enabled.billing === "subscription" && principal !== null && (leadSession === null || leadSession.ceilingDigest !== principal.ceilingDigest) && !requestContext.getStore()?.workspaceRead) {
251
- startLeadConversation(who, principal, enabled, 0, false, now);
252
- leadSession = store.activeLeadSession(who.name);
253
- }
256
+ const notStarted = !settingsView && enabled.ok && enabled.billing === "subscription" && principal !== null &&
257
+ (leadSession === null || leadSession.ceilingDigest !== principal.ceilingDigest);
254
258
  // A session under another ceiling is not continuable from here; a GET
255
259
  // writes nothing (slice-2 review, finding 7) — the mint card below
256
260
  // starts a new conversation, and minting ends the old session.
257
- const ceilingStale = enabled.ok && leadSession !== null && principal !== null && leadSession.ceilingDigest !== principal.ceilingDigest;
261
+ const ceilingStale = !notStarted && enabled.ok && leadSession !== null && principal !== null && leadSession.ceilingDigest !== principal.ceilingDigest;
258
262
  const catchUp = leadBriefHtml(assignmentCatchUp(store, now, { principal: "operator", repos }, { limit: 6 }, evidenceRoot));
259
263
  const chatProjects = repos.map((repo, index) => {
260
264
  let peek = null;
@@ -288,11 +292,12 @@ export function createChatHandlers(runtime) {
288
292
  const withFirstRun = (shown) => firstRun === undefined && phone === undefined && home === null ? shown
289
293
  : { ...shown, ...(home === null ? {} : { refreshSeconds: liveRefreshSeconds() }),
290
294
  workspace: { ...shown.workspace, ...(firstRun === undefined ? {} : { firstRun }), ...(phone === undefined ? {} : { phone }), ...(home === null ? {} : { home }) } };
291
- if (enabled.ok && leadSession !== null && principal !== null && !ceilingStale) {
295
+ if (enabled.ok && principal !== null && (notStarted || (leadSession !== null && !ceilingStale))) {
292
296
  {
293
- const said = takeLeadNote(who.session.csrf, leadSession.id);
297
+ const said = takeLeadNote(who.session.csrf, notStarted ? null : leadSession?.id ?? null);
294
298
  return sendScreen(response, 200, withFirstRun(leadPage(chromeFor(null, "chat", undefined, "all"), {
295
- session: leadSession,
299
+ session: notStarted ? null : leadSession,
300
+ approver: who.name,
296
301
  ...leadConversationRows(who, principal, focusTask, now, chatProject),
297
302
  chatProject,
298
303
  latched,
@@ -663,7 +668,9 @@ export function createChatHandlers(runtime) {
663
668
  const body = readForm(posted, CONSOLE_FORMS.leadFollow);
664
669
  if (who.via !== "cookie")
665
670
  return refuse(response, who, 403, "Open the conversation to change automatic updates.");
666
- const principal = leadPrincipal(who), session = store.activeLeadSession(who.name), thread = store.liveLeadThreadFor(who.name);
671
+ const principal = leadPrincipal(who), session = store.activeLeadSession(who.name);
672
+ // A conversation its first message started from a task or project page has no main thread yet: this act opens it.
673
+ const thread = principal && session && session.ceilingDigest === principal.ceilingDigest ? store.openLeadThread(who.name, principal.ceilingDigest, now).thread : null;
667
674
  if (!principal || !session || !thread || !configureLeadFollow(store, principal, session, thread, body.get("enabled") === "yes", now))
668
675
  return refuse(response, who, 409, "Conversation access changed. Start chat again.", "/chat");
669
676
  return redirect(response, safeChatReturn(body.get("return")));
@@ -875,10 +882,52 @@ export function createChatHandlers(runtime) {
875
882
  // A live mate session: the message is a mate turn — no password, the
876
883
  // session's ceremony already covered it (§1); the engine refuses on
877
884
  // its own terms and the thread shows why.
878
- const leadSession = who.role === "approver" ? store.activeLeadSession(who.name) : null;
885
+ let leadSession = who.role === "approver" ? store.activeLeadSession(who.name) : null;
886
+ // Quiet page visits: a membership conversation starts with its first message, never with a page view. A page
887
+ // shown before one existed binds its sends to session 0; that send starts it (the same approver standing a visit
888
+ // used to need), and a retry or double submit of the same request lands in the conversation it started.
889
+ let startedHere = false;
890
+ const binding = body.getAll("request-session");
891
+ const unbound = binding.length === 0 || (binding.length === 1 && binding[0] === String(NOT_STARTED_SESSION));
892
+ if (who.role === "approver" && enabled.billing === "subscription" && unbound) {
893
+ const principal = leadPrincipal(who);
894
+ if (principal === null)
895
+ return wantsJson ? said(403, "your approver standing changed — sign in again") : refuse(response, who, 403, "your approver standing changed — sign in again", back);
896
+ const sentRequest = body.get("request");
897
+ if (sentRequest !== null && (body.getAll("request").length !== 1 || !/^[a-f0-9]{32}$/.test(sentRequest)))
898
+ return said(409, "This conversation changed. Reload it before sending your message.");
899
+ const usable = leadSession !== null && leadSession.ceilingDigest === principal.ceilingDigest && leadSession.approverGeneration === principal.generation;
900
+ // The conversation this very request started, if it started one: a retry or double submit lands only there.
901
+ const startedBy = sentRequest === null ? null : store.leadStartReceipt(who.name, sentRequest);
902
+ if (startedBy !== null) {
903
+ if (!usable || leadSession === null || leadSession.id !== startedBy.session)
904
+ return said(409, "This conversation ended or changed. Reload it before sending your message.", leadSession?.id ?? null);
905
+ startedHere = true;
906
+ }
907
+ else if (usable && leadSession !== null) {
908
+ // A page that showed no conversation never sends into one it did not start.
909
+ if (binding.length === 1)
910
+ return said(409, "This conversation changed. Reload it before sending your message.", leadSession.id);
911
+ }
912
+ else {
913
+ // Nothing starts for a message that would be refused anyway.
914
+ const message = (body.get("message") ?? "").trim();
915
+ if (message === "" || message.length > LEAD_MESSAGE_MAX_CHARS)
916
+ return said(400, `a message is 1 to ${LEAD_MESSAGE_MAX_CHARS} characters`);
917
+ if (scanForSecrets(message).length > 0)
918
+ return said(400, "that looks like a credential — chat never forwards or stores those");
919
+ const scope = chatScopeOf(focusTask, chatProject);
920
+ // The start and its record are one write: a retry finds it, and never starts a second conversation.
921
+ const started = startLeadConversation(who, principal, enabled, 0, false, now, scope, sentRequest ?? undefined);
922
+ leadSession = store.activeLeadSession(who.name);
923
+ if (leadSession === null || leadSession.id !== started)
924
+ return said(409, "This conversation changed. Reload it before sending your message.");
925
+ startedHere = true;
926
+ }
927
+ }
879
928
  if (leadSession !== null) {
880
929
  const sentRequest = body.get("request");
881
- if (sentRequest !== null && (body.getAll("request").length !== 1 || body.getAll("request-session").length !== 1 || body.get("request-session") !== String(leadSession.id))) {
930
+ if (sentRequest !== null && !startedHere && (body.getAll("request").length !== 1 || body.getAll("request-session").length !== 1 || body.get("request-session") !== String(leadSession.id))) {
882
931
  return said(409, "This conversation changed. Reload it before sending your message.");
883
932
  }
884
933
  // Every send has one identity (a form without one gets it here): a message that waits is known by it.
@@ -1136,16 +1185,18 @@ export function createChatHandlers(runtime) {
1136
1185
  const previous = liveTurns.get(thread);
1137
1186
  if (previous?.expiry)
1138
1187
  clearTimeout(previous.expiry);
1139
- const live = { steps: [], done: false, ok: false, listeners: previous?.listeners ?? new Set() };
1188
+ const live = { steps: [], done: false, ok: false, listeners: previous?.listeners ?? new Set(), startedAt: clock().getTime(), stepAt: [] };
1140
1189
  liveTurns.set(thread, live);
1141
1190
  // A page that has this conversation open follows the reply (GET /live, its chat room).
1142
1191
  runtime.liveTurnStarted(thread);
1143
- const current = () => live.steps.at(-1) ?? (live.steps.push({ tools: [], toolCalls: [], text: "" }), live.steps.at(-1));
1192
+ const newStep = () => { live.steps.push({ tools: [], toolCalls: [], text: "" }); live.stepAt.push(clock().getTime()); };
1193
+ const current = () => live.steps.at(-1) ?? (newStep(), live.steps.at(-1));
1144
1194
  const progress = (event) => {
1145
1195
  if (live.done)
1146
1196
  return;
1197
+ live.turn ??= event.turn;
1147
1198
  if (event.kind === "step")
1148
- live.steps.push({ tools: [], toolCalls: [], text: "" });
1199
+ newStep();
1149
1200
  else if (event.kind === "tool") {
1150
1201
  current().tools.push(event.label);
1151
1202
  current().toolCalls.push({ id: event.id, label: event.label, state: "running" });
@@ -1159,8 +1210,10 @@ export function createChatHandlers(runtime) {
1159
1210
  }
1160
1211
  }
1161
1212
  }
1162
- else if (event.kind === "text")
1213
+ else if (event.kind === "text") {
1163
1214
  current().text = event.text;
1215
+ (live.confirmed ??= [])[live.steps.length - 1] = event.final === true;
1216
+ }
1164
1217
  for (const listener of live.listeners)
1165
1218
  listener();
1166
1219
  };
@@ -1175,6 +1228,7 @@ export function createChatHandlers(runtime) {
1175
1228
  return;
1176
1229
  live.done = true;
1177
1230
  live.ok = ok;
1231
+ live.endedAt = clock().getTime();
1178
1232
  for (const listener of live.listeners)
1179
1233
  listener();
1180
1234
  live.expiry = setTimeout(() => { if (liveTurns.get(thread) === live)
@@ -396,7 +396,7 @@ export function chromeScript() {
396
396
  `if(ev.key==="?"){toggleHelp();ev.preventDefault();return;}` +
397
397
  `if(ev.key==="j"||ev.key==="k"){rove(ev.key==="j"?1:-1,ev);return;}` +
398
398
  `if(pending==="g"){pending=null;` +
399
- `var map={b:"/board",i:"/",w:"/workbench",d:"/done",q:"/board?view=order",f:"/fleet",t:"/tasks",a:"/activity",p:"/projects"};` +
399
+ `var map={b:"/board",i:"/",w:"/workbench",d:"/done",q:"/board?view=order",f:"/fleet",t:"/work",a:"/activity",p:"/projects"};` +
400
400
  `if(map[ev.key]){go(map[ev.key]);ev.preventDefault();}return;}` +
401
401
  `if(ev.key==="g"){pending="g";setTimeout(function(){pending=null;},800);}});` +
402
402
  `})();`);
@@ -717,7 +717,7 @@ export function workToolRows(scoped = false, _offersChat = false, offersCode = f
717
717
  ...(offersCode ? [{ key: "code", href: "/term", label: "Live", hint: "your own terminals: Claude Code, Codex or a shell" }] : []),
718
718
  { key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
719
719
  { key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
720
- { key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
720
+ { key: "tasks", href: "/work", label: "Task list", hint: "everything, filterable by state" },
721
721
  { key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
722
722
  { key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
723
723
  { key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
@@ -172,6 +172,30 @@
172
172
  color: color-mix(in srgb, var(--running) 72%, var(--foreground)); border-color: var(--border);
173
173
  }
174
174
  .badge-cut { background: var(--muted); }
175
+ /* The status chip (components/ui/badge.tsx `size="chip"`, after ReUI's light badge with a dot): 20px, 6px
176
+ corners, a soft wash, 11.5px words and a 6px dot. Needs you is the amber wash with its words leaned toward ink
177
+ (4.5:1); the dot is decoration and the word carries the meaning. */
178
+ .chip {
179
+ display: inline-flex; align-items: center; gap: .375rem; height: 1.25rem; padding: 0 .375rem; border-radius: var(--so-radius-sm);
180
+ font-family: var(--font-sans); font-size: .71875rem; font-weight: 500; line-height: 1; white-space: nowrap; vertical-align: middle;
181
+ background: var(--so-neutral-soft); color: var(--so-ink);
182
+ }
183
+ /* Ink words, the tone in the dot (the Agents → Live chip); Needs you keeps amber words leaned toward ink. */
184
+ .chip-dot { width: .375rem; height: .375rem; flex-shrink: 0; border-radius: var(--so-radius-full); background: var(--so-neutral-ink); }
185
+ .chip-needs { background: var(--so-warning-soft); color: color-mix(in oklab, var(--so-warning) 85%, var(--so-ink)); }
186
+ .chip-needs .chip-dot { background: var(--so-warning); }
187
+ .chip-live { background: var(--so-info-soft); }
188
+ .chip-live .chip-dot { background: var(--so-info); }
189
+ .chip-done { background: var(--so-success-soft); }
190
+ .chip-done .chip-dot { background: var(--so-success); }
191
+ .chip-failed { background: var(--so-danger-soft); }
192
+ .chip-failed .chip-dot { background: var(--so-danger); }
193
+ /* The frame (components/ui/frame.tsx, after ReUI's Frame): a raised well with a hairline, its heading on the
194
+ well's fill, and paper panels whose corners sit concentric inside it. No shadow on a panel. */
195
+ .frame { display: flex; flex-direction: column; gap: .25rem; padding: .25rem; border: 1px solid var(--so-line); border-radius: var(--so-radius-card); background: var(--so-raised); }
196
+ .frame-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; min-height: 2rem; padding: .25rem .625rem; }
197
+ .frame-panel { position: relative; border: 1px solid var(--so-line); border-radius: calc(var(--so-radius-card) - .25rem - 1px); background: var(--so-paper); padding: .75rem .875rem; }
198
+ .frame-panel.waits { border-color: var(--so-warning); }
175
199
 
176
200
  .card {
177
201
  border: 1px solid var(--glass-border); border-radius: var(--radius); background: var(--glass);
@@ -1749,13 +1773,13 @@
1749
1773
  says which way it folds. */
1750
1774
  @media (max-width: 760px) {
1751
1775
  .board { display: flex; flex-direction: column; align-items: stretch; gap: .5rem; padding-bottom: 0; }
1752
- .board .lane { min-height: 0; padding: 0 .625rem .125rem; }
1776
+ .board .lane { min-height: 0; padding: 0 .25rem .25rem; }
1753
1777
  .board .lane > summary {
1754
1778
  cursor: pointer; display: flex; align-items: center; min-height: 2.75rem; margin: 0;
1755
1779
  -webkit-tap-highlight-color: transparent;
1756
1780
  }
1757
- .board .lane > summary h2 { flex: 1; font-size: .75rem; }
1758
- .board .lane > summary .lane-count { border: 1px solid var(--border); border-radius: 999px; padding: .04rem .5rem; margin-left: .25rem; }
1781
+ .board .lane > summary { padding: 0 .375rem; }
1782
+ .board .lane > summary h2 { flex: 1; }
1759
1783
  .board .lane > summary::after {
1760
1784
  content: ""; width: .5rem; height: .5rem; flex: none; margin-right: .25rem;
1761
1785
  border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);
@@ -1763,63 +1787,66 @@
1763
1787
  }
1764
1788
  .board .lane[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }
1765
1789
  .board .lane .hint { margin-bottom: .25rem; }
1766
- .board .lane .lane-card:last-of-type { margin-bottom: .5rem; }
1790
+ .board .lane .lane-card:last-of-type { margin-bottom: 0; }
1767
1791
  .board .lane-attention { order: 0; }
1768
1792
  .board .lane-building { order: 1; }
1769
1793
  .board .lane-queued { order: 2; }
1770
1794
  .board .lane-waiting { order: 3; }
1771
1795
  .board .lane-done { order: 4; }
1772
1796
  }
1797
+ /* A lane is a frame (components/ui/frame.tsx, after ReUI's Frame): a raised well with a hairline, its head a
1798
+ status chip and a mono count on the well's fill, its cards paper panels at concentric corners. Only the lane
1799
+ that waits on a person is amber: its chip, and each card's thin full outline. No fill, no stripe, no shadow. */
1773
1800
  .lane {
1774
- background: color-mix(in srgb, var(--card) 45%, var(--background)); border: 1px solid var(--border);
1775
- border-radius: var(--radius); padding: .625rem; min-height: 12rem;
1801
+ background: var(--so-raised); border: 1px solid var(--so-line);
1802
+ border-radius: var(--so-radius-card); padding: .25rem; min-height: 12rem;
1776
1803
  }
1777
- .lane > summary { list-style: none; cursor: default; margin: 0 0 .125rem; }
1804
+ .lane > summary { list-style: none; cursor: default; margin: 0; padding: .375rem .375rem .125rem; }
1778
1805
  .lane > summary::-webkit-details-marker { display: none; }
1779
- .lane h2 { display: flex; align-items: center; gap: .4rem; margin: 0; font-size: .6875rem; }
1806
+ .lane h2 { display: flex; align-items: center; gap: .5rem; margin: 0; font-size: .8125rem; font-weight: 600; }
1780
1807
  .lane h2 a { color: inherit; text-decoration: none; }
1781
1808
  @media (hover: hover) and (pointer: fine) { .lane h2 a:hover { text-decoration: underline; } }
1782
- .lane .hint { margin-top: 0; }
1783
- .lane-count { color: var(--muted-foreground); font-weight: 400; font-variant-numeric: tabular-nums; }
1784
- .lane h2::before {
1785
- content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full); flex: none;
1786
- background: var(--muted-foreground); opacity: .55;
1787
- }
1788
- .lane-attention h2::before { background: var(--brand); opacity: 1; }
1789
- .lane-building h2::before { background: var(--running); opacity: 1; }
1790
- .lane-done h2::before { background: var(--muted-foreground); opacity: 1; }
1809
+ .lane .hint { margin: 0; padding: .125rem .375rem .375rem; }
1810
+ .lane-count { color: var(--muted-foreground); font-family: var(--font-mono); font-size: .6875rem; font-weight: 500; font-variant-numeric: tabular-nums; }
1791
1811
  .lane-card {
1792
- display: block; text-decoration: none; color: inherit;
1793
- background: var(--card); border: 1px solid var(--border);
1794
- border-radius: calc(var(--radius) - 2px); padding: .5rem .625rem; margin-top: .5rem;
1795
- }
1796
- @media (hover: hover) and (pointer: fine) { .lane-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); } }
1797
- .lane-card .id { display: block; font-family: var(--font-mono); font-size: .6875rem; color: var(--muted-foreground); margin-bottom: .125rem; overflow-wrap: anywhere; }
1798
- .lane-card .t { display: block; font-size: .8125rem; font-weight: 500; line-height: 1.35; }
1812
+ display: flex; flex-direction: column; text-decoration: none; color: inherit;
1813
+ background: var(--so-paper); border: 1px solid var(--so-line);
1814
+ border-radius: calc(var(--so-radius-card) - .25rem - 1px); padding: .625rem .75rem; margin-top: .25rem;
1815
+ }
1816
+ .lane-attention .lane-card { border-color: var(--so-warning); }
1817
+ /* The order view's columns are frames too, with roomier insides (a form, a note) and a quiet dot by the name. */
1818
+ .lane.queue-column { padding: .5rem; }
1819
+ .queue-column > h2::before { content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full); flex: none; background: var(--muted-foreground); opacity: .55; }
1820
+ @media (hover: hover) and (pointer: fine) { .lane-card:hover { background: var(--so-raised); } }
1821
+ /* The title leads; the task id is a machine fact under it, in mono. */
1822
+ .lane-card .t { display: block; font-size: .84375rem; font-weight: 500; line-height: 1.35; }
1823
+ .lane-card .why { display: block; margin-top: .125rem; font-size: .75rem; color: var(--muted-foreground); }
1824
+ .lane-card .id { display: block; margin-top: .375rem; font-family: var(--font-mono); font-size: .75rem; color: var(--muted-foreground); overflow-wrap: anywhere; }
1825
+ .lane-card .who { display: block; margin-top: .125rem; font-size: .75rem; color: var(--muted-foreground); overflow-wrap: anywhere; }
1826
+ .lane-card .who .mono { display: inline; margin: 0; font-family: var(--font-mono); font-size: .6875rem; }
1799
1827
  .lane-card .dot { margin-right: .4rem; }
1800
1828
  .lane-card .meta, .lane-card .mono { display: block; margin-top: .125rem; font-size: .75rem; }
1801
- .lane-card .why { display: block; margin-top: .125rem; font-size: .75rem; color: var(--muted-foreground); }
1802
- /* The facts: mono key–value pairs, keys dim, values ink — one grammar on every lane. */
1803
- .lane-card .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .0625rem .625rem; margin-top: .4rem; font-size: .6875rem; line-height: 1.5; }
1829
+ /* The facts: a muted word, then the value; only a machine's own words (a name, a model, a branch, a clock) are mono. */
1830
+ .lane-card .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .0625rem .625rem; margin-top: .25rem; font-size: .75rem; line-height: 1.5; }
1804
1831
  .lane-card .fact { display: contents; }
1805
- .lane-card .fact .k { color: var(--muted-foreground); font-family: var(--font-mono); }
1806
- .lane-card .fact .v { font-family: var(--font-mono); color: var(--foreground); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
1807
- .lane-card .chips { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .4rem; }
1832
+ .lane-card .fact .k { color: var(--muted-foreground); }
1833
+ .lane-card .fact .v { color: var(--foreground); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
1834
+ .lane-card .fact .v.mono { display: inline; margin: 0; font-family: var(--font-mono); font-size: .6875rem; }
1835
+ .lane-card .chips { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .5rem; }
1808
1836
  .lane-card .chips .badge { margin: 0; }
1809
- /* The live strip on a building card: stage and clock in an inset well —
1810
- the run's own facts, never a percent. */
1837
+ /* The live strip on a building card: the stage in words, the clock in mono, on a sunken well. */
1811
1838
  .lane-card .live-line {
1812
1839
  display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .375rem;
1813
- padding: .3rem .5rem; border-radius: calc(var(--radius) - 4px); background: var(--muted);
1814
- font-family: var(--font-mono); font-size: .6875rem; color: var(--running); font-variant-numeric: tabular-nums;
1840
+ padding: .3rem .5rem; border-radius: var(--so-radius-sm); background: var(--so-raised);
1841
+ font-size: .75rem; color: var(--so-info);
1815
1842
  }
1816
- .lane-card .live-line .clock { color: var(--muted-foreground); }
1843
+ .lane-card .live-line .clock { color: var(--muted-foreground); font-family: var(--font-mono); font-size: .6875rem; font-variant-numeric: tabular-nums; }
1817
1844
  /* The build's steps — set up, writing, checking and committing: three quiet
1818
1845
  ticks, the current one in the running hue. A stage, never a percent. */
1819
1846
  .lane-card .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .1875rem; margin-top: .5rem; }
1820
- .lane-card .steps i { height: .1875rem; border-radius: 999px; background: var(--border); }
1821
- .lane-card .steps i.on { background: color-mix(in srgb, var(--running) 45%, var(--border)); }
1822
- .lane-card .steps i.now { background: var(--running); }
1847
+ .lane-card .steps i { height: .1875rem; border-radius: var(--so-radius-full); background: var(--so-line); }
1848
+ .lane-card .steps i.on { background: color-mix(in srgb, var(--so-info) 45%, var(--so-line)); }
1849
+ .lane-card .steps i.now { background: var(--so-info); }
1823
1850
 
1824
1851
  /* The worker strip: one quiet tile per worker above the lanes — what it is
1825
1852
  building here, the stage, the clock. Ink and lines only; the running dot
@@ -2613,8 +2640,9 @@ button { min-height: 44px; }
2613
2640
  .chat-workspace.projects-open::before { animation: fade 180ms ease-out; }
2614
2641
  .switcher[open] .switcher-menu { animation: rise 160ms ease-out; }
2615
2642
  @media (hover: hover) and (pointer: fine) {
2616
- .lane-card, .decide-card, .menu-row, .chat-overview-item, .proposal { transition: border-color .15s, transform .15s, box-shadow .15s; }
2617
- .lane-card:hover, .decide-card:hover, .menu-row:hover {
2643
+ .lane-card, .decide-card, .menu-row, .chat-overview-item, .proposal { transition: border-color .15s, background-color .15s, transform .15s, box-shadow .15s; }
2644
+ /* Board cards are panels in a frame: a hover fills them raised, never a shadow. */
2645
+ .decide-card:hover, .menu-row:hover {
2618
2646
 
2619
2647
  box-shadow: 0 2px 8px -2px rgb(0 0 0 / .35);
2620
2648
  }
@@ -12,13 +12,13 @@ import { run as execRun } from "../exec.js";
12
12
  import { setFlowSecret } from "../flow-actions.js";
13
13
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowDefinitionOf, moveCardInFlow } from "../flow-engine.js";
14
14
  import { galleryHtml, galleryUseHtml } from "../flow-gallery-ui.js";
15
- import { galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
15
+ import { galleryAdmitsAnyone, galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
16
16
  import { flowInsights } from "../flow-insights.js";
17
17
  import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people.js";
18
18
  import { saveScript } from "../flow-scripts.js";
19
19
  import { chooseFlowCard } from "../flow-send.js";
20
20
  import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
21
- import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries } from "../flow-triggers.js";
21
+ import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries, rawAdmitsAnyone } from "../flow-triggers.js";
22
22
  import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
23
23
  import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
24
24
  import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
@@ -32,7 +32,7 @@ import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWo
32
32
  import { isAlive as runnerAlive } from "../runner.js";
33
33
  import { createSubagentFrom, labelOf, nameOf, saveSoul, sendSubagentSummaries, setSubagentState, subagentSettings } from "../subagent-admin.js";
34
34
  import { addRoutine, removeRoutine, runRoutine } from "../subagent-desk.js";
35
- import { editMemory, forgetMemory, tellSubagent } from "../subagent-memory.js";
35
+ import { ACCEPT as ACCEPT_SUGGESTION, editMemory, forgetMemory, tellSubagent } from "../subagent-memory.js";
36
36
  import { grantTool, revokeTool, rulesFromForm, setToolRules } from "../subagent-tools.js";
37
37
  import { sendSubagentWeeklies, undoCall } from "../subagent-week.js";
38
38
  import { answerSubagentQuestion } from "../subagent-work.js";
@@ -301,11 +301,16 @@ export function createFlowsHandlers(runtime) {
301
301
  return redirect(response, `/kits?problem=${encodeURIComponent("Choose one of your projects.")}`);
302
302
  const page = (key, words) => redirect(response, `/kits/${kit.id}?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
303
303
  if (step === "setup") {
304
- const made = await setUpKit(store, kit, repo, who.name, now, options.configDir ?? null, { toolHome });
304
+ // Its tools come with it only on the person's proof (a password, or the session under Passwordless): a grant widens.
305
+ const proved = authenticateApprover(who, body.get("password") ?? "", repo).ok;
306
+ const made = await setUpKit(store, kit, repo, who.name, now, options.configDir ?? null, { toolHome }, proved ? { widening: "proved" } : {});
305
307
  return page(made.ok ? "said" : "problem", made.said);
306
308
  }
307
309
  if (step === "github") {
308
- const made = addKitGithubTrigger(store, kit, repo, who.name, now, options.configDir ?? null);
310
+ // Issues from anyone on GitHub come into a flow: a widening, so it takes a password (chat-management.md §0a.3).
311
+ if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
312
+ return page("problem", "Enter your Toolroll password to bring in GitHub issues. Nothing changed.");
313
+ const made = addKitGithubTrigger(store, kit, repo, who.name, now, options.configDir ?? null, "proved");
309
314
  return page(made.ok ? "said" : "problem", made.said);
310
315
  }
311
316
  const tried = addKitSample(store, kit, repo, who.name, now);
@@ -330,7 +335,10 @@ export function createFlowsHandlers(runtime) {
330
335
  const card = question === null ? null : store.getFlowCard(question.card);
331
336
  const flow = card === null ? null : store.getFlow(card.flow);
332
337
  const done = question === null || flow === null || !visible(flow.repo) ? { ok: false, said: "No such question." }
333
- : answerSubagentQuestion(store, question.id, { choice: body.get("choice"), text: body.get("text"), by: who.name, via: "web" }, now);
338
+ // Accepting a looser rule it suggested takes the password (chat-management.md §0a.3); every other answer doesn't.
339
+ : question.suggestion !== null && body.get("choice") === ACCEPT_SUGGESTION && !authenticateApprover(who, body.get("password") ?? "").ok
340
+ ? { ok: false, said: "Enter your Toolroll password to change the rule." }
341
+ : answerSubagentQuestion(store, question.id, { choice: body.get("choice"), text: body.get("text"), by: who.name, via: "web", ...(question.suggestion !== null ? { widening: "proved" } : {}) }, now);
334
342
  if (wantsJson)
335
343
  return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
336
344
  const back = question === null ? "/settings/lead/subagents" : `/settings/lead/subagents/${question.subagent}`;
@@ -403,9 +411,23 @@ export function createFlowsHandlers(runtime) {
403
411
  // v94: which project tools it may use, and its rule for each action.
404
412
  const tool = body.get("tool") ?? "", op = body.get("op");
405
413
  const grant = store.subagentGrant(mate.id, tool);
406
- const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome })
414
+ // Giving a subagent a tool widens what it does unasked: a password (chat-management.md §0a.3). Revoking and
415
+ // tightening a rule don't need one.
416
+ if (op === "grant" && !authenticateApprover(who, body.get("password") ?? "", mate.repo).ok) {
417
+ return redirect(response, `${back}?problem=${encodeURIComponent(`Enter your Toolroll password to let ${nameOf(mate)} use ${tool}. Nothing changed.`)}#tools`);
418
+ }
419
+ const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome }, { widening: "proved" })
407
420
  : op === "revoke" ? revokeTool(store, mate, tool, who.name, now)
408
- : grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : setToolRules(store, mate, tool, rulesFromForm(grant, key => body.sent.get(key)), who.name, now);
421
+ : grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : (() => {
422
+ // Loosening a rule widens what it does unasked: a password (chat-management.md §0a.3); tightening needs none.
423
+ const changes = rulesFromForm(grant, key => body.sent.get(key));
424
+ const tried = setToolRules(store, mate, tool, changes, who.name, now);
425
+ if (tried.ok || !/takes your password/.test(tried.said))
426
+ return tried;
427
+ if (!authenticateApprover(who, body.get("password") ?? "", mate.repo).ok)
428
+ return { ok: false, said: `Enter your Toolroll password to let ${nameOf(mate)} do more on its own. Nothing changed.` };
429
+ return setToolRules(store, mate, tool, changes, who.name, now, { widening: "proved" });
430
+ })();
409
431
  return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
410
432
  }
411
433
  const done = part === "state" ? setSubagentState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
@@ -445,7 +467,11 @@ export function createFlowsHandlers(runtime) {
445
467
  }
446
468
  if (digest === null || digest !== body.get("previewed"))
447
469
  return sendGalleryUse(response, who, template, projects, repo, answers, name, digest === null ? null : "Your answers changed. Check the preview, then create the flow.");
448
- const used = useGalleryTemplate(store, template, repo, answers, { name, by: who.name, now: clock(), dir: options.configDir ?? null });
470
+ // A template whose trigger brings in items from anyone takes the password (chat-management.md §0a.3).
471
+ const anyone = galleryAdmitsAnyone(template);
472
+ if (anyone && !authenticateApprover(who, body.get("password") ?? "", repo).ok)
473
+ return sendGalleryUse(response, who, template, projects, repo, answers, name, "Enter your Toolroll password: this flow brings in items from anyone.");
474
+ const used = useGalleryTemplate(store, template, repo, answers, { name, by: who.name, now: clock(), dir: options.configDir ?? null, ...(anyone ? { widening: "proved" } : {}) });
449
475
  if (!used.ok)
450
476
  return sendGalleryUse(response, who, template, projects, repo, answers, name, used.said);
451
477
  return redirect(response, `/flows/${used.flow}`);
@@ -576,7 +602,11 @@ export function createFlowsHandlers(runtime) {
576
602
  const ingress = rawTakesDeliveries(raw);
577
603
  if (ingress && !authenticateCredential(who, body.get("password") ?? "").ok)
578
604
  return answer(403, { ok: false, said: "Enter your Toolroll password to make a webhook address." });
579
- const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir, ingress ? { ingress: "proved" } : {});
605
+ // Items from anyone: a widening, an ordinary step-up (chat-management.md §0a.3).
606
+ const anyone = !ingress && rawAdmitsAnyone(raw);
607
+ if (anyone && !authenticateApprover(who, body.get("password") ?? "", flow.repo).ok)
608
+ return answer(403, { ok: false, said: "Enter your Toolroll password to bring in items from anyone." });
609
+ const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir, ingress ? { ingress: "proved" } : anyone ? { widening: "proved" } : {});
580
610
  if (made.ok)
581
611
  await prepareUnattendedCandidates(store, flow.id);
582
612
  return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
@@ -45,6 +45,10 @@ export type LiveRoom = {
45
45
  } | {
46
46
  kind: "agent";
47
47
  run: number;
48
+ }
49
+ /** The Agents view's Live grid (agents-live-room.ts): per viewer, watching only. */
50
+ | {
51
+ kind: "agents-live";
48
52
  };
49
53
  /** A room's name, read strictly: anything else is not a room. */
50
54
  export declare function parseLiveRoom(name: string): LiveRoom | null;