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
@@ -1,136 +1,10 @@
1
1
  /** Durable private-chat receipts. Each transport has its own tables and lease. */
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { MATE_ASK_TTL_MS } from "./store.js";
4
- const CHAT_SCHEMA = `
5
- CREATE TABLE IF NOT EXISTS chat_binding (
6
- id INTEGER PRIMARY KEY AUTOINCREMENT, installation TEXT NOT NULL,
7
- team TEXT NOT NULL, app TEXT NOT NULL, member TEXT NOT NULL, channel TEXT NOT NULL,
8
- approver TEXT NOT NULL, generation INTEGER NOT NULL, created TEXT NOT NULL, revoked TEXT
9
- );
10
- CREATE UNIQUE INDEX IF NOT EXISTS chat_one_binding_member ON chat_binding(installation, member) WHERE revoked IS NULL;
11
- CREATE TABLE IF NOT EXISTS chat_pair (
12
- hash TEXT PRIMARY KEY, installation TEXT NOT NULL, approver TEXT NOT NULL,
13
- generation INTEGER NOT NULL, expires TEXT NOT NULL, consumed TEXT
14
- );
15
- CREATE TABLE IF NOT EXISTS chat_event (
16
- id TEXT PRIMARY KEY, installation TEXT NOT NULL, binding INTEGER REFERENCES chat_binding(id),
17
- kind TEXT NOT NULL CHECK(kind IN ('message','action','pair','notice')),
18
- channel TEXT NOT NULL, member TEXT NOT NULL, ts TEXT NOT NULL, thread TEXT NOT NULL,
19
- payload TEXT NOT NULL, created TEXT NOT NULL,
20
- session INTEGER REFERENCES mate_session(id), state TEXT NOT NULL DEFAULT 'queued' CHECK(state IN ('queued','done','dropped')),
21
- next_at TEXT, problem TEXT
22
- );
23
- CREATE INDEX IF NOT EXISTS chat_pending_event ON chat_event(installation,state,next_at);
24
- CREATE TABLE IF NOT EXISTS chat_part (
25
- id INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL REFERENCES chat_event(id), ordinal INTEGER NOT NULL,
26
- payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','sent','dropped')),
27
- message TEXT, file TEXT, uploaded INTEGER NOT NULL DEFAULT 0, attempts INTEGER NOT NULL DEFAULT 0,
28
- uncertain INTEGER NOT NULL DEFAULT 0, next_at TEXT, problem TEXT, created TEXT NOT NULL,
29
- UNIQUE(event,ordinal)
30
- );
31
- CREATE TABLE IF NOT EXISTS chat_action (
32
- token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
33
- proposal INTEGER NOT NULL REFERENCES mate_proposal(id), phase TEXT NOT NULL CHECK(phase IN ('confirm','dismiss','yes','cancel')),
34
- expires TEXT NOT NULL, consumed TEXT
35
- );
36
- CREATE TABLE IF NOT EXISTS chat_progress (
37
- binding INTEGER NOT NULL REFERENCES chat_binding(id), run INTEGER NOT NULL REFERENCES run(id),
38
- part INTEGER NOT NULL REFERENCES chat_part(id), digest TEXT NOT NULL,
39
- PRIMARY KEY(binding,run)
40
- );
41
- CREATE TABLE IF NOT EXISTS chat_runtime (
42
- installation TEXT PRIMARY KEY, owner TEXT, lease_until TEXT, connected TEXT,
43
- problem TEXT, retry_at TEXT, notification INTEGER NOT NULL DEFAULT 0
44
- );
45
- `;
46
- /** v73: a chat's place in the shared team conversations — a channel follows one
47
- * conversation, a DM may select one. Rows are revoked, never deleted. */
48
- const CHAT_ROOM_SCHEMA = `
49
- CREATE TABLE IF NOT EXISTS chat_room (
50
- id INTEGER PRIMARY KEY AUTOINCREMENT, installation TEXT NOT NULL, chat TEXT NOT NULL,
51
- kind TEXT NOT NULL CHECK(kind IN ('group','private')), conversation TEXT NOT NULL,
52
- binding INTEGER NOT NULL REFERENCES chat_binding(id),
53
- bound_by TEXT NOT NULL, bound TEXT NOT NULL, cursor INTEGER NOT NULL DEFAULT 0,
54
- revoked TEXT, revoked_by TEXT
55
- );
56
- CREATE UNIQUE INDEX IF NOT EXISTS chat_room_live ON chat_room(installation, chat) WHERE revoked IS NULL;
57
- CREATE UNIQUE INDEX IF NOT EXISTS chat_room_group ON chat_room(conversation) WHERE revoked IS NULL AND kind='group';
58
- CREATE TABLE IF NOT EXISTS chat_meta (
59
- installation TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, updated TEXT NOT NULL,
60
- PRIMARY KEY(installation, key)
61
- );
62
- `;
63
- /** v88: flow decisions in the chat app, as on Telegram (telegram-flow.ts).
64
- * A button is one opaque token for one visit (card, entry) of one card, on
65
- * the part it rides; Edit and Send back open a prompt the person's next
66
- * message in their DM answers. */
67
- const CHAT_FLOW_SCHEMA = `
68
- CREATE TABLE IF NOT EXISTS chat_flow_action (
69
- token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
70
- card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
71
- action TEXT NOT NULL CHECK(action IN ('approve','edit','send-back')), expires TEXT NOT NULL, consumed TEXT
72
- );
73
- CREATE INDEX IF NOT EXISTS chat_flow_action_visit ON chat_flow_action(card, entry);
74
- CREATE TABLE IF NOT EXISTS chat_flow_prompt (
75
- id INTEGER PRIMARY KEY AUTOINCREMENT, binding INTEGER NOT NULL REFERENCES chat_binding(id),
76
- card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
77
- mode TEXT NOT NULL CHECK(mode IN ('edit','send-back')), created TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
78
- );
79
- CREATE INDEX IF NOT EXISTS chat_flow_prompt_open ON chat_flow_prompt(binding, consumed);
80
- `;
81
- /** A flow's "Person chooses" zone in the chat app (flow-send.ts): one button per option (choice is its index, label its words
82
- * when sent) for one visit of one card, on the part it rides. A reply in that notice's thread is the note; chat_flow_note
83
- * is "Use this as your note?" Yes / No about one other message (held: its event; words: what it said, kept only until
84
- * answered, as an event's own payload is cleared once it is handled), asked once per visit.
85
- * Never in CHAT_TABLES: an older database has no such table until this schema creates it. */
86
- const CHAT_FLOW_CHOICE_SCHEMA = `
87
- CREATE TABLE IF NOT EXISTS chat_flow_choice (
88
- token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
89
- card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
90
- choice INTEGER NOT NULL, label TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
91
- );
92
- CREATE INDEX IF NOT EXISTS chat_flow_choice_visit ON chat_flow_choice(card, entry);
93
- CREATE TABLE IF NOT EXISTS chat_flow_note (
94
- token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
95
- card INTEGER NOT NULL REFERENCES flow_card(id), entry INTEGER NOT NULL,
96
- held TEXT NOT NULL, words TEXT, answer TEXT NOT NULL CHECK(answer IN ('yes','no')), expires TEXT NOT NULL, consumed TEXT
97
- );
98
- CREATE INDEX IF NOT EXISTS chat_flow_note_visit ON chat_flow_note(card, entry);
99
- `;
100
- /** v93: a teammate's question in the chat app. One button per option (choice)
101
- * and one to answer in words (choice NULL); that one opens a prompt the
102
- * person's next message in their DM answers. Never in CHAT_TABLES: an older
103
- * database has no such tables until this schema creates them. */
104
- const CHAT_QUESTION_SCHEMA = `
105
- CREATE TABLE IF NOT EXISTS chat_question_action (
106
- token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id),
107
- question INTEGER NOT NULL REFERENCES teammate_question(id), choice TEXT, expires TEXT NOT NULL, consumed TEXT
108
- );
109
- CREATE INDEX IF NOT EXISTS chat_question_action_question ON chat_question_action(question);
110
- CREATE TABLE IF NOT EXISTS chat_question_prompt (
111
- id INTEGER PRIMARY KEY AUTOINCREMENT, binding INTEGER NOT NULL REFERENCES chat_binding(id),
112
- question INTEGER NOT NULL REFERENCES teammate_question(id), created TEXT NOT NULL, expires TEXT NOT NULL, consumed TEXT
113
- );
114
- CREATE INDEX IF NOT EXISTS chat_question_prompt_open ON chat_question_prompt(binding, consumed);
115
- `;
116
- /** The lead's question to its owner (ask_owner) in the chat app: one button per option (choice is its index) and one for
117
- * "Something else" (choice NULL). A tap sends the option as the owner's next message. Never in CHAT_TABLES. */
118
- const CHAT_ASK_SCHEMA = `
119
- CREATE TABLE IF NOT EXISTS chat_ask_action (
120
- token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id) ON DELETE CASCADE,
121
- turn INTEGER NOT NULL, choice INTEGER, expires TEXT NOT NULL, consumed TEXT
122
- );
123
- CREATE INDEX IF NOT EXISTS chat_ask_action_turn ON chat_ask_action(turn);
124
- `;
125
- const CHAT_TABLES = [
126
- "chat_binding",
127
- "chat_pair",
128
- "chat_event",
129
- "chat_part",
130
- "chat_action",
131
- "chat_progress",
132
- "chat_runtime",
133
- ];
4
+ import { chatSchema, chatTables } from "./contracts/chat-tables.js";
5
+ import { readChatPart, savedChatEventBody, savedChatPart } from "./contracts/chat-content.js";
6
+ export { savedChatPart };
7
+ export { chatSchema, chatTables };
134
8
  export const chatHash = (text) => createHash("sha256").update(text).digest("hex");
135
9
  export class ChatState {
136
10
  store;
@@ -275,8 +149,9 @@ export class ChatState {
275
149
  return this.bindingFor(identity.installation, member);
276
150
  });
277
151
  }
152
+ /** Save a received event with its body (contracts/chat-content.ts), versioned. */
278
153
  enqueue(event) {
279
- return (Number(this.prepare("INSERT OR IGNORE INTO chat_event(id,installation,binding,kind,channel,member,ts,thread,payload,created) VALUES(?,?,?,?,?,?,?,?,?,?)").run(event.id, event.installation, event.binding, event.kind, event.channel, event.member, event.ts, event.thread, event.payload, event.created).changes) === 1);
154
+ return (Number(this.prepare("INSERT OR IGNORE INTO chat_event(id,installation,binding,kind,channel,member,ts,thread,payload,created) VALUES(?,?,?,?,?,?,?,?,?,?)").run(event.id, event.installation, event.binding, event.kind, event.channel, event.member, event.ts, event.thread, savedChatEventBody(event.payload), event.created).changes) === 1);
280
155
  }
281
156
  event(id) {
282
157
  return (this.prepare("SELECT * FROM chat_event WHERE id=?").get(id) ?? null);
@@ -296,7 +171,7 @@ export class ChatState {
296
171
  plan(event, parts, now) {
297
172
  this.store.transact(() => {
298
173
  for (const [ordinal, part] of parts.entries()) {
299
- const inserted = this.prepare("INSERT OR IGNORE INTO chat_part(event,ordinal,payload,created) VALUES(?,?,?,?)").run(event, ordinal, JSON.stringify(part), now.toISOString());
174
+ const inserted = this.prepare("INSERT OR IGNORE INTO chat_part(event,ordinal,payload,created) VALUES(?,?,?,?)").run(event, ordinal, savedChatPart(part), now.toISOString());
300
175
  if (Number(inserted.changes) && part.proposal)
301
176
  this.tokens(Number(inserted.lastInsertRowid), part.proposal, ["confirm", "dismiss"], now);
302
177
  if (Number(inserted.changes) && part.question)
@@ -332,16 +207,23 @@ export class ChatState {
332
207
  return !!this.prepare("SELECT 1 FROM chat_runtime WHERE installation=? AND owner=? AND lease_until>?").get(installation, owner, now.toISOString());
333
208
  }
334
209
  }
335
- export const chatSchema = (channel) => (CHAT_SCHEMA + CHAT_ROOM_SCHEMA + CHAT_FLOW_SCHEMA + CHAT_FLOW_CHOICE_SCHEMA + CHAT_QUESTION_SCHEMA + CHAT_ASK_SCHEMA).replaceAll("chat_", `${channel}_`);
336
- export const chatTables = (channel) => CHAT_TABLES.map((name) => name.replace("chat_", `${channel}_`));
210
+ /** A saved part's content. One that can't be read is a delivery problem that names the field, never a guess. */
211
+ export function partContent(payload) {
212
+ const read = readChatPart(payload);
213
+ if (!read.ok)
214
+ throw new ChatDeliveryError(`This saved message can't be read: ${read.issues.map(issue => issue.line).join("; ")}`, 0, false, true);
215
+ return read.value;
216
+ }
337
217
  export class ChatDeliveryError extends Error {
338
218
  code;
339
219
  retryMs;
340
220
  uncertain;
341
- constructor(code, retryMs = 5000, uncertain = false) {
221
+ permanent;
222
+ constructor(code, retryMs = 5000, uncertain = false, permanent = false) {
342
223
  super(code);
343
224
  this.code = code;
344
225
  this.retryMs = retryMs;
345
226
  this.uncertain = uncertain;
227
+ this.permanent = permanent;
346
228
  }
347
229
  }
@@ -7,7 +7,9 @@ import { shapeReplyParts } from "./reply-shape.js";
7
7
  import { warmTurn } from "./chat-warmth.js";
8
8
  import { confirmMateProposal, dismissMateProposal, } from "./mate-doors.js";
9
9
  import { ceilingDigestOf, verifyApproverStanding } from "./principal.js";
10
- import { ChatState, ChatDeliveryError, chatHash, } from "./chat-delivery-state.js";
10
+ import { ChatState, ChatDeliveryError, chatHash, savedChatPart, } from "./chat-delivery-state.js";
11
+ import { readChatActionBody, readChatMessageBody, readChatPairBody, readChatPart } from "./contracts/chat-content.js";
12
+ import { readProposalActionRow } from "./contracts/chat-callback-rows.js";
11
13
  import { answerChatFlowPrompt, answerChoiceMessage, applyChatFlowTap, flowDecisionParts, flowSendParts } from "./chat-flow.js";
12
14
  import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
13
15
  import { triggerConfigOf } from "./flow-triggers.js";
@@ -84,7 +86,10 @@ export async function processChatEvent(options) {
84
86
  if (!state.owns(identity.installation, options.owner, nowOf(options)) ||
85
87
  !options.current())
86
88
  return false;
87
- const paired = state.pair(identity, String(object(JSON.parse(event.payload)).hash), event.member, event.channel, nowOf(options));
89
+ const body = readChatPairBody(event.payload);
90
+ const paired = body.ok
91
+ ? state.pair(identity, body.value.hash, event.member, event.channel, nowOf(options))
92
+ : null;
88
93
  if (!paired) {
89
94
  state.finish(event.id, true);
90
95
  return true;
@@ -112,7 +117,12 @@ export async function processChatEvent(options) {
112
117
  applyChatAction(options, event, binding, repos);
113
118
  return true;
114
119
  }
115
- const input = object(JSON.parse(event.payload));
120
+ const body = readChatMessageBody(event.payload);
121
+ if (!body.ok) {
122
+ state.plan(event.id, [{ text: `That message couldn't be read (${body.issues.map(issue => issue.line).join("; ")}). Send it again.` }], nowOf(options));
123
+ return true;
124
+ }
125
+ const input = body.value;
116
126
  if (typeof input.unsupported === "string") {
117
127
  state.plan(event.id, [{ text: input.unsupported }], nowOf(options));
118
128
  return true;
@@ -193,8 +203,7 @@ export async function processChatEvent(options) {
193
203
  if (text.length > MATE_MESSAGE_MAX_CHARS) {
194
204
  state.plan(event.id, [
195
205
  {
196
- text: tooLongText(Number(object(JSON.parse(event.payload)).originalLength) ||
197
- text.length),
206
+ text: tooLongText(input.originalLength || text.length),
198
207
  },
199
208
  ], nowOf(options));
200
209
  return true;
@@ -237,7 +246,8 @@ export async function processChatEvent(options) {
237
246
  const contexts = state
238
247
  .prepare("SELECT p.payload FROM chat_part p JOIN chat_event e ON e.id=p.event WHERE e.binding=? AND e.channel=? AND (p.message=? OR e.thread=?) AND p.state='sent' ORDER BY p.id DESC LIMIT 100")
239
248
  .all(binding.id, binding.channel, event.thread, event.thread)
240
- .map((row) => JSON.parse(String(row.payload)))
249
+ // Context only: a part that can't be read adds none.
250
+ .flatMap((row) => { const read = readChatPart(String(row.payload)); return read.ok ? [read.value] : []; })
241
251
  // A card is about the task it names, or the one confirming it filed.
242
252
  .map((c) => {
243
253
  if (c.task || c.proposal === undefined)
@@ -385,6 +395,11 @@ export async function processChatEvent(options) {
385
395
  const problem = error instanceof ChatDeliveryError
386
396
  ? error
387
397
  : new ChatDeliveryError(`${options.label} is waiting to retry`);
398
+ if (problem.permanent) {
399
+ state.plan(event.id, [{ text: problem.message }], nowOf(options));
400
+ state.prepare("UPDATE chat_event SET next_at=NULL,problem=? WHERE id=?").run(problem.message, event.id);
401
+ return true;
402
+ }
388
403
  state
389
404
  .prepare("UPDATE chat_runtime SET problem=? WHERE installation=? AND owner=?")
390
405
  .run(problem.message, identity.installation, options.owner);
@@ -405,8 +420,11 @@ async function channelInboxEvent(options, event) {
405
420
  const binding = event.binding === null ? null : state.bindingById(event.binding);
406
421
  if (binding !== null && event.channel === binding.channel)
407
422
  return false;
408
- const input = object(JSON.parse(event.payload));
409
- const text = String(input.text ?? "");
423
+ const body = readChatMessageBody(event.payload);
424
+ // One that can't be read is answered, by path, on the ordinary path.
425
+ if (!body.ok)
426
+ return false;
427
+ const text = body.value.text ?? "";
410
428
  if (FLOW_WORDS.test(text.trim())) {
411
429
  // Only a paired approver connects a channel, and only to a flow in their projects.
412
430
  if (binding === null || !state.live(binding)) {
@@ -450,7 +468,14 @@ export function applyChatAction(options, event, binding, repos) {
450
468
  event.member !== binding.member ||
451
469
  (event.channel !== binding.channel && state.room(options.identity.installation, event.channel)?.kind !== "group"))
452
470
  return;
453
- const token = String(object(JSON.parse(event.payload)).token);
471
+ // A button the paired person tapped whose data couldn't be read is answered plainly: what was wrong, and that nothing was done.
472
+ const body = readChatActionBody(event.payload);
473
+ if (!body.ok || "problem" in body.value) {
474
+ const problem = body.ok ? ("problem" in body.value ? body.value.problem : "") : body.issues.map(issue => issue.line).join("; ");
475
+ state.plan(event.id, [{ text: `That button couldn't be read (${problem}). Nothing was done.` }], now);
476
+ return;
477
+ }
478
+ const token = body.value.token;
454
479
  // A flow decision's button (v88) is answered by the flow's own door.
455
480
  if (applyChatFlowTap({ store, state, label: options.label }, event, binding, token, repos, now))
456
481
  return;
@@ -460,16 +485,19 @@ export function applyChatAction(options, event, binding, repos) {
460
485
  // The lead's question to its owner: the tapped option becomes their next message.
461
486
  if (applyChatAskTap({ store, state }, event, binding, token, now))
462
487
  return;
463
- const action = state
488
+ const saved = state
464
489
  .prepare("SELECT a.*,p.message,e.binding,e.channel,e.thread FROM chat_action a JOIN chat_part p ON p.id=a.part JOIN chat_event e ON e.id=p.event WHERE token=?")
465
490
  .get(token);
491
+ // The saved button, read by its schema: one that can't be read is a spent button, never a guess.
492
+ const read = saved === undefined ? null : readProposalActionRow({ ...saved });
493
+ const action = read?.ok === true ? read.value : null;
466
494
  const invalid = !action ||
467
495
  action.binding !== binding.id ||
468
496
  action.channel !== event.channel ||
469
497
  action.message !== event.ts ||
470
498
  (options.state.channel === "slack" && action.thread !== event.thread) ||
471
499
  action.consumed !== null ||
472
- String(action.expires) <= now.toISOString();
500
+ action.expires <= now.toISOString();
473
501
  if (invalid) {
474
502
  state.plan(event.id, [
475
503
  {
@@ -478,7 +506,7 @@ export function applyChatAction(options, event, binding, repos) {
478
506
  ], now);
479
507
  return;
480
508
  }
481
- const proposal = store.getMateProposal(Number(action.proposal));
509
+ const proposal = store.getMateProposal(action.proposal);
482
510
  const verified = verifyApproverStanding(store, binding.approver, binding.generation, repos);
483
511
  if (!proposal || !verified.ok) {
484
512
  state.finish(event.id, true);
@@ -490,14 +518,14 @@ export function applyChatAction(options, event, binding, repos) {
490
518
  edit: event.ts,
491
519
  };
492
520
  const preview = proposalPreview(store, proposal, repos, options.state.channel);
493
- const phase = String(action.phase);
521
+ const phase = action.phase;
494
522
  if (proposal.state !== "pending")
495
523
  content = { text: proposalOutcomeText(proposal), edit: event.ts };
496
524
  else if (!preview.buttons ||
497
525
  preview.text.length > (options.maxProposal ?? 10_000))
498
526
  content = { ...content, text: "Review this action in Toolroll." };
499
527
  else if (phase === "cancel") {
500
- state.tokens(Number(action.part), proposal.id, ["confirm", "dismiss"], now);
528
+ state.tokens(action.part, proposal.id, ["confirm", "dismiss"], now);
501
529
  }
502
530
  else if (phase === "dismiss") {
503
531
  const dismissed = dismissMateProposal(store, verified.who, proposal.id, now);
@@ -512,7 +540,7 @@ export function applyChatAction(options, event, binding, repos) {
512
540
  proposal.kind === "answer" &&
513
541
  proposal.payload.reversible === false) {
514
542
  content = { ...content, phase: "armed" };
515
- state.tokens(Number(action.part), proposal.id, ["yes", "cancel"], now);
543
+ state.tokens(action.part, proposal.id, ["yes", "cancel"], now);
516
544
  }
517
545
  else {
518
546
  const outcome = confirmMateProposal(store, verified.who, proposal.id, now, {
@@ -524,7 +552,7 @@ export function applyChatAction(options, event, binding, repos) {
524
552
  });
525
553
  if (!outcome.ok && outcome.reason === "needs-confirm") {
526
554
  content = { ...content, phase: "armed" };
527
- state.tokens(Number(action.part), proposal.id, ["yes", "cancel"], now);
555
+ state.tokens(action.part, proposal.id, ["yes", "cancel"], now);
528
556
  }
529
557
  else {
530
558
  content = {
@@ -540,7 +568,7 @@ export function applyChatAction(options, event, binding, repos) {
540
568
  // Repaint the original persisted card; retain one placement and fresh tokens.
541
569
  state
542
570
  .prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?")
543
- .run(JSON.stringify(content), Number(action.part));
571
+ .run(savedChatPart(content), action.part);
544
572
  if (!content.proposal)
545
573
  state
546
574
  .prepare("UPDATE chat_action SET consumed=? WHERE proposal=? AND consumed IS NULL")
@@ -607,7 +635,7 @@ export async function planRoomMessages(options) {
607
635
  const id = chatHash(`${state.channel}:room:${room.id}:${message.id}`);
608
636
  const target = carrier;
609
637
  store.transact(() => {
610
- if (parts.length > 0 && state.enqueue({ id, installation: identity.installation, binding: target.id, kind: "message", channel: room.chat, member: target.member, ts: "", thread: "", payload: "{}", created: now.toISOString() }))
638
+ if (parts.length > 0 && state.enqueue({ id, installation: identity.installation, binding: target.id, kind: "message", channel: room.chat, member: target.member, ts: "", thread: "", payload: {}, created: now.toISOString() }))
611
639
  state.plan(id, parts, now);
612
640
  state.advanceRoomCursor(room.id, message.id);
613
641
  });
@@ -629,13 +657,13 @@ function planQuietCard(options, binding, notification, now) {
629
657
  if (card.message !== null) {
630
658
  if (card.digest === shown)
631
659
  return;
632
- state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(JSON.stringify(content), now.toISOString(), Number(card.message));
660
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(card.message));
633
661
  store.setChatCardMessage(card.id, card.message, shown);
634
662
  return;
635
663
  }
636
664
  const id = chatHash(`${state.channel}:card:${binding.id}:${card.id}`);
637
665
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
638
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
666
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
639
667
  state.plan(id, [content], now);
640
668
  const part = state.prepare("SELECT id FROM chat_part WHERE event=?").get(id);
641
669
  if (part !== undefined)
@@ -659,13 +687,13 @@ function planFinished(options, binding, taskRef, run, now, notification = null)
659
687
  if (batch.message !== null) {
660
688
  if (batch.digest === shown)
661
689
  return true;
662
- state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(JSON.stringify(content), now.toISOString(), Number(batch.message));
690
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(batch.message));
663
691
  store.setChatBatchMessage(batch.id, batch.message, shown);
664
692
  return true;
665
693
  }
666
694
  const id = chatHash(`${state.channel}:batch:${binding.id}:${batch.id}`);
667
695
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
668
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
696
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
669
697
  state.plan(id, [content], now);
670
698
  const part = state.prepare("SELECT id FROM chat_part WHERE event=?").get(id);
671
699
  if (part !== undefined)
@@ -714,7 +742,7 @@ function planResultShots(options, binding, notification, repos, id, now) {
714
742
  image: image(plan.shots[0].artifact, plan.shots[0].sha256), shot: { follows: null }, ...where }]
715
743
  : plan.shots.map(one => ({ text: one.caption, image: image(one.artifact, one.sha256), shot: { follows }, ...where }));
716
744
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
717
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
745
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
718
746
  state.plan(id, parts, now);
719
747
  }
720
748
  /** One progress card per exact result; separate urgent facts retain their own review link. */
@@ -770,11 +798,11 @@ export async function planChatNotifications(options) {
770
798
  const earlier = leadSayEarlier(store, notification).map(one => state.prepare("SELECT id FROM chat_part WHERE event=?")
771
799
  .get(chatHash(`${options.state.channel}:notice:${binding.id}:${one}`))).find(one => one !== undefined);
772
800
  if (earlier !== undefined) {
773
- state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(JSON.stringify(content), now.toISOString(), Number(earlier.id));
801
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(earlier.id));
774
802
  return;
775
803
  }
776
804
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
777
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
805
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
778
806
  state.plan(id, [content], now);
779
807
  return;
780
808
  }
@@ -826,7 +854,7 @@ export async function planChatNotifications(options) {
826
854
  member: binding.member,
827
855
  ts: "",
828
856
  thread: "",
829
- payload: "{}",
857
+ payload: {},
830
858
  created: now.toISOString(),
831
859
  });
832
860
  state.plan(id, [content], now);
@@ -843,20 +871,20 @@ export async function planChatNotifications(options) {
843
871
  const parts = flowDecisionParts(store, notification, state.channel);
844
872
  if (parts !== null) {
845
873
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
846
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
874
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
847
875
  state.plan(id, parts, now);
848
876
  }
849
877
  }
850
878
  else if (personal && flowSendParts(store, notification, state.channel) !== null) {
851
879
  // A flow's "Send to me" (flow-send.ts): what was done, with its links as buttons.
852
880
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
853
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
881
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
854
882
  state.plan(id, flowSendParts(store, notification, state.channel), now);
855
883
  }
856
884
  else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
857
885
  // A teammate's question (v93): its options and "Answer in words" on the notice, for the person it asks.
858
886
  state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
859
- ts: "", thread: "", payload: "{}", created: now.toISOString() });
887
+ ts: "", thread: "", payload: {}, created: now.toISOString() });
860
888
  state.plan(id, questionParts(store, notification, binding), now);
861
889
  }
862
890
  else if (notification.pushClass !== null || personal || quiet) {
@@ -869,7 +897,7 @@ export async function planChatNotifications(options) {
869
897
  member: binding.member,
870
898
  ts: "",
871
899
  thread: "",
872
- payload: "{}",
900
+ payload: {},
873
901
  created: now.toISOString(),
874
902
  });
875
903
  state.plan(id, [
package/dist/chat-flow.js CHANGED
@@ -34,7 +34,8 @@ import { renderReply } from "./reply-shape.js";
34
34
  import { deciderOf, replyTarget } from "./flows.js";
35
35
  import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
36
36
  import { verifyApproverStanding } from "./principal.js";
37
- import { chatHash } from "./chat-delivery-state.js";
37
+ import { chatHash, partContent, savedChatPart } from "./chat-delivery-state.js";
38
+ import { readHeldWords } from "./contracts/chat-content.js";
38
39
  import { FLOW_DECIDE_KEY, flowDecisionAt } from "./telegram-flow.js";
39
40
  import { phoneCommand } from "./telegram-status.js";
40
41
  export const CHAT_FLOW_LABELS = { approve: "Approve", edit: "Edit", "send-back": "Send back" };
@@ -144,11 +145,11 @@ const retire = (state, card, entry, now) => {
144
145
  /** Show the tapped notice again with what happened, and without its buttons. */
145
146
  function repaint(state, part, event, line) {
146
147
  const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
147
- const before = row === undefined ? { text: "" } : JSON.parse(String(row["payload"]));
148
+ const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
148
149
  // An item list (flow-items.ts) stays whole and keeps its bold titles and labelled links; only plain text is capped.
149
150
  const content = { text: before.voice === true ? `${before.text}\n\n${line}` : `${before.text}\n\n${line}`.slice(0, PART_CHARS + 400), edit: event.ts, ...(before.voice === true ? { voice: true } : {}),
150
151
  ...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
151
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(JSON.stringify(content), part);
152
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
152
153
  }
153
154
  /**
154
155
  * A tapped flow button, inside the action's transaction. false when the
@@ -351,8 +352,9 @@ function applyChatNoteTap(options, event, binding, token, repos, now) {
351
352
  }
352
353
  state.prepare("UPDATE chat_flow_note SET consumed=?,words=NULL WHERE part=?").run(now.toISOString(), part);
353
354
  const held = state.event(String(tapped["held"]));
354
- const words = typeof tapped["words"] === "string" ? JSON.parse(tapped["words"]) : null;
355
- if (held === null || words === null || typeof words.text !== "string") {
355
+ const kept = typeof tapped["words"] === "string" ? readHeldWords(tapped["words"]) : null;
356
+ const words = kept?.ok === true ? kept.value : null;
357
+ if (held === null || words === null) {
356
358
  repaint(state, part, event, "That message is no longer kept; nothing was changed.");
357
359
  state.finish(event.id);
358
360
  return true;
@@ -360,7 +362,7 @@ function applyChatNoteTap(options, event, binding, token, repos, now) {
360
362
  if (tapped["answer"] === "no") {
361
363
  // Passed on as the message it was, to the lead, once.
362
364
  state.enqueue({ id: chatHash(`${held.id}:lead`), installation: held.installation, binding: held.binding, kind: "message", channel: held.channel, member: held.member,
363
- ts: held.ts, thread: held.thread, payload: JSON.stringify({ ...words, lead: true }), created: now.toISOString() });
365
+ ts: held.ts, thread: held.thread, payload: { ...words, lead: true }, created: now.toISOString() });
364
366
  repaint(state, part, event, "Passed to the lead.");
365
367
  state.finish(event.id);
366
368
  return true;
@@ -116,7 +116,7 @@ export function replyInChannel(store, source, visit, text, now) {
116
116
  const id = chatHash(`${source.app}:flow-reply:${visit.card}:${visit.entry}`);
117
117
  store.transact(() => {
118
118
  if (state.enqueue({ id, installation: grant.installation, binding: grant.id, kind: "message", channel: source.conversation, member: grant.member,
119
- ts: source.thread, thread: source.thread, payload: "{}", created: now.toISOString() }))
119
+ ts: source.thread, thread: source.thread, payload: {}, created: now.toISOString() }))
120
120
  state.plan(id, [{ text: text.slice(0, 3500), channel: source.conversation }], now);
121
121
  });
122
122
  return { ok: true };
@@ -1,43 +1,6 @@
1
1
  import type { Store } from './store.js';
2
- export type CodingContextMetadata = {
3
- version: 1;
4
- repo: string;
5
- identity: string;
6
- baseRevision: string;
7
- knowledge: {
8
- revision: number;
9
- selectionSha256: string;
10
- references: {
11
- id: string;
12
- title: string;
13
- path: string | null;
14
- sourceSha: string | null;
15
- sourceRevision: string | null;
16
- }[];
17
- omitted: {
18
- title: string;
19
- reason: string;
20
- }[];
21
- };
22
- skills: {
23
- revision: number;
24
- packages: {
25
- name: string;
26
- sha256: string;
27
- skillFile: string;
28
- }[];
29
- };
30
- directory: string | null;
31
- files: {
32
- path: string;
33
- sha256: string;
34
- }[];
35
- };
36
- export type CodingContext = {
37
- text: string;
38
- metadata: CodingContextMetadata;
39
- sha256: string;
40
- };
2
+ import { type CodingContext } from './contracts/coding-context.js';
3
+ export type { CodingContext, CodingContextMetadata } from './contracts/coding-context.js';
41
4
  /**
42
5
  * Call before thread/start with the canonical project and the exact worktree
43
6
  * base. Persist the returned object with the coding session and reuse it on
@@ -5,6 +5,7 @@ import { basename, dirname, join, resolve, relative, isAbsolute, sep } from 'nod
5
5
  import { KNOWLEDGE_GUIDANCE, selectProjectKnowledge } from './project-knowledge.js';
6
6
  import { materializeProjectSkills, PROJECT_SKILLS_GUIDANCE, selectProjectSkills } from './project-skills.js';
7
7
  import { identityMatches, learningIdentity, learningSha } from './project-learning.js';
8
+ import { CODING_CONTEXT_VERSION } from './contracts/coding-context.js';
8
9
  function freeze(value) {
9
10
  if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
10
11
  for (const child of Object.values(value))
@@ -70,7 +71,7 @@ export function prepareCodingContext(store, args) {
70
71
  directory,
71
72
  files: captured.skills.packages.flatMap(skill => skill.files.map(file => ({ path: `${skill.name}/${file.path}`, sha256: fileDigest(Buffer.from(file.base64, 'base64')) }))),
72
73
  };
73
- return freeze({ text, metadata, sha256: digest(text, metadata) });
74
+ return freeze({ version: CODING_CONTEXT_VERSION, text, metadata, sha256: digest(text, metadata) });
74
75
  }
75
76
  /** Integrity check for a persisted capture. The caller must separately recheck current account access. */
76
77
  export function verifyCodingContext(context, repo, baseRevision) {
@@ -1,28 +1,7 @@
1
- import { type AcceptanceCriterion } from './scope.js';
1
+ import { type CodingHandoffReceipt } from './contracts/coding-handoff.js';
2
2
  import type { Store } from './store.js';
3
- export type CodingHandoffPreview = {
4
- sessionId: string;
5
- repo: string;
6
- base: string;
7
- candidate: string;
8
- title: string;
9
- originalPrompt: string;
10
- changedPaths: string[];
11
- };
12
- type ReceiptTerms = CodingHandoffPreview & {
13
- version: 1;
14
- identity: string;
15
- actor: string;
16
- generation: number;
17
- goal: string;
18
- acceptance: AcceptanceCriterion[];
19
- outOfScope: string;
20
- };
21
- export type CodingHandoffReceipt = ReceiptTerms & {
22
- id: string;
23
- taskId: string;
24
- branch: string;
25
- };
3
+ export type { CodingHandoffReceipt } from './contracts/coding-handoff.js';
4
+ export type CodingHandoffPreview = Pick<CodingHandoffReceipt, 'sessionId' | 'repo' | 'base' | 'candidate' | 'title' | 'originalPrompt' | 'changedPaths'>;
26
5
  export type CodingHandoffInput = {
27
6
  sessionId: string;
28
7
  repo: string;
@@ -42,6 +21,9 @@ export declare function codingHandoffPreview(store: Store, input: {
42
21
  sessionId: string;
43
22
  actor: string;
44
23
  }): CodingHandoffPreview;
24
+ /** The saved bytes are verified before they are parsed; identity and branch are checked on the parsed terms. */
25
+ export declare function parseReceipt(row: Record<string, unknown>): CodingHandoffReceipt;
26
+ export declare function sealPayload(receipt: CodingHandoffReceipt, scopeDigest: string): string;
45
27
  /** Returns null only for ordinary tasks. A coding marker always requires its receipt. */
46
28
  export declare function readCodingHandoff(store: Store, taskId: string): Handoff | null;
47
29
  /** No agent dispatch, approval, push, or modification of an existing Git ref. */
@@ -54,4 +36,3 @@ export declare function verifyCodingHandoffBase(store: Store, input: {
54
36
  branch: string;
55
37
  head: string;
56
38
  }): void;
57
- export {};