toolroll 0.9.14 → 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 (84) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/assignment-brief.d.ts +10 -0
  3. package/dist/assignment-brief.js +49 -26
  4. package/dist/assignment-ui.d.ts +1 -0
  5. package/dist/assignment-ui.js +6 -4
  6. package/dist/assignment.d.ts +34 -1
  7. package/dist/assignment.js +86 -7
  8. package/dist/browser/workspace.css +2 -2
  9. package/dist/browser/workspace.js +38 -38
  10. package/dist/browser-crew.d.ts +2 -1
  11. package/dist/browser-crew.js +3 -1
  12. package/dist/browser-workspace.d.ts +93 -18
  13. package/dist/chat-actions.d.ts +1 -1
  14. package/dist/chat-actions.js +6 -4
  15. package/dist/chat-ask.d.ts +28 -0
  16. package/dist/chat-ask.js +77 -0
  17. package/dist/chat-channel.js +6 -1
  18. package/dist/chat-controls.d.ts +8 -0
  19. package/dist/chat-controls.js +3 -0
  20. package/dist/chat-delivery-state.d.ts +7 -2
  21. package/dist/chat-delivery-state.js +15 -2
  22. package/dist/chat-delivery.js +20 -3
  23. package/dist/chat-quiet.js +4 -2
  24. package/dist/chat-voice.d.ts +6 -9
  25. package/dist/chat-voice.js +11 -5
  26. package/dist/demo.js +82 -2
  27. package/dist/disclosure.d.ts +6 -0
  28. package/dist/disclosure.js +13 -0
  29. package/dist/discord-chat.js +10 -0
  30. package/dist/lead-commitments.d.ts +84 -0
  31. package/dist/lead-commitments.js +208 -0
  32. package/dist/lead-context.d.ts +32 -3
  33. package/dist/lead-context.js +133 -19
  34. package/dist/lead-follow.js +3 -0
  35. package/dist/lead-identity.d.ts +23 -0
  36. package/dist/lead-identity.js +33 -0
  37. package/dist/lead-status.js +2 -2
  38. package/dist/lead-voice.d.ts +12 -9
  39. package/dist/lead-voice.js +25 -16
  40. package/dist/ledger-view.js +3 -2
  41. package/dist/mate-cli.d.ts +1 -1
  42. package/dist/mate-cli.js +8 -2
  43. package/dist/mate-contract.d.ts +1 -1
  44. package/dist/mate-contract.js +11 -4
  45. package/dist/mate-progress.js +5 -0
  46. package/dist/mate-tools.d.ts +18 -1
  47. package/dist/mate-tools.js +250 -6
  48. package/dist/mate.d.ts +9 -0
  49. package/dist/mate.js +34 -38
  50. package/dist/needs-you.d.ts +48 -0
  51. package/dist/needs-you.js +93 -1
  52. package/dist/operate.js +2 -1
  53. package/dist/project-delete.js +3 -1
  54. package/dist/result-acts.d.ts +63 -5
  55. package/dist/result-acts.js +53 -3
  56. package/dist/result-review.d.ts +4 -3
  57. package/dist/result-review.js +7 -3
  58. package/dist/retention.js +6 -0
  59. package/dist/serve.d.ts +3 -0
  60. package/dist/serve.js +1220 -694
  61. package/dist/slack-chat.js +11 -1
  62. package/dist/store.d.ts +51 -0
  63. package/dist/store.js +115 -1
  64. package/dist/surface.js +2 -2
  65. package/dist/task-status.d.ts +26 -19
  66. package/dist/task-status.js +59 -21
  67. package/dist/team-runtime.js +2 -1
  68. package/dist/teams-chat.js +4 -1
  69. package/dist/telegram-mate.d.ts +4 -1
  70. package/dist/telegram-mate.js +17 -1
  71. package/dist/telegram-progress.js +3 -2
  72. package/dist/telegram.js +43 -3
  73. package/dist/test-temp.js +1 -1
  74. package/dist/transitions-recipes.d.ts +1 -1
  75. package/dist/transitions-recipes.js +5 -9
  76. package/dist/when-html.d.ts +24 -2
  77. package/dist/when-html.js +98 -11
  78. package/dist/work-index.d.ts +5 -1
  79. package/dist/work-index.js +22 -9
  80. package/dist/work-summary.d.ts +1 -1
  81. package/dist/workspace-motion.js +3 -3
  82. package/dist/workspace-ui.d.ts +44 -20
  83. package/dist/workspace-ui.js +63 -37
  84. package/package.json +1 -1
@@ -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,8 +360,17 @@ 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
- // [Look first] beside [Merge] or [Mark complete]: each button's own action id.
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}` }))),
365
375
  ];
366
376
  const target = content.edit ?? row.message;
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
  /**
package/dist/store.js CHANGED
@@ -183,6 +183,16 @@ CREATE TABLE IF NOT EXISTS chat_batch_item (
183
183
  PRIMARY KEY (batch, task_ref)
184
184
  );
185
185
  `;
186
+ /** Settings → Lead (no version bump: additive only): what one person calls their lead and the short persona it
187
+ * speaks with. No row reads as the defaults (lead-identity.ts). */
188
+ const LEAD_CONFIG_SCHEMA = `
189
+ CREATE TABLE IF NOT EXISTS lead_config (
190
+ account TEXT PRIMARY KEY,
191
+ name TEXT NOT NULL,
192
+ persona TEXT NOT NULL,
193
+ updated_at TEXT NOT NULL
194
+ );
195
+ `;
186
196
  /** Pings follow responsibility (no version bump: additive only). `lead_credential` is a lead token: an agent acting
187
197
  * for `owner`, only its hash kept. `task_act` is who filed, approved, cancelled, completed or handed on a task
188
198
  * (`lead` 1: the owner's lead; `person`/`why`: who the lead asked, and why). `notification_actor` is whose act made
@@ -227,6 +237,31 @@ CREATE TABLE IF NOT EXISTS project_mute (
227
237
  PRIMARY KEY (account, repo)
228
238
  );
229
239
  `;
240
+ /** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
241
+ * predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
242
+ * next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
243
+ export const LEAD_COMMITMENT_SCHEMA = `
244
+ CREATE TABLE IF NOT EXISTS lead_commitment (
245
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
246
+ owner TEXT NOT NULL,
247
+ repo TEXT,
248
+ thread INTEGER NOT NULL,
249
+ turn INTEGER,
250
+ channel TEXT NOT NULL DEFAULT 'chat',
251
+ what TEXT NOT NULL,
252
+ condition_json TEXT NOT NULL,
253
+ check_at TEXT NOT NULL,
254
+ expires_at TEXT NOT NULL,
255
+ state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'done', 'cancelled', 'expired')),
256
+ created_at TEXT NOT NULL,
257
+ checked_at TEXT,
258
+ closed_at TEXT,
259
+ closed_by TEXT,
260
+ outcome TEXT
261
+ );
262
+ CREATE INDEX IF NOT EXISTS lead_commitment_due ON lead_commitment (state, check_at);
263
+ CREATE INDEX IF NOT EXISTS lead_commitment_owner ON lead_commitment (owner, state, id);
264
+ `;
230
265
  /** What still pings on the lead's own work: a security alert. */
231
266
  const SECURITY_KINDS = new Set(["secret-detected", "security-release"]);
232
267
  /** A failure that leaves nothing for the lead to try: the attempts are spent. */
@@ -245,6 +280,22 @@ export function stopFactOf(store, runId) {
245
280
  : /cannot be proven|could not be established|incomplete native/.test(problem) ? "unknown" : "unprovable";
246
281
  return { run: Number(/run #(\d+)/.exec(problem)?.[1] ?? runId), kind, problem };
247
282
  }
283
+ /** The lead's one question to its owner (no version bump): at most one per answered turn, two to four short options;
284
+ * "Something else" is implied. The tapped option comes back as the owner's next message, so nothing here is an answer:
285
+ * a question is open while no later message from the owner sits in its thread. */
286
+ export const MATE_ASK_SCHEMA = `
287
+ CREATE TABLE IF NOT EXISTS mate_ask (
288
+ turn INTEGER PRIMARY KEY REFERENCES mate_turn(id) ON DELETE CASCADE,
289
+ thread INTEGER NOT NULL REFERENCES mate_thread(id) ON DELETE CASCADE,
290
+ question TEXT NOT NULL,
291
+ options_json TEXT NOT NULL,
292
+ created_at TEXT NOT NULL
293
+ );
294
+ CREATE INDEX IF NOT EXISTS mate_ask_thread ON mate_ask (thread, turn);
295
+ `;
296
+ export const MATE_ASK_OTHER = "Something else";
297
+ /** How long the question's buttons work, on every channel. */
298
+ export const MATE_ASK_TTL_MS = 7 * 86_400_000;
248
299
  /** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
249
300
  * nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
250
301
  * failure across checks, so a row can say both. `account` is a display name only, never a credential. */
@@ -3830,6 +3881,7 @@ function initializeStore(db, file) {
3830
3881
  db.exec(LEDGER_CHAIN_SCHEMA);
3831
3882
  db.exec(MONITORING_SCHEMA);
3832
3883
  db.exec(INTEGRATIONS_SCHEMA);
3884
+ db.exec(MATE_ASK_SCHEMA);
3833
3885
  db.exec(SPEND_SCHEMA);
3834
3886
  // Sign-in pauses: one row per incident of a provider's sign-in no longer working.
3835
3887
  db.exec(PROVIDER_AUTH_SCHEMA);
@@ -3848,6 +3900,8 @@ function initializeStore(db, file) {
3848
3900
  // The update a batch line speaks for: its newest fact about that task (null: a finished result).
3849
3901
  addColumn(db, "chat_batch_item", "notification", "INTEGER");
3850
3902
  db.exec(LEAD_QUIET_SCHEMA);
3903
+ db.exec(LEAD_CONFIG_SCHEMA);
3904
+ db.exec(LEAD_COMMITMENT_SCHEMA);
3851
3905
  mergeCheckTables(db);
3852
3906
  addColumn(db, "monitoring_status", "target", "TEXT");
3853
3907
  // v105: how a teammate's turn was billed (this computer's Claude sign-in, as last seen).
@@ -19636,6 +19690,46 @@ export class Store {
19636
19690
  .run(proposal.thread, proposal.turn, proposal.kind, JSON.stringify(proposal.payload), proposal.ceilingDigest, now.toISOString());
19637
19691
  return Number(inserted.lastInsertRowid);
19638
19692
  }
19693
+ // ---- the lead's question to its owner (ask_owner) -----------------------------
19694
+ /** Record this turn's one question; false when the turn already asked one. */
19695
+ recordMateAsk(ask, now) {
19696
+ const inserted = this.db.prepare("INSERT OR IGNORE INTO mate_ask (turn, thread, question, options_json, created_at) VALUES (?, ?, ?, ?, ?)")
19697
+ .run(ask.turn, ask.thread, ask.question, JSON.stringify(ask.options), now.toISOString());
19698
+ return Number(inserted.changes) === 1;
19699
+ }
19700
+ /** A failed turn keeps no question, as it keeps no drafts. */
19701
+ dropMateAsk(turn) {
19702
+ this.db.prepare("DELETE FROM mate_ask WHERE turn = ?").run(turn);
19703
+ }
19704
+ /** The question an ANSWERED turn asked; null when it asked none or did not answer. */
19705
+ mateAsk(turn) {
19706
+ const row = this.db.prepare("SELECT a.* FROM mate_ask a JOIN mate_turn t ON t.id = a.turn WHERE a.turn = ? AND t.state = 'answered'").get(turn);
19707
+ if (row === undefined)
19708
+ return null;
19709
+ const options = JSON.parse(String(row["options_json"]));
19710
+ return { turn: Number(row["turn"]), thread: Number(row["thread"]), question: String(row["question"]), options: Array.isArray(options) ? options.map(String) : [], createdAt: String(row["created_at"]) };
19711
+ }
19712
+ /** What a tap on the question finds: open; answered (the owner has written in its thread since the reply that asked it);
19713
+ * or expired (older than MATE_ASK_TTL_MS, or the question or the reply that asked it is gone, as after a retention purge). */
19714
+ mateAskState(turn, now) {
19715
+ const ask = this.mateAsk(turn);
19716
+ if (ask === null)
19717
+ return { state: "expired" };
19718
+ const asking = this.db.prepare("SELECT MAX(id) AS id FROM mate_message WHERE thread = ? AND turn = ? AND role = 'assistant'").get(ask.thread, turn);
19719
+ if (asking === undefined || asking["id"] === null)
19720
+ return { state: "expired" };
19721
+ const later = this.db.prepare("SELECT 1 AS hit FROM mate_message WHERE thread = ? AND role = 'operator' AND id > ? LIMIT 1").get(ask.thread, Number(asking["id"]));
19722
+ if (later !== undefined)
19723
+ return { state: "answered" };
19724
+ if (new Date(ask.createdAt).getTime() + MATE_ASK_TTL_MS <= now.getTime())
19725
+ return { state: "expired" };
19726
+ return { state: "open", ask };
19727
+ }
19728
+ /** The question while it is open (see mateAskState); null otherwise. */
19729
+ mateAskOpen(turn, now) {
19730
+ const found = this.mateAskState(turn, now);
19731
+ return found.state === "open" ? found.ask : null;
19732
+ }
19639
19733
  // ---- the screenshots a turn selected (v64) ------------------------------------
19640
19734
  /**
19641
19735
  * Record the screenshots one turn selected for an exact result, under the
@@ -21612,6 +21706,10 @@ export class Store {
21612
21706
  .run(row.binding.id, row.binding.botId, row.binding.chatId, row.binding.userId, row.binding.approver, row.binding.approverGeneration, row.updateId, row.messageId, row.replyTo, row.request, row.text, row.context, row.taskId, row.sourceRun, now.toISOString());
21613
21707
  return Number(inserted.lastInsertRowid);
21614
21708
  }
21709
+ /** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
21710
+ telegramConversationWaitingOn(binding, messageId) {
21711
+ return this.db.prepare("SELECT 1 AS hit FROM telegram_conversation WHERE binding = ? AND message_id = ? AND state IN ('queued', 'running') LIMIT 1").get(binding, messageId) !== undefined;
21712
+ }
21615
21713
  getTelegramConversation(id) {
21616
21714
  const row = this.db.prepare("SELECT * FROM telegram_conversation WHERE id = ?").get(id);
21617
21715
  return row === undefined ? null : readTelegramConversation(row);
@@ -21742,7 +21840,7 @@ export class Store {
21742
21840
  VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`);
21743
21841
  parts.forEach((part, ordinal) => {
21744
21842
  const image = part.kind === "image" ? part : null;
21745
- insert.run(id, ordinal, part.kind, part.text, part.kind === "reply" ? part.replyTo ?? null : null, part.kind === "card" ? part.proposal : null, part.kind === "card" && part.keyboard != null ? JSON.stringify(part.keyboard) : null, stamp, image?.taskId ?? null, image?.run ?? null, image?.artifact ?? null, image?.sha256 ?? null);
21843
+ insert.run(id, ordinal, part.kind, part.text, part.kind === "reply" ? part.replyTo ?? null : null, part.kind === "card" ? part.proposal : null, (part.kind === "card" || part.kind === "reply") && part.keyboard != null ? JSON.stringify(part.keyboard) : null, stamp, image?.taskId ?? null, image?.run ?? null, image?.artifact ?? null, image?.sha256 ?? null);
21746
21844
  });
21747
21845
  return true;
21748
21846
  });
@@ -21921,6 +22019,10 @@ export class Store {
21921
22019
  this.db.prepare(`UPDATE notification_delivery SET receipt = '${TELEGRAM_SKIPPED_ELSEWHERE}'
21922
22020
  WHERE destination = ? AND delivered_at IS NULL AND receipt IS NULL
21923
22021
  AND notification IN (SELECT id FROM notification WHERE recipient IS NOT NULL AND recipient <> ?)`).run(destination, binding.approver);
22022
+ // A promise the lead made on another chat is reported there (lead-commitments.ts), not here.
22023
+ this.db.prepare(`UPDATE notification_delivery SET receipt = '${TELEGRAM_SKIPPED_OTHER_CHAT}'
22024
+ WHERE destination = ? AND delivered_at IS NULL AND receipt IS NULL
22025
+ AND notification IN (SELECT id FROM notification WHERE dedupe_key LIKE 'lead-promise:%' AND dedupe_key NOT LIKE 'lead-promise:telegram:%')`).run(destination);
21924
22026
  // Pings follow responsibility (the lead's work, this person's own act, a muted project): settled here, unsent.
21925
22027
  for (const raw of this.db.prepare(`SELECT n.* FROM notification n JOIN notification_delivery d ON d.notification = n.id AND d.destination = ?
21926
22028
  WHERE n.resolved_at IS NULL AND d.delivered_at IS NULL AND d.receipt IS NULL AND (d.claim_owner IS NULL OR d.claim_expires_at <= ?)`).all(destination, now.toISOString())) {
@@ -22143,6 +22245,16 @@ export class Store {
22143
22245
  .run(day, account, day).changes) === 1;
22144
22246
  }
22145
22247
  // ---- pings follow responsibility: the lead, who acted, replacements and muted projects ----
22248
+ /** What this person named their lead and its persona, or null when they never saved one. */
22249
+ leadConfig(account) {
22250
+ const row = this.db.prepare("SELECT name, persona FROM lead_config WHERE account = ?").get(account);
22251
+ return row === undefined ? null : { name: String(row["name"]), persona: String(row["persona"]) };
22252
+ }
22253
+ /** Save this person's lead name and persona (checked by the caller: lead-identity.ts). */
22254
+ setLeadConfig(account, name, persona, now) {
22255
+ this.db.prepare(`INSERT INTO lead_config (account, name, persona, updated_at) VALUES (?, ?, ?, ?)
22256
+ ON CONFLICT(account) DO UPDATE SET name = excluded.name, persona = excluded.persona, updated_at = excluded.updated_at`).run(account, name, persona, now.toISOString());
22257
+ }
22146
22258
  /** A lead token for one person, shown once. A new one ends their earlier ones; the ledger names it "lead for <owner>". */
22147
22259
  mintLeadCredential(owner, by, now) {
22148
22260
  return this.transact(() => {
@@ -22879,6 +22991,8 @@ function readHold(row) {
22879
22991
  export const TELEGRAM_SKIPPED_RECEIPT = "skipped:before-pairing";
22880
22992
  /** v83: a notification for another person, settled for this destination without sending. */
22881
22993
  export const TELEGRAM_SKIPPED_ELSEWHERE = "skipped:for-another-person";
22994
+ /** A met promise the lead made on another chat: reported there, settled here without sending. */
22995
+ export const TELEGRAM_SKIPPED_OTHER_CHAT = "skipped:for-another-chat";
22882
22996
  /** A fact this person is not messaged about: the lead's work, their own act, or a project they muted. */
22883
22997
  export const TELEGRAM_SKIPPED_QUIET = "skipped:quiet";
22884
22998
  /** A destination receipt (`d` = notification_delivery) that still owes a
package/dist/surface.js CHANGED
@@ -194,7 +194,7 @@ export const COMMAND_GUIDE = [
194
194
  invocation: `assignment ${action}`,
195
195
  synopsis: action === "show" ? "read the root, current work, owner and exact receipt"
196
196
  : action === "updates" ? "read durable updates after a cursor; save nextCursor after processing"
197
- : action === "claim" ? "record lead ownership; with the lead token the task reads \"Your lead is on it\" and leaves Needs you until done, handed over, or 2 hours idle; grants no approval or execution authority"
197
+ : action === "claim" ? "record lead ownership; with the lead token the task reads \"<the lead's name> is on it\" and leaves Needs you until done, handed over, or 2 hours idle; grants no approval or execution authority"
198
198
  : action === "brief" ? "catch up from current assignments and project knowledge in the local database"
199
199
  : action === "inbox" ? "receive or replay a saved batch of lead status updates"
200
200
  : action === "ack" ? "acknowledge delivery of an inbox batch; leaves task completion unchanged"
@@ -215,7 +215,7 @@ export const COMMAND_GUIDE = [
215
215
  })),
216
216
  operator("task state", "set a task's state by hand — an operator correction, not a workflow step (--replaced-by <id> with cancelled)"),
217
217
  operator("lead token", "mint the lead's credential behind your password (--revoke ends it): its own work pings nobody"),
218
- { invocation: "lead say", synopsis: "as the lead: one short message in its person's chat from \"Your lead\"; several within two minutes are one message",
218
+ { invocation: "lead say", synopsis: "as the lead: one short message in its person's chat under the lead's name (Settings → Lead); several within two minutes are one message",
219
219
  audience: "agent", agentMayInvoke: true, mutation: "unkeyed",
220
220
  positionals: [{ name: "text", required: true, meaning: "one short line in plain words" }],
221
221
  flags: [jsonFlag, dbFlag,
@@ -1,15 +1,3 @@
1
- /** One answer per task, then the details (docs/design/task-status.md). Every
2
- * surface — the Tasks list, the task page, the build page, Crew, the Chat
3
- * result, the Telegram/Slack/Discord/Teams cards and `toolroll status` — reads
4
- * its headline, sentence, tone and detail rows from `taskStatusOf`, so the
5
- * words cannot drift apart again. Pure: it reads facts the existing
6
- * projections already recorded and never changes a check, approval or
7
- * completion.
8
- *
9
- * Severity: red belongs to the Failed headline alone. A detail problem that
10
- * doesn't undo the outcome (a pull request that couldn't open, shortened
11
- * output) is an amber note on its own row with one action; the exact
12
- * technical reason stays one tap away in `why`. */
13
1
  import type { AssignmentSnapshot } from "./assignment.js";
14
2
  import { type NeedAction, type NeedContext, type NeedKey, type WaitKey } from "./needs-you.js";
15
3
  export type { NeedKey, WaitKey } from "./needs-you.js";
@@ -20,6 +8,11 @@ export declare const HEADLINES: readonly ["Queued", "Planning", "Needs you", "Wa
20
8
  export type Headline = (typeof HEADLINES)[number];
21
9
  export type HeadlineTone = "neutral" | "live" | "attention" | "ready" | "success" | "danger";
22
10
  export declare const HEADLINE_TONE: Readonly<Record<Headline, HeadlineTone>>;
11
+ /** The project's own checks and the pull request's CI are two different things, never one word (Checks beside CI). */
12
+ export declare const CHECKS_LABEL = "Project checks";
13
+ export declare const PR_CI_LABEL = "PR CI";
14
+ /** The one verb for opening a saved result, on every surface. */
15
+ export declare const OPEN_RESULT = "Open result";
23
16
  export type DetailKey = "checks" | "pull-request" | "requirements" | "evidence";
24
17
  /** The row's small icon: the only coloured part of a detail. `failed` is red
25
18
  * and only ever appears under the Failed headline; `note` is amber. */
@@ -86,11 +79,13 @@ export type TaskStatusFacts = {
86
79
  report?: boolean;
87
80
  checks?: ChecksFact | null;
88
81
  pullRequest?: PullRequestFact | null;
89
- /** `unverified`: the report is refuted, so no requirement counts as met however it was marked. */
82
+ /** `unverified`: the report is refuted, so no requirement counts as met however it was marked. `missed`: the ones it
83
+ * failed or left unanswered. */
90
84
  requirements?: {
91
85
  met: number;
92
86
  total: number;
93
87
  yours: number;
88
+ missed?: number;
94
89
  unverified?: boolean;
95
90
  } | null;
96
91
  evidence?: {
@@ -109,14 +104,16 @@ export type TaskStatusFacts = {
109
104
  };
110
105
  /** Exact technical reasons, shown only on request. */
111
106
  why?: readonly string[];
112
- /** The person's lead took this on (lead-voice.ts): "on-it" reads "Your lead is on it" and needs nobody; "lapsed"
107
+ /** The person's lead took this on (lead-voice.ts): "on-it" reads "<name> is on it" and needs nobody; "lapsed"
113
108
  * (two hours without a lead act) is back with the person, and says so. */
114
109
  lead?: "on-it" | "lapsed" | null;
110
+ /** What the person calls their lead (Settings → Lead); "Lead" when unnamed. */
111
+ leadName?: string;
115
112
  };
116
113
  export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report">): Headline;
117
114
  /** What the lead's claim says, in place of "waits for you" (lead-voice.ts). */
118
- export declare const LEAD_ON_IT = "Your lead is on it.";
119
- export declare const LEAD_LAPSED = "Your lead hasn't acted on this for 2 hours, so it's back with you.";
115
+ export declare const leadOnIt: (name?: string) => string;
116
+ export declare const leadLapsed: (name?: string) => string;
120
117
  export declare function taskStatusOf(facts: TaskStatusFacts): TaskStatus;
121
118
  export declare function plainReasonOf(stage: TaskStage, code: string, detail: string | null | undefined): string | null;
122
119
  export declare function stageOfCode(code: string, options?: {
@@ -153,14 +150,19 @@ export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "
153
150
  need?: TaskStatusFacts["need"];
154
151
  wait?: WaitKey;
155
152
  };
156
- /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
157
- export declare function requirementsOf(matrix: readonly {
153
+ type RequirementRow = {
158
154
  state: string;
159
155
  assessment?: {
160
156
  evidenceState?: string;
161
157
  } | undefined;
162
158
  review?: unknown;
163
- }[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
159
+ };
160
+ /** One requirement's state in words: the card's Requirements row and the Checks tab read this one source, so they
161
+ * can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does. */
162
+ export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Not met" | "Unverified";
163
+ export declare function requirementWordOf(row: RequirementRow, verdict?: string | null): RequirementWord;
164
+ /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
165
+ export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
164
166
  /** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
165
167
  export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
166
168
  /** Saved evidence health from the receipt's artifact list and its caveats. */
@@ -188,6 +190,8 @@ export declare function headlineEmoji(headline: Headline): string;
188
190
  export declare const statusIconSvg: (mark: DetailMark) => string;
189
191
  /** The detail rows as server HTML: quiet rows, colour only on the icon. */
190
192
  export declare function statusDetailsHtml(status: TaskStatus): string;
193
+ /** The status card's fold of exact reasons: "More", never a third thing called Details. */
194
+ export declare const STATUS_MORE = "More";
191
195
  /** The technical reasons, one tap away. */
192
196
  export declare function statusWhyHtml(status: TaskStatus, extra?: readonly string[], diagnostics?: readonly {
193
197
  token: string;
@@ -211,3 +215,6 @@ export declare function stageOfDispatch(d: {
211
215
  stage: TaskStage;
212
216
  need?: TaskStatusFacts["need"];
213
217
  };
218
+ /** The demo runs no checks: a Checks row that would offer to run one (or open one that never ran) says so instead. */
219
+ export declare const DEMO_CHECKS = "Can't run in the demo";
220
+ export declare function demoChecksOf(status: TaskStatus): TaskStatus;
@@ -1,3 +1,16 @@
1
+ /** One answer per task, then the details (docs/design/task-status.md). Every
2
+ * surface — the Tasks list, the task page, the build page, Crew, the Chat
3
+ * result, the Telegram/Slack/Discord/Teams cards and `toolroll status` — reads
4
+ * its headline, sentence, tone and detail rows from `taskStatusOf`, so the
5
+ * words cannot drift apart again. Pure: it reads facts the existing
6
+ * projections already recorded and never changes a check, approval or
7
+ * completion.
8
+ *
9
+ * Severity: red belongs to the Failed headline alone. A detail problem that
10
+ * doesn't undo the outcome (a pull request that couldn't open, shortened
11
+ * output) is an amber note on its own row with one action; the exact
12
+ * technical reason stays one tap away in `why`. */
13
+ import { DEFAULT_LEAD_NAME } from "./lead-identity.js";
1
14
  import { CODE_NEED, NEEDS, needSentence, waitSentence } from "./needs-you.js";
2
15
  /** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
3
16
  export const replacedWords = (successor) => `Replaced by ${successor}`;
@@ -8,6 +21,11 @@ export const HEADLINE_TONE = {
8
21
  Queued: "neutral", Planning: "live", "Needs you": "attention", Waiting: "neutral", Building: "live",
9
22
  "Ready for review": "ready", Complete: "success", Failed: "danger", Stopped: "neutral",
10
23
  };
24
+ /** The project's own checks and the pull request's CI are two different things, never one word (Checks beside CI). */
25
+ export const CHECKS_LABEL = "Project checks";
26
+ export const PR_CI_LABEL = "PR CI";
27
+ /** The one verb for opening a saved result, on every surface. */
28
+ export const OPEN_RESULT = "Open result";
11
29
  const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
12
30
  export function headlineOf(facts) {
13
31
  switch (facts.stage) {
@@ -76,19 +94,20 @@ function detailsOf(headline, facts) {
76
94
  const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
77
95
  const quick = checks.level === "quick";
78
96
  if (checks.running != null)
79
- row("checks", "Checks", `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
97
+ row("checks", CHECKS_LABEL, `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
80
98
  else if (checks.status === "passed")
81
- row("checks", "Checks", `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
99
+ row("checks", CHECKS_LABEL, `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
82
100
  else if (checks.status === "failed")
83
- row("checks", "Checks", `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
101
+ row("checks", CHECKS_LABEL, `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
84
102
  else if (checks.status === "running")
85
- row("checks", "Checks", "Running", "running");
103
+ row("checks", CHECKS_LABEL, "Running", "running");
86
104
  else if (checks.level === "off")
87
- row("checks", "Checks", "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
105
+ row("checks", CHECKS_LABEL, "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
88
106
  else if (checks.status === "not-run")
89
- row("checks", "Checks", "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
107
+ row("checks", CHECKS_LABEL, "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
108
+ // A saved check that can't be read is run again where that is possible.
90
109
  else
91
- row("checks", "Checks", "Couldn't be read", "note", { action: { label: "Open the result", href: facts.links?.result ?? null } });
110
+ row("checks", CHECKS_LABEL, "Couldn't be read", "note", { action: runChecks ?? { label: OPEN_RESULT, href: facts.links?.result ?? null } });
92
111
  }
93
112
  const pr = facts.pullRequest;
94
113
  if (pr != null) {
@@ -105,16 +124,19 @@ function detailsOf(headline, facts) {
105
124
  else if (pr.state === "failed")
106
125
  row("pull-request", "Pull request", "Couldn't open", "note", { action: pr.compareUrl ? { label: "Open it on GitHub", href: pr.compareUrl } : { label: "See why", href: facts.links?.pullRequest ?? null }, why: pr.error === null ? "Publishing gave up. The commit is safe locally." : `${pr.error} The commit is safe locally.` });
107
126
  else if (pr.ci === "running")
108
- row("pull-request", "Pull request", `${name} open, CI running`, "running", { href: github });
127
+ row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} running`, "running", { href: github });
109
128
  else if (pr.ci === "passing")
110
- row("pull-request", "Pull request", `${name} open, CI passed`, "ok", { href: github });
129
+ row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} passed`, "ok", { href: github });
111
130
  else if (pr.ci === "failing")
112
- row("pull-request", "Pull request", `${name} open, CI failing`, "note", { action: { label: "Open it on GitHub", href: github } });
131
+ row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} failing`, "note", { action: { label: "Open it on GitHub", href: github } });
113
132
  else
114
133
  row("pull-request", "Pull request", `${name} open`, "none", { href: github });
115
134
  }
116
135
  const req = facts.requirements;
117
- if (req != null && req.total > 0 && req.unverified === true)
136
+ // A failed result names how many it missed, refuted or not: that is what failed.
137
+ if (req != null && req.total > 0 && headline === "Failed" && (req.missed ?? 0) > 0)
138
+ row("requirements", "Requirements", `${req.missed} missed`, "failed");
139
+ else if (req != null && req.total > 0 && req.unverified === true)
118
140
  row("requirements", "Requirements", "Unverified", "none");
119
141
  else if (req != null && req.total > 0) {
120
142
  const unmet = req.total - req.met - req.yours;
@@ -136,16 +158,16 @@ function detailsOf(headline, facts) {
136
158
  return rows;
137
159
  }
138
160
  /** What the lead's claim says, in place of "waits for you" (lead-voice.ts). */
139
- export const LEAD_ON_IT = "Your lead is on it.";
140
- export const LEAD_LAPSED = "Your lead hasn't acted on this for 2 hours, so it's back with you.";
161
+ export const leadOnIt = (name = DEFAULT_LEAD_NAME) => `${name} is on it.`;
162
+ export const leadLapsed = (name = DEFAULT_LEAD_NAME) => `${name} hasn't acted on this for 2 hours, so it's back with you.`;
141
163
  /** The headlines a lead's claim speaks for: the ones that would otherwise wait on a person. */
142
164
  const LEAD_HEADLINES = new Set(["Needs you", "Failed", "Ready for review", "Stopped"]);
143
165
  function leadSentence(headline, facts) {
144
166
  const sha = short(facts.checks?.head ?? null);
145
167
  // A failed check stays said: the claim changes who acts next, never the result.
146
168
  if (headline === "Failed" && facts.checks?.status === "failed")
147
- return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. ${LEAD_ON_IT}`;
148
- return LEAD_ON_IT;
169
+ return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. ${leadOnIt(facts.leadName)}`;
170
+ return leadOnIt(facts.leadName);
149
171
  }
150
172
  export function taskStatusOf(facts) {
151
173
  const read = headlineOf(facts);
@@ -156,7 +178,7 @@ export function taskStatusOf(facts) {
156
178
  const need = headline === "Needs you" ? { key: facts.need ?? "other", action: NEEDS[facts.need ?? "other"].action } : null;
157
179
  const ownWords = need !== null && (need.key === "other" || need.key === "review-result") && facts.action != null;
158
180
  const primaryAction = need === null || ownWords ? facts.action ?? null : { label: need.action.label, href: facts.action?.href ?? null };
159
- const sentence = lead === "on-it" ? leadSentence(headline, facts) : lead === "lapsed" ? `${LEAD_LAPSED} ${sentenceOf(headline, facts)}` : sentenceOf(headline, facts);
181
+ const sentence = lead === "on-it" ? leadSentence(headline, facts) : lead === "lapsed" ? `${leadLapsed(facts.leadName)} ${sentenceOf(headline, facts)}` : sentenceOf(headline, facts);
160
182
  return { headline, tone: HEADLINE_TONE[headline], sentence, details: detailsOf(headline, facts),
161
183
  primaryAction, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== ""), need };
162
184
  }
@@ -258,13 +280,21 @@ export function assignmentStageOf(assignment, work, planning = false) {
258
280
  }
259
281
  }
260
282
  }
283
+ /** Evidence that passed and only awaits the retired assessment step is met. */
284
+ const requirementMet = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
285
+ export function requirementWordOf(row, verdict) {
286
+ if (verdict === "refuted")
287
+ return "Unverified";
288
+ if (requirementMet(row))
289
+ return "Met";
290
+ return row.state === "manual-review" ? "You check" : row.state === "missing" ? "Not shown yet" : "Not met";
291
+ }
261
292
  /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
262
293
  export function requirementsOf(matrix) {
263
294
  if (!matrix || matrix.length === 0)
264
295
  return null;
265
- // Evidence that passed and only awaits the retired assessment step is met.
266
- const met = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
267
- return { met: matrix.filter(met).length, total: matrix.length, yours: matrix.filter(row => !met(row) && row.state === "manual-review").length };
296
+ return { met: matrix.filter(requirementMet).length, total: matrix.length, yours: matrix.filter(row => requirementWordOf(row) === "You check").length,
297
+ missed: matrix.filter(row => !requirementMet(row) && (row.state === "failed" || row.state === "missing")).length };
268
298
  }
269
299
  /** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
270
300
  export function unverifiedWhenRefuted(requirements, verdict) {
@@ -347,18 +377,20 @@ export const statusIconSvg = (mark) => `<svg class="status-icon status-icon--${m
347
377
  export function statusDetailsHtml(status) {
348
378
  if (status.details.length === 0)
349
379
  return "";
350
- return `<ul class="status-details" aria-label="Details">` + status.details.map(one => {
380
+ return `<ul class="status-details" aria-label="Status">` + status.details.map(one => {
351
381
  const text = one.href === null ? escapeHtml(one.text) : `<a href="${escapeHtml(one.href)}">${escapeHtml(one.text)}</a>`;
352
382
  const action = one.action === null ? "" : one.action.href === null ? `<span class="status-detail-act">${escapeHtml(one.action.label)}</span>` : `<a class="status-detail-act" href="${escapeHtml(one.action.href)}">${escapeHtml(one.action.label)}</a>`;
353
383
  return `<li class="status-detail status-detail--${one.mark}" data-status-detail="${one.key}" data-mark="${one.mark}">${statusIconSvg(one.mark)}<span class="status-detail-label">${escapeHtml(one.label)}</span><span class="status-detail-text"><span>${text}</span>${action}</span></li>`;
354
384
  }).join("") + `</ul>`;
355
385
  }
386
+ /** The status card's fold of exact reasons: "More", never a third thing called Details. */
387
+ export const STATUS_MORE = "More";
356
388
  /** The technical reasons, one tap away. */
357
389
  export function statusWhyHtml(status, extra = [], diagnostics = []) {
358
390
  const lines = [...new Set([...status.details.flatMap(one => one.why === null ? [] : [`${one.label}: ${one.why}`]), ...status.why, ...extra])];
359
391
  if (lines.length === 0 && diagnostics.length === 0)
360
392
  return "";
361
- return `<details class="status-why"><summary>Details</summary>${lines.map(one => `<p class="meta">${escapeHtml(one)}</p>`).join("")}` +
393
+ return `<details class="status-why"><summary>${STATUS_MORE}</summary>${lines.map(one => `<p class="meta">${escapeHtml(one)}</p>`).join("")}` +
362
394
  diagnostics.map(one => `<p class="meta" data-work-diagnostic="${escapeHtml(one.token)}">${escapeHtml(one.label)} · ${escapeHtml(one.detail)}</p>`).join("") + `</details>`;
363
395
  }
364
396
  /** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
@@ -377,3 +409,9 @@ export function stageOfDispatch(d, options = {}) {
377
409
  return { stage: "needs-you", need: "review-result" };
378
410
  return stageOfCode(d.code, { needsPerson: d.condition === "waiting" && d.action !== null, planning: d.condition === "running" && d.role === "planner", operatorHold: d.action === "unhold" });
379
411
  }
412
+ /** The demo runs no checks: a Checks row that would offer to run one (or open one that never ran) says so instead. */
413
+ export const DEMO_CHECKS = "Can't run in the demo";
414
+ export function demoChecksOf(status) {
415
+ return { ...status, details: status.details.map(one => one.key !== "checks" || one.mark === "ok" || one.mark === "failed" || one.mark === "running" ? one
416
+ : { ...one, text: DEMO_CHECKS, mark: "none", href: null, action: null, why: null }) };
417
+ }
@@ -148,7 +148,8 @@ export function createTeamRuntime(options) {
148
148
  turnId: receipt.turn, ...(turn?.state === 'answered' ? {} : { error: 'Inspect the saved response; this message was not sent again.' }) }, clock());
149
149
  return;
150
150
  }
151
- const result = await runMateTurn({ store, who: proof.who, session, thread, ...provider, message: claim.text, requestId,
151
+ const result = await runMateTurn({ store, who: proof.who, session, thread, ...provider, message: claim.text, requestId, channel: "console",
152
+ ...(() => { const name = view.leads.find(lead => lead.id === claim.leadId)?.name; return name === undefined ? {} : { leadName: name }; })(),
152
153
  queuedMessageId: claim.messageId,
153
154
  onAdmitted: turn => { if (!domain.current(claim) || !domain.bindTurn(claim, turn))
154
155
  throw new Error('The queued message changed before admission.'); },