toolroll 0.9.15 → 0.9.16

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 (67) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/assignment-brief.d.ts +10 -0
  3. package/dist/assignment-brief.js +49 -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-ask.d.ts +28 -0
  11. package/dist/chat-ask.js +77 -0
  12. package/dist/chat-channel.js +6 -1
  13. package/dist/chat-controls.d.ts +8 -0
  14. package/dist/chat-controls.js +3 -0
  15. package/dist/chat-delivery-state.d.ts +6 -1
  16. package/dist/chat-delivery-state.js +15 -2
  17. package/dist/chat-delivery.js +20 -3
  18. package/dist/chat-quiet.js +4 -2
  19. package/dist/chat-voice.d.ts +3 -6
  20. package/dist/chat-voice.js +8 -2
  21. package/dist/discord-chat.js +10 -0
  22. package/dist/lead-commitments.d.ts +84 -0
  23. package/dist/lead-commitments.js +208 -0
  24. package/dist/lead-context.d.ts +32 -3
  25. package/dist/lead-context.js +133 -19
  26. package/dist/lead-follow.js +3 -0
  27. package/dist/lead-identity.d.ts +23 -0
  28. package/dist/lead-identity.js +33 -0
  29. package/dist/lead-voice.d.ts +12 -9
  30. package/dist/lead-voice.js +25 -16
  31. package/dist/ledger-view.js +3 -2
  32. package/dist/mate-cli.d.ts +1 -1
  33. package/dist/mate-cli.js +8 -2
  34. package/dist/mate-contract.d.ts +1 -1
  35. package/dist/mate-contract.js +11 -4
  36. package/dist/mate-progress.js +5 -0
  37. package/dist/mate-tools.d.ts +18 -1
  38. package/dist/mate-tools.js +250 -6
  39. package/dist/mate.d.ts +9 -0
  40. package/dist/mate.js +34 -38
  41. package/dist/operate.js +2 -1
  42. package/dist/project-delete.js +3 -1
  43. package/dist/result-acts.d.ts +14 -0
  44. package/dist/result-acts.js +19 -0
  45. package/dist/retention.js +6 -0
  46. package/dist/serve.js +111 -29
  47. package/dist/slack-chat.js +10 -0
  48. package/dist/store.d.ts +51 -0
  49. package/dist/store.js +115 -1
  50. package/dist/surface.js +2 -2
  51. package/dist/task-status.d.ts +5 -15
  52. package/dist/task-status.js +18 -5
  53. package/dist/team-runtime.js +2 -1
  54. package/dist/teams-chat.js +4 -1
  55. package/dist/telegram-mate.d.ts +4 -1
  56. package/dist/telegram-mate.js +17 -1
  57. package/dist/telegram-progress.js +3 -2
  58. package/dist/telegram.js +43 -3
  59. package/dist/test-temp.js +1 -1
  60. package/dist/when-html.d.ts +8 -3
  61. package/dist/when-html.js +23 -3
  62. package/dist/work-index.d.ts +2 -0
  63. package/dist/work-index.js +9 -6
  64. package/dist/work-summary.d.ts +1 -1
  65. package/dist/workspace-ui.d.ts +2 -14
  66. package/dist/workspace-ui.js +4 -22
  67. package/package.json +1 -1
@@ -23,6 +23,25 @@
23
23
  * which act is ink; the page recomputes it as the person answers each check. */
24
24
  /** The words over the reason field an Accept of a refuted result requires. */
25
25
  export const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
26
+ export const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
27
+ export function evidenceProblemOf(verdict, reasons) {
28
+ if (verdict === "verified" || verdict === "attested")
29
+ return "none";
30
+ if (verdict === "refuted")
31
+ return reasons.some(reason => FAILED_CHECK.test(reason)) ? "checks-failed" : "mismatched";
32
+ return "missing";
33
+ }
34
+ /** Why a result can't be accepted as it stands, in one plain line, or null
35
+ * when nothing refutes it (a person's acceptance settles it). The result page
36
+ * shows it before Request changes, its one ink act. */
37
+ export function cantAcceptYetOf(verdict, reasons, accepted) {
38
+ if (accepted || verdict !== "refuted")
39
+ return null;
40
+ // A mismatch is the status card's own headline; here only what accepting takes.
41
+ return evidenceProblemOf(verdict, reasons) === "checks-failed"
42
+ ? "Can't accept yet: the project's check failed on these changes."
43
+ : ACCEPT_NEEDS_REASON;
44
+ }
26
45
  export function resultActsOf(facts) {
27
46
  const request = facts.canRequest ? "request-changes" : null;
28
47
  const accept = facts.accept === null ? null : "accept";
package/dist/retention.js CHANGED
@@ -263,11 +263,17 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
263
263
  if (periods.chat !== null) {
264
264
  const team = db.prepare("DELETE FROM team_message WHERE message = ? AND status NOT IN ('queued', 'running', 'uncertain')");
265
265
  const message = db.prepare("DELETE FROM mate_message WHERE id = ? AND NOT EXISTS (SELECT 1 FROM team_message WHERE message = ?)");
266
+ const asked = db.prepare("SELECT turn FROM mate_message WHERE id = ? AND role = 'assistant' AND turn IS NOT NULL");
267
+ // The lead's question goes with the reply that asked it; its buttons then say it expired.
268
+ const ask = db.prepare("DELETE FROM mate_ask WHERE turn = ? AND NOT EXISTS (SELECT 1 FROM mate_message WHERE turn = ? AND role = 'assistant')");
266
269
  for (const one of plan.items.chat) {
267
270
  team.run(one.id);
271
+ const turn = asked.get(one.id)?.["turn"];
268
272
  if (Number(message.run(one.id, one.id).changes) === 1) {
269
273
  done.chat.count++;
270
274
  done.chat.bytes += one.bytes;
275
+ if (turn !== undefined && turn !== null)
276
+ ask.run(Number(turn), Number(turn));
271
277
  }
272
278
  }
273
279
  }
package/dist/serve.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
1
2
  import { withActor } from "./actor.js";
2
3
  import { repositoryContext, repositoryContextRead } from './repository-context.js';
3
4
  import { ADD_TESTS_ACTION, followUpChecksOf, requestFollowUpChecks, fileAddTestsTask, resultCheckLevel } from './result-follow-ups.js';
@@ -6,6 +7,7 @@ import { repositoryContextHtml } from './repository-context-ui.js';
6
7
  import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
7
8
  import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
8
9
  import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
10
+ import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
9
11
  import { startMaintenance } from './maintenance.js';
10
12
  import { codingHandoffPreview, createCodingHandoff } from './coding-handoff.js';
11
13
  import { codingShippingHtml, CODING_SHIPPING_CSS } from './coding-shipping-ui.js';
@@ -65,7 +67,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
65
67
  import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
66
68
  import { leadActivity } from "./lead-voice.js";
67
69
  import { openWorkDecisionOf } from "./work-summary.js";
68
- import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, personCheckPending } from './assignment.js';
70
+ import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, owedAcceptance } from './assignment.js';
69
71
  import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
70
72
  import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
71
73
  import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
@@ -185,7 +187,7 @@ import { dirname } from "node:path";
185
187
  import { AsyncLocalStorage } from "node:async_hooks";
186
188
  import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
187
189
  import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
188
- import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
190
+ import { verifiedAuthor, LEAD_THREAD, isDigestTime, MATE_ASK_OTHER } from "./store.js";
189
191
  import { digestTimes } from "./digest-times.js";
190
192
  import { updateRepos, addRepos, removeRepos } from "./repos.js";
191
193
  import { run as execRun } from "./exec.js";
@@ -2465,6 +2467,7 @@ export function createDecisionServer(options) {
2465
2467
  csrf,
2466
2468
  canRetryReview: who.via === "cookie" && who.role === "approver",
2467
2469
  noted: url.searchParams.get("noted") !== null,
2470
+ refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
2468
2471
  tab: parseResultTab(url.searchParams.get("tab")),
2469
2472
  user: who.name,
2470
2473
  now,
@@ -2914,8 +2917,9 @@ export function createDecisionServer(options) {
2914
2917
  const version = url.searchParams.get("version");
2915
2918
  if (version !== null && !family.versions.some(one => one.id === version))
2916
2919
  return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
2920
+ // 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
2921
  if (task.taskId !== family.root.id)
2918
- return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
2922
+ return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
2919
2923
  return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
2920
2924
  }
2921
2925
  // v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
@@ -3482,7 +3486,7 @@ export function createDecisionServer(options) {
3482
3486
  const fragments = known === version
3483
3487
  ? null
3484
3488
  : {
3485
- thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id) }),
3489
+ thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id), chatProject }),
3486
3490
  after: mateAfterComposerHtml({ messages: rows.messages, pending: rows.pending, focusTask, csrf }),
3487
3491
  live: focusTask === null ? null : taskChatLiveRegion(focusTask, csrf, true, rows.pending !== null),
3488
3492
  };
@@ -4132,8 +4136,9 @@ export function createDecisionServer(options) {
4132
4136
  returnTo: "/settings/lead",
4133
4137
  };
4134
4138
  const signedIn = localSignIn?.states.claude === "connected" ? "Claude Code" : localSignIn?.states.codex === "connected" ? "Codex" : null;
4139
+ const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
4135
4140
  return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
4136
- said: url.searchParams.get("said") }), { chrome: chromeFor(project, "settings") }));
4141
+ said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises }), { chrome: chromeFor(project, "settings") }));
4137
4142
  }
4138
4143
  if (url.pathname === "/settings/telegram") {
4139
4144
  // Any approver pairs their OWN phone here; the bot token stays on /settings.
@@ -4432,8 +4437,16 @@ export function createDecisionServer(options) {
4432
4437
  // One reply runs at a time per person; this thread shows it only when
4433
4438
  // the reply is its own.
4434
4439
  const live = store.liveMateTurnFor(who.name);
4440
+ const messages = store.listMateMessages(opened.thread.id, 40);
4441
+ // The lead's question with buttons, while its reply is the last word in the thread.
4442
+ const last = messages.at(-1);
4443
+ const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
4444
+ // Answered questions stay readable above the answer; only their buttons go.
4445
+ 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
4446
  return {
4436
- messages: store.listMateMessages(opened.thread.id, 40),
4447
+ messages,
4448
+ ask,
4449
+ asks,
4437
4450
  proposals,
4438
4451
  decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
4439
4452
  coordinatorProposals,
@@ -4508,7 +4521,7 @@ export function createDecisionServer(options) {
4508
4521
  const rows = mateConversationRows(who, principal, focusTask, now, chatProject);
4509
4522
  return {
4510
4523
  sessionId: session.id, user: session.approver, version: mateChatVersion({ ...rows, focusTask }),
4511
- messages: mateBrowserMessages(rows, who.session.csrf, back),
4524
+ messages: mateBrowserMessages(rows, who.session.csrf, back, { task: focusTask?.id ?? null, project: chatProject }),
4512
4525
  pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: MATE_MESSAGE_MAX_CHARS,
4513
4526
  taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
4514
4527
  };
@@ -4976,7 +4989,8 @@ export function createDecisionServer(options) {
4976
4989
  try {
4977
4990
  const project = s.chrome.active === 'chat' ? null : s.chrome.project;
4978
4991
  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 });
4992
+ // The reader's own lead's claims read "<name> is on it" here too.
4993
+ : browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false, viewer: requestFacts.actor ?? null }, { evidenceRoot, project });
4980
4994
  }
4981
4995
  catch {
4982
4996
  notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
@@ -5018,6 +5032,7 @@ export function createDecisionServer(options) {
5018
5032
  const request = requestFacts.workspaceRequest;
5019
5033
  const workspace = {
5020
5034
  version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
5035
+ ...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
5021
5036
  refreshUrl: path.pathname + path.search,
5022
5037
  receipt: conversation !== null && request && REQUEST_TOKEN.test(request)
5023
5038
  ? { request, received: store.mateRequestReceipt(conversation.sessionId, request) !== null } : null,
@@ -5123,7 +5138,7 @@ export function createDecisionServer(options) {
5123
5138
  // One line: what this person's lead is doing now and when it last acted.
5124
5139
  const activity = leadActivity(store, who.name);
5125
5140
  const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
5126
- const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
5141
+ const lead = activity === null ? null : { name: activity.name, doing: activity.doing, at: activity.at,
5127
5142
  href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
5128
5143
  return {
5129
5144
  agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
@@ -5224,7 +5239,7 @@ export function createDecisionServer(options) {
5224
5239
  };
5225
5240
  }
5226
5241
  function workAccess() {
5227
- // The reader's own lead's claims read "Your lead is on it" (lead-voice.ts); nobody else's.
5242
+ // The reader's own lead's claims read "<name> is on it" (lead-voice.ts); nobody else's.
5228
5243
  return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
5229
5244
  }
5230
5245
  // All project counts are read together. No task artifacts or processes are
@@ -9206,6 +9221,25 @@ export function createDecisionServer(options) {
9206
9221
  }
9207
9222
  // Turn the lead on with the agent signed in on this computer: its membership spends no dollars, so like starting a
9208
9223
  // conversation it needs no second password; the full form (Advanced) still asks for one.
9224
+ // Settings → Lead: the name and persona this person's lead speaks with.
9225
+ if (url.pathname === "/settings/lead/identity") {
9226
+ if (who.via !== "cookie" || who.role !== "approver")
9227
+ return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
9228
+ const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
9229
+ if (!checked.ok)
9230
+ return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
9231
+ store.setLeadConfig(who.name, checked.identity.name, checked.identity.persona, now);
9232
+ return redirect(response, "/settings/lead?saved=1");
9233
+ }
9234
+ // Settings → Lead: stop the lead following up on one of your promises.
9235
+ if (url.pathname === "/settings/lead/promise/cancel") {
9236
+ if (who.via !== "cookie" || who.role !== "approver")
9237
+ return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
9238
+ const id = Number(body.get("promise"));
9239
+ const done = Number.isSafeInteger(id) && cancelCommitment(store, who.name, id, who.name, "Cancelled in Settings.", now);
9240
+ // The promise leaving the list says it was cancelled; only a refusal needs words.
9241
+ return redirect(response, done ? "/settings/lead" : chatReturnWithSaid("/settings/lead", "That promise was already closed."));
9242
+ }
9209
9243
  if (url.pathname === "/settings/lead/on") {
9210
9244
  if (who.via !== "cookie" || who.role !== "approver")
9211
9245
  return refuse(response, who, 403, "An approver turns the lead on.", "/settings/lead");
@@ -9519,7 +9553,7 @@ export function createDecisionServer(options) {
9519
9553
  }
9520
9554
  const opened = store.openMateThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
9521
9555
  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 })
9556
+ 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
9557
  .then(outcome => {
9524
9558
  if (!outcome.ok)
9525
9559
  noteMate(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
@@ -11019,8 +11053,14 @@ export function createDecisionServer(options) {
11019
11053
  throw error;
11020
11054
  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
11055
  }
11022
- if (!saved.ok)
11023
- return taskScreen(response, who, taskId, saved.message, saved.status, body);
11056
+ if (!saved.ok) {
11057
+ // Refused because the plan moved on: the draft comes back with the version now on file, so saving it
11058
+ // again (having read the reason) can succeed instead of being refused the same way.
11059
+ const draft = new URLSearchParams(body);
11060
+ if (saved.status === 409)
11061
+ draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
11062
+ return taskScreen(response, who, taskId, saved.message, saved.status, draft);
11063
+ }
11024
11064
  return redirect(response, taskHref(taskId));
11025
11065
  }
11026
11066
  case "approve": {
@@ -11135,8 +11175,13 @@ export function createDecisionServer(options) {
11135
11175
  note = validated.note;
11136
11176
  }
11137
11177
  // 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);
11178
+ if (note === null && store.getTask(taskId)?.state === "failed") {
11179
+ // Posted from the result page: back there, where the reason field is, with the refusal said on it.
11180
+ const back = body.get("return") === null ? null : safeReturn(body.get("return"));
11181
+ if (back !== null && back.startsWith("/review?"))
11182
+ return redirect(response, withRefusal(back, "reason"));
11183
+ return taskScreen(response, who, taskId, ACCEPT_ANYWAY_NEEDS_REASON, 400);
11184
+ }
11140
11185
  store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
11141
11186
  return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
11142
11187
  }
@@ -11166,7 +11211,7 @@ export function createDecisionServer(options) {
11166
11211
  const publish = body.get("publish") === "1";
11167
11212
  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
11213
  if (!finished.ok)
11169
- return taskScreen(response, who, taskId, finished.message, 409);
11214
+ return taskScreen(response, who, taskId, finished.message, finished.reason === "needs-reason" ? 400 : 409);
11170
11215
  return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
11171
11216
  }
11172
11217
  if (body.get("publish") === "1") {
@@ -17548,6 +17593,14 @@ function leadSettingsHtml(data) {
17548
17593
  `<p><a href="/settings">Settings</a></p><h1>Lead</h1>`,
17549
17594
  data.said === null ? "" : `<p class="problem" role="status">${escape(data.said)}</p>`,
17550
17595
  `<section class="card lead-settings" data-lead-settings>${summary}</section>`,
17596
+ data.identity === undefined ? "" : `<form method="post" action="/settings/lead/identity" class="card lead-identity" data-lead-identity>` +
17597
+ `<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
17598
+ `<label>Name your lead<input name="name" value="${escape(data.identity.name)}" maxlength="${LEAD_NAME_MAX}" autocomplete="off" required></label>` +
17599
+ `<label>Persona<textarea name="persona" rows="4" maxlength="${LEAD_PERSONA_MAX}">${escape(data.identity.persona)}</textarea></label>` +
17600
+ `<button type="submit">Save</button>${data.saved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
17601
+ // What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
17602
+ (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>` +
17603
+ `<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
17604
  `<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
17552
17605
  leadConfigForm(config, facts),
17553
17606
  config === null ? "" : `<form method="post" action="/chat/config" class="inline">${hidden}<input type="hidden" name="off" value="1">` +
@@ -18041,13 +18094,30 @@ function coordinatorProposalsSection(proposals, decisions, csrf, now, heading =
18041
18094
  proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
18042
18095
  `</div>`);
18043
18096
  }
18097
+ /** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
18098
+ * "Something else" moves to the composer. Nothing shows while a reply is running. */
18099
+ function mateAskHtml(ask, csrf, target, composer, open = true) {
18100
+ if (!open)
18101
+ return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p></div>`;
18102
+ const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
18103
+ (target.task !== null ? `<input type="hidden" name="task" value="${escape(target.task)}">` : target.project !== null ? `<input type="hidden" name="project" value="${escape(target.project)}">` : "");
18104
+ return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p>` +
18105
+ `<div class="so-owner-ask-options" role="group" aria-label="Answer options">` +
18106
+ 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("") +
18107
+ `<label for="${escape(composer)}" class="so-suggestion so-owner-ask-other">${escape(MATE_ASK_OTHER)}</label></div></div>`;
18108
+ }
18044
18109
  /** The thread's messages as the React conversation renders them, each
18045
18110
  * with the proposal cards its turn produced; `back` is where a card's
18046
18111
  * confirm or dismiss returns. */
18047
- function mateBrowserMessages(rows, csrf, back) {
18112
+ function mateBrowserMessages(rows, csrf, back, target = { task: null, project: null }) {
18113
+ const open = rows.pending === null ? rows.ask?.turn ?? null : null;
18114
+ const asked = (turn) => {
18115
+ const ask = turn === null ? undefined : rows.asks?.get(turn);
18116
+ return ask === undefined ? "" : mateAskHtml(ask, csrf, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
18117
+ };
18048
18118
  return rows.messages.map(message => ({
18049
18119
  id: message.id, role: message.role, text: message.text,
18050
- html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text),
18120
+ html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text) + asked(message.turn),
18051
18121
  activity: message.activity, createdAt: message.createdAt,
18052
18122
  ...(() => {
18053
18123
  const parts = message.turn === null ? [] : rows.proposals.filter(one => one.turn === message.turn)
@@ -18070,6 +18140,7 @@ function mateChatVersion(rows) {
18070
18140
  rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
18071
18141
  [...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
18072
18142
  rows.pending === null ? null : [rows.pending.id, rows.pending.steps],
18143
+ rows.ask?.turn ?? null,
18073
18144
  rows.recent[0] === undefined ? null : [rows.recent[0].id, rows.recent[0].state, rows.recent[0].failureReason],
18074
18145
  focus === null
18075
18146
  ? null
@@ -18125,6 +18196,10 @@ function mateThreadHtml(data) {
18125
18196
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
18126
18197
  parts.push(`<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? ' id="latest"' : ""}>` +
18127
18198
  renderChatText(message.text) +
18199
+ (() => {
18200
+ const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
18201
+ 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);
18202
+ })() +
18128
18203
  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
18204
  `<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${escape(message.createdAt)}">${escape(relativeAge(message.createdAt, data.now))}</time></div>` +
18130
18205
  `</div>`);
@@ -18185,12 +18260,12 @@ function matePage(chrome, data) {
18185
18260
  }
18186
18261
  const lastMessage = data.messages.at(-1);
18187
18262
  const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
18188
- conversation.push(mateThreadHtml({ ...data, problem: null }));
18263
+ conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
18189
18264
  conversation.push(
18190
18265
  // The New update action (package 2): hidden until a live update lands
18191
18266
  // while the reader is above the latest message; a real button, so the
18192
18267
  // 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>`,
18268
+ `<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
18269
  // Concise pass (2026-09-13): the status line speaks only when there is
18195
18270
  // a state to report — a reply in progress here, the connection from
18196
18271
  // the continuity script — and the composer carries no second intro.
@@ -18202,7 +18277,7 @@ function matePage(chrome, data) {
18202
18277
  workspace: {
18203
18278
  conversation: {
18204
18279
  sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
18205
- messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo),
18280
+ messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
18206
18281
  pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
18207
18282
  taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
18208
18283
  },
@@ -19529,6 +19604,15 @@ function accentHead() {
19529
19604
  }
19530
19605
  const requestContext = new AsyncLocalStorage();
19531
19606
  /** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
19607
+ /** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
19608
+ const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
19609
+ const RESULT_REFUSALS = { reason: ACCEPT_ANYWAY_NEEDS_REASON };
19610
+ /** A same-site page address with a refusal code added, before any fragment. */
19611
+ function withRefusal(href, code) {
19612
+ const at = href.indexOf("#");
19613
+ const [path, hash] = at === -1 ? [href, ""] : [href.slice(0, at), href.slice(at)];
19614
+ return `${path}${path.includes("?") ? "&" : "?"}refused=${code}${hash}`;
19615
+ }
19532
19616
  function safeReturn(raw) {
19533
19617
  if (raw === null || raw === undefined)
19534
19618
  return "/";
@@ -21779,12 +21863,13 @@ function reviewCockpitPage(chrome, data) {
21779
21863
  capped: queue.length >= data.queueCap ? data.queueCap : null,
21780
21864
  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
21865
  beyond: data.beyondQueue && selected !== null,
21782
- selected: detailParts === null ? null : detailParts.selected,
21866
+ selected: detailParts === null ? null : data.refusal == null ? detailParts.selected : { ...detailParts.selected, problem: data.refusal },
21783
21867
  };
21784
21868
  return screen("review", [
21785
21869
  selected === null ? `<h1>Results</h1>` : "",
21786
21870
  missingNote,
21787
21871
  beyondNote,
21872
+ data.refusal == null || selected === null ? "" : `<p class="problem" data-result-refusal>${escape(data.refusal)}</p>`,
21788
21873
  `<div class="cockpit">${queuePane}${detail}</div>`,
21789
21874
  ].join("\n"), {
21790
21875
  chrome,
@@ -22011,14 +22096,11 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
22011
22096
  },
22012
22097
  };
22013
22098
  }
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. */
22099
+ /** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
22100
+ * (owedAcceptance), so the form asks for exactly what the completion requires. */
22016
22101
  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;
22102
+ const owed = owedAcceptance(receipt);
22103
+ return owed === null ? null : owed === "person-check" ? { note: null } : { note: "Why is this safe to accept?" };
22022
22104
  }
22023
22105
  function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
22024
22106
  // Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
@@ -1,5 +1,6 @@
1
1
  import { chatFlowButtons } from "./chat-flow.js";
2
2
  import { chatQuestionButtons } from "./teammate-question.js";
3
+ import { chatAskButtons } from "./chat-ask.js";
3
4
  import { channelInbox } from "./chat-inbox.js";
4
5
  import { roomCommand } from "./chat-rooms.js";
5
6
  import { processChatEvent, applyChatAction, planChatNotifications, channelAccess, planRoomMessages, } from "./chat-delivery.js";
@@ -359,6 +360,15 @@ export async function deliverSlackPart(options) {
359
360
  value: one.token,
360
361
  }))
361
362
  : []),
363
+ // The lead's question to its owner: its options, then "Something else".
364
+ ...(content.ask
365
+ ? chatAskButtons(state, row.id, now).map((one) => ({
366
+ type: "button",
367
+ text: { type: "plain_text", text: one.label },
368
+ action_id: one.words ? "toolroll_question_words" : "toolroll_question_choice",
369
+ value: one.token,
370
+ }))
371
+ : []),
362
372
  ...linkButton(options.origin(), content.link),
363
373
  // [Look first] beside [Merge] or [Accept and finish]: each button's own action id.
364
374
  ...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
package/dist/store.d.ts CHANGED
@@ -21,6 +21,10 @@ import { type RetentionKind, type RetentionPeriods } from "./retention.js";
21
21
  import { type CheckoutCleanup } from "./storage.js";
22
22
  import { type LedgerChainReport } from "./ledger-chain.js";
23
23
  import { type ApprovalGate, type ApprovalRules, type ApproverKind, type Filer } from "./approval-policy.js";
24
+ /** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
25
+ * predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
26
+ * next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
27
+ export declare const LEAD_COMMITMENT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS lead_commitment (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n owner TEXT NOT NULL,\n repo TEXT,\n thread INTEGER NOT NULL,\n turn INTEGER,\n channel TEXT NOT NULL DEFAULT 'chat',\n what TEXT NOT NULL,\n condition_json TEXT NOT NULL,\n check_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'done', 'cancelled', 'expired')),\n created_at TEXT NOT NULL,\n checked_at TEXT,\n closed_at TEXT,\n closed_by TEXT,\n outcome TEXT\n);\nCREATE INDEX IF NOT EXISTS lead_commitment_due ON lead_commitment (state, check_at);\nCREATE INDEX IF NOT EXISTS lead_commitment_owner ON lead_commitment (owner, state, id);\n";
24
28
  /** What stops a finished run reading stopped (Store.stopQuiescenceFact). */
25
29
  export type StopFactKind = "open" | "alive" | "elsewhere" | "unprovable" | "unknown";
26
30
  export type TaskAct = "filed" | "approved" | "cancelled" | "completed" | "asked";
@@ -39,6 +43,20 @@ export type TaskActRow = {
39
43
  why: string | null;
40
44
  at: string;
41
45
  };
46
+ /** The lead's one question to its owner (no version bump): at most one per answered turn, two to four short options;
47
+ * "Something else" is implied. The tapped option comes back as the owner's next message, so nothing here is an answer:
48
+ * a question is open while no later message from the owner sits in its thread. */
49
+ export declare const MATE_ASK_SCHEMA = "\nCREATE TABLE IF NOT EXISTS mate_ask (\n turn INTEGER PRIMARY KEY REFERENCES mate_turn(id) ON DELETE CASCADE,\n thread INTEGER NOT NULL REFERENCES mate_thread(id) ON DELETE CASCADE,\n question TEXT NOT NULL,\n options_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS mate_ask_thread ON mate_ask (thread, turn);\n";
50
+ export type MateAsk = {
51
+ turn: number;
52
+ thread: number;
53
+ question: string;
54
+ options: string[];
55
+ createdAt: string;
56
+ };
57
+ export declare const MATE_ASK_OTHER = "Something else";
58
+ /** How long the question's buttons work, on every channel. */
59
+ export declare const MATE_ASK_TTL_MS: number;
42
60
  /** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
43
61
  * nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
44
62
  * failure across checks, so a row can say both. `account` is a display name only, never a credential. */
@@ -1532,6 +1550,7 @@ export type TelegramConversationPartPlan = {
1532
1550
  kind: "reply";
1533
1551
  text: string;
1534
1552
  replyTo?: string | null;
1553
+ keyboard?: TelegramConversationPart["keyboard"];
1535
1554
  } | {
1536
1555
  kind: "card";
1537
1556
  text: string;
@@ -6672,6 +6691,27 @@ export declare class Store {
6672
6691
  payload: Record<string, unknown>;
6673
6692
  ceilingDigest: string;
6674
6693
  }, now: Date): number;
6694
+ /** Record this turn's one question; false when the turn already asked one. */
6695
+ recordMateAsk(ask: {
6696
+ turn: number;
6697
+ thread: number;
6698
+ question: string;
6699
+ options: readonly string[];
6700
+ }, now: Date): boolean;
6701
+ /** A failed turn keeps no question, as it keeps no drafts. */
6702
+ dropMateAsk(turn: number): void;
6703
+ /** The question an ANSWERED turn asked; null when it asked none or did not answer. */
6704
+ mateAsk(turn: number): MateAsk | null;
6705
+ /** What a tap on the question finds: open; answered (the owner has written in its thread since the reply that asked it);
6706
+ * or expired (older than MATE_ASK_TTL_MS, or the question or the reply that asked it is gone, as after a retention purge). */
6707
+ mateAskState(turn: number, now: Date): {
6708
+ state: "open";
6709
+ ask: MateAsk;
6710
+ } | {
6711
+ state: "answered" | "expired";
6712
+ };
6713
+ /** The question while it is open (see mateAskState); null otherwise. */
6714
+ mateAskOpen(turn: number, now: Date): MateAsk | null;
6675
6715
  /**
6676
6716
  * Record the screenshots one turn selected for an exact result, under the
6677
6717
  * turn, so a channel that delivers files can plan them from the completed
@@ -7287,6 +7327,8 @@ export declare class Store {
7287
7327
  taskId: string | null;
7288
7328
  sourceRun: number | null;
7289
7329
  }, now: Date): number;
7330
+ /** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
7331
+ telegramConversationWaitingOn(binding: number, messageId: string): boolean;
7290
7332
  getTelegramConversation(id: number): TelegramConversation | null;
7291
7333
  listTelegramConversations(botId: string): TelegramConversation[];
7292
7334
  /**
@@ -7459,6 +7501,13 @@ export declare class Store {
7459
7501
  }[];
7460
7502
  /** Claim one local day's digest for one person: true once, false if it was already considered. */
7461
7503
  claimEveningDigestDay(account: string, day: string): boolean;
7504
+ /** What this person named their lead and its persona, or null when they never saved one. */
7505
+ leadConfig(account: string): {
7506
+ name: string;
7507
+ persona: string;
7508
+ } | null;
7509
+ /** Save this person's lead name and persona (checked by the caller: lead-identity.ts). */
7510
+ setLeadConfig(account: string, name: string, persona: string, now: Date): void;
7462
7511
  /** A lead token for one person, shown once. A new one ends their earlier ones; the ledger names it "lead for <owner>". */
7463
7512
  mintLeadCredential(owner: string, by: string, now: Date): {
7464
7513
  id: string;
@@ -7667,6 +7716,8 @@ export declare class Store {
7667
7716
  export declare const TELEGRAM_SKIPPED_RECEIPT = "skipped:before-pairing";
7668
7717
  /** v83: a notification for another person, settled for this destination without sending. */
7669
7718
  export declare const TELEGRAM_SKIPPED_ELSEWHERE = "skipped:for-another-person";
7719
+ /** A met promise the lead made on another chat: reported there, settled here without sending. */
7720
+ export declare const TELEGRAM_SKIPPED_OTHER_CHAT = "skipped:for-another-chat";
7670
7721
  /** A fact this person is not messaged about: the lead's work, their own act, or a project they muted. */
7671
7722
  export declare const TELEGRAM_SKIPPED_QUIET = "skipped:quiet";
7672
7723
  /**