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
package/dist/serve.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { SCREENSHOT_CAPTURE, structuredHandoffView, terminalDiffView } from "./result-evidence-readers.js";
1
2
  import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
2
3
  import { aboutYouOf, checkAboutYou, saveAboutYou, ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "./lead-about.js";
3
4
  import { withActor } from "./actor.js";
@@ -53,7 +54,8 @@ import { FORM_PATH, flowFormPage, receiveFlowForm, shareFlowButton, stopSharingF
53
54
  import { addFlowTriggerTo, checkFlowTriggerNow, HOOK_PATH, pressFlowButton, receiveFlowHook, removeFlowTrigger, renewFlowHook, removeLinearKey, saveHooksBase, saveLinearKey, saveLinearSigningSecret } from "./flow-triggers.js";
54
55
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, FLOW_HREF, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
55
56
  import { chooseFlowCard } from "./flow-send.js";
56
- import { FLOW_TEMPLATES, validateFlowDefinition } from "./flows.js";
57
+ import { FlowContractError, FLOW_TEMPLATES, validateFlowDefinition, withZoneNames } from "./flows.js";
58
+ import { stageReferenceProblems } from "./contracts/stage-output.js";
57
59
  import { TOOL_CATALOG, addToolTo, catalogTool, discoverTools, localAppOf, projectToolsOf, removeToolFrom, secretsSetFor, setToolSecret, splitCommandLine, testToolOf } from "./project-tools.js";
58
60
  import { changeLearning, learningView } from "./project-learning.js";
59
61
  import { applySavedKnowledge, changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
@@ -722,7 +724,7 @@ export function createDecisionServer(options) {
722
724
  tools: tools.map(tool => ({ tool, secretsSet: secretsSetFor(repo, tool.spec, toolHome) })),
723
725
  // A service that connects by signing in (or an app's own Connect) is offered only that way.
724
726
  catalog: TOOL_CATALOG.filter(one => !names.has(one.name) && oneClickOf(one.name) === null && localConnectOf(one.name) === null),
725
- connections: connectionsOf(store, repo), kit, wanted,
727
+ connections: connectionsOf(store, repo, toolHome), kit, wanted,
726
728
  found: discoverTools(repo, codex, toolHome).filter(one => !names.has(one.spec.name)),
727
729
  };
728
730
  };
@@ -1136,6 +1138,8 @@ export function createDecisionServer(options) {
1136
1138
  response.end(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${escape(to)}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${escape(words)} <a href="${escape(to)}">Back to Toolroll</a></p>`);
1137
1139
  };
1138
1140
  const state = url.searchParams.get("state") ?? "";
1141
+ // Every arrival is logged (never the code or state): a sign-in that never came back is then visible as an absence.
1142
+ logEvent("info", "connect.callback", { error: url.searchParams.get("error"), known: connectVisits.has(state) });
1139
1143
  if (!startedHere(request, state))
1140
1144
  return done("/settings/tools", "problem", "That sign-in was started in another browser. Connect again from this one.");
1141
1145
  const visit = connectVisits.get(state);
@@ -7179,12 +7183,17 @@ export function createDecisionServer(options) {
7179
7183
  return redirect(response, "/flows");
7180
7184
  }
7181
7185
  if (action === "save") {
7182
- let saved;
7186
+ let saved, drawn;
7183
7187
  try {
7184
- saved = validateFlowDefinition(JSON.parse(body.get("definition") ?? "null"));
7188
+ drawn = JSON.parse(body.get("definition") ?? "null");
7189
+ saved = validateFlowDefinition(drawn);
7190
+ // A {{stage.…}} this save adds must be one its zone hands on; ones the saved flow had stay.
7191
+ const references = stageReferenceProblems(saved, flowDefinitionOf(flow));
7192
+ if (references.length > 0)
7193
+ throw new FlowContractError(references.map(one => one.line));
7185
7194
  }
7186
7195
  catch (error) {
7187
- return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof Error ? error.message : "That flow isn't valid." });
7196
+ return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof FlowContractError ? withZoneNames(error.lines, drawn) : error instanceof Error ? error.message : "That flow isn't valid." });
7188
7197
  }
7189
7198
  const name = (body.get("name") ?? flow.name).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || flow.name;
7190
7199
  // The owner (v86) is whom "the owner decides" zones ask: someone who can approve on this project.
@@ -7830,7 +7839,9 @@ export function createDecisionServer(options) {
7830
7839
  const origin = consoleOrigin(request.headers.host);
7831
7840
  if (origin === null)
7832
7841
  return back("Connect tools from this computer (localhost) or from your https address.");
7833
- const started = await startConnect({ service: service.id, repo: target, by: who.name, origin, kit: also === null ? kit : null, template }, options.connectFetch ?? fetch);
7842
+ // Reconnect read-only (from a connected service's Tools entry): the same sign-in, asking only to read.
7843
+ const readOnly = also === null && body.get("access") === "read";
7844
+ const started = await startConnect({ service: service.id, repo: target, by: who.name, origin, kit: also === null ? kit : null, template, readOnly }, options.connectFetch ?? fetch);
7834
7845
  if (!started.ok)
7835
7846
  return back(started.said);
7836
7847
  for (const [key, visit] of connectVisits)
@@ -22504,32 +22515,6 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
22504
22515
  const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
22505
22516
  return screen("builds", [`<h1>Builds <a class="badge" href="/peek">Peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
22506
22517
  }
22507
- function structuredHandoffView(artifacts, root) {
22508
- const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
22509
- if (artifact === undefined)
22510
- return null;
22511
- const read = readVerifiedArtifact(root, artifact);
22512
- if (!read.ok)
22513
- return null;
22514
- try {
22515
- const parsed = JSON.parse(read.content.toString("utf8"));
22516
- if (parsed === null || typeof parsed !== "object" || typeof parsed["conclusion"] !== "string")
22517
- return null;
22518
- const conclusion = oneLineOf(parsed["conclusion"], 600);
22519
- if (conclusion === "" || hasForbiddenControls(conclusion))
22520
- return null;
22521
- const list = (name) => Array.isArray(parsed[name])
22522
- ? parsed[name]
22523
- .filter((one) => typeof one === "string" && one.trim() !== "" && !hasForbiddenControls(one))
22524
- .slice(0, 8)
22525
- .map(one => oneLineOf(one, 240))
22526
- : [];
22527
- return { conclusion, changes: list("changes"), verification: list("verification"), followUps: list("followUps") };
22528
- }
22529
- catch {
22530
- return null;
22531
- }
22532
- }
22533
22518
  /** What a scout found, each with its link and picture, then any screenshot no item shows. Every link is the cited
22534
22519
  * http(s) address the report parser admitted; every picture is served from verified evidence. */
22535
22520
  function reportItemsHtml(items, shots, runId) {
@@ -22606,7 +22591,6 @@ function sharedResultFactsOf(run, proof, terminal, handoff, report, publication)
22606
22591
  publicationWords: receiptPublicationWords(publication),
22607
22592
  };
22608
22593
  }
22609
- const SCREENSHOT_CAPTURE = /^agent-claimed screenshot at (.+) \(validated (?:png|jpeg)\)/;
22610
22594
  function proofBundleView(store, run, artifacts, root) {
22611
22595
  const verdictRow = store.proofVerdictFor(run.id);
22612
22596
  const acceptanceRow = store.proofAcceptance(run.id);
@@ -22722,74 +22706,6 @@ function completionReceiptView(store, run, artifacts, root, review = null) {
22722
22706
  facts: sharedResultFactsOf(run, proof, terminal, handoff, report, publication),
22723
22707
  };
22724
22708
  }
22725
- const CAPTURE_EXIT = /\(exit ([0-9]{1,4})\)\s*$/;
22726
- /**
22727
- * Assemble the terminal-diff card's facts. Every branch names its state:
22728
- * a failed capture (nonzero exit recorded in the capture string) reads as
22729
- * the failure it is, unverifiable bytes read as their problem, and absence
22730
- * returns null so old runs simply show nothing rather than a broken card.
22731
- */
22732
- function terminalDiffView(artifacts, root) {
22733
- const patchArtifact = artifacts.find(one => one.kind === "terminal-diff");
22734
- const statArtifact = artifacts.find(one => one.kind === "diff-stat");
22735
- if (patchArtifact === undefined && statArtifact === undefined)
22736
- return null;
22737
- const view = { patch: null, stat: null };
22738
- if (patchArtifact !== undefined) {
22739
- const exit = CAPTURE_EXIT.exec(patchArtifact.capture);
22740
- if (exit !== null && exit[1] !== "0") {
22741
- view.patch = { problem: `capture failed — ${patchArtifact.capture}` };
22742
- }
22743
- else {
22744
- const read = readVerifiedArtifact(root, patchArtifact);
22745
- view.patch = read.ok
22746
- ? { text: read.content.toString("utf8"), truncated: patchArtifact.truncated, artifactId: patchArtifact.id }
22747
- : { problem: `stored but unverifiable — ${read.problem}` };
22748
- }
22749
- }
22750
- if (statArtifact !== undefined) {
22751
- const exit = CAPTURE_EXIT.exec(statArtifact.capture);
22752
- if (exit !== null && exit[1] !== "0") {
22753
- view.stat = { problem: `capture failed — ${statArtifact.capture}` };
22754
- }
22755
- else {
22756
- const read = readVerifiedArtifact(root, statArtifact);
22757
- if (!read.ok) {
22758
- view.stat = { problem: `stored but unverifiable — ${read.problem}` };
22759
- }
22760
- else {
22761
- try {
22762
- const parsed = JSON.parse(read.content.toString("utf8"));
22763
- // Every field this page renders is type-proved (arc 6, finding 7):
22764
- // "an object with a base key" was accepting any shape at all.
22765
- const count = (value) => typeof value === "number" && Number.isFinite(value);
22766
- const wellFormed = parsed !== null &&
22767
- typeof parsed === "object" &&
22768
- typeof parsed["base"] === "string" &&
22769
- typeof parsed["head"] === "string" &&
22770
- count(parsed["fileCount"]) &&
22771
- count(parsed["additions"]) &&
22772
- count(parsed["deletions"]) &&
22773
- count(parsed["binaryCount"]) &&
22774
- typeof parsed["filesTruncated"] === "boolean" &&
22775
- Array.isArray(parsed["files"]) &&
22776
- parsed["files"].every(one => one !== null &&
22777
- typeof one === "object" &&
22778
- typeof one["path"] === "string" &&
22779
- (count(one["additions"]) || one["additions"] === null) &&
22780
- (count(one["deletions"]) || one["deletions"] === null));
22781
- view.stat = wellFormed
22782
- ? parsed
22783
- : { problem: "stat is not the shape this page knows" };
22784
- }
22785
- catch {
22786
- view.stat = { problem: "stat did not parse as JSON" };
22787
- }
22788
- }
22789
- }
22790
- }
22791
- return view;
22792
- }
22793
22709
  const REVIEW_DIFF_LINE_CAP = 4_000;
22794
22710
  /** Parse only the stable structure Git's unified patch format guarantees.
22795
22711
  * Unknown metadata remains visible, and a patch that cannot be structured
@@ -11,6 +11,9 @@ import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
11
11
  import { renderReply } from "./reply-shape.js";
12
12
  import {} from "./mate-doors.js";
13
13
  import { SlackState, slackHash, } from "./slack-state.js";
14
+ import { ChatDeliveryError, partContent } from "./chat-delivery-state.js";
15
+ import { readChatPart } from "./contracts/chat-content.js";
16
+ import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./contracts/slack-callback.js";
14
17
  import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
15
18
  import { resultImageFileName, safeResultImageCaption, verifyResultImage, } from "./chat-evidence.js";
16
19
  import {} from "./store.js";
@@ -70,25 +73,23 @@ export function receiveSlack(state, identity, type, raw, now) {
70
73
  id = slackHash(`${identity.installation}:event:${body.event_id}`);
71
74
  }
72
75
  else if (type === "interactive" && body.type === "block_actions") {
73
- const actions = Array.isArray(body.actions) ? body.actions : [];
74
- if (actions.length !== 1)
76
+ const interaction = readSlackBlockActions(body);
77
+ if (!interaction.ok || interaction.value.container.type !== "message")
75
78
  return false;
76
- const action = object(actions[0]), container = object(body.container);
77
- if (
78
- // toolroll_* since the rename; standing_orders_* buttons on older messages still work.
79
- !/^(?:toolroll|standing_orders)_(confirm|dismiss|yes|cancel|flow_approve|flow_edit|flow_send_back|flow_choose_[0-3]|flow_note_(?:yes|no)|question_choice|question_words)$/.test(String(action.action_id)) ||
80
- typeof action.value !== "string" ||
81
- !/^[a-f0-9]{32}$/.test(action.value) ||
82
- !slackTs(action.action_ts) ||
83
- container.type !== "message")
79
+ const { actions, container, user } = interaction.value;
80
+ // A link button opens its page itself: nothing to do, and nothing to say.
81
+ if (actions.length === 1 && SLACK_LINK_ACTION.test(actions[0].action_id ?? ""))
84
82
  return false;
85
- member = object(body.user).id;
83
+ member = user.id;
86
84
  channel = container.channel_id;
87
85
  ts = container.message_ts;
88
- thread = object(body.message).thread_ts ?? ts;
86
+ thread = interaction.value.message?.thread_ts ?? ts;
89
87
  kind = "action";
90
- payload = { token: action.value };
91
- id = slackHash(`${identity.installation}:action:${member}:${ts}:${action.value}:${action.action_ts}`);
88
+ // A button that isn't one Toolroll made is answered with why, once the tap is proved to be the paired person's below.
89
+ const button = readSlackButton(interaction.value);
90
+ payload = button.ok ? { token: button.value.value } : { problem: button.issues.map(issue => issue.line).join("; ") };
91
+ const tapped = actions[0];
92
+ id = slackHash(`${identity.installation}:action:${member}:${ts}:${tapped?.value ?? ""}:${tapped?.action_ts ?? ""}`);
92
93
  }
93
94
  else
94
95
  return false;
@@ -101,9 +102,10 @@ export function receiveSlack(state, identity, type, raw, now) {
101
102
  // A channel or private group is a room: accepted only while it follows a
102
103
  // conversation, or for the `/team` words that make it follow one.
103
104
  const isRoom = !slackId(channel, "D");
105
+ const words = "text" in payload ? payload.text ?? "" : "";
104
106
  // v89: a channel that feeds a flow takes anyone's message (as a card, with no say over anything); "flow 12" connects one.
105
- const inbox = isRoom && kind === "message" ? channelInbox(state.store, "slack", identity.installation, String(channel), String(payload.text ?? "")) : { watched: false, command: false };
106
- const roomish = isRoom && (state.room(identity.installation, String(channel)) !== null || (kind === "message" && roomCommand(String(payload.text ?? "")) !== null) || inbox.watched || inbox.command);
107
+ const inbox = isRoom && kind === "message" ? channelInbox(state.store, "slack", identity.installation, String(channel), words) : { watched: false, command: false };
108
+ const roomish = isRoom && (state.room(identity.installation, String(channel)) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
107
109
  if (isRoom && !roomish)
108
110
  return false;
109
111
  const binding = state.bindingFor(identity.installation, member);
@@ -124,7 +126,7 @@ export function receiveSlack(state, identity, type, raw, now) {
124
126
  member,
125
127
  ts,
126
128
  thread,
127
- payload: JSON.stringify(payload),
129
+ payload,
128
130
  created: now.toISOString(),
129
131
  });
130
132
  }
@@ -227,7 +229,7 @@ export async function deliverSlackPart(options) {
227
229
  const repos = await access(options, binding, session?.ceilingDigest);
228
230
  if (event.kind === "notice" && options.canNotify?.() === false)
229
231
  return false;
230
- const content = JSON.parse(row.payload);
232
+ const content = partContent(row.payload);
231
233
  const destination = content.channel ?? binding.channel;
232
234
  let text = content.text, buttons = [];
233
235
  if (content.task &&
@@ -435,9 +437,13 @@ export async function deliverSlackPart(options) {
435
437
  return true;
436
438
  }
437
439
  catch (error) {
440
+ if (error instanceof ChatDeliveryError && error.permanent) {
441
+ state.prepare("UPDATE chat_part SET state='dropped',next_at=NULL,problem=? WHERE id=?").run(error.message, row.id);
442
+ return true;
443
+ }
438
444
  // No permission to upload files here: one plain line instead of the result's screenshots.
439
- const content = JSON.parse(row.payload);
440
- if (content.image && content.shot && error instanceof SlackError && ["missing_scope", "no_permission"].includes(error.code)) {
445
+ const saved = readChatPart(row.payload), content = saved.ok ? saved.value : null;
446
+ if (content !== null && content.image && content.shot && error instanceof SlackError && ["missing_scope", "no_permission"].includes(error.code)) {
441
447
  refuseResultShots(state, store, row, content, "Slack", nowOf(options));
442
448
  return true;
443
449
  }
@@ -1,5 +1,6 @@
1
1
  /** Slack compatibility names for the shared durable transport state. */
2
- import { ChatState, chatSchema, chatTables } from "./chat-delivery-state.js";
2
+ import { ChatState } from "./chat-delivery-state.js";
3
+ import { chatSchema, chatTables } from "./contracts/chat-tables.js";
3
4
  export { chatHash as slackHash } from "./chat-delivery-state.js";
4
5
  export const SLACK_SCHEMA = chatSchema("slack"), SLACK_TABLES = chatTables("slack");
5
6
  export class SlackState extends ChatState {
package/dist/store.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { DecisionOption } from "./contracts/decision.js";
2
+ import type { FlowCardChange, FlowCardFields } from "./contracts/flow-card.js";
1
3
  import { type Actor } from "./actor.js";
2
4
  import { type CriterionMatrixRow, type CriterionJudgementWord } from "./proof.js";
3
5
  import { type ExecutionProfile, type ChainEntry, type UnattendedPermissionMode, type AcceptanceCriterion } from "./scope.js";
@@ -764,24 +766,9 @@ export type FlowCardSource = {
764
766
  binding: number;
765
767
  };
766
768
  };
767
- export type FlowCardRow = {
768
- id: number;
769
- flow: number;
770
- title: string;
771
- description: string | null;
772
- stage: string;
773
- entry: number;
774
- state: "active" | "done" | "cancelled";
775
- task: string | null;
776
- primaryTask: string | null;
777
- note: string | null;
778
- waiting: string | null;
779
- outputs: Record<string, string>;
780
- createdBy: string;
781
- createdAt: string;
782
- updatedAt: string;
769
+ /** A flow card as read (src/contracts/flow-card.ts), and where it came from. */
770
+ export type FlowCardRow = FlowCardFields & {
783
771
  source: FlowCardSource | null;
784
- owner: string | null;
785
772
  };
786
773
  /** One line of a card's discussion: a comment (and whom it @mentioned), or who became its owner. */
787
774
  export type FlowCommentRow = {
@@ -1361,13 +1348,8 @@ export type PushPair = {
1361
1348
  lastError: string | null;
1362
1349
  acceptedAt: string | null;
1363
1350
  };
1364
- /** One option of a decision. `reversible` is a field so a scheduler can refuse to auto-apply. */
1365
- export type DecisionOption = {
1366
- id: string;
1367
- label: string;
1368
- consequence: string;
1369
- reversible: boolean;
1370
- };
1351
+ /** One option of a decision (src/contracts/decision.ts). `reversible` is a field so a scheduler can refuse to auto-apply. */
1352
+ export type { DecisionOption };
1371
1353
  /** The judgement call an agent refused to guess at (§7). Identity = its run. */
1372
1354
  export type Decision = {
1373
1355
  id: number;
@@ -6389,13 +6371,9 @@ export declare class Store {
6389
6371
  }, now: Date): boolean;
6390
6372
  /** What a card's current zone is doing: its task, the card's main work, what it waits on, what zones reported. */
6391
6373
  /** Read and written in one transaction, so a pass changing one field never writes back another pass's older task or waiting. */
6392
- updateFlowCard(id: number, change: {
6393
- task?: string | null;
6394
- primaryTask?: string;
6395
- waiting?: string | null;
6396
- outputs?: Record<string, string>;
6397
- state?: "done" | "cancelled";
6398
- }, now: Date): void;
6374
+ /** Outputs are written only when they change (src/contracts/stage-output.ts), never over a row a newer Toolroll wrote;
6375
+ * a zone's attachment stays only while its output does. */
6376
+ updateFlowCard(id: number, change: FlowCardChange, now: Date): void;
6399
6377
  flowEvents(card: number): {
6400
6378
  fromStage: string | null;
6401
6379
  toStage: string;
@@ -8216,4 +8194,3 @@ export type VerifiedAuthor = string & {
8216
8194
  readonly __verifiedAuthor: unique symbol;
8217
8195
  };
8218
8196
  export declare function verifiedAuthor(name: string): VerifiedAuthor;
8219
- export {};
package/dist/store.js CHANGED
@@ -1,5 +1,4 @@
1
- import { chatSchema, chatTables } from "./chat-delivery-state.js";
2
- import { SLACK_SCHEMA, SLACK_TABLES } from "./slack-state.js";
1
+ import { chatSchema, chatTables } from "./contracts/chat-tables.js";
3
2
  import { assessmentFromSavedEvidence, verificationEvidence } from "./verification-evidence.js";
4
3
  import { LEARNING_SCHEMA, queueLearning } from "./project-learning.js";
5
4
  import { SKILLS_SCHEMA } from "./project-skills.js";
@@ -8,6 +7,7 @@ import { MEMORY_SCHEMA } from "./project-memory.js";
8
7
  import { MODELS_SCHEMA } from "./model-catalog.js";
9
8
  import { validateTaskText } from "./task-text.js";
10
9
  import { flowDefinitionForStore, flowDefinitionFromStore } from "./flows.js";
10
+ import { cardOutputsForStore, cardOutputsFromStore } from "./contracts/stage-output.js";
11
11
  import { chatControlHref, chatResultHref } from "./chat-controls.js";
12
12
  import { actorLabel, currentActor, leadSecretMatches, mintLeadToken, parseLeadToken } from "./actor.js";
13
13
  import { scanForSecrets } from "./evidence.js";
@@ -687,18 +687,11 @@ function readFlowStepRunRow(row) {
687
687
  durationMs: number("duration_ms"), exitCode: number("exit_code"), result: optional("result"), log: optional("log"), decisionJson: optional("decision_json") };
688
688
  }
689
689
  function readFlowCardRow(row) {
690
- let outputs = {};
691
- try {
692
- const parsed = JSON.parse(String(row["outputs_json"] ?? "{}"));
693
- if (parsed !== null && typeof parsed === "object")
694
- outputs = Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === "string"));
695
- }
696
- catch {
697
- outputs = {};
698
- }
690
+ // What finished zones handed on (src/contracts/stage-output.ts): a row saved before it carried a version reads as it always did.
691
+ const { outputs, attached } = cardOutputsFromStore(String(row["outputs_json"] ?? "{}"));
699
692
  const optional = (key) => row[key] === null || row[key] === undefined ? null : String(row[key]);
700
693
  return { id: Number(row["id"]), flow: Number(row["flow"]), title: String(row["title"]), description: optional("description"), stage: String(row["stage"]), entry: Number(row["entry"]),
701
- state: String(row["state"]), task: optional("task"), primaryTask: optional("primary_task"), note: optional("note"), waiting: optional("waiting"), outputs,
694
+ state: String(row["state"]), task: optional("task"), primaryTask: optional("primary_task"), note: optional("note"), waiting: optional("waiting"), outputs, attached,
702
695
  createdBy: String(row["created_by"]), createdAt: String(row["created_at"]), updatedAt: String(row["updated_at"]), source: readFlowCardSource(row["source_json"]), owner: optional("owner") };
703
696
  }
704
697
  function readFlowCardSource(value) {
@@ -3950,7 +3943,7 @@ function initializeStore(db, file) {
3950
3943
  throw new Error(`${file}: shared action history has an unknown shape; refusing to recreate it`);
3951
3944
  }
3952
3945
  if (preflight !== null && Math.abs(preflight) >= 67) {
3953
- for (const table of SLACK_TABLES)
3946
+ for (const table of chatTables("slack"))
3954
3947
  if (!tableExists(db, table))
3955
3948
  throw new Error(`${file}: Slack history is missing; refusing to recreate receipts`);
3956
3949
  }
@@ -3971,7 +3964,7 @@ function initializeStore(db, file) {
3971
3964
  db.exec(MEMORY_SCHEMA);
3972
3965
  db.exec(MODELS_SCHEMA);
3973
3966
  db.exec(SKILLS_SCHEMA);
3974
- db.exec(SLACK_SCHEMA);
3967
+ db.exec(chatSchema("slack"));
3975
3968
  db.exec(chatSchema("discord"));
3976
3969
  db.exec(chatSchema("teams"));
3977
3970
  db.exec(TEAM_SCHEMA);
@@ -19362,12 +19355,22 @@ export class Store {
19362
19355
  }
19363
19356
  /** What a card's current zone is doing: its task, the card's main work, what it waits on, what zones reported. */
19364
19357
  /** Read and written in one transaction, so a pass changing one field never writes back another pass's older task or waiting. */
19358
+ /** Outputs are written only when they change (src/contracts/stage-output.ts), never over a row a newer Toolroll wrote;
19359
+ * a zone's attachment stays only while its output does. */
19365
19360
  updateFlowCard(id, change, now) {
19366
19361
  this.transact(() => {
19367
19362
  const card = this.getFlowCard(id);
19368
19363
  if (card === null)
19369
19364
  return;
19370
- this.db.prepare("UPDATE flow_card SET task = ?, primary_task = ?, waiting = ?, outputs_json = ?, state = ?, updated_at = ? WHERE id = ?").run(change.task === undefined ? card.task : change.task, change.primaryTask ?? card.primaryTask, change.waiting === undefined ? card.waiting : change.waiting, JSON.stringify(change.outputs ?? card.outputs), change.state ?? card.state, now.toISOString(), id);
19365
+ this.db.prepare("UPDATE flow_card SET task = ?, primary_task = ?, waiting = ?, state = ?, updated_at = ? WHERE id = ?").run(change.task === undefined ? card.task : change.task, change.primaryTask ?? card.primaryTask, change.waiting === undefined ? card.waiting : change.waiting, change.state ?? card.state, now.toISOString(), id);
19366
+ if (change.outputs === undefined && change.attached === undefined)
19367
+ return;
19368
+ const saved = this.db.prepare("SELECT outputs_json FROM flow_card WHERE id = ?").get(id);
19369
+ if (cardOutputsFromStore(String(saved?.["outputs_json"] ?? "{}")).newer)
19370
+ return;
19371
+ const outputs = change.outputs ?? card.outputs;
19372
+ const attached = { ...Object.fromEntries(Object.entries(card.attached).filter(([zone]) => outputs[zone] === card.outputs[zone])), ...change.attached };
19373
+ this.db.prepare("UPDATE flow_card SET outputs_json = ? WHERE id = ?").run(cardOutputsForStore(outputs, attached), id);
19371
19374
  });
19372
19375
  }
19373
19376
  flowEvents(card) {
@@ -8,12 +8,13 @@
8
8
  * subject to the phase's strict parser.
9
9
  */
10
10
  import { redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
11
+ import { TEXT_LIMITS } from "./text-limits.js";
11
12
  export const STRUCTURED_REPAIR_ATTEMPTS = 2;
12
13
  export const STRUCTURED_REPAIR_MAX_TURNS = 4;
13
14
  export const STRUCTURED_REPAIR_TIMEOUT_MS = 5 * 60_000;
14
15
  /** Native parsing counts UTF-16 code units. A shorter writing target keeps
15
16
  * notes concise without imposing a second, stricter transport contract. */
16
- export const REVIEW_OUTPUT_LIMITS = { comments: 40, criteria: 12, note: 500, path: 300 };
17
+ export const REVIEW_OUTPUT_LIMITS = { comments: 40, criteria: 12, note: TEXT_LIMITS.reviewNote, path: TEXT_LIMITS.reviewPath };
17
18
  export const REVIEW_NOTE_CODE_POINTS = Math.floor(REVIEW_OUTPUT_LIMITS.note / 2);
18
19
  export const REVIEW_NOTE_GUIDANCE = `Keep each comment and criterion note to one concise finding and its evidence. Aim for ${REVIEW_NOTE_CODE_POINTS} characters or fewer; the hard limit is ${REVIEW_OUTPUT_LIMITS.note} UTF-16 units. Preserve the conclusion and any needed provenance; omit repeated rubric text.`;
19
20
  /** Strip only unambiguous, whole-payload wrappers. */
@@ -9,6 +9,10 @@ export type SubscriptionMateRequest = {
9
9
  dataDocument: string;
10
10
  history: readonly MateHistoryMessage[];
11
11
  tools: readonly MateToolSchema[];
12
+ /** A structured answer of another shape (the memory pass's verdict): this JSON Schema is the harness's
13
+ * `--json-schema` (Codex's `--output-schema`) in place of the mate envelope, and its JSON text comes back as the
14
+ * answer's text, unread, for the caller's own schema to parse. */
15
+ outputSchema?: Record<string, unknown>;
12
16
  timeoutMs: number;
13
17
  /** The turn's own deadline: aborting it ends the harness's whole process group. */
14
18
  signal?: AbortSignal;
@@ -191,7 +191,7 @@ export async function performSubscriptionMateRequest(request, runner = run) {
191
191
  try {
192
192
  const prompt = composeSubscriptionMatePrompt(request);
193
193
  const streaming = request.provider !== "codex-subscription" && request.onText !== undefined;
194
- const schema = outputSchema(request.tools);
194
+ const schema = request.outputSchema ?? outputSchema(request.tools);
195
195
  let command;
196
196
  let args;
197
197
  if (request.provider === "codex-subscription") {
@@ -271,6 +271,9 @@ export async function performSubscriptionMateRequest(request, runner = run) {
271
271
  const output = request.provider === "codex-subscription" ? codexOutput(result.stdout) : claudeOutput(streaming ? lastResultLine(result.stdout) : result.stdout);
272
272
  if (output.text === null)
273
273
  return { ok: false, problem: "malformed-reply" };
274
+ if (request.outputSchema !== undefined) {
275
+ return { ok: true, answer: { text: output.text, calls: [], tokensIn: output.tokensIn, tokensOut: output.tokensOut, reportedCostMicrousd: null } };
276
+ }
274
277
  return parseSubscriptionMateAnswer(output.text, output);
275
278
  }
276
279
  finally {
package/dist/surface.js CHANGED
@@ -278,7 +278,14 @@ export const COMMAND_GUIDE = [
278
278
  operator("task approve", "the yes — nothing builds without one; binds to the scope digest"),
279
279
  operator("task requeue", "exit a stall: incidents resolved, strikes cleared, queued again"),
280
280
  operator("task regate", "explicitly rerun the approved check on the last exact commit — no agent or automatic retry"),
281
- operator("task review", "show a run's automatic review: HIGH findings, suggested follow-ups, or why it was not reviewed"),
281
+ { invocation: "task review", synopsis: "read a task's compact result packet with --brief, or a run's automatic findings without it", audience: "agent", agentMayInvoke: true, mutation: "none",
282
+ positionals: [{ name: "id", required: true, meaning: "task id with --brief; run id otherwise" }],
283
+ flags: [jsonFlag, dbFlag,
284
+ { name: "brief", takesValue: false, meaning: "saved conclusion, changed files, checks, findings, screenshots, plan and next actions; missing fields say not recorded" },
285
+ { name: "run", takesValue: true, meaning: "with --brief, select an exact finished attempt instead of the latest" },
286
+ { name: "all", takesValue: false, meaning: "include LOW findings within the compact packet's limit; extra findings are counted" },
287
+ { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }],
288
+ notableReasons: ["usage", "unauthenticated", "not-found"] },
282
289
  operator("task accept", "record an explicit acceptance with limitations; does not change checks or prove deployment"),
283
290
  operator("task repair", "inspect or approve an existing historical repair draft; use task revise for new feedback"),
284
291
  operator("task route", "which configured agents plan, build and repair this task; phase and risk changes require renewed approval"),
@@ -0,0 +1,69 @@
1
+ /** A read-only packet of saved result facts. Never opens a worktree or emits logs, patches or saved shell commands. */
2
+ import type { Store } from './store.js';
3
+ import { type AssignmentAccess } from './assignment.js';
4
+ import { type BuildFinding as Finding, type FindingSeverity } from './contracts/review-findings.js';
5
+ declare const MISSING: 'not recorded';
6
+ type Missing = typeof MISSING;
7
+ export type ReviewBrief = {
8
+ header: {
9
+ task: string;
10
+ run: number | Missing;
11
+ provider: string;
12
+ model: string;
13
+ minutes: number | Missing;
14
+ outcome: string;
15
+ base: string;
16
+ head: string;
17
+ state: string;
18
+ };
19
+ conclusion: string;
20
+ changedFiles: {
21
+ files: {
22
+ path: string;
23
+ additions: number | Missing;
24
+ deletions: number | Missing;
25
+ }[];
26
+ total: number;
27
+ omitted: number;
28
+ } | Missing;
29
+ check: {
30
+ status: string;
31
+ exit: number | Missing;
32
+ failed: string[] | Missing;
33
+ };
34
+ review: {
35
+ state: string;
36
+ reason: string;
37
+ findings: Finding[];
38
+ counts: Record<FindingSeverity, number>;
39
+ omitted: Record<FindingSeverity, number>;
40
+ } | Missing;
41
+ screenshots: {
42
+ files: {
43
+ path: string;
44
+ bytes: number | Missing;
45
+ }[];
46
+ omitted: number;
47
+ } | Missing;
48
+ plan: {
49
+ titles: string[];
50
+ omitted: number;
51
+ } | Missing;
52
+ nextActions: string[];
53
+ };
54
+ /** Select only finished build/report attempts in the admitted family. An explicit run never crosses that boundary. */
55
+ export declare function taskReviewBrief(store: Store, task: string, root: string, now: Date, access: AssignmentAccess, options?: {
56
+ run?: number;
57
+ all?: boolean;
58
+ secrets?: readonly string[];
59
+ }): {
60
+ ok: true;
61
+ brief: ReviewBrief;
62
+ } | {
63
+ ok: false;
64
+ reason: string;
65
+ message: string;
66
+ };
67
+ /** Text and JSON share this bounded projection; --all includes LOW details within the same line budget. */
68
+ export declare function renderReviewBrief(brief: ReviewBrief): string[];
69
+ export {};