toolroll 0.9.34 → 0.9.37

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 (195) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -0
  3. package/dist/assignment-adapters.d.ts +12 -48
  4. package/dist/assignment-adapters.js +33 -43
  5. package/dist/assignment.d.ts +1 -0
  6. package/dist/assignment.js +2 -2
  7. package/dist/browser-workspace.d.ts +3 -3
  8. package/dist/build-review.d.ts +1 -1
  9. package/dist/build-review.js +2 -1
  10. package/dist/builder.js +4 -7
  11. package/dist/chat-acceptance.d.ts +2 -2
  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-decide.d.ts +14 -1
  16. package/dist/chat-decide.js +12 -6
  17. package/dist/chat-delivery-state.d.ts +12 -70
  18. package/dist/chat-delivery-state.js +17 -135
  19. package/dist/chat-delivery.js +58 -30
  20. package/dist/chat-flow.js +8 -6
  21. package/dist/chat-inbox.js +1 -1
  22. package/dist/coding-context.d.ts +2 -39
  23. package/dist/coding-context.js +2 -1
  24. package/dist/coding-handoff.d.ts +6 -25
  25. package/dist/coding-handoff.js +11 -5
  26. package/dist/coding-types.d.ts +2 -26
  27. package/dist/coding-workspace.d.ts +2 -0
  28. package/dist/coding-workspace.js +50 -14
  29. package/dist/contracts/acceptance-terms.d.ts +14 -0
  30. package/dist/contracts/acceptance-terms.js +16 -0
  31. package/dist/contracts/chat-actions.d.ts +328 -0
  32. package/dist/contracts/chat-actions.js +133 -0
  33. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  34. package/dist/contracts/chat-callback-rows.js +46 -0
  35. package/dist/contracts/chat-content.d.ts +276 -0
  36. package/dist/contracts/chat-content.js +127 -0
  37. package/dist/contracts/chat-tables.d.ts +6 -0
  38. package/dist/contracts/chat-tables.js +136 -0
  39. package/dist/contracts/coding-context.d.ts +97 -0
  40. package/dist/contracts/coding-context.js +60 -0
  41. package/dist/contracts/coding-handoff.d.ts +44 -0
  42. package/dist/contracts/coding-handoff.js +47 -0
  43. package/dist/contracts/coding-workspace.d.ts +165 -0
  44. package/dist/contracts/coding-workspace.js +71 -0
  45. package/dist/contracts/contract.d.ts +40 -7
  46. package/dist/contracts/contract.js +155 -19
  47. package/dist/contracts/decision.d.ts +45 -0
  48. package/dist/contracts/decision.js +45 -0
  49. package/dist/contracts/discord-callback.d.ts +45 -0
  50. package/dist/contracts/discord-callback.js +34 -0
  51. package/dist/contracts/flow-card.d.ts +50 -0
  52. package/dist/contracts/flow-card.js +46 -0
  53. package/dist/contracts/flow-propose.d.ts +325 -0
  54. package/dist/contracts/flow-propose.js +41 -0
  55. package/dist/contracts/flow-send.d.ts +119 -0
  56. package/dist/contracts/flow-send.js +112 -0
  57. package/dist/contracts/flow.d.ts +19405 -0
  58. package/dist/contracts/flow.js +364 -0
  59. package/dist/contracts/gateway-tools.d.ts +374 -0
  60. package/dist/contracts/gateway-tools.js +94 -0
  61. package/dist/contracts/handoff.d.ts +91 -0
  62. package/dist/contracts/handoff.js +82 -0
  63. package/dist/contracts/lead-context.d.ts +165 -0
  64. package/dist/contracts/lead-context.js +73 -0
  65. package/dist/contracts/lead-tools.d.ts +1905 -0
  66. package/dist/contracts/lead-tools.js +265 -0
  67. package/dist/contracts/memory-pass.d.ts +158 -0
  68. package/dist/contracts/memory-pass.js +102 -0
  69. package/dist/contracts/project-knowledge.d.ts +126 -0
  70. package/dist/contracts/project-knowledge.js +93 -0
  71. package/dist/contracts/project-memory.d.ts +117 -0
  72. package/dist/contracts/project-memory.js +51 -0
  73. package/dist/contracts/project-skills.d.ts +121 -0
  74. package/dist/contracts/project-skills.js +76 -0
  75. package/dist/contracts/proof.d.ts +136 -0
  76. package/dist/contracts/proof.js +110 -0
  77. package/dist/contracts/review-findings.d.ts +46 -0
  78. package/dist/contracts/review-findings.js +48 -0
  79. package/dist/contracts/route.d.ts +484 -0
  80. package/dist/contracts/route.js +128 -0
  81. package/dist/contracts/scope.d.ts +105 -0
  82. package/dist/contracts/scope.js +114 -0
  83. package/dist/contracts/scout-report.d.ts +234 -0
  84. package/dist/contracts/scout-report.js +141 -0
  85. package/dist/contracts/slack-callback.d.ts +43 -0
  86. package/dist/contracts/slack-callback.js +35 -0
  87. package/dist/contracts/sort-answer.d.ts +83 -0
  88. package/dist/contracts/sort-answer.js +77 -0
  89. package/dist/contracts/stage-output.d.ts +127 -0
  90. package/dist/contracts/stage-output.js +166 -0
  91. package/dist/contracts/task-sizing.d.ts +24 -0
  92. package/dist/contracts/task-sizing.js +28 -0
  93. package/dist/contracts/teammate-turn.d.ts +43 -0
  94. package/dist/contracts/teammate-turn.js +51 -0
  95. package/dist/contracts/teams-callback.d.ts +14 -0
  96. package/dist/contracts/teams-callback.js +14 -0
  97. package/dist/contracts/telegram-callback.d.ts +120 -0
  98. package/dist/contracts/telegram-callback.js +97 -0
  99. package/dist/contracts/verification-receipt.d.ts +148 -0
  100. package/dist/contracts/verification-receipt.js +87 -0
  101. package/dist/decision.d.ts +11 -20
  102. package/dist/decision.js +86 -167
  103. package/dist/demo.js +28 -11
  104. package/dist/discord-chat.js +31 -20
  105. package/dist/evidence.d.ts +3 -40
  106. package/dist/evidence.js +5 -1
  107. package/dist/flow-engine.js +14 -17
  108. package/dist/flow-gallery.d.ts +3 -2
  109. package/dist/flow-items.d.ts +4 -13
  110. package/dist/flow-items.js +1 -15
  111. package/dist/flow-send.d.ts +6 -37
  112. package/dist/flow-send.js +18 -14
  113. package/dist/flow-share.d.ts +10 -32
  114. package/dist/flow-share.js +72 -84
  115. package/dist/flow-sort.d.ts +7 -26
  116. package/dist/flow-sort.js +25 -20
  117. package/dist/flow-starters.d.ts +2 -1
  118. package/dist/flow-steps.js +3 -2
  119. package/dist/flow-triggers.d.ts +15 -72
  120. package/dist/flow-triggers.js +94 -83
  121. package/dist/flows.d.ts +37 -220
  122. package/dist/flows.js +356 -387
  123. package/dist/invoke.js +4 -4
  124. package/dist/lead-context.d.ts +4 -2
  125. package/dist/lead-context.js +24 -17
  126. package/dist/mate-tools.d.ts +4 -0
  127. package/dist/mate-tools.js +111 -308
  128. package/dist/mcp-connect.d.ts +27 -4
  129. package/dist/mcp-connect.js +92 -17
  130. package/dist/mcp.js +84 -216
  131. package/dist/memory-pass.d.ts +17 -42
  132. package/dist/memory-pass.js +54 -26
  133. package/dist/operate.js +31 -2
  134. package/dist/phase-routing.d.ts +19 -72
  135. package/dist/phase-routing.js +34 -164
  136. package/dist/plan.d.ts +5 -5
  137. package/dist/plan.js +5 -25
  138. package/dist/process-recovery-eligibility.d.ts +2 -2
  139. package/dist/process-recovery-eligibility.js +13 -8
  140. package/dist/project-knowledge.d.ts +16 -35
  141. package/dist/project-knowledge.js +42 -21
  142. package/dist/project-memory.d.ts +2 -28
  143. package/dist/project-memory.js +28 -13
  144. package/dist/project-skills.d.ts +16 -38
  145. package/dist/project-skills.js +41 -32
  146. package/dist/project-tools.d.ts +1 -0
  147. package/dist/project-tools.js +4 -4
  148. package/dist/proof.d.ts +15 -91
  149. package/dist/proof.js +90 -249
  150. package/dist/provider.d.ts +2 -0
  151. package/dist/provider.js +4 -16
  152. package/dist/result-evidence-readers.d.ts +42 -0
  153. package/dist/result-evidence-readers.js +91 -0
  154. package/dist/result-shots.d.ts +1 -1
  155. package/dist/result-shots.js +2 -1
  156. package/dist/review-switch.d.ts +3 -8
  157. package/dist/reviewer.d.ts +8 -12
  158. package/dist/reviewer.js +11 -35
  159. package/dist/scope.d.ts +21 -43
  160. package/dist/scope.js +42 -128
  161. package/dist/scout-report.d.ts +9 -194
  162. package/dist/scout-report.js +113 -264
  163. package/dist/scout.js +15 -11
  164. package/dist/serve.js +17 -101
  165. package/dist/slack-chat.js +26 -20
  166. package/dist/slack-state.js +2 -1
  167. package/dist/store.d.ts +9 -32
  168. package/dist/store.js +18 -15
  169. package/dist/structured-output.js +2 -1
  170. package/dist/subscription-chat.d.ts +4 -0
  171. package/dist/subscription-chat.js +4 -1
  172. package/dist/surface.js +8 -1
  173. package/dist/task-review-brief.d.ts +69 -0
  174. package/dist/task-review-brief.js +179 -0
  175. package/dist/task-sizing.d.ts +3 -27
  176. package/dist/task-sizing.js +11 -26
  177. package/dist/task-text.d.ts +0 -5
  178. package/dist/task-text.js +1 -3
  179. package/dist/teammate-question.js +7 -5
  180. package/dist/teammate-work.js +9 -5
  181. package/dist/teammates.d.ts +13 -64
  182. package/dist/teammates.js +32 -33
  183. package/dist/teams-chat.js +20 -11
  184. package/dist/telegram-flow.js +4 -3
  185. package/dist/telegram-mate.d.ts +4 -14
  186. package/dist/telegram-mate.js +8 -7
  187. package/dist/telegram-team.d.ts +2 -19
  188. package/dist/telegram.d.ts +1 -1
  189. package/dist/telegram.js +52 -42
  190. package/dist/text-limits.d.ts +212 -1
  191. package/dist/text-limits.js +214 -1
  192. package/dist/tools-ui.d.ts +2 -1
  193. package/dist/tools-ui.js +7 -4
  194. package/dist/verification-evidence.js +24 -12
  195. package/package.json +1 -1
@@ -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 {};
@@ -10,6 +10,8 @@ import { parseAcceptanceCriteria, proposeGuarded } from './scope.js';
10
10
  import { validateTaskText } from './task-text.js';
11
11
  import { PROOF_LIMITS } from './proof.js';
12
12
  import { readPreparedEvidence } from './prepared-evidence.js';
13
+ import { parseCodingHandoffReceipt } from './contracts/coding-handoff.js';
14
+ import { parseCodingSessionDocument } from './contracts/coding-workspace.js';
13
15
  import { BRANCH_PREFIXES, taskBranch } from './names.js';
14
16
  const PREFIX = 'coding:';
15
17
  const SHA = /^[a-f0-9]{40}$/;
@@ -84,7 +86,7 @@ function sessionFor(db, store, sessionId, actor) {
84
86
  const row = db.prepare('SELECT owner,generation,repo,document FROM coding_session WHERE id=?').get(sessionId);
85
87
  if (!row || row['owner'] !== actor)
86
88
  throw Error('This coding session is not available to your account.');
87
- const session = JSON.parse(String(row['document']));
89
+ const session = parseCodingSessionDocument(String(row['document']));
88
90
  const generation = actorGeneration(store, actor, String(row['repo']));
89
91
  if (row['generation'] !== generation || session.id !== sessionId || session.owner !== actor || session.generation !== generation || session.repo !== row['repo'])
90
92
  throw Error('The coding session no longer matches your account or project.');
@@ -132,18 +134,22 @@ function makeReceipt(terms) {
132
134
  const id = learningSha(JSON.stringify(terms)).slice(0, 32), taskId = `coding-review-${id}`;
133
135
  return { ...terms, id, taskId, branch: taskBranch(taskId) };
134
136
  }
135
- function parseReceipt(row) {
137
+ /** The saved bytes are verified before they are parsed; identity and branch are checked on the parsed terms. */
138
+ export function parseReceipt(row) {
136
139
  const payload = String(row['payload']);
137
140
  if (learningSha(payload) !== row['sha'])
138
141
  throw Error('The coding handoff receipt could not be verified.');
139
- const receipt = JSON.parse(payload);
142
+ const read = parseCodingHandoffReceipt(payload);
143
+ if (!read.ok)
144
+ throw Error(`The coding handoff receipt could not be read: ${read.issues.map(issue => issue.line).join('; ')}`);
145
+ const receipt = read.value;
140
146
  const { id, taskId, branch, ...terms } = receipt;
141
147
  const expected = makeReceipt(terms);
142
- if (receipt.version !== 1 || row['id'] !== id || id !== expected.id || taskId !== expected.taskId || !BRANCH_PREFIXES.some(prefix => branch === `${prefix}${taskId}`) || !SHA.test(receipt.base) || !SHA.test(receipt.candidate))
148
+ if (row['id'] !== id || id !== expected.id || taskId !== expected.taskId || !BRANCH_PREFIXES.some(prefix => branch === `${prefix}${taskId}`) || !SHA.test(receipt.base) || !SHA.test(receipt.candidate))
143
149
  throw Error('The coding handoff receipt identity could not be verified.');
144
150
  return receipt;
145
151
  }
146
- function sealPayload(receipt, scopeDigest) {
152
+ export function sealPayload(receipt, scopeDigest) {
147
153
  return JSON.stringify({ version: 1, id: receipt.id, receiptSha256: learningSha(JSON.stringify(receipt)), scopeDigest });
148
154
  }
149
155
  function verifySeal(db, receipt, digest) {
@@ -1,29 +1,5 @@
1
- import type { CodingContext } from './coding-context.js';
2
- export type CodingStatus = 'starting' | 'ready' | 'working' | 'needs-input' | 'stopping' | 'interrupted' | 'failed' | 'uncertain' | 'closed';
3
- export type CodingSession = {
4
- id: string;
5
- owner: string;
6
- generation: number;
7
- repo: string;
8
- title: string;
9
- provider: 'codex';
10
- model: string | null;
11
- branch: string;
12
- base: string;
13
- worktree: string;
14
- nativeThreadId: string | null;
15
- turnId: string | null;
16
- status: CodingStatus;
17
- error: string | null;
18
- createdAt: string;
19
- updatedAt: string;
20
- deliveryReviewRequired?: boolean;
21
- initialRequest?: {
22
- requestId: string;
23
- prompt: string;
24
- };
25
- context?: CodingContext;
26
- };
1
+ export type { CodingSession, CodingStatus } from './contracts/coding-workspace.js';
2
+ import type { CodingSession } from './contracts/coding-workspace.js';
27
3
  export type CodingItem = {
28
4
  id: string;
29
5
  type: string;
@@ -60,6 +60,8 @@ export declare class CodingWorkspace {
60
60
  constructor(options: Options);
61
61
  private authorize;
62
62
  private read;
63
+ /** Preserve legacy reads, but never mutate an unidentified row or save an unreadable record's error projection. */
64
+ private catalogRow;
63
65
  private admit;
64
66
  private releaseOperation;
65
67
  list(actor: CodingActor): CodingSession[];