toolroll 0.9.55 → 0.9.57

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 (223) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +24 -8
  3. package/dist/action-ledger.d.ts +2 -4
  4. package/dist/action-ledger.js +2 -10
  5. package/dist/approval-policy.d.ts +5 -5
  6. package/dist/approval-policy.js +4 -4
  7. package/dist/approval-rules-ui.js +2 -2
  8. package/dist/assignment-adapters.d.ts +5 -0
  9. package/dist/assignment.d.ts +24 -3
  10. package/dist/assignment.js +36 -10
  11. package/dist/browser/workspace.js +5 -5
  12. package/dist/browser-workspace.d.ts +12 -12
  13. package/dist/browser-workspace.js +1 -1
  14. package/dist/budget-alerts.js +3 -3
  15. package/dist/chat-actions.d.ts +23 -18
  16. package/dist/chat-actions.js +110 -88
  17. package/dist/chat-ask.d.ts +2 -2
  18. package/dist/chat-ask.js +5 -5
  19. package/dist/chat-channel.d.ts +15 -15
  20. package/dist/chat-channel.js +20 -20
  21. package/dist/chat-controls.d.ts +0 -4
  22. package/dist/chat-controls.js +0 -1
  23. package/dist/chat-core.js +1 -1
  24. package/dist/chat-delivery-state.js +2 -2
  25. package/dist/chat-delivery.d.ts +2 -2
  26. package/dist/chat-delivery.js +30 -37
  27. package/dist/chat-flow.js +3 -3
  28. package/dist/chat-messages.d.ts +1 -1
  29. package/dist/chat-messages.js +4 -4
  30. package/dist/chat-migration.d.ts +1 -1
  31. package/dist/chat-migration.js +11 -11
  32. package/dist/chat-rooms.js +3 -3
  33. package/dist/chat-voice.js +1 -1
  34. package/dist/chat-warmth.d.ts +2 -2
  35. package/dist/check-levels.d.ts +3 -0
  36. package/dist/check-levels.js +5 -0
  37. package/dist/cli.d.ts +1 -1
  38. package/dist/cli.js +6 -7
  39. package/dist/contracts/chat-actions.d.ts +32 -27
  40. package/dist/contracts/chat-actions.js +29 -11
  41. package/dist/contracts/chat-content.d.ts +1 -1
  42. package/dist/contracts/chat-content.js +1 -1
  43. package/dist/contracts/chat-tables.js +4 -4
  44. package/dist/contracts/console-api.d.ts +6 -6
  45. package/dist/contracts/console-api.js +7 -7
  46. package/dist/contracts/flow-propose.d.ts +3 -3
  47. package/dist/contracts/flow.d.ts +132 -125
  48. package/dist/contracts/flow.js +27 -12
  49. package/dist/contracts/lead-context.d.ts +1 -1
  50. package/dist/contracts/lead-context.js +2 -2
  51. package/dist/contracts/lead-tools.d.ts +20 -18
  52. package/dist/contracts/lead-tools.js +15 -15
  53. package/dist/contracts/stage-output.d.ts +1 -1
  54. package/dist/contracts/stage-output.js +1 -1
  55. package/dist/contracts/store-json.d.ts +23 -22
  56. package/dist/contracts/store-json.js +17 -17
  57. package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
  58. package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
  59. package/dist/converse.d.ts +23 -23
  60. package/dist/converse.js +28 -28
  61. package/dist/coordinator-proposals.d.ts +2 -2
  62. package/dist/coordinator-proposals.js +1 -1
  63. package/dist/coordinator.js +1 -1
  64. package/dist/deprecations.d.ts +12 -0
  65. package/dist/deprecations.js +20 -0
  66. package/dist/desktop-update-gate.js +8 -4
  67. package/dist/desktop-update.js +6 -1
  68. package/dist/discord-chat.js +6 -6
  69. package/dist/dispatch.d.ts +1 -1
  70. package/dist/dispatch.js +1 -1
  71. package/dist/dispose.js +0 -5
  72. package/dist/envelope.js +0 -1
  73. package/dist/export-ui.js +1 -1
  74. package/dist/export.js +7 -7
  75. package/dist/flow-engine.d.ts +2 -2
  76. package/dist/flow-engine.js +21 -21
  77. package/dist/flow-insights.d.ts +1 -1
  78. package/dist/flow-share.js +13 -8
  79. package/dist/flow-steps.d.ts +5 -5
  80. package/dist/flow-steps.js +19 -19
  81. package/dist/flow-triggers.d.ts +1 -1
  82. package/dist/flow-triggers.js +1 -1
  83. package/dist/flows-cli.js +1 -1
  84. package/dist/flows-ui.js +20 -20
  85. package/dist/flows.js +19 -19
  86. package/dist/guides.js +4 -3
  87. package/dist/invoke.js +1 -1
  88. package/dist/kits-ui.js +5 -5
  89. package/dist/kits.d.ts +12 -12
  90. package/dist/kits.js +31 -31
  91. package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
  92. package/dist/{mate-cli.js → lead-cli.js} +38 -38
  93. package/dist/lead-commitments.js +4 -4
  94. package/dist/lead-context.d.ts +2 -2
  95. package/dist/lead-context.js +9 -9
  96. package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
  97. package/dist/{mate-contract.js → lead-contract.js} +7 -6
  98. package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
  99. package/dist/{mate-doors.js → lead-doors.js} +24 -24
  100. package/dist/lead-follow.d.ts +5 -5
  101. package/dist/lead-follow.js +9 -9
  102. package/dist/lead-people.d.ts +5 -5
  103. package/dist/lead-people.js +14 -14
  104. package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
  105. package/dist/{mate-progress.js → lead-progress.js} +3 -3
  106. package/dist/lead-status.d.ts +1 -1
  107. package/dist/lead-status.js +2 -2
  108. package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
  109. package/dist/{mate-tools.js → lead-tools.js} +109 -101
  110. package/dist/{mate.d.ts → lead.d.ts} +27 -27
  111. package/dist/{mate.js → lead.js} +102 -102
  112. package/dist/limits-ui.d.ts +1 -1
  113. package/dist/limits-ui.js +1 -1
  114. package/dist/mcp-connect.d.ts +1 -1
  115. package/dist/mcp-connect.js +3 -3
  116. package/dist/mcp-core.js +1 -1
  117. package/dist/memory-pass.d.ts +2 -2
  118. package/dist/memory-pass.js +7 -7
  119. package/dist/notification-rows.d.ts +2 -2
  120. package/dist/operate.d.ts +3 -3
  121. package/dist/operate.js +52 -42
  122. package/dist/policy.d.ts +1 -1
  123. package/dist/principal.d.ts +1 -1
  124. package/dist/principal.js +2 -2
  125. package/dist/project-delete-ui.js +1 -1
  126. package/dist/project-delete.d.ts +1 -1
  127. package/dist/project-delete.js +26 -26
  128. package/dist/project-memory.js +1 -1
  129. package/dist/project-tools.d.ts +1 -1
  130. package/dist/project-tools.js +1 -1
  131. package/dist/proof.d.ts +9 -0
  132. package/dist/proof.js +12 -0
  133. package/dist/recipe-ui.js +1 -1
  134. package/dist/release-coverage.d.ts +16 -0
  135. package/dist/release-coverage.js +92 -0
  136. package/dist/retention.js +4 -4
  137. package/dist/scope.d.ts +2 -2
  138. package/dist/scout-report.d.ts +1 -1
  139. package/dist/scout-report.js +1 -1
  140. package/dist/serve.js +40 -45
  141. package/dist/server/chat.d.ts +4 -4
  142. package/dist/server/chat.js +105 -103
  143. package/dist/server/chrome.js +3 -3
  144. package/dist/server/flows.js +69 -62
  145. package/dist/server/guards.js +3 -3
  146. package/dist/server/http.d.ts +3 -3
  147. package/dist/server/http.js +1 -1
  148. package/dist/server/live.js +6 -6
  149. package/dist/server/people-tokens.js +1 -1
  150. package/dist/server/remote.js +12 -10
  151. package/dist/server/render-chat.d.ts +30 -30
  152. package/dist/server/render-chat.js +33 -33
  153. package/dist/server/render-pages.js +2 -2
  154. package/dist/server/render-results.d.ts +4 -1
  155. package/dist/server/render-results.js +23 -9
  156. package/dist/server/render-settings.d.ts +7 -0
  157. package/dist/server/render-settings.js +3 -0
  158. package/dist/server/route-table.js +9 -7
  159. package/dist/server/runtime.d.ts +13 -13
  160. package/dist/server/settings.js +14 -13
  161. package/dist/server/tasks.js +34 -30
  162. package/dist/setup-guide.d.ts +2 -2
  163. package/dist/slack-chat.d.ts +2 -2
  164. package/dist/slack-chat.js +6 -6
  165. package/dist/spend-ui.d.ts +3 -3
  166. package/dist/spend-ui.js +9 -9
  167. package/dist/spend.d.ts +14 -14
  168. package/dist/spend.js +31 -31
  169. package/dist/store-baseline.d.ts +19 -0
  170. package/dist/store-baseline.js +2873 -0
  171. package/dist/store.d.ts +200 -254
  172. package/dist/store.js +830 -3152
  173. package/dist/subagent-admin.d.ts +53 -0
  174. package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
  175. package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
  176. package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
  177. package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
  178. package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
  179. package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
  180. package/dist/{teammate-question.js → subagent-question.js} +17 -17
  181. package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
  182. package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
  183. package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
  184. package/dist/{teammate-week.js → subagent-week.js} +23 -23
  185. package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
  186. package/dist/{teammate-work.js → subagent-work.js} +50 -50
  187. package/dist/subagents-ui.d.ts +22 -0
  188. package/dist/{teammates-ui.js → subagents-ui.js} +45 -45
  189. package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
  190. package/dist/{teammates.js → subagents.js} +21 -21
  191. package/dist/subscription-chat.d.ts +13 -13
  192. package/dist/subscription-chat.js +19 -19
  193. package/dist/surface.d.ts +19 -2
  194. package/dist/surface.js +7 -31
  195. package/dist/task-outcome-cli.js +3 -1
  196. package/dist/task-status.d.ts +59 -7
  197. package/dist/task-status.js +132 -16
  198. package/dist/team-cli.d.ts +1 -0
  199. package/dist/team-cli.js +14 -2
  200. package/dist/team-leads.js +13 -13
  201. package/dist/team-runtime.d.ts +2 -2
  202. package/dist/team-runtime.js +9 -9
  203. package/dist/teams-chat.js +6 -6
  204. package/dist/telegram-flow.d.ts +3 -3
  205. package/dist/telegram-flow.js +3 -3
  206. package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
  207. package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
  208. package/dist/telegram-status.d.ts +11 -4
  209. package/dist/telegram-status.js +18 -11
  210. package/dist/telegram-team.d.ts +1 -1
  211. package/dist/telegram-team.js +5 -5
  212. package/dist/telegram.d.ts +1 -1
  213. package/dist/telegram.js +13 -34
  214. package/dist/text-limits.d.ts +18 -18
  215. package/dist/text-limits.js +18 -18
  216. package/dist/toolroll-update.d.ts +8 -0
  217. package/dist/toolroll-update.js +45 -8
  218. package/dist/work-index.d.ts +6 -1
  219. package/dist/work-index.js +50 -8
  220. package/dist/workspace-ui.js +2 -1
  221. package/package.json +1 -2
  222. package/dist/teammate-admin.d.ts +0 -53
  223. package/dist/teammates-ui.d.ts +0 -21
@@ -12,15 +12,15 @@ import { isSubscriptionChatProvider } from "../converse.js";
12
12
  import {} from "../dispatch.js";
13
13
  import { firstTaskJourney, START_COMMAND } from "../first-run.js";
14
14
  import { FLOW_HREF } from "../flow-engine.js";
15
- import { PROPOSAL_WAIT_REASON, proposalActGate } from "../mate-doors.js";
16
- import {} from "../mate-progress.js";
17
- import { MATE_MESSAGE_MAX_CHARS } from "../mate.js";
15
+ import { PROPOSAL_WAIT_REASON, proposalActGate } from "../lead-doors.js";
16
+ import {} from "../lead-progress.js";
17
+ import { LEAD_MESSAGE_MAX_CHARS } from "../lead.js";
18
18
  import {} from "../needs-you.js";
19
19
  import { projectName } from "../project.js";
20
20
  import { replyHtmlInline, shapeReply } from "../reply-shape.js";
21
21
  import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
22
22
  import {} from "../scope.js";
23
- import { MATE_ASK_OTHER } from "../store.js";
23
+ import { LEAD_ASK_OTHER } from "../store.js";
24
24
  import {} from "../task-control.js";
25
25
  import { assignmentStageOf, stageOfCode } from "../task-status.js";
26
26
  import {} from "../team-chat-authorization.js";
@@ -207,7 +207,7 @@ export function chatFleetOverview(snapshot, projects, interactive) {
207
207
  : html `<a href="/board?scope=all" class="chat-overview-link">open board</a>`}</div><div class="chat-overview-stats"><a href="/work?view=needs-you" class="chat-overview-stat attention"><b>${needsYou}${snapshot.attentionCount?.saturated ? "+" : ""}</b><span>need you</span></a><a href="/board?scope=all" class="chat-overview-stat live"><b>${running}</b><span>building</span></a><a href="/board?scope=all&amp;view=order" class="chat-overview-stat"><b>${queued}</b><span>queued</span></a><a href="/work" class="chat-overview-stat"><b>${done}</b><span>finished today</span></a></div>${rows.length === 0 ? html `<p class="chat-overview-clear">${needsYou === 0 && running === 0 ? html `<span class="dot dot-ok"></span>No tasks are waiting and no builds are running.` : html `<a href="/work">Open Tasks to inspect current work.</a>`}</p>` : html `<div class="chat-overview-items">${rows}</div>`}${completedWorkHtml(snapshot, projects.map(project => project.label), snapshot.assignmentStates)}${saturated ? html `<p class="meta chat-overview-note">Some items aren’t shown. Open Work to see more.</p>` : ""}</section>`;
208
208
  }
209
209
  /**
210
- * The mate's project rail: one bounded pulse per admitted project, plus
210
+ * The lead's project rail: one bounded pulse per admitted project, plus
211
211
  * two roads that preserve the plane's contracts. "ask" sends the stable
212
212
  * rN alias the model already sees; "board" uses the existing POST switch
213
213
  * instead of smuggling a project change through a GET parameter.
@@ -234,7 +234,7 @@ export function chatProjectRail(projects, inert) {
234
234
  return html `<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation"><div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span><button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div><div class="chat-project-list">${rows}</div></aside>`;
235
235
  }
236
236
  /** Spend-authorized one-click questions: ordinary /chat posts, not a new door. */
237
- export function matePromptStarters(focus = null) {
237
+ export function leadPromptStarters(focus = null) {
238
238
  const prompts = focus === null
239
239
  ? [
240
240
  ["brief me", "Brief me on what needs my attention, what is building, and the highest-leverage next action across every project."],
@@ -380,8 +380,8 @@ export function chatPage(chrome, data) {
380
380
  if (!data.canManage) {
381
381
  parts.push(html `<div class="card chat-readonly"><strong>Read-only view</strong><p class="meta">An approver can start the unified conversation and confirm its proposed actions. You can still open every live card and project board here.</p></div>`);
382
382
  }
383
- if (data.canManage && data.mateMint !== undefined)
384
- parts.push(data.mateMint);
383
+ if (data.canManage && data.leadMint !== undefined)
384
+ parts.push(data.leadMint);
385
385
  if (data.canManage && data.coordinatorProposals !== undefined)
386
386
  parts.push(data.coordinatorProposals);
387
387
  parts.push(data.focusTask === null && !data.catchUp ? html `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, false)}</details>` : "", chatLimitsHtml({ provider: config.provider, model: config.model, turnsToday: data.turnsToday, dailyTurns: config.dailyTurns, subscription, weekly: subscription ? null : { spent: data.weeklySpent, ceiling: config.weeklyCeilingMicrousd } }));
@@ -437,7 +437,7 @@ export function chatAckPage(chrome, turn, nonce) {
437
437
  ], "\n"), { chrome });
438
438
  }
439
439
  /** The card that starts a conversation: the one password ceremony (mate arc §1). */
440
- export function mateMintCard(enabled, returnTo = "/chat") {
440
+ export function leadMintCard(enabled, returnTo = "/chat") {
441
441
  const subscription = enabled.billing === "subscription";
442
442
  return html `<div class="card mate-mint" id="latest">\n<p><strong>Start a conversation</strong></p>\n${postForm("/chat/mate/mint", joinHtml([
443
443
  html `<div class="mate-terms">`,
@@ -617,7 +617,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
617
617
  const options = decision === null
618
618
  ? html `<p class="meta">The decision is gone</p>`
619
619
  : html `<ul class="answer-options">${decision.options.map(one => html `<li${one.id === pick ? html ` class="picked"` : ""}><strong>${one.label}</strong>${one.reversible ? "" : html ` <span class="badge">Irreversible</span>`}${one.id === decision.recommendation ? html ` <span class="meta">— the builder recommends this</span>` : ""}${one.id === pick ? html ` <span class="meta">— proposed</span>` : ""}<p class="meta">${one.consequence}</p></li>`)}</ul>`;
620
- what = html `<h3>Answer <a href="/d/${decisionId}">decision #${decisionId}</a> on <a href="${taskHref(task)}">${task}</a></h3>${decision === null ? "" : html `<p class="proposal-summary">${decision.question}</p>`}${options}<div class="proposal-rationale"><span class="eyebrow">proposed answer</span><strong>${text("optionLabel")}</strong><p>${text("rationale")}</p></div><p class="meta proposal-disclosure">${payload["readConsequences"] === true ? `${view.by.mate ? "The mate" : "The coordinator"} read every consequence but not the builder's recommendation` : `${view.by.mate ? "The mate" : "The coordinator"} did not read the consequences`}. You see both here${decision !== null && decision.state !== "open" ? " · this decision is no longer open" : ""}.</p>`;
620
+ what = html `<h3>Answer <a href="/d/${decisionId}">decision #${decisionId}</a> on <a href="${taskHref(task)}">${task}</a></h3>${decision === null ? "" : html `<p class="proposal-summary">${decision.question}</p>`}${options}<div class="proposal-rationale"><span class="eyebrow">proposed answer</span><strong>${text("optionLabel")}</strong><p>${text("rationale")}</p></div><p class="meta proposal-disclosure">${payload["readConsequences"] === true ? `${view.by.mate ? "The lead" : "The coordinator"} read every consequence but not the builder's recommendation` : `${view.by.mate ? "The lead" : "The coordinator"} did not read the consequences`}. You see both here${decision !== null && decision.state !== "open" ? " · this decision is no longer open" : ""}.</p>`;
621
621
  }
622
622
  else {
623
623
  what = html `<h3>Cancel <a href="${taskHref(task)}">${task}</a></h3><p class="proposal-summary">${text("reason")}</p>${facts(["project", mono(repoId)])}`;
@@ -686,10 +686,10 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
686
686
  };
687
687
  return { html: cardHtml, card };
688
688
  }
689
- export function mateProposalCard(proposal, inert, decision, returnTo = null) {
690
- return mateProposalCardParts(proposal, inert, decision, returnTo).html;
689
+ export function leadProposalCard(proposal, inert, decision, returnTo = null) {
690
+ return leadProposalCardParts(proposal, inert, decision, returnTo).html;
691
691
  }
692
- export function mateProposalCardParts(proposal, inert, decision, returnTo = null) {
692
+ export function leadProposalCardParts(proposal, inert, decision, returnTo = null) {
693
693
  return proposalCardParts({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: true }, actionBase: "/chat/proposal" }, inert, decision, returnTo);
694
694
  }
695
695
  /** Review and inline team cards share the same server-decided controls and reasons. The team API renders these
@@ -698,7 +698,7 @@ export function teamProposalCardParts(store, actor, proposal, snapshot, csrf, de
698
698
  const gate = proposalActGate(store, actor, proposal.thread, now, provider);
699
699
  const reason = !gate.ok ? gate.said : null;
700
700
  const back = '/chat?conversation=' + encodeURIComponent(snapshot.selected.id);
701
- const parts = withFormToken(csrf, () => mateProposalCardParts(proposal, reason !== null, decision, back));
701
+ const parts = withFormToken(csrf, () => leadProposalCardParts(proposal, reason !== null, decision, back));
702
702
  if (reason !== null && reason !== PROPOSAL_WAIT_REASON && proposal.state === 'pending') {
703
703
  parts.html = replaceMarkup(parts.html, new RegExp(htmlString(html `${PROPOSAL_WAIT_REASON}`).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), () => html `${reason}`);
704
704
  parts.card = { ...parts.card, note: reason };
@@ -717,7 +717,7 @@ export function coordinatorProposalsSection(proposals, decisions, now, heading =
717
717
  export const REPLACED_THREAD_DIVIDER = "New conversation — the projects I can reach changed";
718
718
  /** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
719
719
  export function replacedBrowserMessages(previous) {
720
- return mateBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, null).map(message => ({
720
+ return leadBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, null).map(message => ({
721
721
  ...message, cardsHtml: "",
722
722
  cards: (message.cards ?? []).map(card => ({ ...card, primary: null, dismissable: false })),
723
723
  }));
@@ -732,20 +732,20 @@ export function replacedThreadHtml(previous) {
732
732
  }
733
733
  /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
734
734
  * "Something else" moves to the composer. Nothing shows while a reply is running. */
735
- export function mateAskHtml(ask, target, composer, open = true) {
735
+ export function leadAskHtml(ask, target, composer, open = true) {
736
736
  if (!open)
737
737
  return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p></div>`;
738
738
  const hidden = target.task !== null ? { task: target.task } : target.project !== null ? { project: target.project } : {};
739
- return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><div class="so-owner-ask-options" role="group" aria-label="Answer options">${ask.options.map(option => postForm("/chat", html `<button type="submit" name="message" value="${option}" class="so-suggestion quiet">${option}</button>`, { attrs: { class: "inline" }, hidden }))}<label for="${composer}" class="so-suggestion so-owner-ask-other">${MATE_ASK_OTHER}</label></div></div>`;
739
+ return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><div class="so-owner-ask-options" role="group" aria-label="Answer options">${ask.options.map(option => postForm("/chat", html `<button type="submit" name="message" value="${option}" class="so-suggestion quiet">${option}</button>`, { attrs: { class: "inline" }, hidden }))}<label for="${composer}" class="so-suggestion so-owner-ask-other">${LEAD_ASK_OTHER}</label></div></div>`;
740
740
  }
741
741
  /** The thread's messages as the React conversation renders them, each
742
742
  * with the proposal cards its turn produced; `back` is where a card's
743
743
  * confirm or dismiss returns. */
744
- export function mateBrowserMessages(rows, back, target = { task: null, project: null }) {
744
+ export function leadBrowserMessages(rows, back, target = { task: null, project: null }) {
745
745
  const open = rows.pending === null ? rows.ask?.turn ?? null : null;
746
746
  const asked = (turn) => {
747
747
  const ask = turn === null ? undefined : rows.asks?.get(turn);
748
- return ask === undefined ? "" : mateAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
748
+ return ask === undefined ? "" : leadAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
749
749
  };
750
750
  // The owner's message each reply answers: when it asked for ids, the reply keeps them.
751
751
  const askedBy = new Map();
@@ -759,7 +759,7 @@ export function mateBrowserMessages(rows, back, target = { task: null, project:
759
759
  ...(() => {
760
760
  // Under the lead's message only, as the server-rendered thread does: the person's message shares the turn.
761
761
  const parts = message.turn === null || message.role === 'operator' ? [] : rows.proposals.filter(one => one.turn === message.turn)
762
- .map(one => mateProposalCardParts(one, rows.pending !== null, rows.decisions.get(typeof one.payload['decision'] === 'number' ? one.payload['decision'] : -1) ?? null, back));
762
+ .map(one => leadProposalCardParts(one, rows.pending !== null, rows.decisions.get(typeof one.payload['decision'] === 'number' ? one.payload['decision'] : -1) ?? null, back));
763
763
  return { cardsHtml: htmlString(joinHtml(parts.map(one => one.html))), cards: parts.map(one => one.card) };
764
764
  })(),
765
765
  }));
@@ -770,7 +770,7 @@ export function mateBrowserMessages(rows, back, target = { task: null, project:
770
770
  * decisions the cards name, and the task lens's own facts — and nothing
771
771
  * that merely ticks: no relative age, no freshly minted nonce, no csrf.
772
772
  * Equal versions mean equal fragments; the poll fetches nothing else. */
773
- export function mateChatVersion(rows) {
773
+ export function leadChatVersion(rows) {
774
774
  const focus = rows.focusTask;
775
775
  const facts = [
776
776
  rows.messages.map(one => [one.id, one.role, one.turn]),
@@ -805,7 +805,7 @@ export function mateChatVersion(rows) {
805
805
  * populated thread. Every child carries a stable `data-key` so the page
806
806
  * keeps an unchanged node (its open disclosures, its focus) and replaces
807
807
  * only what the server rendered differently. The composer is never here. */
808
- export function mateThreadHtml(data) {
808
+ export function leadThreadHtml(data) {
809
809
  const returnTo = data.focusTask === null ? "/chat" : taskChatHref(data.focusTask.id);
810
810
  const parts = [];
811
811
  if (data.problem !== null)
@@ -834,7 +834,7 @@ export function mateThreadHtml(data) {
834
834
  }
835
835
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
836
836
  const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
837
- thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" : mateAskHtml(ask, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn)}${cards.map(one => mateProposalCard(one, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo))}<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${message.createdAt}">${relativeAge(message.createdAt, data.now)}</time></div></div>`);
837
+ thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" : leadAskHtml(ask, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn)}${cards.map(one => leadProposalCard(one, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo))}<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${message.createdAt}">${relativeAge(message.createdAt, data.now)}</time></div></div>`);
838
838
  }
839
839
  parts.push(html `<div class="thread" data-key="thread" data-chat-list>\n${joinHtml(thread, "\n")}\n</div>`);
840
840
  if (data.pending !== null) {
@@ -850,10 +850,10 @@ export function mateThreadHtml(data) {
850
850
  }
851
851
  /** What follows the composer (package 2): the starters of an EMPTY thread
852
852
  * sit under the box; once the first message lands they move above it. */
853
- export function mateAfterComposerHtml(data) {
854
- return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ? matePromptStarters(data.focusTask) : ""}</div>`;
853
+ export function leadAfterComposerHtml(data) {
854
+ return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ? leadPromptStarters(data.focusTask) : ""}</div>`;
855
855
  }
856
- export function matePage(chrome, data) {
856
+ export function leadPage(chrome, data) {
857
857
  const subscription = isSubscriptionChatProvider(data.config.provider);
858
858
  const chatProject = data.focusTask === null ? data.chatProject ?? null : null;
859
859
  const returnTo = data.focusTask !== null ? taskChatHref(data.focusTask.id) : chatProject !== null ? projectChatHref(chatProject) : "/chat";
@@ -888,16 +888,16 @@ export function matePage(chrome, data) {
888
888
  const lastMessage = data.messages.at(-1);
889
889
  const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
890
890
  conversation.push(replacedThreadHtml(data.previous));
891
- conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
891
+ conversation.push(leadThreadHtml({ ...data, problem: null, chatProject }));
892
892
  conversation.push(
893
893
  // The New update action (package 2): hidden until a live update lands
894
894
  // while the reader is above the latest message; a real button, so the
895
895
  // keyboard reaches it. Only this act moves the reader.
896
896
  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([
897
- html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
897
+ 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>`,
898
898
  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>`,
899
899
  ], "\n"), {
900
- attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the mate", "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": mateChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
900
+ 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 ?? "" },
901
901
  hidden: { request: randomBytes(16).toString("hex"), "request-session": data.session.id, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
902
902
  }),
903
903
  // Concise pass (2026-09-13): the status line speaks only when there is
@@ -906,13 +906,13 @@ export function matePage(chrome, data) {
906
906
  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>`,
907
907
  // The explicit reconnection (package 2): shown only once the session
908
908
  // or sign-in changed under this page; it reloads on the reader's act.
909
- html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`, mateAfterComposerHtml(data), controlsHtml);
909
+ html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`, leadAfterComposerHtml(data), controlsHtml);
910
910
  return screen("chat", chatWorkspace(joinHtml(conversation, "\n"), data.projects, false, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_CONTINUITY_SCRIPT + CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: true },
911
911
  workspace: {
912
912
  conversation: {
913
- sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
914
- messages: mateBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
915
- pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
913
+ sessionId: data.session.id, user: data.session.approver, version: leadChatVersion(data),
914
+ messages: leadBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
915
+ pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: LEAD_MESSAGE_MAX_CHARS,
916
916
  taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
917
917
  ...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous) } }),
918
918
  },
@@ -551,9 +551,9 @@ export function workPage(chrome, data) {
551
551
  ? html `<div class="work-empty" data-work-empty="${data.view}"><p>${data.previous ? 'There are no more tasks on this page.' : current.empty}</p>${data.view === 'all' && !data.previous ? html `<p><a class="button-link" href="${chrome.chat === true ? '/chat' : '/tasks/new'}">${chrome.chat === true ? 'Start in chat' : 'Add a task'}</a></p>` : html `<a href="${href('all')}">See all tasks →</a>`}</div>`
552
552
  : html `<div class="work-list">${data.work.items.map(rowHtml)}</div>`;
553
553
  const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
554
- const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
554
+ const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
555
555
  .map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
556
- const toolLinks = [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
556
+ const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
557
557
  const view = {
558
558
  kind: 'tasks',
559
559
  tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
@@ -12,7 +12,7 @@ import { type BuildReviewView } from "../review-switch.js";
12
12
  import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
13
13
  import { type ReportItem } from "../scout-report.js";
14
14
  import { type Artifact, type DiffComment, type Publication, type RepairChainRow, type ReviewRetryState, type Run, type Store, type TaskState } from "../store.js";
15
- import { type PullRequestFact, type TaskStatus } from "../task-status.js";
15
+ import { type ReleaseState, type PullRequestFact, type TaskStatus } from "../task-status.js";
16
16
  import { type DisplayStatus, type PublicationFacts, type ReviewFacts } from "../workspace-ui.js";
17
17
  import { type Chrome, type Screen } from "./chrome.js";
18
18
  import { type Html } from "../html.js";
@@ -78,6 +78,7 @@ export declare function criterionMatrixHtml(matrix: readonly CriterionMatrixRow[
78
78
  links?: EvidenceLinkMap;
79
79
  fileAnchors?: ReadonlyMap<string, string>;
80
80
  verdict?: string | null;
81
+ release?: ReleaseState | undefined;
81
82
  }): Html;
82
83
  /** One policy summary beneath the requirements. Per-requirement concerns
83
84
  * are already visible in the matrix; do not repeat their full text here. */
@@ -674,6 +675,8 @@ export declare const CHECK_EXCERPT_LINES = 12;
674
675
  * change's first file, labelled as not cited), the screenshots it cites
675
676
  * (none cited: every validated one), and the agent's own note. */
676
677
  export declare function personCheckItems(proof: ProofBundleView, patchText: string | null, shots: readonly ResultScreenshot[], runId: number): BrowserCheckItem[];
678
+ /** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
679
+ export declare function hardBlockerOf(assignment: AssignmentSnapshot | null | undefined): string | null;
677
680
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
678
681
  * stopped behind the password. Null under every other headline. */
679
682
  export declare function needActionOf(assignment: AssignmentSnapshot | null, status: TaskStatus | null, csrf: string, returnTo: string, result?: {
@@ -21,7 +21,7 @@ import {} from "../scope.js";
21
21
  import { parseReport } from "../scout-report.js";
22
22
  import {} from "../store.js";
23
23
  import { runCostWords } from "../summary.js";
24
- import { demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
24
+ import { checkBackingOf, CHECKED_AT_RELEASE, demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
25
25
  import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, MISMATCH_HEADLINE, receiptHeadingOf, receiptPublicationWords, reportMismatchesOf, RESULT_DECISION_SENTENCE, resultHeadlineOf, resultStatusOf, REVIEW_TOKENS } from "../workspace-ui.js";
26
26
  import { createHash, randomBytes } from "node:crypto";
27
27
  import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
@@ -173,13 +173,14 @@ export function criterionMatrixHtml(matrix, options = {}) {
173
173
  const confirmed = row.assessment !== undefined && state === "pass";
174
174
  // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
175
175
  // The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
176
- const label = requirementWordOf(row, options.verdict);
176
+ const label = requirementWordOf(row, options.verdict, options.release);
177
177
  const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
178
178
  const warnings = [];
179
179
  // Only replace the known boilerplate. Other recorded failure details
180
180
  // remain verbatim, so concision cannot hide a different problem.
181
181
  const detail = row.detail.filter(line => !(row.assessment && row.review && line === `${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`) && !(state === "manual-review" && /^criterion "[^"\n]+" requires manual-review evidence — an operator must accept it before this can verify$/.test(line)));
182
- if (!awaitingAssessment && state !== "pass" && detail.length > 0)
182
+ // A requirement a release check covers, or that waits only on one, has nothing wrong to say.
183
+ if (!awaitingAssessment && state !== "pass" && label !== "Met" && label !== CHECKED_AT_RELEASE && detail.length > 0)
183
184
  warnings.push(html `<ul class="requirement-issues">${detail.map(line => html `<li>${line}</li>`)}</ul>`);
184
185
  const review = row.review;
185
186
  if (review !== null && review.judgement !== "upholds") {
@@ -671,13 +672,16 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
671
672
  action: false,
672
673
  });
673
674
  parts.push(html `<div id="verification" data-cockpit-section="result">${panel.html}</div>`);
674
- const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
675
+ // What no acceptance resolves (a failed check, a HIGH finding): Complete is refused, so no Accept is offered and
676
+ // the one line says the next step.
677
+ const hardStop = hardBlockerOf(assignment);
678
+ const blocked = hardStop ?? cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
675
679
  const youCheck = panel.panel.youCheck;
676
680
  // Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
677
681
  // report that doesn't match its changes asks for), the same request records it first.
678
682
  const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
679
683
  : blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
680
- const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
684
+ const complete = hardStop === null && assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
681
685
  ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
682
686
  if (complete !== null)
683
687
  parts.push(completionForm(view.taskId, run.id, complete.receipt, view.detail?.pullRequestTo ?? null, owed));
@@ -686,8 +690,10 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
686
690
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
687
691
  const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
688
692
  const base = !acceptsHere ? null : acceptWordsOf({
689
- checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
690
- unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
693
+ // Checks Off that a release check already covers read as passed (release-coverage.ts).
694
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? checks.release != null ? "passed" : "off" : checks.status,
695
+ // Release-aware words (task-status.ts): a requirement waiting only on a release check, or covered by one, isn't unmet.
696
+ unmet: matrix.filter(row => !["Met", "You check", CHECKED_AT_RELEASE].includes(requirementWordOf(row, null, checks === undefined ? undefined : checkBackingOf(checks)))).length,
691
697
  action: complete !== null ? "complete" : "accept",
692
698
  publishing: view.detail?.publishing ?? "other",
693
699
  proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
@@ -700,7 +706,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
700
706
  const followUps = view.detail.followUps ?? null;
701
707
  const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
702
708
  const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
703
- (checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
709
+ (checks === undefined || ((checks.status === "not-run" || checks.status === "unavailable") && checks.release == null))
704
710
  // Back on this result's Checks tab, where #follow-ups shows the run it started.
705
711
  ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
706
712
  const need = panel.panel.need;
@@ -1774,7 +1780,8 @@ export function resultPanelParts(detail, o) {
1774
1780
  checkParts.push(detail.signedCriteria > 0 ? html `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : html `<p class="meta">This scope signed no acceptance checks.</p>`);
1775
1781
  }
1776
1782
  else {
1777
- checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
1783
+ checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict,
1784
+ release: current?.receipt == null ? undefined : checkBackingOf(current.receipt.checks) }));
1778
1785
  }
1779
1786
  checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
1780
1787
  if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
@@ -1930,11 +1937,18 @@ export function personCheckItems(proof, patchText, shots, runId) {
1930
1937
  };
1931
1938
  });
1932
1939
  }
1940
+ /** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
1941
+ export function hardBlockerOf(assignment) {
1942
+ return assignment?.readiness?.blockers.find(one => one.key === "check-failed" || one.key === "high")?.message ?? null;
1943
+ }
1933
1944
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
1934
1945
  * stopped behind the password. Null under every other headline. */
1935
1946
  export function needActionOf(assignment, status, csrf, returnTo, result) {
1936
1947
  if (assignment === null || status === null || status.headline !== "Needs you" || status.need == null)
1937
1948
  return null;
1949
+ // Accepting can't resolve it: Request changes is the way on, beside the reason.
1950
+ if (result !== undefined && hardBlockerOf(assignment) !== null)
1951
+ return null;
1938
1952
  const action = assignment.primaryAction;
1939
1953
  const rebuild = { href: null, confirm: null, rebuild: { action: `${taskHref(assignment.rootId)}/requeue` } };
1940
1954
  // Built to an earlier plan: Build again, in place (the task page's requeue).
@@ -44,6 +44,13 @@ export declare function leadSettingsHtml(data: {
44
44
  when: string;
45
45
  until: string;
46
46
  }[];
47
+ /** D5: the lead's subagents in the projects this person can use: name and role, project, and whether it's paused. */
48
+ subagents?: {
49
+ id: number;
50
+ label: string;
51
+ project: string;
52
+ paused: boolean;
53
+ }[];
47
54
  }): Html;
48
55
  /** The lead's full settings (Settings → Lead → Advanced): provider, model, limits and a direct API key. */
49
56
  export declare function leadConfigForm(current: import("../store.js").ChatConfig | null, data: LeadFormFacts): Html;
@@ -53,6 +53,9 @@ export function leadSettingsHtml(data) {
53
53
  // What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
54
54
  (data.promises ?? []).length === 0 ? "" : html `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => html `<li data-promise="${one.id}"><p>${one.what}</p><p class="meta">${one.when.charAt(0).toUpperCase() + one.when.slice(1)} · <span class="nowrap">until ${new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })}</span></p>\
55
55
  ${postForm("/settings/lead/promise/cancel", html `<button type="submit" class="secondary">Cancel</button>`, { attrs: { class: "inline" }, returnTo, hidden: { promise: one.id } })}</li>`)}</ul></section>`,
56
+ // D5: the lead's subagents, one line each; everything else about one is on its own page.
57
+ data.subagents === undefined ? "" : html `<section class="card lead-subagents" id="subagents" data-lead-subagents><h2>Subagents</h2>${data.subagents.length === 0 ? html `<p class="meta">None yet. Your lead can ask one for help once it's here.</p>`
58
+ : html `<ul class="lead-subagent-list">${data.subagents.map(one => html `<li data-subagent="${one.id}"><a href="/settings/lead/subagents/${one.id}">${one.label}</a><span class="meta">${one.project}${one.paused ? " · Paused" : ""}</span></li>`)}</ul>`}<p><a class="button-link" href="/settings/lead/subagents?add=1#add" data-add-subagent>Add subagent</a></p></section>`,
56
59
  html `<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
57
60
  leadConfigForm(config, facts),
58
61
  config === null ? "" : postForm("/chat/config", html `<input type="password" name="token" placeholder="your password" autocomplete="current-password" aria-label="Your password"><button type="submit" class="secondary">Turn the lead off</button>`, { attrs: { class: "inline" }, returnTo, hidden: { off: "1" } }),
@@ -148,9 +148,11 @@ get("flows", "flows.gallery", "^\\/flows\\/new\\/[a-z-]{1,40}$", { limited: "res
148
148
  get("flows", "flows.page", "/flows", { limited: "resource", ...free });
149
149
  get("flows", "kits", "/kits");
150
150
  get("flows", "kit.page", "^\\/kits\\/[a-z-]{1,40}$", { sample: "/kits/support" });
151
- get("flows", "teammates", "/teammates", free);
152
- get("flows", "teammate.page", `^\\/teammates\\/[1-9][0-9]{0,9}$`, { ...free, sample: "/teammates/1" });
153
- get("flows", "teammate.soul-file", `^\\/teammates\\/[1-9][0-9]{0,9}\\/soul\\.md$`, { ...free, sample: "/teammates/1/soul.md" });
151
+ get("flows", "subagents", "/settings/lead/subagents", free);
152
+ // D5: subagents were teammates; old links to them land on the same pages under Settings → Lead.
153
+ get("flows", "subagents.legacy", "^\\/teammates(\\/[1-9][0-9]{0,9}(\\/soul\\.md)?)?$", { ...free, sample: "/teammates/1" });
154
+ get("flows", "subagent.page", `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}$`, { ...free, sample: "/settings/lead/subagents/1" });
155
+ get("flows", "subagent.soul-file", `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}\\/soul\\.md$`, { ...free, sample: "/settings/lead/subagents/1/soul.md" });
154
156
  get("flows", "flow.insights", `^\\/flows\\/${FLOW}\\/insights$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/insights" });
155
157
  get("flows", "flow.run", `^\\/flows\\/${FLOW}\\/runs\\/${FLOW}\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/runs/2/3" });
156
158
  get("flows", "flow.export", `^\\/flows\\/${FLOW}\\/export$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/export" });
@@ -222,11 +224,11 @@ post("settings", "settings.skills-import", "/settings/skills/import", { project:
222
224
  post("settings", "settings.skills-change", "/settings/skills/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to manage skills.", "/settings/skills") });
223
225
  for (const one of ["setup", "sample", "github"])
224
226
  post("flows", `kit.${one}`, `^\\/kits\\/[a-z-]{1,40}\\/${one}$`, { project: "form", sample: `/kits/support/${one}`, ...approver("Sign in as an approver to set kits up.", "/kits") });
225
- const TEAMMATES = anyApprover("Sign in as an approver to look after teammates.", "/teammates");
226
- post("flows", "teammate.new", "/teammates/new", { project: "form", ...TEAMMATES });
227
+ const SUBAGENTS = anyApprover("Sign in as an approver to look after subagents.", "/settings/lead/subagents");
228
+ post("flows", "subagent.new", "/settings/lead/subagents/new", { project: "form", ...SUBAGENTS });
227
229
  for (const one of ["soul", "state", "note", "settings", "summary", "tools", "memory", "routines", "week"])
228
- post("flows", `teammate.${one}`, `^\\/teammates\\/[1-9][0-9]{0,9}\\/${one}$`, { sample: `/teammates/1/${one}`, ...TEAMMATES });
229
- post("flows", "teammate.answer", "^\\/teammates\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/teammates/questions/1/answer" });
230
+ post("flows", `subagent.${one}`, `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}\\/${one}$`, { sample: `/settings/lead/subagents/1/${one}`, ...SUBAGENTS });
231
+ post("flows", "subagent.answer", "^\\/settings\\/lead\\/subagents\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/settings/lead/subagents/questions/1/answer" });
230
232
  post("flows", "flows.gallery-create", "^\\/flows\\/new\\/[a-z-]{1,40}$", { project: "form", limited: "resource", sample: "/flows/new/triage" });
231
233
  // Flow editing answers JSON: a caller without approver standing hears it in the editor's own format.
232
234
  const CREATE_FLOWS = approver("Sign in as an approver to create flows.", "/flows");
@@ -24,8 +24,8 @@ import { type ProviderConnection } from "../provider-connection.js";
24
24
  import { type ProviderId } from "../provider.js";
25
25
  import { authenticateApprover as checkApproverPassword, type Scope } from "../scope.js";
26
26
  import { SourceBudget } from "../sign-in-guard.js";
27
- import type { CoordinatorProposal, DirectChatProviderId, MateMessage, MateProposal, MateTurn, PlanRevision, TaskFamily, TaskRef } from "../store.js";
28
- import { type Decision, type MateAsk, type MateThreadScope, type Run, type Store, type Task, type TaskState } from "../store.js";
27
+ import type { CoordinatorProposal, DirectChatProviderId, LeadMessage, LeadProposal, LeadTurn, PlanRevision, TaskFamily, TaskRef } from "../store.js";
28
+ import { type Decision, type LeadAsk, type LeadThreadScope, type Run, type Store, type Task, type TaskState } from "../store.js";
29
29
  import type { TeamChatProviderResolver } from '../team-chat-authorization.js';
30
30
  import type { TeamResponse } from '../team-contract.js';
31
31
  import { createTeamRuntime } from '../team-runtime.js';
@@ -108,24 +108,24 @@ export interface ServerRuntime {
108
108
  flowRooms: FlowRooms;
109
109
  providerHome: string;
110
110
  toolHome: string;
111
- matePrincipal: (who: Who & {
111
+ leadPrincipal: (who: Who & {
112
112
  via: "cookie";
113
113
  }) => VerifiedApprover | null;
114
- chatScopeOf: (focusTask: TaskChatFocus | null, chatProject: string | null) => MateThreadScope;
114
+ chatScopeOf: (focusTask: TaskChatFocus | null, chatProject: string | null) => LeadThreadScope;
115
115
  liveTurns: Map<number, LiveTurn>;
116
116
  /** A lead reply started on a thread: pages with that conversation open follow it (server/live.ts). */
117
117
  liveTurnStarted: (thread: number) => void;
118
- mateConversationRows: (who: Who & {
118
+ leadConversationRows: (who: Who & {
119
119
  via: "cookie";
120
120
  }, principal: VerifiedApprover, focusTask: TaskChatFocus | null, now: Date, chatProject?: string | null) => {
121
- messages: MateMessage[];
122
- proposals: MateProposal[];
121
+ messages: LeadMessage[];
122
+ proposals: LeadProposal[];
123
123
  decisions: Map<number, Decision>;
124
124
  coordinatorProposals: CoordinatorProposal[];
125
- pending: MateTurn | null;
126
- recent: MateTurn[];
127
- ask: MateAsk | null;
128
- asks: Map<number, MateAsk>;
125
+ pending: LeadTurn | null;
126
+ recent: LeadTurn[];
127
+ ask: LeadAsk | null;
128
+ asks: Map<number, LeadAsk>;
129
129
  previous: ReplacedThread | null;
130
130
  };
131
131
  demoLeadHere: () => DemoLead | null;
@@ -137,7 +137,7 @@ export interface ServerRuntime {
137
137
  teamChatProvider: TeamChatProviderResolver;
138
138
  pullRequestTargetOf: (runId: number) => string | null;
139
139
  chatEnablement: () => ChatEnablement;
140
- startMateConversation: (who: Who & {
140
+ startLeadConversation: (who: Who & {
141
141
  via: "cookie";
142
142
  }, principal: VerifiedApprover, enabled: Extract<ChatEnablement, {
143
143
  ok: true;
@@ -185,7 +185,7 @@ export interface ServerRuntime {
185
185
  status: number;
186
186
  message: string;
187
187
  } | null;
188
- mateSaid: Map<string, {
188
+ leadSaid: Map<string, {
189
189
  turn: number | null;
190
190
  message: string;
191
191
  }>;
@@ -8,8 +8,8 @@ import { skillTestResult } from "../project-skills.js";
8
8
  import { discoverTools, projectToolsOf } from "../project-tools.js";
9
9
  import { publishingOf } from '../pull-request-flow.js';
10
10
  import { skillTestFeedbackHtml } from "../skills-ui.js";
11
- import { nameOf } from "../teammate-admin.js";
12
- import { grantTool } from "../teammate-tools.js";
11
+ import { labelOf, nameOf } from "../subagent-admin.js";
12
+ import { grantTool } from "../subagent-tools.js";
13
13
  import { learningHtml } from "../workspace-ui.js";
14
14
  import { adapterPolicy } from "./route-policy.js";
15
15
  import { createHash } from "node:crypto";
@@ -100,7 +100,7 @@ import { skillsHtml, skillsScript } from "../skills-ui.js";
100
100
  import { checkSlackCredentials, clearSlackCredentials, loadSlackCredentials, saveSlackCredentials, SLACK_MANIFEST, SlackError } from "../slack-api.js";
101
101
  import { slackSettingsHtml } from "../slack-settings.js";
102
102
  import { spendCsv, spendHtml } from "../spend-ui.js";
103
- import { budgetStates, monthNamed, spendItems, usd as spendUsd, teammateNames as teammateNamesOf } from "../spend.js";
103
+ import { budgetStates, monthNamed, spendItems, usd as spendUsd, subagentNames as subagentNamesOf } from "../spend.js";
104
104
  import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "../sso-settings.js";
105
105
  import { ssoSettingsHtml } from "../sso-ui.js";
106
106
  import { lastSweep, saveSweep, storageSweepOff } from "../storage-sweep.js";
@@ -129,23 +129,23 @@ export function createSettingsHandlers(runtime) {
129
129
  if (month === null)
130
130
  return refuse(response, who, 400, "Choose a month like 2026-09.", "/spend");
131
131
  const items = spendItems(store.handle, month.from, month.to);
132
- const teammates = store.teammates([...new Set([...managedRepos(), ...store.knownRepos()])]);
133
- const teammateNames = teammateNamesOf(store.handle);
132
+ const subagents = store.subagents([...new Set([...managedRepos(), ...store.knownRepos()])]);
133
+ const subagentNames = subagentNamesOf(store.handle);
134
134
  if (url.searchParams.get("format") === "csv") {
135
135
  response.writeHead(200, { "content-type": "text/csv; charset=utf-8", "content-disposition": `attachment; filename="standing-orders-spend-${month.name}.csv"`, "cache-control": "no-store", "x-content-type-options": "nosniff" });
136
- return void response.end(spendCsv(items, teammateNames));
136
+ return void response.end(spendCsv(items, subagentNames));
137
137
  }
138
138
  const shift = (by) => { const at = new Date(`${month.name}-01T00:00:00.000Z`); at.setUTCMonth(at.getUTCMonth() + by); return at.toISOString().slice(0, 7); };
139
139
  const projects = consoleProjects();
140
140
  const people = store.accountFacts().filter(one => one.revokedAt === null).map(one => one.name);
141
141
  const view = {
142
142
  month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1),
143
- items, budgets: budgetStates(store.budgets(), items), teammateNames, csrf: who.via === "cookie" ? who.session.csrf : "",
143
+ items, budgets: budgetStates(store.budgets(), items), subagentNames, csrf: who.via === "cookie" ? who.session.csrf : "",
144
144
  targets: [
145
145
  { value: "installation:*", label: "Everything", group: "Everything" },
146
146
  ...projects.map(repo => ({ value: `project:${repo}`, label: projectName(repo), group: "Projects" })),
147
147
  ...people.map(name => ({ value: `person:${name}`, label: name, group: "People" })),
148
- ...teammates.map(one => ({ value: `teammate:${one.id}`, label: `${teammateNames.get(one.id) ?? one.handle} (${projectName(one.repo)})`, group: "Teammates" })),
148
+ ...subagents.map(one => ({ value: `subagent:${one.id}`, label: `${subagentNames.get(one.id) ?? one.handle} (${projectName(one.repo)})`, group: "Subagents" })),
149
149
  ],
150
150
  };
151
151
  return sendScreen(response, 200, screen("Spend", spendHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }), { chrome: chromeFor(null, "spend") }));
@@ -536,7 +536,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
536
536
  const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
537
537
  return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
538
538
  said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises,
539
- about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about" }), { chrome: chromeFor(project, "settings") }));
539
+ about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about",
540
+ subagents: store.subagents(consoleProjects().filter(repo => store.accountCanAccess(who.name, repo))).map(one => ({ id: one.id, label: labelOf(one), project: projectName(one.repo), paused: one.state === "paused" })) }), { chrome: chromeFor(project, "settings") }));
540
541
  }
541
542
  async function telegramPage(ctx) {
542
543
  const { response, now, project } = ctx;
@@ -754,16 +755,16 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
754
755
  const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
755
756
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
756
757
  return back("problem", "Enter your Toolroll password to change a budget.");
757
- const target = /^(installation|project|person|teammate):(.+)$/.exec(body.get("target") ?? "");
758
+ const target = /^(installation|project|person|subagent):(.+)$/.exec(body.get("target") ?? "");
758
759
  if (target === null)
759
760
  return back("problem", "Choose what the budget is for.");
760
761
  const scope = target[1], key = target[2];
761
762
  const known = scope === "installation" ? key === "*"
762
763
  : scope === "project" ? consoleProjects().includes(key)
763
764
  : scope === "person" ? store.accountFacts().some(one => one.name === key && one.revokedAt === null)
764
- : store.teammates([...new Set([...managedRepos(), ...store.knownRepos()])]).some(one => String(one.id) === key);
765
+ : store.subagents([...new Set([...managedRepos(), ...store.knownRepos()])]).some(one => String(one.id) === key);
765
766
  if (!known)
766
- return back("problem", "That isn't a project, person or teammate here.");
767
+ return back("problem", "That isn't a project, person or subagent here.");
767
768
  const label = budgetLabel({ scope, key }).replace(/'s$/, "");
768
769
  if (body.get("action") === "remove") {
769
770
  const existing = store.budgets().find(one => one.scope === scope && one.key === key);
@@ -2315,7 +2316,7 @@ ${modeWords(terms)
2315
2316
  return done(back, "problem", finished.said);
2316
2317
  // From a kit: its teammate may use the tool now (reading freely, the rest after a person approves each call).
2317
2318
  const set = kit === null || !kit.tools.some(one => one.tool === service.id) ? null : kitInstalled(store, kit, visit.repo);
2318
- if (set !== null && store.teammateGrant(set.mate.id, service.id) === null) {
2319
+ if (set !== null && store.subagentGrant(set.mate.id, service.id) === null) {
2319
2320
  const granted = await grantTool(store, set.mate, service.id, visit.by, now, { toolHome });
2320
2321
  if (granted.ok)
2321
2322
  return done(back, "said", `${service.label} is connected to ${to}, and ${nameOf(set.mate)} can use it: reading freely, the rest after you approve each call.`);