toolroll 0.9.15 → 0.9.17

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 (89) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/assignment-brief.d.ts +10 -0
  3. package/dist/assignment-brief.js +56 -26
  4. package/dist/assignment.d.ts +8 -7
  5. package/dist/assignment.js +13 -8
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +22 -22
  8. package/dist/browser-crew.js +1 -0
  9. package/dist/browser-workspace.d.ts +5 -0
  10. package/dist/chat-actions.d.ts +7 -0
  11. package/dist/chat-actions.js +40 -4
  12. package/dist/chat-ask.d.ts +28 -0
  13. package/dist/chat-ask.js +91 -0
  14. package/dist/chat-channel.js +8 -1
  15. package/dist/chat-controls.d.ts +8 -0
  16. package/dist/chat-controls.js +3 -0
  17. package/dist/chat-delivery-state.d.ts +8 -1
  18. package/dist/chat-delivery-state.js +15 -2
  19. package/dist/chat-delivery.d.ts +3 -0
  20. package/dist/chat-delivery.js +49 -17
  21. package/dist/chat-quiet.js +4 -2
  22. package/dist/chat-voice.d.ts +3 -6
  23. package/dist/chat-voice.js +8 -2
  24. package/dist/chat-warmth.d.ts +21 -0
  25. package/dist/chat-warmth.js +36 -0
  26. package/dist/discord-api.d.ts +1 -1
  27. package/dist/discord-api.js +1 -1
  28. package/dist/discord-chat.d.ts +3 -2
  29. package/dist/discord-chat.js +28 -3
  30. package/dist/lead-about.d.ts +35 -0
  31. package/dist/lead-about.js +62 -0
  32. package/dist/lead-capabilities.d.ts +64 -0
  33. package/dist/lead-capabilities.js +199 -0
  34. package/dist/lead-commitments.d.ts +84 -0
  35. package/dist/lead-commitments.js +214 -0
  36. package/dist/lead-context.d.ts +34 -3
  37. package/dist/lead-context.js +155 -19
  38. package/dist/lead-follow.d.ts +1 -1
  39. package/dist/lead-follow.js +6 -2
  40. package/dist/lead-identity.d.ts +23 -0
  41. package/dist/lead-identity.js +33 -0
  42. package/dist/lead-people.d.ts +55 -0
  43. package/dist/lead-people.js +128 -0
  44. package/dist/lead-voice.d.ts +13 -9
  45. package/dist/lead-voice.js +26 -16
  46. package/dist/ledger-view.js +3 -2
  47. package/dist/mate-cli.d.ts +4 -1
  48. package/dist/mate-cli.js +23 -16
  49. package/dist/mate-contract.d.ts +1 -1
  50. package/dist/mate-contract.js +13 -4
  51. package/dist/mate-progress.js +7 -0
  52. package/dist/mate-tools.d.ts +26 -1
  53. package/dist/mate-tools.js +357 -7
  54. package/dist/mate.d.ts +35 -1
  55. package/dist/mate.js +121 -61
  56. package/dist/operate.js +4 -1
  57. package/dist/phase-routing.d.ts +1 -1
  58. package/dist/project-delete.js +3 -1
  59. package/dist/provider-auth.d.ts +4 -0
  60. package/dist/provider-auth.js +1 -1
  61. package/dist/reply-shape.d.ts +96 -0
  62. package/dist/reply-shape.js +452 -0
  63. package/dist/result-acts.d.ts +14 -0
  64. package/dist/result-acts.js +19 -0
  65. package/dist/retention.js +6 -0
  66. package/dist/serve.js +162 -55
  67. package/dist/slack-chat.d.ts +2 -0
  68. package/dist/slack-chat.js +30 -3
  69. package/dist/store.d.ts +60 -0
  70. package/dist/store.js +152 -3
  71. package/dist/surface.js +2 -2
  72. package/dist/task-status.d.ts +5 -15
  73. package/dist/task-status.js +18 -5
  74. package/dist/team-runtime.js +3 -2
  75. package/dist/teams-chat.js +16 -2
  76. package/dist/telegram-mate.d.ts +6 -1
  77. package/dist/telegram-mate.js +43 -11
  78. package/dist/telegram-progress.js +3 -2
  79. package/dist/telegram-team.js +6 -3
  80. package/dist/telegram.js +43 -3
  81. package/dist/test-temp.js +1 -1
  82. package/dist/when-html.d.ts +8 -3
  83. package/dist/when-html.js +23 -3
  84. package/dist/work-index.d.ts +2 -0
  85. package/dist/work-index.js +9 -6
  86. package/dist/work-summary.d.ts +1 -1
  87. package/dist/workspace-ui.d.ts +2 -14
  88. package/dist/workspace-ui.js +4 -22
  89. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
2
+ import { aboutYouOf, checkAboutYou, saveAboutYou, ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "./lead-about.js";
1
3
  import { withActor } from "./actor.js";
2
4
  import { repositoryContext, repositoryContextRead } from './repository-context.js';
3
5
  import { ADD_TESTS_ACTION, followUpChecksOf, requestFollowUpChecks, fileAddTestsTask, resultCheckLevel } from './result-follow-ups.js';
@@ -6,6 +8,7 @@ import { repositoryContextHtml } from './repository-context-ui.js';
6
8
  import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
7
9
  import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
8
10
  import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
11
+ import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
9
12
  import { startMaintenance } from './maintenance.js';
10
13
  import { codingHandoffPreview, createCodingHandoff } from './coding-handoff.js';
11
14
  import { codingShippingHtml, CODING_SHIPPING_CSS } from './coding-shipping-ui.js';
@@ -18,7 +21,7 @@ import { ChatState } from "./chat-delivery-state.js";
18
21
  import { slackSettingsHtml } from "./slack-settings.js";
19
22
  import { checkSlackCredentials, loadSlackCredentials, saveSlackCredentials, clearSlackCredentials, SLACK_MANIFEST, SlackError } from "./slack-api.js";
20
23
  import { SlackState } from "./slack-state.js";
21
- import { CHAT_ACTIONS, sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, mintSharedActionReview } from './chat-actions.js';
24
+ import { CHAT_ACTIONS, OWNER_ACTIONS, sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, mintSharedActionReview } from './chat-actions.js';
22
25
  import { changeSkills, githubSkill, importSkill, readSkillsSnapshot, reviseSkillTest, skillTestResult, skillsView, testSkill } from "./project-skills.js";
23
26
  import { skillsHtml, skillsScript, skillsSnapshotHtml, skillTestFeedbackHtml, SKILLS_CSS } from "./skills-ui.js";
24
27
  import { toolsHtml, TOOLS_CSS } from "./tools-ui.js";
@@ -65,7 +68,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
65
68
  import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
66
69
  import { leadActivity } from "./lead-voice.js";
67
70
  import { openWorkDecisionOf } from "./work-summary.js";
68
- import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, personCheckPending } from './assignment.js';
71
+ import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, owedAcceptance } from './assignment.js';
69
72
  import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
70
73
  import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
71
74
  import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
@@ -185,7 +188,7 @@ import { dirname } from "node:path";
185
188
  import { AsyncLocalStorage } from "node:async_hooks";
186
189
  import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
187
190
  import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
188
- import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
191
+ import { verifiedAuthor, LEAD_THREAD, isDigestTime, MATE_ASK_OTHER } from "./store.js";
189
192
  import { digestTimes } from "./digest-times.js";
190
193
  import { updateRepos, addRepos, removeRepos } from "./repos.js";
191
194
  import { run as execRun } from "./exec.js";
@@ -210,7 +213,7 @@ import { authorizedProject, canonicalProject, isGitRepo, projectName, sameRepo,
210
213
  import { tally, spendLine, runCostWords } from "./summary.js";
211
214
  import { classify, holdOwnerWords, attentionCardForUnverifiedDone } from "./board.js";
212
215
  import { approveRoutine, describeSchedule, fireRoutine, parseSchedule, refreshRoutineAgents, routineAgentsState, routineDigestOf, validateRoutineTerms, ROUTINE_NAME } from "./routine.js";
213
- import { effectivePrimary, isMessagingChannel, savePrimary } from "./webhooks.js";
216
+ import { effectivePrimary, isMessagingChannel, loadConsoleUrl, savePrimary } from "./webhooks.js";
214
217
  import { resolvePhaseAgent, resolveRoutineAuthority, INSTALLATION_SCOPE, routeOfTask, agentChoicesFor } from "./agentconfig.js";
215
218
  import { isRiskLevel, projectRoute, riskTitle, riskConsequence, chosenWords, agentsSummary, postureWords, RISK_CHOICES, RISK_LEVELS, PHASES as ROUTE_PHASES } from "./phase-routing.js";
216
219
  import { ALL_CREDENTIAL_ENV, isProviderId, reportsCost, PROVIDER_IDS, validModelId, validateSpec } from "./provider.js";
@@ -258,6 +261,7 @@ import { handleTeamsHttp } from "./teams.js";
258
261
  import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "./teams-api.js";
259
262
  import { verifyApproverByPassword, verifyApproverStanding } from "./principal.js";
260
263
  import { runMateTurn, MATE_MESSAGE_MAX_CHARS } from "./mate.js";
264
+ import { replyHtmlInline, shapeReply } from "./reply-shape.js";
261
265
  import { confirmCoordinatorProposal, confirmMateProposal, dismissCoordinatorProposal, dismissMateProposal } from "./mate-doors.js";
262
266
  import { envValue } from "./names.js";
263
267
  import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOff } from "./releases.js";
@@ -1527,6 +1531,8 @@ export function createDecisionServer(options) {
1527
1531
  actor: who.name,
1528
1532
  csrf: who.via === "cookie" ? who.session.csrf : "",
1529
1533
  returnTo: safeReturn(url.pathname + url.search),
1534
+ // This console's own addresses: a lead reply's links there read as "the task", "the result", "Settings → Lead".
1535
+ appOrigins: [consoleOrigin(request.headers.host), publicOrigin?.origin ?? null, options.configDir === undefined ? null : loadConsoleUrl(process.env, options.configDir)],
1530
1536
  // Every signed-in page shares the workspace shell; pages showing a one-time secret opt out (forceSensitive).
1531
1537
  browser: who.via === 'cookie',
1532
1538
  // v100: signed in with the identity provider: its label, and whether it checked them recently enough to stand in for a password.
@@ -1632,7 +1638,7 @@ export function createDecisionServer(options) {
1632
1638
  }
1633
1639
  if (admitted && proposal) {
1634
1640
  const taskId = typeof action?.request['task'] === 'string' ? action.request['task'] : typeof proposal.payload['task'] === 'string' ? proposal.payload['task'] : null;
1635
- const repo = action?.repo ?? (taskId ? store.lookupRef(taskId)?.repo : null) ?? (typeof proposal.payload['repo'] === 'string' ? proposal.payload['repo'] : null);
1641
+ const repo = (action?.repo || null) ?? (taskId ? store.lookupRef(taskId)?.repo : null) ?? (typeof proposal.payload['repo'] === 'string' ? proposal.payload['repo'] : null);
1636
1642
  return { repo, taskId, runId: typeof action?.request['run'] === 'number' ? action.request['run'] : null, action: action?.operation ?? `chat ${proposal.kind}` };
1637
1643
  }
1638
1644
  }
@@ -1697,6 +1703,9 @@ export function createDecisionServer(options) {
1697
1703
  }
1698
1704
  if (action && proposal && store.getMateThread(proposal.thread)?.approver === who.name && visible(action.repo) && store.accountCanAccess(who.name, action.repo))
1699
1705
  return true;
1706
+ // A card about the person themselves (what their lead knows about them) is in no project.
1707
+ if (action && proposal && action.repo === '' && OWNER_ACTIONS.has(action.operation) && store.getMateThread(proposal.thread)?.approver === who.name)
1708
+ return true;
1700
1709
  refuse(response, who, 404, 'No such action in your projects.', '/projects');
1701
1710
  return false;
1702
1711
  }
@@ -2465,6 +2474,7 @@ export function createDecisionServer(options) {
2465
2474
  csrf,
2466
2475
  canRetryReview: who.via === "cookie" && who.role === "approver",
2467
2476
  noted: url.searchParams.get("noted") !== null,
2477
+ refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
2468
2478
  tab: parseResultTab(url.searchParams.get("tab")),
2469
2479
  user: who.name,
2470
2480
  now,
@@ -2914,8 +2924,9 @@ export function createDecisionServer(options) {
2914
2924
  const version = url.searchParams.get("version");
2915
2925
  if (version !== null && !family.versions.some(one => one.id === version))
2916
2926
  return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
2927
+ // A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
2917
2928
  if (task.taskId !== family.root.id)
2918
- return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
2929
+ return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
2919
2930
  return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
2920
2931
  }
2921
2932
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
@@ -3482,7 +3493,7 @@ export function createDecisionServer(options) {
3482
3493
  const fragments = known === version
3483
3494
  ? null
3484
3495
  : {
3485
- thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id) }),
3496
+ thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id), chatProject }),
3486
3497
  after: mateAfterComposerHtml({ messages: rows.messages, pending: rows.pending, focusTask, csrf }),
3487
3498
  live: focusTask === null ? null : taskChatLiveRegion(focusTask, csrf, true, rows.pending !== null),
3488
3499
  };
@@ -4132,8 +4143,10 @@ export function createDecisionServer(options) {
4132
4143
  returnTo: "/settings/lead",
4133
4144
  };
4134
4145
  const signedIn = localSignIn?.states.claude === "connected" ? "Claude Code" : localSignIn?.states.codex === "connected" ? "Codex" : null;
4146
+ const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
4135
4147
  return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
4136
- said: url.searchParams.get("said") }), { chrome: chromeFor(project, "settings") }));
4148
+ said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises,
4149
+ about: aboutYouOf(store, who.name), aboutSaved: url.searchParams.get("saved") === "about" }), { chrome: chromeFor(project, "settings") }));
4137
4150
  }
4138
4151
  if (url.pathname === "/settings/telegram") {
4139
4152
  // Any approver pairs their OWN phone here; the bot token stays on /settings.
@@ -4432,8 +4445,16 @@ export function createDecisionServer(options) {
4432
4445
  // One reply runs at a time per person; this thread shows it only when
4433
4446
  // the reply is its own.
4434
4447
  const live = store.liveMateTurnFor(who.name);
4448
+ const messages = store.listMateMessages(opened.thread.id, 40);
4449
+ // The lead's question with buttons, while its reply is the last word in the thread.
4450
+ const last = messages.at(-1);
4451
+ const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
4452
+ // Answered questions stay readable above the answer; only their buttons go.
4453
+ const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
4435
4454
  return {
4436
- messages: store.listMateMessages(opened.thread.id, 40),
4455
+ messages,
4456
+ ask,
4457
+ asks,
4437
4458
  proposals,
4438
4459
  decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
4439
4460
  coordinatorProposals,
@@ -4508,7 +4529,7 @@ export function createDecisionServer(options) {
4508
4529
  const rows = mateConversationRows(who, principal, focusTask, now, chatProject);
4509
4530
  return {
4510
4531
  sessionId: session.id, user: session.approver, version: mateChatVersion({ ...rows, focusTask }),
4511
- messages: mateBrowserMessages(rows, who.session.csrf, back),
4532
+ messages: mateBrowserMessages(rows, who.session.csrf, back, { task: focusTask?.id ?? null, project: chatProject }),
4512
4533
  pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: MATE_MESSAGE_MAX_CHARS,
4513
4534
  taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
4514
4535
  };
@@ -4805,20 +4826,20 @@ export function createDecisionServer(options) {
4805
4826
  if (result.problem.startsWith("status-")) {
4806
4827
  // The provider ANSWERED with an error: nothing billed.
4807
4828
  finish({ state: "failed", failureReason: "provider-error", settledMicrousd: 0 });
4808
- tell(null, "the provider refused the request — nothing was billed");
4829
+ tell(null, "The chat provider turned the request down. Nothing was kept or charged. Try again in a minute.");
4809
4830
  }
4810
4831
  else if (result.problem === "timeout") {
4811
4832
  finish({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
4812
- tell(null, "the turn timed out; its cost is unknown and chat is blocked until you acknowledge it");
4833
+ tell(null, "The reply took too long and was stopped, and its cost isn't known yet, so chat is paused. Confirm the cost on this page to turn chat back on.");
4813
4834
  }
4814
4835
  else if (result.problem === "network") {
4815
4836
  finish({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true });
4816
- tell(null, "the provider could not be reached after dispatch; cost unknown — acknowledge to re-enable chat");
4837
+ tell(null, "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Confirm the cost on this page to turn chat back on.");
4817
4838
  }
4818
4839
  else {
4819
4840
  // 200 with an unusable wrapper: billed, amount unproven.
4820
4841
  finish({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
4821
- tell(null, "the provider's response was malformed and was discarded; cost unknown — acknowledge to re-enable chat");
4842
+ tell(null, "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Confirm the cost on this page to turn chat back on.");
4822
4843
  }
4823
4844
  return;
4824
4845
  }
@@ -4976,7 +4997,8 @@ export function createDecisionServer(options) {
4976
4997
  try {
4977
4998
  const project = s.chrome.active === 'chat' ? null : s.chrome.project;
4978
4999
  crew = extras.team?.tasks ? browserCrewFromIndex(extras.team.tasks, extras.team.selected?.id) : requestFacts.workCrew?.project === project ? browserCrewFromIndex(requestFacts.workCrew.page)
4979
- : browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false }, { evidenceRoot, project });
5000
+ // The reader's own lead's claims read "<name> is on it" here too.
5001
+ : browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false, viewer: requestFacts.actor ?? null }, { evidenceRoot, project });
4980
5002
  }
4981
5003
  catch {
4982
5004
  notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
@@ -5018,6 +5040,7 @@ export function createDecisionServer(options) {
5018
5040
  const request = requestFacts.workspaceRequest;
5019
5041
  const workspace = {
5020
5042
  version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
5043
+ ...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
5021
5044
  refreshUrl: path.pathname + path.search,
5022
5045
  receipt: conversation !== null && request && REQUEST_TOKEN.test(request)
5023
5046
  ? { request, received: store.mateRequestReceipt(conversation.sessionId, request) !== null } : null,
@@ -5123,7 +5146,7 @@ export function createDecisionServer(options) {
5123
5146
  // One line: what this person's lead is doing now and when it last acted.
5124
5147
  const activity = leadActivity(store, who.name);
5125
5148
  const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
5126
- const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
5149
+ const lead = activity === null ? null : { name: activity.name, doing: activity.doing, at: activity.at,
5127
5150
  href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
5128
5151
  return {
5129
5152
  agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
@@ -5224,7 +5247,7 @@ export function createDecisionServer(options) {
5224
5247
  };
5225
5248
  }
5226
5249
  function workAccess() {
5227
- // The reader's own lead's claims read "Your lead is on it" (lead-voice.ts); nobody else's.
5250
+ // The reader's own lead's claims read "<name> is on it" (lead-voice.ts); nobody else's.
5228
5251
  return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
5229
5252
  }
5230
5253
  // All project counts are read together. No task artifacts or processes are
@@ -9206,6 +9229,35 @@ export function createDecisionServer(options) {
9206
9229
  }
9207
9230
  // Turn the lead on with the agent signed in on this computer: its membership spends no dollars, so like starting a
9208
9231
  // conversation it needs no second password; the full form (Advanced) still asks for one.
9232
+ // Settings → Lead: the name and persona this person's lead speaks with.
9233
+ if (url.pathname === "/settings/lead/identity") {
9234
+ if (who.via !== "cookie" || who.role !== "approver")
9235
+ return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
9236
+ const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
9237
+ if (!checked.ok)
9238
+ return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
9239
+ store.setLeadConfig(who.name, checked.identity.name, checked.identity.persona, now);
9240
+ return redirect(response, "/settings/lead?saved=1");
9241
+ }
9242
+ // Settings → Lead: what this person's lead knows about them, one line each.
9243
+ if (url.pathname === "/settings/lead/about") {
9244
+ if (who.via !== "cookie" || who.role !== "approver")
9245
+ return refuse(response, who, 403, "An approver edits what their lead knows about them.", "/settings/lead");
9246
+ const checked = checkAboutYou(body.get("about") ?? "");
9247
+ if (!checked.ok)
9248
+ return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
9249
+ saveAboutYou(store, who.name, checked.lines, now);
9250
+ return redirect(response, "/settings/lead?saved=about");
9251
+ }
9252
+ // Settings → Lead: stop the lead following up on one of your promises.
9253
+ if (url.pathname === "/settings/lead/promise/cancel") {
9254
+ if (who.via !== "cookie" || who.role !== "approver")
9255
+ return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
9256
+ const id = Number(body.get("promise"));
9257
+ const done = Number.isSafeInteger(id) && cancelCommitment(store, who.name, id, who.name, "Cancelled in Settings.", now);
9258
+ // The promise leaving the list says it was cancelled; only a refusal needs words.
9259
+ return redirect(response, done ? "/settings/lead" : chatReturnWithSaid("/settings/lead", "That promise was already closed."));
9260
+ }
9209
9261
  if (url.pathname === "/settings/lead/on") {
9210
9262
  if (who.via !== "cookie" || who.role !== "approver")
9211
9263
  return refuse(response, who, 403, "An approver turns the lead on.", "/settings/lead");
@@ -9519,7 +9571,7 @@ export function createDecisionServer(options) {
9519
9571
  }
9520
9572
  const opened = store.openMateThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
9521
9573
  const onProgress = beginLiveTurn(opened.thread.id);
9522
- void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
9574
+ void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, channel: "console", ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
9523
9575
  .then(outcome => {
9524
9576
  if (!outcome.ok)
9525
9577
  noteMate(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
@@ -11019,8 +11071,14 @@ export function createDecisionServer(options) {
11019
11071
  throw error;
11020
11072
  saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
11021
11073
  }
11022
- if (!saved.ok)
11023
- return taskScreen(response, who, taskId, saved.message, saved.status, body);
11074
+ if (!saved.ok) {
11075
+ // Refused because the plan moved on: the draft comes back with the version now on file, so saving it
11076
+ // again (having read the reason) can succeed instead of being refused the same way.
11077
+ const draft = new URLSearchParams(body);
11078
+ if (saved.status === 409)
11079
+ draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
11080
+ return taskScreen(response, who, taskId, saved.message, saved.status, draft);
11081
+ }
11024
11082
  return redirect(response, taskHref(taskId));
11025
11083
  }
11026
11084
  case "approve": {
@@ -11135,8 +11193,13 @@ export function createDecisionServer(options) {
11135
11193
  note = validated.note;
11136
11194
  }
11137
11195
  // A failed task's result is accepted only anyway, on purpose: with a reason.
11138
- if (note === null && store.getTask(taskId)?.state === "failed")
11139
- return taskScreen(response, who, taskId, "Accepting anyway needs a reason.", 400);
11196
+ if (note === null && store.getTask(taskId)?.state === "failed") {
11197
+ // Posted from the result page: back there, where the reason field is, with the refusal said on it.
11198
+ const back = body.get("return") === null ? null : safeReturn(body.get("return"));
11199
+ if (back !== null && back.startsWith("/review?"))
11200
+ return redirect(response, withRefusal(back, "reason"));
11201
+ return taskScreen(response, who, taskId, ACCEPT_ANYWAY_NEEDS_REASON, 400);
11202
+ }
11140
11203
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
11141
11204
  return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
11142
11205
  }
@@ -11166,7 +11229,7 @@ export function createDecisionServer(options) {
11166
11229
  const publish = body.get("publish") === "1";
11167
11230
  const finished = acceptAndCompleteAsOperator(store, taskId, { runId: Number(namedRun), receiptDigest: digest, note }, principal, now, evidenceRoot, publish ? accepted => completeAndOpenPullRequest(store, { taskId, digest: accepted, runId: Number(namedRun), who: principal, root: evidenceRoot }, now) : undefined);
11168
11231
  if (!finished.ok)
11169
- return taskScreen(response, who, taskId, finished.message, 409);
11232
+ return taskScreen(response, who, taskId, finished.message, finished.reason === "needs-reason" ? 400 : 409);
11170
11233
  return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
11171
11234
  }
11172
11235
  if (body.get("publish") === "1") {
@@ -17432,13 +17495,13 @@ function chatHeading(copy, projectCount, showProjectToggle = true) {
17432
17495
  `${strokeIcon(`<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/>`)}<span>projects</span><span class="badge">${projectCount}</span></button>`
17433
17496
  : "") + `</div></div>`);
17434
17497
  }
17435
- /** A deliberately small rich-text grammar for model copy. Input is escaped
17436
- * before tags are introduced: headings, bullets, numbered steps, bold, and
17437
- * inline code are presentation only—never executable HTML or external links. */
17438
- function renderChatText(text) {
17439
- const inline = (value) => escape(value)
17440
- .replace(/`([^`\n]{1,240})`/g, "<code>$1</code>")
17441
- .replace(/\*\*([^*\n]{1,500})\*\*/g, "<strong>$1</strong>");
17498
+ /** A deliberately small rich-text grammar for model copy. The reply is shaped first (the lead's voice, enforced:
17499
+ * no headers, at most three bold anchors, labelled links, no internal ids unless `asked` wanted them); input is
17500
+ * escaped before tags are introduced: bullets, numbered steps, bold, inline code and labelled http(s) links are
17501
+ * presentation only—never executable HTML. */
17502
+ function renderChatText(raw, asked) {
17503
+ const inline = replyHtmlInline;
17504
+ const text = shapeReply(raw, { appOrigin: requestContext.getStore()?.appOrigins ?? null, ...(asked === undefined ? {} : { asked }) });
17442
17505
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
17443
17506
  const out = [];
17444
17507
  let paragraph = [];
@@ -17456,15 +17519,9 @@ function renderChatText(text) {
17456
17519
  list = null;
17457
17520
  };
17458
17521
  for (const line of lines) {
17459
- const heading = /^(?:#{1,3}\s+)(.+)$/.exec(line);
17460
17522
  const bullet = /^\s*[-*]\s+(.+)$/.exec(line);
17461
17523
  const numbered = /^\s*(\d{1,9})[.)]\s+(.+)$/.exec(line);
17462
- if (heading !== null) {
17463
- flushParagraph();
17464
- closeList();
17465
- out.push(`<h3>${inline(heading[1] ?? "")}</h3>`);
17466
- }
17467
- else if (bullet !== null || numbered !== null) {
17524
+ if (bullet !== null || numbered !== null) {
17468
17525
  flushParagraph();
17469
17526
  const wanted = bullet !== null ? "ul" : "ol";
17470
17527
  if (list !== wanted) {
@@ -17548,6 +17605,20 @@ function leadSettingsHtml(data) {
17548
17605
  `<p><a href="/settings">Settings</a></p><h1>Lead</h1>`,
17549
17606
  data.said === null ? "" : `<p class="problem" role="status">${escape(data.said)}</p>`,
17550
17607
  `<section class="card lead-settings" data-lead-settings>${summary}</section>`,
17608
+ data.identity === undefined ? "" : `<form method="post" action="/settings/lead/identity" class="card lead-identity" data-lead-identity>` +
17609
+ `<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
17610
+ `<label>Name your lead<input name="name" value="${escape(data.identity.name)}" maxlength="${LEAD_NAME_MAX}" autocomplete="off" required></label>` +
17611
+ `<label>Persona<textarea name="persona" rows="4" maxlength="${LEAD_PERSONA_MAX}">${escape(data.identity.persona)}</textarea></label>` +
17612
+ `<button type="submit">Save</button>${data.saved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
17613
+ // What the lead knows about this person: lines they confirmed in chat or wrote here. Every project, every chat.
17614
+ data.about === undefined ? "" : `<form method="post" action="/settings/lead/about" class="card lead-about" data-lead-about>` +
17615
+ `<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
17616
+ `<label>What your lead knows about you<textarea name="about" rows="${Math.min(10, Math.max(4, data.about.length + 1))}" maxlength="${ABOUT_YOU_MAX_LINES * (ABOUT_YOU_LINE_MAX + 1)}" placeholder="Keep copy terse.&#10;I test changes myself.">${escape(data.about.join("\n"))}</textarea></label>` +
17617
+ `<p class="meta">One per line, up to ${ABOUT_YOU_MAX_LINES}.</p>` +
17618
+ `<button type="submit">Save</button>${data.aboutSaved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
17619
+ // What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
17620
+ (data.promises ?? []).length === 0 ? "" : `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => `<li data-promise="${one.id}"><p>${escape(one.what)}</p><p class="meta">${escape(one.when.charAt(0).toUpperCase() + one.when.slice(1))} · <span class="nowrap">until ${escape(new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))}</span></p>` +
17621
+ `<form method="post" action="/settings/lead/promise/cancel" class="inline">${hidden}<input type="hidden" name="promise" value="${one.id}"><button type="submit" class="secondary">Cancel</button></form></li>`).join("")}</ul></section>`,
17551
17622
  `<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
17552
17623
  leadConfigForm(config, facts),
17553
17624
  config === null ? "" : `<form method="post" action="/chat/config" class="inline">${hidden}<input type="hidden" name="off" value="1">` +
@@ -17646,8 +17717,8 @@ function chatPage(chrome, data) {
17646
17717
  parts.push(data.coordinatorProposals);
17647
17718
  parts.push(data.focusTask === null && !data.catchUp ? `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, data.csrf, 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 } }));
17648
17719
  for (const turn of data.latched) {
17649
- parts.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
17650
- `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
17720
+ parts.push(`<div class="problem"><strong>Chat is paused.</strong> An earlier reply stopped before its cost was known; it may have cost up to ${chatMoney(turn.reservedMicrousd)}. ` +
17721
+ `<a href="/chat/ack/${turn.id}">Confirm that cost</a> to turn chat back on.</div>`);
17651
17722
  }
17652
17723
  if (data.pending !== null) {
17653
17724
  parts.push(chatWorkingHtml({ details: `Turn #${data.pending.id}${subscription ? " · subscription" : ` · up to ${chatMoney(data.pending.reservedMicrousd)} reserved`}` }));
@@ -18041,13 +18112,35 @@ function coordinatorProposalsSection(proposals, decisions, csrf, now, heading =
18041
18112
  proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
18042
18113
  `</div>`);
18043
18114
  }
18115
+ /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
18116
+ * "Something else" moves to the composer. Nothing shows while a reply is running. */
18117
+ function mateAskHtml(ask, csrf, target, composer, open = true) {
18118
+ if (!open)
18119
+ return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p></div>`;
18120
+ const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
18121
+ (target.task !== null ? `<input type="hidden" name="task" value="${escape(target.task)}">` : target.project !== null ? `<input type="hidden" name="project" value="${escape(target.project)}">` : "");
18122
+ return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p>` +
18123
+ `<div class="so-owner-ask-options" role="group" aria-label="Answer options">` +
18124
+ ask.options.map(option => `<form method="post" action="/chat" class="inline">${hidden}<button type="submit" name="message" value="${escape(option)}" class="so-suggestion quiet">${escape(option)}</button></form>`).join("") +
18125
+ `<label for="${escape(composer)}" class="so-suggestion so-owner-ask-other">${escape(MATE_ASK_OTHER)}</label></div></div>`;
18126
+ }
18044
18127
  /** The thread's messages as the React conversation renders them, each
18045
18128
  * with the proposal cards its turn produced; `back` is where a card's
18046
18129
  * confirm or dismiss returns. */
18047
- function mateBrowserMessages(rows, csrf, back) {
18130
+ function mateBrowserMessages(rows, csrf, back, target = { task: null, project: null }) {
18131
+ const open = rows.pending === null ? rows.ask?.turn ?? null : null;
18132
+ const asked = (turn) => {
18133
+ const ask = turn === null ? undefined : rows.asks?.get(turn);
18134
+ return ask === undefined ? "" : mateAskHtml(ask, csrf, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
18135
+ };
18136
+ // The owner's message each reply answers: when it asked for ids, the reply keeps them.
18137
+ const askedBy = new Map();
18138
+ rows.messages.reduce((last, message) => { if (message.role === 'operator')
18139
+ return message.text; if (last !== undefined)
18140
+ askedBy.set(message.id, last); return last; }, undefined);
18048
18141
  return rows.messages.map(message => ({
18049
18142
  id: message.id, role: message.role, text: message.text,
18050
- html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text),
18143
+ html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text, askedBy.get(message.id)) + asked(message.turn),
18051
18144
  activity: message.activity, createdAt: message.createdAt,
18052
18145
  ...(() => {
18053
18146
  const parts = message.turn === null ? [] : rows.proposals.filter(one => one.turn === message.turn)
@@ -18070,6 +18163,7 @@ function mateChatVersion(rows) {
18070
18163
  rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
18071
18164
  [...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
18072
18165
  rows.pending === null ? null : [rows.pending.id, rows.pending.steps],
18166
+ rows.ask?.turn ?? null,
18073
18167
  rows.recent[0] === undefined ? null : [rows.recent[0].id, rows.recent[0].state, rows.recent[0].failureReason],
18074
18168
  focus === null
18075
18169
  ? null
@@ -18117,14 +18211,20 @@ function mateThreadHtml(data) {
18117
18211
  parts.push(`<div class="chat-empty" data-key="empty"><strong>${data.focusTask === null ? "What do you want to get done?" : "What do you want to understand or change?"}</strong>` +
18118
18212
  `<p class="meta">${data.focusTask === null ? "Describe a task or ask about your projects." : "Ask about progress, review results, or adjust the plan."}</p></div>`);
18119
18213
  }
18214
+ let asked;
18120
18215
  for (const message of data.messages) {
18121
18216
  if (message.role === "operator") {
18217
+ asked = message.text;
18122
18218
  parts.push(`<div class="msg op" data-message-role="operator" data-key="m${message.id}"><p style="white-space:pre-wrap">${escape(message.text)}</p></div>`);
18123
18219
  continue;
18124
18220
  }
18125
18221
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
18126
18222
  parts.push(`<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? ' id="latest"' : ""}>` +
18127
- renderChatText(message.text) +
18223
+ renderChatText(message.text, asked) +
18224
+ (() => {
18225
+ const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
18226
+ return ask === undefined ? "" : mateAskHtml(ask, data.csrf, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn);
18227
+ })() +
18128
18228
  cards.map(one => mateProposalCard(one, data.csrf, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo)).join("") +
18129
18229
  `<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${escape(message.createdAt)}">${escape(relativeAge(message.createdAt, data.now))}</time></div>` +
18130
18230
  `</div>`);
@@ -18180,17 +18280,17 @@ function matePage(chrome, data) {
18180
18280
  if (data.problem !== null)
18181
18281
  conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
18182
18282
  for (const turn of data.latched) {
18183
- conversation.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
18184
- `<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
18283
+ conversation.push(`<div class="problem"><strong>Chat is paused.</strong> An earlier reply stopped before its cost was known; it may have cost up to ${chatMoney(turn.reservedMicrousd)}. ` +
18284
+ `<a href="/chat/ack/${turn.id}">Confirm that cost</a> to turn chat back on.</div>`);
18185
18285
  }
18186
18286
  const lastMessage = data.messages.at(-1);
18187
18287
  const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
18188
- conversation.push(mateThreadHtml({ ...data, problem: null }));
18288
+ conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
18189
18289
  conversation.push(
18190
18290
  // The New update action (package 2): hidden until a live update lands
18191
18291
  // while the reader is above the latest message; a real button, so the
18192
18292
  // keyboard reaches it. Only this act moves the reader.
18193
- `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" 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="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>Message<textarea name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>Send</button>`, `</form>`,
18293
+ `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" 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="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<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 ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>Send</button>`, `</form>`,
18194
18294
  // Concise pass (2026-09-13): the status line speaks only when there is
18195
18295
  // a state to report — a reply in progress here, the connection from
18196
18296
  // the continuity script — and the composer carries no second intro.
@@ -18202,7 +18302,7 @@ function matePage(chrome, data) {
18202
18302
  workspace: {
18203
18303
  conversation: {
18204
18304
  sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
18205
- messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo),
18305
+ messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
18206
18306
  pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
18207
18307
  taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
18208
18308
  },
@@ -19529,6 +19629,15 @@ function accentHead() {
19529
19629
  }
19530
19630
  const requestContext = new AsyncLocalStorage();
19531
19631
  /** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
19632
+ /** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
19633
+ const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
19634
+ const RESULT_REFUSALS = { reason: ACCEPT_ANYWAY_NEEDS_REASON };
19635
+ /** A same-site page address with a refusal code added, before any fragment. */
19636
+ function withRefusal(href, code) {
19637
+ const at = href.indexOf("#");
19638
+ const [path, hash] = at === -1 ? [href, ""] : [href.slice(0, at), href.slice(at)];
19639
+ return `${path}${path.includes("?") ? "&" : "?"}refused=${code}${hash}`;
19640
+ }
19532
19641
  function safeReturn(raw) {
19533
19642
  if (raw === null || raw === undefined)
19534
19643
  return "/";
@@ -21779,12 +21888,13 @@ function reviewCockpitPage(chrome, data) {
21779
21888
  capped: queue.length >= data.queueCap ? data.queueCap : null,
21780
21889
  missing: data.missing === null ? null : `No completed task ${data.missing} is in view here — it may not be finished, or it is outside this console's projects.`,
21781
21890
  beyond: data.beyondQueue && selected !== null,
21782
- selected: detailParts === null ? null : detailParts.selected,
21891
+ selected: detailParts === null ? null : data.refusal == null ? detailParts.selected : { ...detailParts.selected, problem: data.refusal },
21783
21892
  };
21784
21893
  return screen("review", [
21785
21894
  selected === null ? `<h1>Results</h1>` : "",
21786
21895
  missingNote,
21787
21896
  beyondNote,
21897
+ data.refusal == null || selected === null ? "" : `<p class="problem" data-result-refusal>${escape(data.refusal)}</p>`,
21788
21898
  `<div class="cockpit">${queuePane}${detail}</div>`,
21789
21899
  ].join("\n"), {
21790
21900
  chrome,
@@ -22011,14 +22121,11 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
22011
22121
  },
22012
22122
  };
22013
22123
  }
22014
- /** What Accept and finish owes before it completes, as the form asks for it: the person's own check (no
22015
- * reason), an exception a report that doesn't match its changes needs a reason for, or nothing. */
22124
+ /** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
22125
+ * (owedAcceptance), so the form asks for exactly what the completion requires. */
22016
22126
  function owedAcceptanceOf(receipt) {
22017
- if (receipt === null || receipt.proofAcceptance !== null)
22018
- return null;
22019
- if (personCheckPending(receipt))
22020
- return { note: null };
22021
- return cantAcceptYetOf(receipt.proof?.verdict ?? null, receipt.proof?.reasons ?? [], false) === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
22127
+ const owed = owedAcceptance(receipt);
22128
+ return owed === null ? null : owed === "person-check" ? { note: null } : { note: "Why is this safe to accept?" };
22022
22129
  }
22023
22130
  function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
22024
22131
  // Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
@@ -25,4 +25,6 @@ export declare const applySlackAction: (options: SlackChatOptions, event: SlackE
25
25
  export declare const planSlackNotifications: (options: SlackChatOptions) => Promise<void>;
26
26
  export declare const planSlackRooms: (options: SlackChatOptions) => Promise<void>;
27
27
  export declare function slackBlocks(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
28
+ /** The lead's reply as mrkdwn sections: no header block, so its first line stays an ordinary line. */
29
+ export declare function slackVoiceBlocks(mrkdwn: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
28
30
  export declare function deliverSlackPart(options: SlackChatOptions): Promise<boolean>;