toolroll 0.9.94 → 0.9.96

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 (191) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/agent-fence.d.ts +6 -0
  3. package/dist/agent-fence.js +2 -2
  4. package/dist/assignment-status.d.ts +1 -1
  5. package/dist/assignment-ui.js +2 -0
  6. package/dist/attest.d.ts +3 -0
  7. package/dist/attest.js +10 -0
  8. package/dist/browser/terminal.js +1 -1
  9. package/dist/browser/workspace.css +1 -1
  10. package/dist/browser/workspace.js +24 -24
  11. package/dist/browser-crew.js +2 -0
  12. package/dist/browser-workspace.d.ts +5 -0
  13. package/dist/build-review.js +2 -0
  14. package/dist/builder.d.ts +7 -2
  15. package/dist/builder.js +183 -79
  16. package/dist/card-catalog.d.ts +209 -0
  17. package/dist/card-catalog.js +133 -0
  18. package/dist/card-cli.d.ts +43 -0
  19. package/dist/card-cli.js +44 -0
  20. package/dist/card-render.d.ts +43 -0
  21. package/dist/card-render.js +260 -0
  22. package/dist/card-sources.d.ts +68 -0
  23. package/dist/card-sources.js +234 -0
  24. package/dist/chat-actions.d.ts +95 -8
  25. package/dist/chat-actions.js +449 -45
  26. package/dist/chat-approval.d.ts +13 -0
  27. package/dist/chat-approval.js +39 -9
  28. package/dist/chat-card-part.d.ts +11 -0
  29. package/dist/chat-card-part.js +18 -0
  30. package/dist/chat-card.d.ts +46 -0
  31. package/dist/chat-card.js +99 -0
  32. package/dist/chat-channel.d.ts +2 -0
  33. package/dist/chat-channel.js +26 -14
  34. package/dist/chat-controls.d.ts +4 -0
  35. package/dist/chat-controls.js +4 -0
  36. package/dist/chat-decide.d.ts +3 -14
  37. package/dist/chat-decide.js +43 -54
  38. package/dist/chat-delivery.js +13 -4
  39. package/dist/chat-flow.d.ts +0 -27
  40. package/dist/chat-flow.js +31 -2
  41. package/dist/chat-quiet.js +1 -1
  42. package/dist/chat-task-actions.d.ts +3 -2
  43. package/dist/chat-task-actions.js +19 -5
  44. package/dist/chat-terms.d.ts +14 -2
  45. package/dist/chat-terms.js +35 -0
  46. package/dist/check-failure.d.ts +2 -0
  47. package/dist/check-failure.js +34 -2
  48. package/dist/check-repair.d.ts +47 -0
  49. package/dist/check-repair.js +240 -0
  50. package/dist/claim.d.ts +6 -0
  51. package/dist/claim.js +13 -2
  52. package/dist/cli-help.d.ts +1 -1
  53. package/dist/cli-help.js +2 -0
  54. package/dist/cli.js +4 -0
  55. package/dist/coding-provider.d.ts +1 -1
  56. package/dist/coding-provider.js +9 -5
  57. package/dist/coding-workspace.d.ts +1 -1
  58. package/dist/coding-workspace.js +4 -4
  59. package/dist/contracts/chat-actions.d.ts +83 -0
  60. package/dist/contracts/chat-actions.js +18 -0
  61. package/dist/contracts/chat-card.d.ts +91 -0
  62. package/dist/contracts/chat-card.js +54 -0
  63. package/dist/contracts/chat-content.d.ts +191 -0
  64. package/dist/contracts/chat-content.js +4 -0
  65. package/dist/contracts/console-api.d.ts +2 -0
  66. package/dist/contracts/console-api.js +2 -0
  67. package/dist/contracts/flow-propose.d.ts +8 -1
  68. package/dist/contracts/flow-propose.js +3 -1
  69. package/dist/contracts/flow-send.d.ts +126 -0
  70. package/dist/contracts/flow-send.js +3 -0
  71. package/dist/contracts/lead-tools.d.ts +173 -0
  72. package/dist/contracts/lead-tools.js +15 -0
  73. package/dist/contracts/store-json.d.ts +16 -0
  74. package/dist/discord-chat.js +14 -0
  75. package/dist/dispatch.d.ts +1 -1
  76. package/dist/dispatch.js +7 -1
  77. package/dist/dispose.d.ts +19 -0
  78. package/dist/dispose.js +47 -7
  79. package/dist/eval-oracle.js +4 -1
  80. package/dist/evidence-pair.d.ts +53 -0
  81. package/dist/evidence-pair.js +128 -0
  82. package/dist/exhaustion.d.ts +17 -4
  83. package/dist/exhaustion.js +44 -4
  84. package/dist/export.js +1 -1
  85. package/dist/failing-first.d.ts +31 -0
  86. package/dist/failing-first.js +44 -0
  87. package/dist/fallback-schema.d.ts +8 -0
  88. package/dist/fallback-schema.js +40 -0
  89. package/dist/fallback.d.ts +138 -0
  90. package/dist/fallback.js +605 -0
  91. package/dist/findings.d.ts +224 -0
  92. package/dist/findings.js +449 -0
  93. package/dist/fleet-schema.d.ts +17 -0
  94. package/dist/fleet-schema.js +135 -0
  95. package/dist/flow-browser.js +2 -2
  96. package/dist/flow-code.d.ts +2 -2
  97. package/dist/flow-code.js +9 -5
  98. package/dist/flow-result-card.d.ts +22 -0
  99. package/dist/flow-result-card.js +55 -0
  100. package/dist/flow-send.js +12 -9
  101. package/dist/flow-steps.js +1 -1
  102. package/dist/flow-triggers.js +1 -1
  103. package/dist/flow-unattended.d.ts +26 -0
  104. package/dist/flow-unattended.js +12 -3
  105. package/dist/flows-ui.js +10 -2
  106. package/dist/follow-through-brief.d.ts +12 -0
  107. package/dist/follow-through-brief.js +31 -0
  108. package/dist/follow-through-cli.d.ts +23 -0
  109. package/dist/follow-through-cli.js +153 -0
  110. package/dist/git-incident.d.ts +16 -0
  111. package/dist/git-incident.js +69 -6
  112. package/dist/handover-note.d.ts +33 -0
  113. package/dist/handover-note.js +106 -0
  114. package/dist/invoke.d.ts +20 -1
  115. package/dist/invoke.js +108 -44
  116. package/dist/lead-cli.js +26 -24
  117. package/dist/lead-contract.js +1 -0
  118. package/dist/lead-doors.d.ts +4 -0
  119. package/dist/lead-doors.js +2 -2
  120. package/dist/lead-progress.js +2 -0
  121. package/dist/lead-tools.js +111 -32
  122. package/dist/lead.js +1 -1
  123. package/dist/local-owner.js +5 -0
  124. package/dist/modes.d.ts +6 -6
  125. package/dist/modes.js +3 -1
  126. package/dist/needs-you.d.ts +5 -2
  127. package/dist/needs-you.js +14 -2
  128. package/dist/operate-remote-arguments.js +1 -0
  129. package/dist/operate-remote.js +1 -1
  130. package/dist/operate.d.ts +1 -1
  131. package/dist/operate.js +165 -14
  132. package/dist/plan-login.d.ts +14 -0
  133. package/dist/plan-login.js +58 -0
  134. package/dist/planner.d.ts +1 -1
  135. package/dist/planner.js +2 -1
  136. package/dist/project-delete-ui.d.ts +12 -0
  137. package/dist/project-delete-ui.js +18 -1
  138. package/dist/project-delete.js +6 -0
  139. package/dist/project-knowledge.d.ts +6 -6
  140. package/dist/project-tools.d.ts +4 -4
  141. package/dist/project-tools.js +23 -7
  142. package/dist/protected-storage-look.d.ts +1 -0
  143. package/dist/protected-storage-look.js +13 -0
  144. package/dist/protected-storage.d.ts +70 -39
  145. package/dist/protected-storage.js +196 -147
  146. package/dist/provider-capture.d.ts +34 -0
  147. package/dist/provider-capture.js +53 -0
  148. package/dist/release-carry.js +10 -1
  149. package/dist/release.js +9 -1
  150. package/dist/scope.d.ts +22 -3
  151. package/dist/scope.js +71 -20
  152. package/dist/scout.d.ts +1 -1
  153. package/dist/scout.js +2 -1
  154. package/dist/serve.js +2 -0
  155. package/dist/server/chat.js +2 -2
  156. package/dist/server/console.css +50 -0
  157. package/dist/server/render-chat.js +14 -1
  158. package/dist/server/render-follow-through.d.ts +11 -0
  159. package/dist/server/render-follow-through.js +68 -0
  160. package/dist/server/render-results.js +1 -1
  161. package/dist/server/render-tasks.d.ts +3 -0
  162. package/dist/server/render-tasks.js +24 -5
  163. package/dist/server/route-table.js +3 -0
  164. package/dist/server/settings.js +37 -12
  165. package/dist/server/task-views.js +5 -0
  166. package/dist/server/tasks.js +76 -0
  167. package/dist/settings-groups-ui.d.ts +1 -0
  168. package/dist/settings-groups-ui.js +3 -1
  169. package/dist/settings-kit.js +3 -2
  170. package/dist/settings-proofs.js +3 -1
  171. package/dist/setup-guide.d.ts +2 -2
  172. package/dist/slack-chat.js +14 -1
  173. package/dist/steer-mailbox.d.ts +8 -1
  174. package/dist/steer-mailbox.js +164 -6
  175. package/dist/store.d.ts +11 -4
  176. package/dist/store.js +93 -30
  177. package/dist/surface.js +24 -4
  178. package/dist/task-control.d.ts +2 -1
  179. package/dist/task-status.js +5 -0
  180. package/dist/teardown-git.d.ts +2 -0
  181. package/dist/teardown-git.js +75 -19
  182. package/dist/telegram-lead.js +23 -6
  183. package/dist/telegram.js +12 -1
  184. package/dist/toolroll-update.js +2 -0
  185. package/dist/work-index.js +22 -7
  186. package/dist/work-summary.d.ts +1 -1
  187. package/dist/work-summary.js +5 -2
  188. package/dist/workspace-ui.d.ts +3 -1
  189. package/dist/workspace-ui.js +10 -1
  190. package/dist/worktree.js +23 -1
  191. package/package.json +2 -1
@@ -27,6 +27,19 @@ export declare function effectiveChatApproval(store: Store, repo: string, approv
27
27
  ok: false;
28
28
  why: string;
29
29
  };
30
+ /** What this person's chat on `channel` may approve on this repository now, and under whose limits: their saved setting,
31
+ * else (only with nothing saved) their own live signed mode that names this chat app, else their standing as an
32
+ * approver. A saved setting that refuses refuses here. `digest` names that for
33
+ * the ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
34
+ export declare function chatApproveMode(store: Store, repo: string | null, approver: string, now: Date, channel: "telegram" | "slack" | "discord" | "teams"): {
35
+ ok: true;
36
+ digest: string;
37
+ limits: ChatApprovalLimits;
38
+ source: "setting" | "mode";
39
+ } | {
40
+ ok: false;
41
+ why: string;
42
+ };
30
43
  export type ChatApprovalChange = {
31
44
  approver: string;
32
45
  scope: string;
@@ -1,15 +1,17 @@
1
- /** Approving from chat as a lasting owner setting. An approver turns it on for all their projects or for one, with the
2
- * limits they agree to (whether a plan may ask for full access, and the most one attempt may cost); a project's own
3
- * row outranks the all-projects one, and an explicit "off" on a project keeps it off there. Turning it off for all
4
- * projects also disables that owner's saved project rows. It is off until turned on.
1
+ /** Approving from chat (owner's decision, docs/plans/toolroll-cards.md): a person with the authority approves from their
2
+ * own linked chat by default, with no limits of their own. This setting is how they say otherwise: off for all their
3
+ * projects or for one, or on with limits (whether a plan may ask for full access, and the most one attempt may cost). A
4
+ * project's own row outranks the all-projects one, and an explicit "off" keeps it off. Turning it off for all projects
5
+ * also disables that owner's saved project rows.
5
6
  *
6
- * It never lapses by date, but it ends with the account: a reset password, revoked access or a lost project ends it.
7
- * Turning it on or changing its limits takes the password; turning it off is one step. Every change is a ledger line.
7
+ * A saved row ends with the account: a reset password, revoked access or a lost project ends it, and it must be saved
8
+ * again. Saving limits takes the password; turning it off is one step. Every change is a ledger line.
8
9
  *
9
10
  * What the setting allows is only that the owner's own two taps in their paired chat count as their yes. Every other
10
11
  * rule still applies at the offer and again at the tap (chat-decide.ts planInChat): protected paths, separation of
11
12
  * duties, organisation policy, written-for-you plans, unresolved routes, and the limits here. */
12
13
  import { createHash } from "node:crypto";
14
+ import { chatApproveChatsOf, chatNames, modeTermsFromJson } from "./modes.js";
13
15
  /** The all-projects row's scope. */
14
16
  export const ALL_PROJECTS = "*";
15
17
  const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
@@ -37,16 +39,44 @@ export function effectiveChatApproval(store, repo, approver) {
37
39
  const all = store.handle.prepare("SELECT * FROM chat_approval_setting WHERE approver = ? AND scope = ?").get(approver, ALL_PROJECTS);
38
40
  const row = project ?? all;
39
41
  const setting = row === undefined ? null : readRow(row);
40
- if (setting === null || !setting.enabled)
41
- return { ok: false, why: "Approving from chat isn't turned on for this project." };
42
42
  const account = store.accountOf(approver);
43
+ if (setting === null) {
44
+ if (account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(approver, repo))
45
+ return { ok: false, why: "Only an approver for this project can approve it." };
46
+ const limits = { fullAccess: true, capMicrousd: null };
47
+ return { ok: true, limits, setting: { ...limits, approver, scope: ALL_PROJECTS, enabled: true, generation: account.generation, digest: digestOf(approver, ALL_PROJECTS, limits, account.generation), updatedBy: approver, updatedAt: "" } };
48
+ }
49
+ if (!setting.enabled)
50
+ return { ok: false, why: "You turned off approving from chat for this project." };
43
51
  if (account === null || account.revokedAt !== null || account.role !== "approver" || account.generation !== setting.generation || !store.accountCanAccess(approver, repo)) {
44
- return { ok: false, why: "Your access changed since you turned on approving from chat. Turn it on again in Settings." };
52
+ return { ok: false, why: "Your access changed since you saved your chat approval setting. Save it again in Settings." };
45
53
  }
46
54
  if (setting.digest !== digestOf(setting.approver, setting.scope, setting, setting.generation))
47
55
  return { ok: false, why: "Your chat approval setting can't be read. Turn it on again in Settings." };
48
56
  return { ok: true, setting, limits: { fullAccess: setting.fullAccess, capMicrousd: setting.capMicrousd } };
49
57
  }
58
+ /** What this person's chat on `channel` may approve on this repository now, and under whose limits: their saved setting,
59
+ * else (only with nothing saved) their own live signed mode that names this chat app, else their standing as an
60
+ * approver. A saved setting that refuses refuses here. `digest` names that for
61
+ * the ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
62
+ export function chatApproveMode(store, repo, approver, now, channel) {
63
+ if (repo === null)
64
+ return { ok: false, why: "Approving from chat isn't turned on for this project." };
65
+ const setting = effectiveChatApproval(store, repo, approver);
66
+ // A saved setting decides: its "off", a lapse after an access change or an unreadable row refuses before any mode is
67
+ // looked at, so a signed mode never reopens what the person turned off. The mode counts only when nothing is saved.
68
+ if (!setting.ok)
69
+ return setting;
70
+ const chosen = { ok: true, source: "setting" };
71
+ if (setting.setting.updatedAt !== "")
72
+ return { ...chosen, digest: setting.setting.digest, limits: setting.limits };
73
+ // Nothing saved: their own signed mode's limits, where it names this chat app; otherwise their standing as an approver.
74
+ const mode = store.activeMode(repo, now);
75
+ const terms = mode === null ? null : modeTermsFromJson(mode.termsJson);
76
+ if (mode !== null && terms !== null && terms.chatApprove && mode.signedBy === approver && store.accountCanAccess(approver, repo) && chatApproveChatsOf(terms).includes(channel))
77
+ return { ok: true, digest: mode.digest, limits: { fullAccess: terms.permissionDefault === "escalated", capMicrousd: terms.perAttemptBudgetMicrousd }, source: "mode" };
78
+ return { ...chosen, digest: setting.setting.digest, limits: setting.limits };
79
+ }
50
80
  /** Save and ledger the setting; all-projects off also disables every saved project row for this owner.
51
81
  * The caller has proved who is asking, and for "on" the password. */
52
82
  export function setChatApproval(store, change, now) {
@@ -0,0 +1,11 @@
1
+ import type { ChatContent } from "./contracts/chat-content.js";
2
+ import type { ChatCard } from "./contracts/chat-card.js";
3
+ /** The buttons of a card, as a part's links. */
4
+ export declare const cardLinks: (card: ChatCard) => Pick<ChatContent, "link" | "also">;
5
+ export declare const cardContent: (card: ChatCard) => ChatContent;
6
+ /** A card that can't be read goes out as the plain words kept with it. */
7
+ export declare function cardPart(proposal: {
8
+ kind: string;
9
+ payload: Record<string, unknown>;
10
+ }): ChatContent;
11
+ export declare function withoutCard(content: ChatContent): ChatContent;
@@ -0,0 +1,18 @@
1
+ /** A card as a chat part: its plain text and link buttons always there, the card beside them. */
2
+ import { cardOfProposal, cardPlainOf } from "./chat-card.js";
3
+ import { cardPlainText } from "./card-render.js";
4
+ /** The buttons of a card, as a part's links. */
5
+ export const cardLinks = (card) => {
6
+ const [first, ...rest] = card.buttons;
7
+ return { ...(first === undefined ? {} : { link: first }), ...(rest.length === 0 ? {} : { also: rest }) };
8
+ };
9
+ export const cardContent = (card) => ({ text: cardPlainText(card), rich: card, ...cardLinks(card) });
10
+ /** A card that can't be read goes out as the plain words kept with it. */
11
+ export function cardPart(proposal) {
12
+ const card = cardOfProposal(proposal);
13
+ return card === null ? { text: cardPlainOf(proposal) } : cardContent(card);
14
+ }
15
+ export function withoutCard(content) {
16
+ const { rich: _rich, ...plain } = content;
17
+ return plain;
18
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * From a checked spec to a card (docs/plans/toolroll-cards.md): Toolroll fills every data part from its records and every
3
+ * button from its own tables. Nothing the model wrote reaches a card except a Text part, and every app labels that as
4
+ * the lead's note, never as one of Toolroll's records (card-render.ts).
5
+ */
6
+ import type { CardPlan } from "./card-catalog.js";
7
+ import { type CardReader } from "./card-sources.js";
8
+ import { type ChatCard } from "./contracts/chat-card.js";
9
+ /** How a Button becomes a page or a proposal: the lead's own checks, so a card can't reach what show_control can't. */
10
+ export type CardButtons = (props: Record<string, unknown>) => {
11
+ ok: true;
12
+ button?: ChatCard["buttons"][number];
13
+ action?: ChatCard["actions"][number];
14
+ } | {
15
+ ok: false;
16
+ message: string;
17
+ };
18
+ /** A plan as a card, read now, or why it can't be one. */
19
+ export declare function buildChatCard(reader: CardReader, plan: CardPlan, repoOf: (id: unknown) => string | null, buttons: CardButtons): {
20
+ ok: true;
21
+ card: ChatCard;
22
+ } | {
23
+ ok: false;
24
+ message: string;
25
+ };
26
+ /** A card kept on a `control` proposal, as its marker says. */
27
+ export declare const CARD_CONTROL = "card";
28
+ export declare function cardOfProposal(proposal: {
29
+ kind: string;
30
+ payload: Record<string, unknown>;
31
+ }): ChatCard | null;
32
+ export declare function isCardProposal(proposal: {
33
+ kind: string;
34
+ payload: Record<string, unknown>;
35
+ }): boolean;
36
+ /** The plain version kept beside a card, for when the card itself can't be read. */
37
+ export declare const cardPlainOf: (proposal: {
38
+ payload: Record<string, unknown>;
39
+ }) => string;
40
+ /** Cards before the proposals they lead to, so each Confirm sits under its card. */
41
+ export declare function cardsFirst<T extends {
42
+ kind: string;
43
+ payload: Record<string, unknown>;
44
+ }>(proposals: readonly T[]): T[];
45
+ /** A card goes only where every project behind it is still reachable. */
46
+ export declare const cardWithin: (card: Pick<ChatCard, "repos">, repos: readonly string[]) => boolean;
@@ -0,0 +1,99 @@
1
+ import { readCardSource } from "./card-sources.js";
2
+ import { publicChatText } from "./chat-display.js";
3
+ import { CARD_LIMITS, readChatCard } from "./contracts/chat-card.js";
4
+ const queryOf = (props, repoOf) => {
5
+ const repo = props["repo"] === undefined ? null : repoOf(props["repo"]);
6
+ if (props["repo"] !== undefined && repo === null)
7
+ return null;
8
+ return { repo, ...(props["task"] === undefined ? {} : { task: String(props["task"]) }), ...(props["state"] === undefined ? {} : { state: String(props["state"]) }), ...(props["limit"] === undefined ? {} : { limit: Number(props["limit"]) }),
9
+ ...(props["flow"] === undefined ? {} : { flow: Number(props["flow"]) }), ...(props["days"] === undefined ? {} : { days: Number(props["days"]) }) };
10
+ };
11
+ function tableOf(data, wanted) {
12
+ const ids = Array.isArray(wanted) ? wanted.map(String) : data.columns.slice(0, CARD_LIMITS.columns).map(one => one.id);
13
+ const columns = ids.map(id => data.columns.find(one => one.id === id));
14
+ const missing = ids.filter((_, index) => columns[index] === undefined);
15
+ if (missing.length > 0)
16
+ return `No column ${missing.join(", ")}. This source has: ${data.columns.map(one => one.id).join(", ")}.`;
17
+ return { kind: "table", heading: data.heading, origin: data.origin ?? "records", columns: columns.map(one => ({ label: one.label, numeric: one.numeric === true })), rows: data.rows.map(row => ids.map(id => row[id] ?? "")), more: data.more };
18
+ }
19
+ function factsOf(data, wanted) {
20
+ const ids = Array.isArray(wanted) ? wanted.map(String) : data.facts.map(one => one.id);
21
+ const items = ids.flatMap(id => data.facts.filter(one => one.id === id));
22
+ if (items.length !== ids.length)
23
+ return `This source's facts are: ${data.facts.map(one => one.id).join(", ")}.`;
24
+ if (items.length === 0)
25
+ return "This source has no facts to show now.";
26
+ return { kind: "facts", heading: data.heading, items: items.slice(0, CARD_LIMITS.facts).map(({ label, value }) => ({ label, value })) };
27
+ }
28
+ /** A plan as a card, read now, or why it can't be one. */
29
+ export function buildChatCard(reader, plan, repoOf, buttons) {
30
+ const blocks = [];
31
+ const repos = new Set();
32
+ let title = null;
33
+ for (const part of plan.parts) {
34
+ if (part.type === "Divider") {
35
+ blocks.push({ kind: "divider" });
36
+ continue;
37
+ }
38
+ if (part.type === "Text") {
39
+ const text = publicChatText(String(part.props["text"]), CARD_LIMITS.text);
40
+ if (text !== "")
41
+ blocks.push({ kind: "text", text });
42
+ continue;
43
+ }
44
+ const query = queryOf(part.props, repoOf);
45
+ if (query === null)
46
+ return { ok: false, message: "repo must be one of the ids from list_repos." };
47
+ const data = readCardSource(reader, part.props["source"], query);
48
+ if ("problem" in data)
49
+ return { ok: false, message: data.problem };
50
+ title ??= data.heading;
51
+ for (const repo of data.repos)
52
+ repos.add(repo);
53
+ if (part.type === "Notes") {
54
+ if (data.notes === undefined || data.notes.length === 0)
55
+ return { ok: false, message: "There are no saved words to show here. Try a Table." };
56
+ const items = data.notes.slice(0, CARD_LIMITS.notes);
57
+ blocks.push({ kind: "notes", heading: data.heading, origin: data.origin ?? "records", items, more: data.notes.length - items.length });
58
+ continue;
59
+ }
60
+ if (part.type === "Chart") {
61
+ if (data.points.length === 0)
62
+ return { ok: false, message: "There are no numbers to chart yet: show a Table or Fields instead." };
63
+ blocks.push({ kind: "chart", heading: data.heading, chart: part.props["chart"], points: [...data.points] });
64
+ continue;
65
+ }
66
+ const block = part.type === "Table" ? tableOf(data, part.props["columns"]) : factsOf(data, part.props["facts"]);
67
+ if (typeof block === "string")
68
+ return { ok: false, message: block };
69
+ blocks.push(block);
70
+ }
71
+ const links = [], actions = [];
72
+ for (const props of plan.buttons) {
73
+ const made = buttons(props);
74
+ if (!made.ok)
75
+ return made;
76
+ if (made.button !== undefined)
77
+ links.push(made.button);
78
+ if (made.action !== undefined)
79
+ actions.push(made.action);
80
+ }
81
+ const card = readChatCard({ version: 1, title: title ?? "Toolroll", asOf: reader.now.toISOString(), repos: [...repos], blocks, buttons: links.slice(0, CARD_LIMITS.buttons), actions: actions.slice(0, CARD_LIMITS.actions) });
82
+ return card === null ? { ok: false, message: "That card doesn't fit. Show fewer parts." } : { ok: true, card };
83
+ }
84
+ /** A card kept on a `control` proposal, as its marker says. */
85
+ export const CARD_CONTROL = "card";
86
+ export function cardOfProposal(proposal) {
87
+ return proposal.kind === "control" && proposal.payload["control"] === CARD_CONTROL ? readChatCard(proposal.payload["card"]) : null;
88
+ }
89
+ export function isCardProposal(proposal) {
90
+ return proposal.kind === "control" && proposal.payload["control"] === CARD_CONTROL;
91
+ }
92
+ /** The plain version kept beside a card, for when the card itself can't be read. */
93
+ export const cardPlainOf = (proposal) => typeof proposal.payload["plain"] === "string" ? proposal.payload["plain"] : "This card can't be shown.";
94
+ /** Cards before the proposals they lead to, so each Confirm sits under its card. */
95
+ export function cardsFirst(proposals) {
96
+ return [...proposals.filter(isCardProposal), ...proposals.filter(one => !isCardProposal(one))];
97
+ }
98
+ /** A card goes only where every project behind it is still reachable. */
99
+ export const cardWithin = (card, repos) => card.repos.every(one => repos.includes(one));
@@ -67,6 +67,8 @@ export type ChatChannelName = "telegram" | "slack" | "discord" | "teams";
67
67
  /** The card text once its challenge is armed: an irreversible answer or a
68
68
  * challenge action, in the same words on every channel. */
69
69
  export declare function armedCardText(proposal: LeadProposal, previewText: string): string;
70
+ /** The second question on an action's card, in words true of what it does. */
71
+ export declare function armedQuestion(proposal: LeadProposal): string;
70
72
  /** The "yes" button's label for an armed card. */
71
73
  export declare function armedYesLabel(proposal: LeadProposal): string;
72
74
  export declare function proposalLink(store: Store, proposal: LeadProposal, repos: readonly string[], channel?: ChatChannelName | null): PhoneLink | null;
@@ -3,10 +3,11 @@ import { parseFeedbackImage } from "./result-review.js";
3
3
  import { FLOW_HREF } from "./flow-engine.js";
4
4
  import { closedQuestionWords, LIMITS } from "./decision.js";
5
5
  import { createHash } from "node:crypto";
6
- import { sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, sharedActionAllowsChallenge, CHAT_ACTIONS, } from "./chat-actions.js";
6
+ import { sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, APPROVAL_TERM_CHARS, sharedActionAllowsChallenge, CHAT_ACTIONS, } from "./chat-actions.js";
7
7
  import { isDirectChatProvider, subscriptionCredentialKey } from "./converse.js";
8
8
  import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
9
9
  import { LEAD_TOOL_SCHEMAS } from "./lead-tools.js";
10
+ import { cardPlainOf, isCardProposal } from "./chat-card.js";
10
11
  import { verifyApproverStanding } from "./principal.js";
11
12
  import { canonicalProject } from "./project.js";
12
13
  import { phoneText, projectLabel } from "./telegram-status.js";
@@ -201,9 +202,14 @@ function taskLink(store, taskId, repos) {
201
202
  export function armedCardText(proposal, previewText) {
202
203
  const body = previewText.split("\n\nConfirm or Dismiss below")[0] ?? previewText;
203
204
  if (proposal.kind === "action")
204
- return `${body}\n\nThis records that you handled this exact result. Confirm?`;
205
+ return `${body}\n\n${armedQuestion(proposal)}`;
205
206
  return `⚠ This answer is IRREVERSIBLE.\n\n${body}\n\nConfirm?`;
206
207
  }
208
+ /** The second question on an action's card, in words true of what it does. */
209
+ export function armedQuestion(proposal) {
210
+ const action = proposal.kind === "action" ? sharedActionPayload(proposal.payload) : null;
211
+ return action?.operation === "result_accept" ? "This records that you handled this exact result. Confirm?" : "You approve exactly what's written above. Confirm?";
212
+ }
207
213
  /** The "yes" button's label for an armed card. */
208
214
  export function armedYesLabel(proposal) {
209
215
  if (proposal.kind === "action") {
@@ -340,7 +346,7 @@ export function proposalPreview(store, proposal, repos, channel = null) {
340
346
  if (!action)
341
347
  return handoff("This action is unavailable.");
342
348
  if (channel !== null && sharedActionAllowsChallenge(action))
343
- return card(phoneText(action.title, 200), action.terms.map((term) => phoneText(term, 1200)), "Confirm asks once more before anything is recorded.");
349
+ return card(phoneText(action.title, 200), action.terms.map((term) => phoneText(term, APPROVAL_TERM_CHARS)), "Confirm asks once more before anything is recorded.");
344
350
  return sharedActionNeedsReview(action)
345
351
  ? handoff(phoneText(action.title, 200), [
346
352
  "Review the full details and confirm this exact action.",
@@ -405,7 +411,7 @@ export function proposalPreview(store, proposal, repos, channel = null) {
405
411
  return card(`Rewrite the scope of ${taskName}:`, [
406
412
  `Goal: ${t("goal", 600)}`,
407
413
  ...(t("not") === "" ? [] : [`Not: ${t("not", 300)}`]),
408
- ], "After confirming, the new scope still needs your password approval before work starts.");
414
+ ], "After confirming, the new scope still needs your approval before work starts.");
409
415
  case "answer": {
410
416
  const id = typeof payload["decision"] === "number" ? payload["decision"] : null;
411
417
  const decision = id === null ? null : store.getDecision(id);
@@ -432,9 +438,7 @@ export function proposalPreview(store, proposal, repos, channel = null) {
432
438
  const dependency = t("dependencyTitle", 120) || t("dependency", 64);
433
439
  if (action === null)
434
440
  return handoff(`An unavailable action was proposed for ${taskName}.`);
435
- return card(`${action.label}: ${taskName}${dependency === "" ? "" : ` · ${dependency}`}`, [action.detail], operation === "resume"
436
- ? "Confirming only requests the resume. Work resumes after the password step on the task, not from here."
437
- : null);
441
+ return card(`${action.label}: ${taskName}${dependency === "" ? "" : ` · ${dependency}`}`, [action.detail], null);
438
442
  }
439
443
  case "review": {
440
444
  const revise = payload["operation"] === "revise";
@@ -469,6 +473,8 @@ export function proposalPreview(store, proposal, repos, channel = null) {
469
473
  "Cancelling is armed on the task itself, never from a card.",
470
474
  ]);
471
475
  case "control": {
476
+ if (isCardProposal(proposal))
477
+ return handoff(cardPlainOf(proposal));
472
478
  const control = payload["control"];
473
479
  const label = isChatControl(control)
474
480
  ? CHAT_CONTROLS[control].label
@@ -630,6 +636,12 @@ export const CHAT_ACTION_PARITY = {
630
636
  how: "Pushes the plan's or flow card's own decision card to the owner's paired chat: Approve arms Yes and Cancel, and only the owner's Yes decides, through sealScopeApproval (as a chat-origin approval) or decideFlowCard. Outside the owner's chat-approval setting or a decision's rules it answers why, and the lead opens the console control.",
631
637
  gap: "A flow card's Send back, Edit and a Person chooses step act from the pushed card; a plan's Send back stays in the console. Fixture proof only, no physical-phone rendering.",
632
638
  },
639
+ get_findings: { support: "direct", how: "Read during a turn; a finding's words are shown as untrusted text.", gap: null },
640
+ show_card: {
641
+ support: "direct",
642
+ how: "A card of Toolroll's own records. Telegram shows it as one message: key facts as lines, tables and charts as monospaced text, its links as url buttons.",
643
+ gap: "A chart is bars of text on Telegram, with every number written out.",
644
+ },
633
645
  show_control: {
634
646
  support: "handoff",
635
647
  how: "The card names the control and the task, with one url button to that exact console control when a trusted https console-url is configured; the button opens the signed-in console and acts on nothing.",
@@ -638,12 +650,12 @@ export const CHAT_ACTION_PARITY = {
638
650
  propose_task: {
639
651
  support: "direct",
640
652
  how: "Card with Confirm/Dismiss through confirmLeadProposal (filed as a lead proposal, via telegram). The confirmed card links Review & start for the filed task while its scope waits; under a signed automatic mode it says the scope is approved and links the task.",
641
- gap: "Under manual approval the password step happens in the console, reached from the card's button.",
653
+ gap: "Under manual approval the plan then needs the person's own yes, on its card.",
642
654
  },
643
655
  propose_sweep: {
644
656
  support: "direct",
645
657
  how: "A protected shared action (sweep_file): the card lists every item, the shared rules and the success test; confirming files them all and approves each scope.",
646
- gap: "Starting a sweep approves work, so it opens the secure review in the console (the password, or Passwordless).",
658
+ gap: "Terms too long for one message open the review page, in the signed-in console.",
647
659
  },
648
660
  get_sweep: {
649
661
  support: "direct",
@@ -658,7 +670,7 @@ export const CHAT_ACTION_PARITY = {
658
670
  propose_scope: {
659
671
  support: "direct",
660
672
  how: "Confirm rewrites the scope through the shared door; the confirmed card links Review & start for the exact task.",
661
- gap: "Approving the rewritten scope needs the password, in the console.",
673
+ gap: "Approving the rewritten scope is a separate yes, on its own card.",
662
674
  },
663
675
  propose_next: {
664
676
  support: "direct",
@@ -697,8 +709,8 @@ export const CHAT_ACTION_PARITY = {
697
709
  },
698
710
  propose_task_action: {
699
711
  support: "direct",
700
- how: "stop, retry, plan, wait_for and stop_waiting confirm through the shared door (a stop is audited via telegram); resume confirms only the request, says nothing has resumed, and links the task where the password step lives.",
701
- gap: "resume completes in the console (incomplete phone action).",
712
+ how: "stop, retry, plan, wait_for and stop_waiting confirm through the shared door (a stop is audited via telegram); resume, on a person's own tap, lifts the stop.",
713
+ gap: null,
702
714
  },
703
715
  propose_answer: {
704
716
  support: "direct",
@@ -755,8 +767,8 @@ export function confirmedCardText(store, outcome, proposal, note) {
755
767
  : revise && state === "approved"
756
768
  ? "Revision created under your automatic approval settings."
757
769
  : said;
758
- const next = resume
759
- ? "Nothing has resumed yet: the password step on the task finishes it."
770
+ const next = resume && said.startsWith("Resume requested")
771
+ ? "Nothing has resumed yet: finish it on the task."
760
772
  : approval && note !== null
761
773
  ? "Approve it in Toolroll on the computer."
762
774
  : "";
@@ -26,6 +26,10 @@ export declare const CHAT_CONTROLS: {
26
26
  readonly label: "Open result";
27
27
  readonly target: "task";
28
28
  };
29
+ readonly findings: {
30
+ readonly label: "Open findings";
31
+ readonly target: "task";
32
+ };
29
33
  readonly acceptance: {
30
34
  readonly label: "Inspect result";
31
35
  readonly target: "task";
@@ -8,6 +8,8 @@ export const CHAT_CONTROLS = {
8
8
  recovery: { label: "Review recovery options", target: "task" },
9
9
  cancel: { label: "Cancel task", target: "task" },
10
10
  result: { label: "Open result", target: "task" },
11
+ // A task's findings, failed checks and evidence: closing a finding and starting a repair happen there.
12
+ findings: { label: "Open findings", target: "task" },
11
13
  acceptance: { label: "Inspect result", target: "task" },
12
14
  publish: { label: "Review publication", target: "task" },
13
15
  projects: { label: "Manage projects", href: "/projects" },
@@ -50,5 +52,7 @@ export function chatControlHref(control, task, run, project) {
50
52
  // Task details owns cancel, retry, publication and other dedicated ceremonies.
51
53
  if (control === "cancel" || control === "recovery" || control === "publish")
52
54
  return "/t/" + encodeURIComponent(task);
55
+ if (control === "findings")
56
+ return `/t/${encodeURIComponent(task)}#findings`;
53
57
  return base + (control === "approval" || control === "planning" ? "#task-chat-action" : "");
54
58
  }
@@ -1,5 +1,5 @@
1
1
  import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
2
- import { type ChatApprovalLimits } from "./chat-approval.js";
2
+ import { chatApproveMode } from "./chat-approval.js";
3
3
  import { type VerifiedApprover } from "./principal.js";
4
4
  import type { Scope } from "./scope.js";
5
5
  import { type ReleaseCard } from "./release-card.js";
@@ -86,19 +86,7 @@ export type DecideOffer = {
86
86
  text: string;
87
87
  };
88
88
  };
89
- /** What lets this person's chat on `channel` approve on this repository now: their lasting chat-approval setting
90
- * (chat-approval.ts), or else a live signed mode carrying the chatApprove term, signed by them, that names this chat
91
- * app (a mode signed before the term named its apps covers Telegram alone). `digest` names that authority for the
92
- * ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
93
- export declare function chatApproveMode(store: Store, repo: string | null, approver: string, now: Date, channel: DecideChannel): {
94
- ok: true;
95
- digest: string;
96
- limits: ChatApprovalLimits;
97
- source: "setting" | "mode";
98
- } | {
99
- ok: false;
100
- why: string;
101
- };
89
+ export { chatApproveMode };
102
90
  /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
103
91
  * Toolroll instead. `digest` binds the scope and the plan document the card shows. */
104
92
  export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, channel: DecideChannel, root?: string,
@@ -109,6 +97,7 @@ from?: "requests"): {
109
97
  source: "setting" | "mode";
110
98
  digest: string;
111
99
  scope: Scope;
100
+ whole: boolean;
112
101
  } | {
113
102
  ok: false;
114
103
  why: string;