toolroll 0.9.36 → 0.9.38

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 (175) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/assignment-adapters.d.ts +12 -48
  3. package/dist/assignment-adapters.js +33 -43
  4. package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +26 -26
  7. package/dist/browser-crew.js +5 -0
  8. package/dist/browser-workspace.d.ts +2 -0
  9. package/dist/build-review.d.ts +1 -1
  10. package/dist/build-review.js +2 -1
  11. package/dist/chat-acceptance.d.ts +1 -1
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-channel.js +20 -9
  16. package/dist/chat-decide.d.ts +18 -4
  17. package/dist/chat-decide.js +31 -11
  18. package/dist/chat-delivery-state.d.ts +12 -70
  19. package/dist/chat-delivery-state.js +17 -135
  20. package/dist/chat-delivery.js +58 -30
  21. package/dist/chat-flow.js +8 -6
  22. package/dist/chat-inbox.js +1 -1
  23. package/dist/coding-context.d.ts +2 -39
  24. package/dist/coding-context.js +2 -1
  25. package/dist/coding-handoff.d.ts +6 -25
  26. package/dist/coding-handoff.js +11 -5
  27. package/dist/coding-types.d.ts +2 -26
  28. package/dist/coding-workspace.d.ts +2 -0
  29. package/dist/coding-workspace.js +50 -14
  30. package/dist/contracts/acceptance-terms.d.ts +14 -0
  31. package/dist/contracts/acceptance-terms.js +16 -0
  32. package/dist/contracts/chat-actions.d.ts +328 -0
  33. package/dist/contracts/chat-actions.js +133 -0
  34. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  35. package/dist/contracts/chat-callback-rows.js +46 -0
  36. package/dist/contracts/chat-content.d.ts +276 -0
  37. package/dist/contracts/chat-content.js +127 -0
  38. package/dist/contracts/chat-tables.d.ts +6 -0
  39. package/dist/contracts/chat-tables.js +136 -0
  40. package/dist/contracts/coding-context.d.ts +97 -0
  41. package/dist/contracts/coding-context.js +60 -0
  42. package/dist/contracts/coding-handoff.d.ts +44 -0
  43. package/dist/contracts/coding-handoff.js +47 -0
  44. package/dist/contracts/coding-workspace.d.ts +165 -0
  45. package/dist/contracts/coding-workspace.js +71 -0
  46. package/dist/contracts/contract.d.ts +7 -2
  47. package/dist/contracts/contract.js +21 -12
  48. package/dist/contracts/decision.d.ts +45 -0
  49. package/dist/contracts/decision.js +45 -0
  50. package/dist/contracts/discord-callback.d.ts +45 -0
  51. package/dist/contracts/discord-callback.js +34 -0
  52. package/dist/contracts/flow-card.d.ts +50 -0
  53. package/dist/contracts/flow-card.js +46 -0
  54. package/dist/contracts/flow-send.d.ts +119 -0
  55. package/dist/contracts/flow-send.js +112 -0
  56. package/dist/contracts/flow.d.ts +5 -0
  57. package/dist/contracts/flow.js +5 -0
  58. package/dist/contracts/gateway-tools.d.ts +374 -0
  59. package/dist/contracts/gateway-tools.js +94 -0
  60. package/dist/contracts/lead-context.d.ts +165 -0
  61. package/dist/contracts/lead-context.js +73 -0
  62. package/dist/contracts/lead-tools.d.ts +1905 -0
  63. package/dist/contracts/lead-tools.js +265 -0
  64. package/dist/contracts/memory-pass.d.ts +158 -0
  65. package/dist/contracts/memory-pass.js +102 -0
  66. package/dist/contracts/project-knowledge.d.ts +126 -0
  67. package/dist/contracts/project-knowledge.js +93 -0
  68. package/dist/contracts/project-memory.d.ts +117 -0
  69. package/dist/contracts/project-memory.js +51 -0
  70. package/dist/contracts/project-skills.d.ts +121 -0
  71. package/dist/contracts/project-skills.js +76 -0
  72. package/dist/contracts/review-findings.d.ts +46 -0
  73. package/dist/contracts/review-findings.js +48 -0
  74. package/dist/contracts/route.d.ts +484 -0
  75. package/dist/contracts/route.js +128 -0
  76. package/dist/contracts/scope.d.ts +105 -0
  77. package/dist/contracts/scope.js +114 -0
  78. package/dist/contracts/scout-report.d.ts +234 -0
  79. package/dist/contracts/scout-report.js +141 -0
  80. package/dist/contracts/slack-callback.d.ts +43 -0
  81. package/dist/contracts/slack-callback.js +35 -0
  82. package/dist/contracts/sort-answer.d.ts +83 -0
  83. package/dist/contracts/sort-answer.js +77 -0
  84. package/dist/contracts/stage-output.d.ts +127 -0
  85. package/dist/contracts/stage-output.js +166 -0
  86. package/dist/contracts/task-sizing.d.ts +24 -0
  87. package/dist/contracts/task-sizing.js +28 -0
  88. package/dist/contracts/teammate-turn.d.ts +43 -0
  89. package/dist/contracts/teammate-turn.js +51 -0
  90. package/dist/contracts/teams-callback.d.ts +14 -0
  91. package/dist/contracts/teams-callback.js +14 -0
  92. package/dist/contracts/telegram-callback.d.ts +120 -0
  93. package/dist/contracts/telegram-callback.js +97 -0
  94. package/dist/decision.d.ts +11 -20
  95. package/dist/decision.js +86 -167
  96. package/dist/desktop-update.d.ts +1 -1
  97. package/dist/discord-chat.js +31 -20
  98. package/dist/flow-engine.js +14 -17
  99. package/dist/flow-items.d.ts +4 -13
  100. package/dist/flow-items.js +1 -15
  101. package/dist/flow-send.d.ts +6 -37
  102. package/dist/flow-send.js +18 -14
  103. package/dist/flow-share.js +7 -1
  104. package/dist/flow-sort.d.ts +7 -26
  105. package/dist/flow-sort.js +25 -20
  106. package/dist/flow-steps.js +3 -2
  107. package/dist/flows.d.ts +2 -4
  108. package/dist/flows.js +9 -5
  109. package/dist/invoke.js +4 -4
  110. package/dist/lead-context.d.ts +4 -2
  111. package/dist/lead-context.js +24 -17
  112. package/dist/mate-progress.d.ts +26 -0
  113. package/dist/mate-progress.js +24 -0
  114. package/dist/mate-tools.d.ts +4 -0
  115. package/dist/mate-tools.js +108 -275
  116. package/dist/mate.js +11 -2
  117. package/dist/mcp-connect.d.ts +24 -4
  118. package/dist/mcp-connect.js +78 -15
  119. package/dist/mcp.js +84 -216
  120. package/dist/memory-pass.d.ts +17 -42
  121. package/dist/memory-pass.js +54 -26
  122. package/dist/phase-routing.d.ts +19 -72
  123. package/dist/phase-routing.js +34 -164
  124. package/dist/project-knowledge.d.ts +16 -35
  125. package/dist/project-knowledge.js +42 -21
  126. package/dist/project-memory.d.ts +2 -28
  127. package/dist/project-memory.js +28 -13
  128. package/dist/project-skills.d.ts +16 -38
  129. package/dist/project-skills.js +41 -32
  130. package/dist/project-tools.d.ts +1 -0
  131. package/dist/project-tools.js +4 -4
  132. package/dist/provider.d.ts +2 -0
  133. package/dist/provider.js +4 -16
  134. package/dist/result-shots.d.ts +1 -1
  135. package/dist/result-shots.js +2 -1
  136. package/dist/review-switch.d.ts +3 -8
  137. package/dist/reviewer.d.ts +8 -12
  138. package/dist/reviewer.js +11 -35
  139. package/dist/scope.d.ts +21 -43
  140. package/dist/scope.js +42 -128
  141. package/dist/scout-report.d.ts +9 -194
  142. package/dist/scout-report.js +113 -264
  143. package/dist/scout.js +15 -11
  144. package/dist/serve.js +23 -5
  145. package/dist/setup-guide.d.ts +2 -2
  146. package/dist/slack-chat.js +26 -20
  147. package/dist/slack-state.js +2 -1
  148. package/dist/store.d.ts +9 -32
  149. package/dist/store.js +18 -15
  150. package/dist/structured-output.js +2 -1
  151. package/dist/subscription-chat.d.ts +4 -0
  152. package/dist/subscription-chat.js +4 -1
  153. package/dist/task-review-brief.d.ts +1 -7
  154. package/dist/task-review-brief.js +6 -6
  155. package/dist/task-sizing.d.ts +3 -27
  156. package/dist/task-sizing.js +11 -26
  157. package/dist/task-text.d.ts +0 -5
  158. package/dist/task-text.js +1 -3
  159. package/dist/teammate-question.js +7 -5
  160. package/dist/teammate-work.js +9 -5
  161. package/dist/teammates.d.ts +13 -64
  162. package/dist/teammates.js +32 -33
  163. package/dist/teams-chat.js +20 -11
  164. package/dist/telegram-flow.d.ts +9 -0
  165. package/dist/telegram-flow.js +44 -8
  166. package/dist/telegram-mate.d.ts +4 -14
  167. package/dist/telegram-mate.js +9 -8
  168. package/dist/telegram-team.d.ts +2 -19
  169. package/dist/telegram.d.ts +1 -1
  170. package/dist/telegram.js +94 -68
  171. package/dist/text-limits.d.ts +142 -1
  172. package/dist/text-limits.js +144 -1
  173. package/dist/tools-ui.d.ts +2 -1
  174. package/dist/tools-ui.js +7 -4
  175. package/package.json +17 -12
@@ -38,6 +38,10 @@ export function browserCrewFromIndex(page, conversationId) {
38
38
  const link = (href) => conversationId && href.startsWith('/chat?') ? href + '&conversation=' + encodeURIComponent(conversationId) : href;
39
39
  const rows = page.items.map(summary => {
40
40
  const status = summary.status;
41
+ // Keep the server's sentence intact; ignore only empty or repeated badge words.
42
+ const words = (text) => text.trim().replace(/\s+/g, ' ').replace(/[.!?]+$/, '').toLowerCase();
43
+ const detail = ['Waiting', 'Failed', 'Needs you'].includes(status.label)
44
+ && words(status.detail) !== '' && words(status.detail) !== words(status.label) ? status.detail : undefined;
41
45
  const resultHref = summary.resultRunId === null || summary.resultTaskId === null || (summary.resultOutcome !== 'built' && summary.resultOutcome !== 'no-change')
42
46
  ? null : chatResultHref(summary.resultTaskId, summary.resultRunId);
43
47
  const href = chatControlHref('task', summary.rootId);
@@ -48,6 +52,7 @@ export function browserCrewFromIndex(page, conversationId) {
48
52
  state: summary.assignmentState, label: status.label, tone: status.tone, href: link(href), resultHref: resultHref === null ? null : link(resultHref),
49
53
  action: action === null || actionHref === null ? null : { label: action.label, href: link(actionHref) },
50
54
  updatedAt: summary.updatedAt,
55
+ ...(detail === undefined ? {} : { detail }),
51
56
  ...(summary.lead === undefined ? {} : { lead: summary.lead }),
52
57
  };
53
58
  return { item, rank: status.rank };
@@ -49,6 +49,8 @@ export type BrowserCrewItem = {
49
49
  } | null;
50
50
  /** When the task family last changed (the index's own sort time), shown as the row's age. */
51
51
  updatedAt: string;
52
+ /** The server's status detail, when Waiting, Failed or Needs you adds to its badge. */
53
+ detail?: string;
52
54
  /** "<name> is on it.": the person's own lead took it on, by the name they gave it. */
53
55
  lead?: string;
54
56
  };
@@ -1,6 +1,6 @@
1
1
  import type { Store } from "./store.js";
2
2
  import type { Runner } from "./builder.js";
3
- import type { BuildFinding } from "./review-switch.js";
3
+ import { type BuildFinding } from "./contracts/review-findings.js";
4
4
  export { buildReviewOf, buildReviewHeadline, buildReviewLines, findingWords, type BuildReviewView } from "./review-switch.js";
5
5
  export declare const AUTOMATIC_REVIEWER = "automatic review";
6
6
  /**
@@ -29,6 +29,7 @@ import { runCheckLevel } from "./check-levels.js";
29
29
  import { requestResultChanges } from "./result-actions.js";
30
30
  import { revisionSourceOf } from "./result-review.js";
31
31
  import { parseBuildFindings, REVIEW_PATCH_NAME } from "./reviewer.js";
32
+ import { FINDINGS_VERSION } from "./contracts/review-findings.js";
32
33
  import { heartbeat as runnerHeartbeat } from "./runner.js";
33
34
  import { TELEGRAM_TOKEN_ENVS } from "./names.js";
34
35
  import { CLAUDE_LIMITS } from "./scope.js";
@@ -90,7 +91,7 @@ function queueLocked(store, repo, runId, root, now) {
90
91
  }
91
92
  function settle(store, runId, state, fields, now) {
92
93
  store.handle.prepare("UPDATE build_review SET state = ?, findings_json = ?, reason = ?, sent_back_as = ?, finished_at = ? WHERE run = ? AND state = 'pending'")
93
- .run(state, fields.findings === undefined ? null : JSON.stringify({ version: 1, findings: fields.findings }), fields.reason ?? null, fields.sentBackAs ?? null, now.toISOString(), runId);
94
+ .run(state, fields.findings === undefined ? null : JSON.stringify({ version: FINDINGS_VERSION, findings: fields.findings }), fields.reason ?? null, fields.sentBackAs ?? null, now.toISOString(), runId);
94
95
  store.bumpWake();
95
96
  }
96
97
  /** Only the first automatic send-back in a task family: its revision's HIGH goes to the person. */
@@ -39,7 +39,7 @@ export declare function readAcceptanceEvidence(store: Store, who: VerifiedApprov
39
39
  requirement: string | null;
40
40
  state: string;
41
41
  details: (string | null)[];
42
- requiredEvidence: readonly import("./scope.js").EvidenceKind[];
42
+ requiredEvidence: readonly ("changed-path" | "check" | "manual-review" | "screenshot")[];
43
43
  reviewer: {
44
44
  judgement: import("./proof.js").CriterionJudgementWord;
45
45
  note: string | null;
@@ -1,5 +1,7 @@
1
1
  import { type Store } from "./store.js";
2
2
  import { type VerifiedApprover } from "./principal.js";
3
+ import { CHAT_ACTION_FIELDS, type SharedAction } from "./contracts/chat-actions.js";
4
+ export { CHAT_ACTION_FIELDS, type SharedAction };
3
5
  export declare const CHAT_ACTIONS: {
4
6
  readonly skill_import: {
5
7
  readonly label: "Add skill";
@@ -235,20 +237,11 @@ export declare const CHALLENGE_ACTIONS: ReadonlySet<ChatAction>;
235
237
  /** Actions about the person themselves, in no project: their card carries no project (repo ""). */
236
238
  export declare const OWNER_ACTIONS: ReadonlySet<ChatAction>;
237
239
  export declare function isChatAction(value: unknown): value is ChatAction;
238
- export type SharedAction = {
239
- operation: ChatAction;
240
- request: Record<string, unknown>;
241
- repo: string;
242
- title: string;
243
- terms: string[];
244
- stamp: string;
245
- state: Record<string, unknown>;
246
- };
247
- export declare const CHAT_ACTION_FIELDS: Record<ChatAction, readonly string[]>;
248
240
  /** No writes, including at preview. Store-derived snapshots are never accepted
249
241
  * from a model. Rebuilding the same request before execution catches changes. */
250
242
  export declare function prepareSharedAction(store: Store, who: VerifiedApprover, operation: ChatAction, input: Record<string, unknown>, root?: string, now?: Date): SharedAction;
251
- export declare function sharedActionPayload(value: Record<string, unknown>): SharedAction | null;
243
+ /** A saved proposal's action, read by its schema (contracts/chat-actions.ts); null when it can't be read. */
244
+ export declare function sharedActionPayload(value: unknown): SharedAction | null;
252
245
  export declare function sharedActionNeedsReview(action: SharedAction): boolean;
253
246
  /** Whether a paired phone may confirm this protected action behind its own
254
247
  * explicit challenge: the action is on the challenge list, needs no
@@ -256,7 +249,20 @@ export declare function sharedActionNeedsReview(action: SharedAction): boolean;
256
249
  export declare function sharedActionAllowsChallenge(action: SharedAction): boolean;
257
250
  export declare function sharedActionReviewPath(id: number): string;
258
251
  export declare function mintSharedActionReview(store: Store, who: VerifiedApprover, id: number, root: string, now: Date): {
259
- payload: SharedAction;
252
+ payload: {
253
+ version: 1;
254
+ operation: "decision_record" | "decision_retire" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "task_cancel" | "task_resume" | "teammate_answer" | "teammate_create" | "teammate_memory" | "teammate_note" | "teammate_routine" | "teammate_soul" | "teammate_state" | "teammate_tools" | "teammate_undo" | "tool_add" | "tool_remove";
255
+ request: {
256
+ [x: string]: unknown;
257
+ };
258
+ repo: string;
259
+ title: string;
260
+ terms: string[];
261
+ stamp: string;
262
+ state: {
263
+ [x: string]: unknown;
264
+ };
265
+ };
260
266
  nonce: string;
261
267
  };
262
268
  export type SharedActionOptions = {
@@ -35,6 +35,8 @@ import { starterFlowOf, starterOf, startersFor, starterTerms, STARTER_FLOWS, swi
35
35
  import { answerTeammateQuestion } from "./teammate-work.js";
36
36
  import { callWords, checkRule, defaultRule, grantListed, revokeTool, ruleWords, setToolRules } from "./teammate-tools.js";
37
37
  import { aboutYouOf, checkAboutYouLine, saveAboutYou, withAboutYouLine } from "./lead-about.js";
38
+ import { CHAT_ACTION_FIELDS, readChatActionRequest, readSharedAction, SHARED_ACTION_VERSION } from "./contracts/chat-actions.js";
39
+ export { CHAT_ACTION_FIELDS };
38
40
  /** A tool from the lead's card: a common tool by its id, or the operator's own program or address. */
39
41
  function toolSpecFromRequest(input) {
40
42
  if (input["catalog"] !== undefined) {
@@ -118,6 +120,9 @@ export const CHAT_ACTIONS = {
118
120
  task_cancel: { label: "Cancel task", protected: true, password: false },
119
121
  task_resume: { label: "Resume task", protected: true, password: true },
120
122
  };
123
+ // Every action has a schema (contracts/chat-actions.ts) and every schema an action: a mismatch fails to compile.
124
+ const SAME_ACTIONS = true;
125
+ void SAME_ACTIONS;
121
126
  /** Protected actions a paired phone may confirm behind its own explicit
122
127
  * yes/cancel challenge instead of the console's secure screen. Password
123
128
  * actions and long or redacted terms never qualify. */
@@ -128,53 +133,6 @@ export function isChatAction(value) {
128
133
  return typeof value === "string" && Object.hasOwn(CHAT_ACTIONS, value);
129
134
  }
130
135
  const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
131
- export const CHAT_ACTION_FIELDS = {
132
- skill_import: ["repo", "content", "files"],
133
- skill_enable: ["repo", "version"],
134
- skill_disable: ["repo", "version"],
135
- skill_restore: ["repo", "restore"],
136
- skill_test: ["repo", "version", "sample", "nonce"],
137
- knowledge_instructions: ["repo", "instructions"],
138
- knowledge_save: ["repo", "title", "content", "id"],
139
- knowledge_remove: ["repo", "id"],
140
- knowledge_restore: ["repo", "restore"],
141
- tool_add: ["repo", "catalog", "name", "command", "args", "url", "secrets", "about"],
142
- tool_remove: ["repo", "name"],
143
- flow_create: ["repo", "name", "definition", "trigger"],
144
- flow_starter: ["repo", "starter"],
145
- flow_edit: ["flow", "name", "definition"],
146
- flow_card_add: ["flow", "title", "description", "zone"],
147
- flow_card_move: ["card", "zone"],
148
- flow_card_approve: ["card", "note"],
149
- flow_card_send_back: ["card", "note"],
150
- flow_card_choose: ["card", "choice", "note"],
151
- flow_card_cancel: ["card"],
152
- flow_card_comment: ["card", "note"],
153
- flow_card_assign: ["card", "owner"],
154
- flow_card_watch: ["card", "watching"],
155
- flow_script_save: ["repo", "script"],
156
- flow_trigger_add: ["flow", "trigger"],
157
- flow_trigger_pause: ["trigger"],
158
- flow_trigger_resume: ["trigger"],
159
- flow_trigger_remove: ["trigger"],
160
- teammate_create: ["repo", "template", "name", "soul"],
161
- teammate_soul: ["teammate", "soul"],
162
- teammate_state: ["teammate", "state"],
163
- teammate_note: ["teammate", "note"],
164
- teammate_answer: ["question", "choice", "text"],
165
- teammate_tools: ["teammate", "tool", "change", "action", "use", "limitField", "limitOver", "undoWith"],
166
- teammate_memory: ["teammate", "memory", "change", "text"],
167
- teammate_routine: ["teammate", "change", "routine", "schedule", "text"],
168
- teammate_undo: ["teammate", "call"],
169
- kit_setup: ["repo", "kit"],
170
- decision_record: ["repo", "claim", "why", "supersedes", "source"],
171
- decision_retire: ["repo", "decision", "reason"],
172
- lead_about_you: ["line", "replaces"],
173
- scope_approve: ["task"],
174
- result_accept: ["task", "run"],
175
- task_cancel: ["task"],
176
- task_resume: ["task", "run"],
177
- };
178
136
  const nonceHash = (value) => createHash("sha256").update(value).digest("hex");
179
137
  function requireActor(store, who, repo, operation) {
180
138
  if (!store.schemaCurrent())
@@ -277,9 +235,11 @@ const quoted = (value) => `“${value}”`;
277
235
  export function prepareSharedAction(store, who, operation, input, root, now = new Date()) {
278
236
  if (!isChatAction(operation))
279
237
  throw Error("Choose an available action.");
280
- const allowed = CHAT_ACTION_FIELDS[operation];
281
- if (Object.keys(input).some((key) => !allowed.includes(key)))
282
- throw Error("This action contains an unsupported field.");
238
+ // The action's own schema: its fields and their kinds, refused by path. The request itself is used as given, so a
239
+ // confirmation rebuilds the same stamp from the same bytes.
240
+ const read = readChatActionRequest(operation, input);
241
+ if (!read.ok)
242
+ throw Error(`This action can't be read — ${read.issues.map(issue => issue.line).join("; ")}.`);
283
243
  if (operation === "lead_about_you") {
284
244
  // The owner's own note: the card shows the line, and the line it replaces beside it.
285
245
  const checked = checkAboutYouLine(input["line"]);
@@ -300,7 +260,7 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
300
260
  const state = replaces === 0 ? {} : { was: lines[replaces - 1] };
301
261
  const title = replaces === 0 ? "Remember about you" : "Update what your lead knows about you";
302
262
  const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
303
- return { operation, request, repo: "", title, terms, stamp, state };
263
+ return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
304
264
  }
305
265
  const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("teammate_") || operation.startsWith("kit_")
306
266
  ? null
@@ -965,22 +925,12 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
965
925
  generation: who.generation,
966
926
  ceiling: who.ceilingDigest,
967
927
  });
968
- return { operation, request, repo, title, terms, stamp, state };
928
+ return { version: SHARED_ACTION_VERSION, operation, request, repo, title, terms, stamp, state };
969
929
  }
930
+ /** A saved proposal's action, read by its schema (contracts/chat-actions.ts); null when it can't be read. */
970
931
  export function sharedActionPayload(value) {
971
- return isChatAction(value["operation"]) &&
972
- typeof value["stamp"] === "string" &&
973
- typeof value["repo"] === "string" &&
974
- typeof value["title"] === "string" &&
975
- Array.isArray(value["terms"]) &&
976
- value["terms"].every((t) => typeof t === "string") &&
977
- value["request"] !== null &&
978
- typeof value["request"] === "object" &&
979
- !Array.isArray(value["request"]) &&
980
- value["state"] !== null &&
981
- typeof value["state"] === "object"
982
- ? value
983
- : null;
932
+ const read = readSharedAction(value);
933
+ return read.ok ? read.value : null;
984
934
  }
985
935
  /** A shortened or redacted preview is not complete consent. This rule lives
986
936
  * in the shared door, so a forged transport callback cannot bypass it. */
package/dist/chat-ask.js CHANGED
@@ -1,4 +1,4 @@
1
- import { chatHash } from "./chat-delivery-state.js";
1
+ import { chatHash, partContent, savedChatPart } from "./chat-delivery-state.js";
2
2
  import { MATE_ASK_OTHER } from "./store.js";
3
3
  /** The question as text with its options, where no buttons are drawn. */
4
4
  export function chatAskText(ask) {
@@ -10,7 +10,7 @@ export function chatAskButtons(state, part, now) {
10
10
  const row = state.prepare("SELECT p.payload,e.channel,b.channel AS own FROM chat_part p JOIN chat_event e ON e.id=p.event JOIN chat_binding b ON b.id=e.binding WHERE p.id=?").get(part);
11
11
  if (row === undefined)
12
12
  return [];
13
- const content = JSON.parse(String(row["payload"]));
13
+ const content = partContent(String(row["payload"]));
14
14
  if ((content.channel ?? row["channel"]) !== row["own"])
15
15
  return [];
16
16
  const options = content.ask?.options ?? [];
@@ -32,9 +32,9 @@ function repaint(state, part, event, line, now) {
32
32
  state.plan(event.id, [{ text: line }], now);
33
33
  return;
34
34
  }
35
- const before = JSON.parse(String(row["payload"]));
35
+ const before = partContent(String(row["payload"]));
36
36
  const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.channel === undefined ? {} : { channel: before.channel }) };
37
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(JSON.stringify(content), part);
37
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
38
38
  state.finish(event.id);
39
39
  }
40
40
  /** A tapped ask button, inside the action's transaction; false when the token isn't one. */
@@ -84,7 +84,7 @@ export function applyChatAskTap(options, event, binding, token, now) {
84
84
  state.enqueue({
85
85
  id: chatHash(`${state.channel}:ask:${event.id}`).slice(0, 32), installation: event.installation, binding: binding.id, kind: "message",
86
86
  channel: event.channel, member: event.member, ts: event.ts, thread: event.thread,
87
- payload: JSON.stringify({ text: option, originalLength: option.length }), created: now.toISOString(),
87
+ payload: { text: option, originalLength: option.length }, created: now.toISOString(),
88
88
  });
89
89
  repaint(state, part, event, `You chose: ${option}`, now);
90
90
  return true;
@@ -273,16 +273,27 @@ export function proposalPreview(store, proposal, repos, channel = null) {
273
273
  : undefined;
274
274
  return path === undefined ? id : projectLabel(path);
275
275
  })();
276
+ // Telegram's card leads with the action and what confirming does, then the exact terms; its buttons say the rest.
277
+ const phone = channel === "telegram";
276
278
  const card = (headline, body = [], consequence = null) => ({
277
- text: [
278
- headline,
279
- ...body,
280
- ...(consequence === null ? [] : ["", consequence]),
281
- "",
282
- "Confirm or Dismiss below. Nothing changes until you confirm.",
283
- ].join("\n"),
279
+ text: (phone
280
+ ? [
281
+ headline,
282
+ ...(consequence === null ? [] : [consequence]),
283
+ ...(body.length === 0 ? [] : ["", ...body]),
284
+ "",
285
+ "Nothing changes until you confirm.",
286
+ ]
287
+ : [
288
+ headline,
289
+ ...body,
290
+ ...(consequence === null ? [] : ["", consequence]),
291
+ "",
292
+ "Confirm or Dismiss below. Nothing changes until you confirm.",
293
+ ]).join("\n"),
284
294
  buttons: true,
285
295
  });
296
+ const lasting = phone ? " ⚠ can't be undone" : " — IRREVERSIBLE";
286
297
  // A handoff's text is origin-free on purpose: it is persisted before the
287
298
  // send, and the button or the missing-setup line joins it at send time.
288
299
  const handoff = (headline, body = []) => ({
@@ -361,11 +372,11 @@ export function proposalPreview(store, proposal, repos, channel = null) {
361
372
  ? ["(the decision is gone)"]
362
373
  : [
363
374
  `Q: ${phoneText(decision.question, 400)}`,
364
- ...decision.options.map((one) => `${one.id === pick ? "→" : " "} ${phoneText(one.label, 120)}${one.reversible ? "" : " — IRREVERSIBLE"}${one.id === decision.recommendation ? " (the builder recommends this)" : ""}: ${phoneText(one.consequence, 300)}`),
375
+ ...decision.options.map((one) => `${one.id === pick ? "→" : " "} ${phoneText(one.label, 120)}${one.reversible ? "" : lasting}${one.id === decision.recommendation ? " (the builder recommends this)" : ""}: ${phoneText(one.consequence, 300)}`),
365
376
  ...(t("rationale") === "" ? [] : [`Why: ${t("rationale", 300)}`]),
366
377
  ];
367
378
  return card(`Answer decision #${String(id ?? "?")} on ${taskName} with "${t("optionLabel", 120) || pick}"`, body, irreversible
368
- ? "⚠ This choice is irreversible. Confirming asks you once more."
379
+ ? phone ? "⚠ This choice can't be undone. Confirming asks you once more." : "⚠ This choice is irreversible. Confirming asks you once more."
369
380
  : null);
370
381
  }
371
382
  case "task_action": {
@@ -1,9 +1,22 @@
1
+ /** Decisions finish in the chat app. A pushed card about a result, a failed task, a plan waiting for approval or a
2
+ * pull request ready to merge carries buttons that act in place: Accept and finish, Request changes, Retry,
3
+ * Approve & start, Merge. Every act that changes something asks twice ("Accept and finish 'X'? [Yes] [Cancel]"),
4
+ * and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
5
+ * newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
6
+ *
7
+ * Approving a plan or merging needs the password, which never goes in chat, so they act in chat only under a
8
+ * repository's signed mode that carries the chatApprove term, for the signer's own chat; otherwise, and for a plan
9
+ * that widens permissions, exceeds the mode's budget or touches protected paths, the card keeps its link.
10
+ *
11
+ * Channel-agnostic: Telegram renders these today, and Slack, Discord and Teams reach the same acts through the same
12
+ * tokens (the `channel` column). Every act runs through the door the console uses. */
13
+ import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
1
14
  import { type ModeTerms } from "./modes.js";
2
15
  import { type VerifiedApprover } from "./principal.js";
3
16
  import type { Scope } from "./scope.js";
4
17
  import type { Store } from "./store.js";
5
18
  /** The tokens live in chat_decide_action and the open "What should change?" in chat_decide_prompt (store.ts). */
6
- export type DecideAct = "accept" | "changes" | "retry" | "approve" | "not-now" | "merge";
19
+ export type DecideAct = (typeof DECIDE_ACTS)[number];
7
20
  export type DecideTarget = {
8
21
  kind: "result";
9
22
  taskId: string;
@@ -95,11 +108,12 @@ export declare function mergeInChat(store: Store, taskId: string, run: number, a
95
108
  ok: false;
96
109
  why: string;
97
110
  };
98
- /** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. */
99
- export declare function planCardText(store: Store, taskId: string, scope: Scope, root?: string): string;
111
+ /** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. On Telegram the yes's
112
+ * terms come second, under the title, and the steps are fewer: the full plan is behind Edit. */
113
+ export declare function planCardText(store: Store, taskId: string, scope: Scope, root?: string, channel?: DecideChannel): string;
100
114
  /** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
101
115
  * link). Reads only; nothing is minted. */
102
- export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, root?: string): DecideOffer | null;
116
+ export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, root?: string, channel?: DecideChannel): DecideOffer | null;
103
117
  /** One fingerprint of what the card offers, without its tokens: a repaint with the same offer keeps its buttons. */
104
118
  export declare function offerFingerprint(offer: DecideOffer): string;
105
119
  /** The card's buttons, minted now (message unknown until it is sent: `placeDecideTokens` puts them on it). */
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Channel-agnostic: Telegram renders these today, and Slack, Discord and Teams reach the same acts through the same
12
12
  * tokens (the `channel` column). Every act runs through the door the console uses. */
13
+ import { DECIDE_ACTS, readDecideActionRow } from "./contracts/chat-callback-rows.js";
13
14
  import { createHash, randomBytes } from "node:crypto";
14
15
  import { acceptAndCompleteAsOperator, assignmentOf, owedAcceptance } from "./assignment.js";
15
16
  import { chatControlHref, chatResultHref } from "./chat-controls.js";
@@ -154,8 +155,9 @@ export function mergeInChat(store, taskId, run, approver, now) {
154
155
  return mode;
155
156
  return { ok: true, modeDigest: mode.digest, head: publication.headSha };
156
157
  }
157
- /** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. */
158
- export function planCardText(store, taskId, scope, root) {
158
+ /** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. On Telegram the yes's
159
+ * terms come second, under the title, and the steps are fewer: the full plan is behind Edit. */
160
+ export function planCardText(store, taskId, scope, root, channel) {
159
161
  const ref = store.lookupRef(taskId);
160
162
  const artifact = ref === null ? null : store.latestPlanArtifact(ref.id);
161
163
  let milestones = [];
@@ -175,6 +177,19 @@ export function planCardText(store, taskId, scope, root) {
175
177
  const goal = scope.goal.split("\n").filter(one => one.trim() !== "").slice(0, 2).map(one => phoneText(one, 160));
176
178
  const allowing = [permissionPlainWords(scope.profile), scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`]
177
179
  .filter((one) => one !== null);
180
+ if (channel === "telegram") {
181
+ return [
182
+ `Plan ready: ${title(store, taskId)}`,
183
+ `Starting allows: ${allowing.join(" · ")}`,
184
+ "",
185
+ ...goal,
186
+ ...(scope.touches.length > 0 ? [`Only in: ${scope.touches.slice(0, 6).map(one => phoneText(one, 80)).join(", ")}${scope.touches.length > 6 ? ", …" : ""}`] : ["Any file in the project."]),
187
+ "",
188
+ "Done when:",
189
+ ...(scope.acceptance.length > 0 ? bullets(scope.acceptance.map(one => one.statement), 5) : ["You decide when you review the result."]),
190
+ ...(milestones.length > 0 ? ["", "Steps:", ...bullets(milestones, 3)] : []),
191
+ ].join("\n");
192
+ }
178
193
  return [
179
194
  `Plan ready: ${title(store, taskId)}`,
180
195
  "",
@@ -193,7 +208,7 @@ export function planCardText(store, taskId, scope, root) {
193
208
  // ---- what a card offers ---------------------------------------------------------------
194
209
  /** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
195
210
  * link). Reads only; nothing is minted. */
196
- export function decideOffer(store, target, who, now, root) {
211
+ export function decideOffer(store, target, who, now, root, channel) {
197
212
  const ref = store.lookupRef(target.taskId);
198
213
  if (ref === null || ref.repo === null || !who.repos.includes(ref.repo))
199
214
  return null;
@@ -221,7 +236,7 @@ export function decideOffer(store, target, who, now, root) {
221
236
  const plan = planInChat(store, target.taskId, who.name, now, root);
222
237
  if (!plan.ok)
223
238
  return null;
224
- return { text: planCardText(store, target.taskId, plan.scope, root),
239
+ return { text: planCardText(store, target.taskId, plan.scope, root, channel),
225
240
  rows: [[{ act: "approve", run: null, digest: plan.digest }], [{ label: DECIDE_LABELS.edit, path: chatControlHref("approval", target.taskId) }, { act: "not-now", run: null, digest: plan.digest }]] };
226
241
  }
227
242
  case "merge": {
@@ -258,11 +273,12 @@ function retryStamp(store, taskId, who) {
258
273
  export function offerFingerprint(offer) {
259
274
  return sha(JSON.stringify(offer));
260
275
  }
261
- function readAction(row) {
262
- return { token: String(row["token"]), channel: String(row["channel"]), binding: Number(row["binding"]), chat: String(row["chat"]),
263
- message: row["message"] == null ? null : String(row["message"]), act: String(row["act"]), phase: String(row["phase"]),
264
- taskId: String(row["task_id"]), run: row["run"] == null ? null : Number(row["run"]), digest: String(row["digest"]),
265
- expiresAt: String(row["expires_at"]), consumedAt: row["consumed_at"] == null ? null : String(row["consumed_at"]) };
276
+ function readAction(raw) {
277
+ const read = readDecideActionRow(raw);
278
+ if (!read.ok)
279
+ return read;
280
+ const { task_id: taskId, expires_at: expiresAt, consumed_at: consumedAt, created_at: _created, ...row } = read.value;
281
+ return { ok: true, value: { ...row, taskId, expiresAt, consumedAt } };
266
282
  }
267
283
  function mint(store, seat, message, spec, taskId, phase, now) {
268
284
  const token = `d:${randomBytes(12).toString("hex")}`;
@@ -378,7 +394,11 @@ export function applyDecideTap(store, seat, input) {
378
394
  const raw = input.token.startsWith("d:") ? store.handle.prepare("SELECT * FROM chat_decide_action WHERE token = ?").get(input.token) : undefined;
379
395
  if (raw === undefined)
380
396
  return null;
381
- const row = readAction(raw);
397
+ const read = readAction(raw);
398
+ // A saved button that can't be read acts on nothing, and says why.
399
+ if (!read.ok)
400
+ return { ack: `That button can't be read (${read.issues.map(issue => issue.line).join("; ")}). Nothing was done.`, ignored: true };
401
+ const row = read.value;
382
402
  const { now } = input;
383
403
  if (row.channel !== seat.channel || row.binding !== seat.binding || row.chat !== seat.chat || (row.message !== null && row.message !== input.message))
384
404
  return { ack: "That button isn't for this chat.", ignored: true };
@@ -408,7 +428,7 @@ export function applyDecideTap(store, seat, input) {
408
428
  }
409
429
  if (row.phase === "cancel") {
410
430
  spend();
411
- const offer = decideOffer(store, target, who, now, input.root);
431
+ const offer = decideOffer(store, target, who, now, input.root, seat.channel);
412
432
  if (offer === null)
413
433
  return { ack: "Cancelled.", edit: { text: body, rows: linksFor(target), tokens: [] } };
414
434
  const minted = mintDecideButtons(store, seat, target, offer, now, input.message);
@@ -1,4 +1,8 @@
1
1
  import { type Store } from "./store.js";
2
+ import { chatSchema, chatTables } from "./contracts/chat-tables.js";
3
+ import { savedChatPart, type ChatContent, type ChatEventBody } from "./contracts/chat-content.js";
4
+ export { savedChatPart, type ChatContent, type ChatEventBody };
5
+ export { chatSchema, chatTables };
2
6
  export type ChatIdentity = {
3
7
  installation: string;
4
8
  team?: string;
@@ -57,72 +61,6 @@ export type ChatRoom = {
57
61
  binding: number;
58
62
  cursor: number;
59
63
  };
60
- export type ChatContent = {
61
- text: string;
62
- /** Where the part is sent when not the binding's own DM: a room's channel. */
63
- channel?: string;
64
- proposal?: number;
65
- image?: {
66
- taskId: string;
67
- run: number;
68
- artifact: number;
69
- sha256: string;
70
- };
71
- /** A screenshot sent with a result (result-shots.ts): it follows that result's message part, in its thread when
72
- * the app allows; a refused upload becomes one plain line, and one removed by retention goes quietly. */
73
- shot?: {
74
- follows: number | null;
75
- };
76
- task?: string;
77
- run?: number;
78
- edit?: string;
79
- phase?: "armed";
80
- link?: {
81
- label: string;
82
- path: string;
83
- };
84
- /** More link buttons after `link`: [Look first] beside [Merge] or [Accept and finish]. */
85
- also?: Array<{
86
- label: string;
87
- path: string;
88
- }>;
89
- /** A flow decision's buttons ride this part (v88): minted when it is planned. */
90
- flow?: {
91
- card: number;
92
- entry: number;
93
- actions: Array<"approve" | "edit" | "send-back">;
94
- };
95
- /** A flow's "Person chooses" buttons ride this part: minted when it is planned. */
96
- choose?: {
97
- card: number;
98
- entry: number;
99
- options: Array<{
100
- choice: number;
101
- label: string;
102
- }>;
103
- };
104
- /** "Use this as your note?" Yes / No about the person's message `held` (an event id), for one choice's visit. */
105
- note?: {
106
- card: number;
107
- entry: number;
108
- held: string;
109
- };
110
- /** A teammate's question's buttons ride this part (v93): each option, then one to answer in words (choice null). */
111
- question?: {
112
- id: number;
113
- choices: Array<{
114
- choice: string | null;
115
- label: string;
116
- }>;
117
- };
118
- /** The lead's question to its owner rides this part: its options, then "Something else". */
119
- ask?: {
120
- turn: number;
121
- options: string[];
122
- };
123
- /** The lead's own reply, already shaped (reply-shape.ts): the channel renders its bold anchors and labelled links in its own format. */
124
- voice?: true;
125
- };
126
64
  export declare const chatHash: (text: string) => string;
127
65
  export declare class ChatState {
128
66
  readonly store: Store;
@@ -170,7 +108,10 @@ export declare class ChatState {
170
108
  revoke(installation: string, now?: Date): void;
171
109
  pairing(installation: string, approver: string, generation: number, now?: Date): string;
172
110
  pair(identity: ChatIdentity, hash: string, member: string, channel: string, now: Date): ChatBinding | null;
173
- enqueue(event: Omit<ChatEvent, "session" | "state" | "next_at">): boolean;
111
+ /** Save a received event with its body (contracts/chat-content.ts), versioned. */
112
+ enqueue(event: Omit<ChatEvent, "session" | "state" | "next_at" | "payload"> & {
113
+ payload: ChatEventBody;
114
+ }): boolean;
174
115
  event(id: string): ChatEvent | null;
175
116
  next(installation: string, now: Date): ChatEvent | null;
176
117
  defer(event: string, problem: string, until: Date): void;
@@ -181,11 +122,12 @@ export declare class ChatState {
181
122
  lease(installation: string, owner: string, now: Date): boolean;
182
123
  owns(installation: string, owner: string, now?: Date): boolean;
183
124
  }
184
- export declare const chatSchema: (channel: "slack" | "discord" | "teams") => string;
185
- export declare const chatTables: (channel: "slack" | "discord" | "teams") => string[];
125
+ /** A saved part's content. One that can't be read is a delivery problem that names the field, never a guess. */
126
+ export declare function partContent(payload: string): ChatContent;
186
127
  export declare class ChatDeliveryError extends Error {
187
128
  readonly code: string;
188
129
  readonly retryMs: number;
189
130
  readonly uncertain: boolean;
190
- constructor(code: string, retryMs?: number, uncertain?: boolean);
131
+ readonly permanent: boolean;
132
+ constructor(code: string, retryMs?: number, uncertain?: boolean, permanent?: boolean);
191
133
  }