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/proof.js CHANGED
@@ -19,7 +19,9 @@
19
19
  * so the review itself stays testable without a filesystem or a database.
20
20
  */
21
21
  import { hasForbiddenControls } from "./decision.js";
22
- import { EVIDENCE_KINDS } from "./scope.js";
22
+ import { contractProblemOf } from "./contracts/contract.js";
23
+ import { PROOF_LIMITS, proofPayloadBody, readProofPayload } from "./contracts/proof.js";
24
+ export { PROOF_LIMITS } from "./contracts/proof.js";
23
25
  /** Shared by preflight and final adjudication; presentation text is never a rubric. */
24
26
  function criterionContractProblems(approved, answer) {
25
27
  if (answer === undefined)
@@ -57,248 +59,96 @@ export function proofSubmissionProblems(proof, rubric) {
57
59
  problems.push(...failedMetChecks(proof).map(failedCheckWords));
58
60
  return problems;
59
61
  }
60
- /** Caps are BYTES of UTF-8, matching the scout report's rule: a payload is
61
- * the same size whatever script it is written in. */
62
- export const PROOF_LIMITS = {
63
- payload: 64 * 1024,
64
- criteria: 12,
65
- criterionId: 40,
66
- criterionStatement: 300,
67
- criterionHow: 500,
68
- evidencePerCriterion: 4,
69
- evidenceRef: 300,
70
- checks: 12,
71
- checkCommand: 300,
72
- checkSummary: 300,
73
- changed: 64,
74
- changedPath: 300,
75
- caveats: 8,
76
- caveat: 300,
77
- screenshots: 8,
78
- screenshotPath: 300,
79
- screenshotCaption: 300,
80
- };
81
62
  function refuse(reason, message) {
82
63
  return { ok: false, problems: [{ reason, message }] };
83
64
  }
84
- function describe(value) {
85
- if (value === undefined)
86
- return "nothing";
87
- if (value === null)
88
- return "null";
89
- if (typeof value === "string")
90
- return `a ${value.length}-char string`;
91
- return `a ${Array.isArray(value) ? "array" : typeof value}`;
92
- }
93
- function prose(value, field, cap, problems) {
94
- if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
95
- problems.push({ reason: `missing-${field}`, message: `${field} is required` });
96
- return null;
97
- }
98
- if (typeof value !== "string") {
99
- problems.push({ reason: `bad-${field}`, message: `${field} must be a string (got ${describe(value)})` });
100
- return null;
101
- }
102
- if (Buffer.byteLength(value, "utf8") > cap) {
103
- problems.push({ reason: `${field}-too-long`, message: `${field} is over ${cap} bytes` });
104
- return null;
105
- }
106
- if (hasForbiddenControls(value)) {
107
- problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
108
- return null;
109
- }
110
- return value;
111
- }
112
- /** A repository-relative path: no leading slash, no drive letter, no `.`/`..`
113
- * segment, no backslash, no control character. The same shape
114
- * `readVerifiedArtifact` enforces on an evidence key, applied here to a
115
- * path an agent claims rather than one the machine already wrote. */
116
- function relativePath(value, field, cap, problems) {
117
- const raw = prose(value, field, cap, problems);
118
- if (raw === null)
119
- return null;
120
- const segments = raw.split("/");
121
- const wellFormed = !raw.startsWith("/") &&
122
- segments.every(segment => segment.length > 0 && segment !== "." && segment !== ".." && !segment.includes("\\"));
123
- if (!wellFormed) {
124
- problems.push({ reason: `${field}-not-relative`, message: `${field} must be a normalized repository-relative path (got ${describe(value)})` });
125
- return null;
126
- }
127
- return raw;
128
- }
129
- /** `evidence` is optional on a criterion (absent → `[]`, exactly like every
130
- * other list here) — the mandatory PRESENCE of an answer for a SIGNED
131
- * criterion is `adjudicate`'s concern, not the parser's; this only proves
132
- * the shape of what is there. */
133
- function parseEvidenceRefs(value, field, problems) {
134
- if (value === undefined || value === null)
135
- return [];
136
- if (!Array.isArray(value)) {
137
- problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
138
- return null;
139
- }
140
- if (value.length > PROOF_LIMITS.evidencePerCriterion) {
141
- problems.push({ reason: `${field}-too-many`, message: `${field} lists ${value.length} — cap is ${PROOF_LIMITS.evidencePerCriterion}` });
142
- return null;
143
- }
144
- const refs = [];
145
- let bad = false;
146
- for (const [index, entry] of value.entries()) {
147
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
148
- problems.push({ reason: `${field}[${index}]-shape`, message: `${field}[${index}] must be an object` });
149
- bad = true;
150
- continue;
151
- }
152
- const one = entry;
153
- const kind = one["kind"];
154
- if (typeof kind !== "string" || !EVIDENCE_KINDS.includes(kind)) {
155
- problems.push({
156
- reason: `${field}[${index}]-bad-kind`,
157
- message: `${field}[${index}].kind must draw from ${EVIDENCE_KINDS.join(", ")} (got ${describe(kind)})`,
158
- });
159
- bad = true;
160
- continue;
161
- }
162
- const ref = prose(one["ref"], `${field}[${index}].ref`, PROOF_LIMITS.evidenceRef, problems);
163
- if (ref === null) {
164
- bad = true;
165
- continue;
166
- }
167
- refs.push({ kind: kind, ref });
168
- }
169
- return bad ? null : refs;
170
- }
171
- function parseCriteria(value, problems) {
172
- if (value === undefined || value === null)
173
- return [];
174
- if (!Array.isArray(value)) {
175
- problems.push({ reason: "bad-criteria", message: `criteria must be an array (got ${describe(value)})` });
176
- return [];
177
- }
178
- if (value.length > PROOF_LIMITS.criteria) {
179
- problems.push({ reason: "criteria-too-many", message: `criteria lists ${value.length} — cap is ${PROOF_LIMITS.criteria}` });
180
- return [];
181
- }
182
- const criteria = [];
183
- const seen = new Set();
184
- for (const [index, entry] of value.entries()) {
185
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
186
- problems.push({ reason: `criteria[${index}]-shape`, message: `criteria[${index}] must be an object` });
187
- continue;
188
- }
189
- const one = entry;
190
- const id = prose(one["id"], `criteria[${index}].id`, PROOF_LIMITS.criterionId, problems);
191
- if (id !== null && seen.has(id)) {
192
- problems.push({ reason: `criteria[${index}]-duplicate-id`, message: `criterion id "${id}" appears twice` });
193
- }
194
- else if (id !== null) {
195
- seen.add(id);
196
- }
197
- const statement = prose(one["statement"], `criteria[${index}].statement`, PROOF_LIMITS.criterionStatement, problems);
198
- const how = prose(one["how"], `criteria[${index}].how`, PROOF_LIMITS.criterionHow, problems);
199
- const verdict = one["verdict"];
200
- if (verdict !== "met" && verdict !== "not-met" && verdict !== "not-checked" && verdict !== "pending-verification") {
201
- problems.push({
202
- reason: `criteria[${index}]-bad-verdict`,
203
- message: `criteria[${index}].verdict must be "met", "not-met", "not-checked", or "pending-verification" (got ${describe(verdict)})`,
204
- });
205
- continue;
206
- }
207
- const evidence = parseEvidenceRefs(one["evidence"], `criteria[${index}].evidence`, problems);
208
- if (id === null || statement === null || how === null || evidence === null)
209
- continue;
210
- criteria.push({ id, statement, how, verdict, evidence });
211
- }
212
- return criteria;
213
- }
214
- function parseChecks(value, problems) {
215
- if (value === undefined || value === null)
216
- return [];
217
- if (!Array.isArray(value)) {
218
- problems.push({ reason: "bad-checks", message: `checks must be an array (got ${describe(value)})` });
219
- return [];
220
- }
221
- if (value.length > PROOF_LIMITS.checks) {
222
- problems.push({ reason: "checks-too-many", message: `checks lists ${value.length} — cap is ${PROOF_LIMITS.checks}` });
223
- return [];
224
- }
225
- const checks = [];
226
- for (const [index, entry] of value.entries()) {
227
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
228
- problems.push({ reason: `checks[${index}]-shape`, message: `checks[${index}] must be an object` });
229
- continue;
230
- }
231
- const one = entry;
232
- const command = prose(one["command"], `checks[${index}].command`, PROOF_LIMITS.checkCommand, problems);
233
- const summary = prose(one["summary"], `checks[${index}].summary`, PROOF_LIMITS.checkSummary, problems);
234
- const exitCode = one["exitCode"];
235
- if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
236
- problems.push({
237
- reason: `checks[${index}]-bad-exit-code`,
238
- message: `checks[${index}].exitCode must be an integer 0-255 (got ${describe(exitCode)})`,
65
+ /** Every text field of a proof body (version 2 shape) that the schema saw as a string. */
66
+ function textFields(body) {
67
+ const out = [];
68
+ const text = (path, value, bytes, relative) => {
69
+ if (typeof value === "string")
70
+ out.push({ path, value, bytes, ...(relative ? { relative } : {}) });
71
+ };
72
+ const records = (field) => {
73
+ const list = body[field];
74
+ return Array.isArray(list) ? list.flatMap((one, index) => (typeof one === "object" && one !== null && !Array.isArray(one) ? [[index, one]] : [])) : [];
75
+ };
76
+ for (const [index, one] of records("criteria")) {
77
+ text(`criteria[${index}].id`, one["id"], PROOF_LIMITS.criterionId);
78
+ text(`criteria[${index}].statement`, one["statement"], PROOF_LIMITS.criterionStatement);
79
+ text(`criteria[${index}].how`, one["how"], PROOF_LIMITS.criterionHow);
80
+ const evidence = one["evidence"];
81
+ if (Array.isArray(evidence)) {
82
+ evidence.forEach((ref, at) => {
83
+ if (typeof ref === "object" && ref !== null)
84
+ text(`criteria[${index}].evidence[${at}].ref`, ref["ref"], PROOF_LIMITS.evidenceRef);
239
85
  });
240
- continue;
241
86
  }
242
- if (command === null || summary === null)
243
- continue;
244
- checks.push({ command, summary, exitCode });
245
87
  }
246
- return checks;
247
- }
248
- function parseStringList(value, field, cap, itemCap, problems) {
249
- if (value === undefined || value === null)
250
- return [];
251
- if (!Array.isArray(value)) {
252
- problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
253
- return [];
88
+ for (const [index, one] of records("checks")) {
89
+ text(`checks[${index}].command`, one["command"], PROOF_LIMITS.checkCommand);
90
+ text(`checks[${index}].summary`, one["summary"], PROOF_LIMITS.checkSummary);
254
91
  }
255
- if (value.length > cap) {
256
- problems.push({ reason: `${field}-too-many`, message: `${field} lists ${value.length} — cap is ${cap}` });
257
- return [];
92
+ for (const field of ["changed", "caveats"]) {
93
+ const list = body[field];
94
+ if (Array.isArray(list))
95
+ list.forEach((one, index) => text(`${field}[${index}]`, one, field === "changed" ? PROOF_LIMITS.changedPath : PROOF_LIMITS.caveat));
258
96
  }
259
- const out = [];
260
- for (const [index, entry] of value.entries()) {
261
- const item = prose(entry, `${field}[${index}]`, itemCap, problems);
262
- if (item !== null)
263
- out.push(item);
97
+ for (const [index, one] of records("screenshots")) {
98
+ text(`screenshots[${index}].path`, one["path"], PROOF_LIMITS.screenshotPath, true);
99
+ text(`screenshots[${index}].caption`, one["caption"], PROOF_LIMITS.screenshotCaption);
264
100
  }
265
101
  return out;
266
102
  }
267
- function parseScreenshots(value, problems) {
268
- if (value === undefined || value === null)
269
- return [];
270
- if (!Array.isArray(value)) {
271
- problems.push({ reason: "bad-screenshots", message: `screenshots must be an array (got ${describe(value)})` });
272
- return [];
273
- }
274
- if (value.length > PROOF_LIMITS.screenshots) {
275
- problems.push({ reason: "screenshots-too-many", message: `screenshots lists ${value.length} — cap is ${PROOF_LIMITS.screenshots}` });
276
- return [];
277
- }
278
- const screenshots = [];
279
- const seen = new Set();
280
- for (const [index, entry] of value.entries()) {
281
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
282
- problems.push({ reason: `screenshots[${index}]-shape`, message: `screenshots[${index}] must be an object` });
283
- continue;
284
- }
285
- const one = entry;
286
- const path = relativePath(one["path"], `screenshots[${index}].path`, PROOF_LIMITS.screenshotPath, problems);
287
- const caption = prose(one["caption"], `screenshots[${index}].caption`, PROOF_LIMITS.screenshotCaption, problems);
288
- if (path !== null && seen.has(path)) {
289
- problems.push({ reason: `screenshots[${index}]-duplicate-path`, message: `screenshot path "${path}" appears twice` });
290
- continue;
291
- }
292
- if (path === null || caption === null)
103
+ /**
104
+ * The rules JSON Schema cannot state, in plain code with path-named errors: blank text, UTF-8 byte limits, control
105
+ * characters, normalized repository-relative screenshot paths (no leading slash, `.`/`..` segment or backslash — the
106
+ * shape `readVerifiedArtifact` enforces on an evidence key), and unique criterion ids and screenshot paths. A field
107
+ * the schema already refused is not reported twice.
108
+ */
109
+ function proofRuleProblems(body, contract) {
110
+ const named = (path) => contract.some(problem => problem.message.startsWith(`${path}:`));
111
+ const problems = [];
112
+ for (const field of textFields(body)) {
113
+ if (named(field.path))
293
114
  continue;
294
- seen.add(path);
295
- screenshots.push({ path, caption });
296
- }
297
- return screenshots;
115
+ const { path, value } = field;
116
+ if (value.trim() === "")
117
+ problems.push({ reason: `missing-${path}`, message: `${path}: must not be blank` });
118
+ else if (Buffer.byteLength(value, "utf8") > field.bytes)
119
+ problems.push({ reason: `${path}-too-long`, message: `${path}: over ${field.bytes.toLocaleString("en-US")} bytes` });
120
+ else if (hasForbiddenControls(value))
121
+ problems.push({ reason: `${path}-controls`, message: `${path}: carries control characters that could become terminal escapes` });
122
+ else if (field.relative && (value.startsWith("/") || !value.split("/").every(segment => segment.length > 0 && segment !== "." && segment !== ".." && !segment.includes("\\")))) {
123
+ problems.push({ reason: `${path}-not-relative`, message: `${path}: must be a normalized repository-relative path` });
124
+ }
125
+ }
126
+ const unique = (field, key, what) => {
127
+ const list = body[field];
128
+ if (!Array.isArray(list) || named(field))
129
+ return;
130
+ const seen = new Set();
131
+ list.forEach((one, index) => {
132
+ const value = typeof one === "object" && one !== null ? one[key] : undefined;
133
+ if (typeof value !== "string")
134
+ return;
135
+ if (seen.has(value))
136
+ problems.push({ reason: `${field}[${index}]-duplicate-${key}`, message: `${field}[${index}].${key}: ${what} ${JSON.stringify(value)} appears twice` });
137
+ seen.add(value);
138
+ });
139
+ };
140
+ unique("criteria", "id", "criterion id");
141
+ unique("screenshots", "path", "screenshot path");
142
+ return problems;
298
143
  }
144
+ /**
145
+ * Read a proof: the payload byte cap and JSON first, then the proof schema (version 2 as itself, the version 1 a
146
+ * builder writes upgraded, a newer one refused plainly), then the plain-code rules. Every problem names its path and
147
+ * is reported at once; fail closed.
148
+ */
299
149
  export function parseProof(raw) {
300
150
  if (Buffer.byteLength(raw, "utf8") > PROOF_LIMITS.payload) {
301
- return refuse("too-large", `the payload is over ${PROOF_LIMITS.payload} bytes`);
151
+ return refuse("too-large", `payload: over ${PROOF_LIMITS.payload.toLocaleString("en-US")} bytes`);
302
152
  }
303
153
  let parsed;
304
154
  try {
@@ -307,29 +157,20 @@ export function parseProof(raw) {
307
157
  catch (error) {
308
158
  return refuse("not-json", `the payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
309
159
  }
310
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
311
- return refuse("not-an-object", "the payload must be one JSON object");
312
- }
313
- const body = parsed;
314
- const problems = [];
315
- if (body["version"] !== 1) {
316
- problems.push({ reason: "bad-version", message: `version must be 1 (got ${describe(body["version"])})` });
317
- }
318
- const criteria = parseCriteria(body["criteria"], problems);
319
- const checks = parseChecks(body["checks"], problems);
320
- const changed = parseStringList(body["changed"], "changed", PROOF_LIMITS.changed, PROOF_LIMITS.changedPath, problems);
321
- const caveats = parseStringList(body["caveats"], "caveats", PROOF_LIMITS.caveats, PROOF_LIMITS.caveat, problems);
322
- const screenshots = parseScreenshots(body["screenshots"], problems);
323
- if (problems.length > 0)
160
+ const body = proofPayloadBody(parsed);
161
+ if (body === null)
162
+ return refuse("not-an-object", "payload: must be one JSON object");
163
+ const read = readProofPayload(parsed);
164
+ const contract = read.ok ? [] : read.issues.map(contractProblemOf);
165
+ if (contract.some(problem => problem.reason === "newer-version" || problem.reason === "bad-version"))
166
+ return { ok: false, problems: contract };
167
+ const problems = [...contract, ...proofRuleProblems(body, contract)];
168
+ if (!read.ok || problems.length > 0)
324
169
  return { ok: false, problems };
325
- return {
326
- ok: true,
327
- proof: { version: 1, criteria, checks, changed, caveats, screenshots },
328
- };
170
+ return { ok: true, proof: read.value };
329
171
  }
330
- /** Re-serialize the validated shape, never the agent's raw bytes (the
331
- * scout report's rule, `scout.ts`): what is stored and later hash-verified
332
- * is exactly what this parser admitted, key order and all. */
172
+ /** Serialize the validated shape, never the agent's raw bytes (the scout report's rule, `scout.ts`): what is stored
173
+ * and later hash-verified is exactly what the schema admitted, in its key order. */
333
174
  export function serializeProof(proof) {
334
175
  return JSON.stringify(proof, null, 2);
335
176
  }
@@ -81,6 +81,8 @@ export type Invocation = {
81
81
  /** A research step's project tools: only these servers, each offering only
82
82
  * these read-only actions (Codex's `enabled_tools`). Others are left out. */
83
83
  readOnlyTools?: Readonly<Record<string, readonly string[]>>;
84
+ /** Why a research step's connected service was left out, by server, when it isn't the usual reason (its grant writes). */
85
+ researchWithheld?: Readonly<Record<string, string>>;
84
86
  };
85
87
  export type ProviderRunner = (file: string, args: readonly string[], options?: RunOptions) => Promise<ExecResult>;
86
88
  /** The harness's own account of how a turn ended. */
package/dist/provider.js CHANGED
@@ -22,6 +22,7 @@ import { claudeFenceSettings, codexFenceArgv } from "./agent-fence.js";
22
22
  import {} from "./exec.js";
23
23
  import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl } from "./exec.js";
24
24
  import { scanForSecrets } from "./evidence.js";
25
+ import { FINDINGS_MODEL_SCHEMA } from "./contracts/review-findings.js";
25
26
  import { REVIEW_OUTPUT_LIMITS } from "./structured-output.js";
26
27
  export const PROVIDER_IDS = ["claude", "codex", "openrouter", "gemini"];
27
28
  export function isProviderId(value) {
@@ -169,22 +170,9 @@ const CLAUDE_REVIEW_JSON_SCHEMA = {
169
170
  additionalProperties: false,
170
171
  },
171
172
  },
172
- // The automatic build review's reply (build-review.ts). Shape only;
173
- // parseBuildFindings is the validator.
174
- findings: {
175
- type: "array",
176
- items: {
177
- type: "object",
178
- properties: {
179
- severity: { type: "string", enum: ["HIGH", "MEDIUM", "LOW"] },
180
- file: { type: "string" },
181
- line: { type: "integer", minimum: 1 },
182
- scenario: { type: "string" },
183
- },
184
- required: ["severity", "file", "line", "scenario"],
185
- additionalProperties: false,
186
- },
187
- },
173
+ // The automatic build review's reply (build-review.ts), from its one
174
+ // schema (src/contracts/review-findings.ts); parseBuildFindings reads it.
175
+ findings: FINDINGS_MODEL_SCHEMA,
188
176
  // The read request rides the same structured reply channel and
189
177
  // session. The machine validates the exact allowlist/hash/range
190
178
  // before supplying bytes; this only shapes the request.
@@ -0,0 +1,42 @@
1
+ /** Shared verified readers for result screens and the compact CLI review. */
2
+ import type { Artifact } from "./store.js";
3
+ export declare const SCREENSHOT_CAPTURE: RegExp;
4
+ /** What the run page shows of the terminal diff — verified bytes or a named problem, never silence. */
5
+ export type TerminalDiffView = {
6
+ patch: {
7
+ text: string;
8
+ truncated: boolean;
9
+ artifactId: number;
10
+ } | {
11
+ problem: string;
12
+ } | null;
13
+ stat: {
14
+ base: string;
15
+ head: string;
16
+ fileCount: number;
17
+ additions: number;
18
+ deletions: number;
19
+ binaryCount: number;
20
+ filesTruncated: boolean;
21
+ files: {
22
+ path: string;
23
+ additions: number | null;
24
+ deletions: number | null;
25
+ renamedFrom?: string;
26
+ }[];
27
+ } | {
28
+ problem: string;
29
+ } | null;
30
+ };
31
+ /** A compact, typed view of the agent-authored handoff. Older v1
32
+ * artifacts simply have no lists, while v2 can present the useful answer
33
+ * before the raw diff and evidence below it. */
34
+ export type StructuredHandoffView = {
35
+ conclusion: string;
36
+ changes: string[];
37
+ verification: string[];
38
+ followUps: string[];
39
+ };
40
+ export declare function structuredHandoffView(artifacts: Artifact[], root: string, displayText?: (text: string) => string): StructuredHandoffView | null;
41
+ /** A failed capture or unverifiable bytes keep their problem; absence returns null. */
42
+ export declare function terminalDiffView(artifacts: Artifact[], root: string): TerminalDiffView | null;
@@ -0,0 +1,91 @@
1
+ import { readVerifiedArtifact } from "./evidence.js";
2
+ import { hasForbiddenControls } from "./decision.js";
3
+ import { parseHandoffArtifact } from "./contracts/handoff.js";
4
+ const CAPTURE_EXIT = /\(exit ([0-9]{1,4})\)\s*$/;
5
+ export const SCREENSHOT_CAPTURE = /^agent-claimed screenshot at (.+) \(validated (?:png|jpeg)\)/;
6
+ const oneLineOf = (text, cap) => {
7
+ const flat = text.replace(/\s+/g, " ").trim();
8
+ return flat.length <= cap ? flat : `${flat.slice(0, cap - 1)}…`;
9
+ };
10
+ export function structuredHandoffView(artifacts, root, displayText = text => text) {
11
+ const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
12
+ if (artifact === undefined)
13
+ return null;
14
+ const read = readVerifiedArtifact(root, artifact);
15
+ if (!read.ok)
16
+ return null;
17
+ const parsed = parseHandoffArtifact(read.content.toString("utf8"));
18
+ if (!parsed.ok)
19
+ return null;
20
+ const handoff = parsed.value;
21
+ const conclusion = oneLineOf(displayText(handoff.conclusion), 600);
22
+ if (conclusion === "" || hasForbiddenControls(conclusion))
23
+ return null;
24
+ const list = (items) => (items ?? [])
25
+ .filter(one => one.trim() !== "" && !hasForbiddenControls(one))
26
+ .slice(0, 8)
27
+ .map(one => oneLineOf(one, 240));
28
+ return { conclusion, changes: list(handoff.changes), verification: list(handoff.verification), followUps: list(handoff.followUps) };
29
+ }
30
+ /** A failed capture or unverifiable bytes keep their problem; absence returns null. */
31
+ export function terminalDiffView(artifacts, root) {
32
+ const patchArtifact = artifacts.find(one => one.kind === "terminal-diff");
33
+ const statArtifact = artifacts.find(one => one.kind === "diff-stat");
34
+ if (patchArtifact === undefined && statArtifact === undefined)
35
+ return null;
36
+ const view = { patch: null, stat: null };
37
+ if (patchArtifact !== undefined) {
38
+ const exit = CAPTURE_EXIT.exec(patchArtifact.capture);
39
+ if (exit !== null && exit[1] !== "0") {
40
+ view.patch = { problem: `capture failed — ${patchArtifact.capture}` };
41
+ }
42
+ else {
43
+ const read = readVerifiedArtifact(root, patchArtifact);
44
+ view.patch = read.ok
45
+ ? { text: read.content.toString("utf8"), truncated: patchArtifact.truncated, artifactId: patchArtifact.id }
46
+ : { problem: `stored but unverifiable — ${read.problem}` };
47
+ }
48
+ }
49
+ if (statArtifact !== undefined) {
50
+ const exit = CAPTURE_EXIT.exec(statArtifact.capture);
51
+ if (exit !== null && exit[1] !== "0") {
52
+ view.stat = { problem: `capture failed — ${statArtifact.capture}` };
53
+ }
54
+ else {
55
+ const read = readVerifiedArtifact(root, statArtifact);
56
+ if (!read.ok) {
57
+ view.stat = { problem: `stored but unverifiable — ${read.problem}` };
58
+ }
59
+ else {
60
+ try {
61
+ const parsed = JSON.parse(read.content.toString("utf8"));
62
+ // Every field this page renders is type-proved (arc 6, finding 7):
63
+ // "an object with a base key" was accepting any shape at all.
64
+ const count = (value) => typeof value === "number" && Number.isFinite(value);
65
+ const wellFormed = parsed !== null &&
66
+ typeof parsed === "object" &&
67
+ typeof parsed["base"] === "string" &&
68
+ typeof parsed["head"] === "string" &&
69
+ count(parsed["fileCount"]) &&
70
+ count(parsed["additions"]) &&
71
+ count(parsed["deletions"]) &&
72
+ count(parsed["binaryCount"]) &&
73
+ typeof parsed["filesTruncated"] === "boolean" &&
74
+ Array.isArray(parsed["files"]) &&
75
+ parsed["files"].every(one => one !== null &&
76
+ typeof one === "object" &&
77
+ typeof one["path"] === "string" &&
78
+ (count(one["additions"]) || one["additions"] === null) &&
79
+ (count(one["deletions"]) || one["deletions"] === null));
80
+ view.stat = wellFormed
81
+ ? parsed
82
+ : { problem: "stat is not the shape this page knows" };
83
+ }
84
+ catch {
85
+ view.stat = { problem: "stat did not parse as JSON" };
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return view;
91
+ }
@@ -1,5 +1,5 @@
1
1
  import { type Notification, type ResultScreenshots, type Store } from "./store.js";
2
- import type { ChatContent, ChatState } from "./chat-delivery-state.js";
2
+ import { type ChatContent, type ChatState } from "./chat-delivery-state.js";
3
3
  /** "Up to 4": the most screenshots one result ever sends. */
4
4
  export declare const RESULT_SHOTS_MAX = 4;
5
5
  /** The choices in the order Settings and the CLI show them. */
@@ -21,6 +21,7 @@ import { publicChatText } from "./chat-display.js";
21
21
  import { chatTitle } from "./chat-voice.js";
22
22
  import { itemCaption, readFlowItems } from "./flow-items.js";
23
23
  import { FLOW_SHOTS_KIND, RESULT_SHOTS_KIND } from "./store.js";
24
+ import { savedChatPart } from "./chat-delivery-state.js";
24
25
  /** "Up to 4": the most screenshots one result ever sends. */
25
26
  export const RESULT_SHOTS_MAX = 4;
26
27
  /** The choices in the order Settings and the CLI show them. */
@@ -168,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
168
169
  const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
169
170
  const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
170
171
  ...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
171
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE id=?").run(JSON.stringify(line), now.toISOString(), part.id);
172
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE id=?").run(savedChatPart(line), now.toISOString(), part.id);
172
173
  state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE event=? AND id<>? AND state='pending'").run(part.event, part.id);
173
174
  }
@@ -2,15 +2,10 @@
2
2
  * build gets (build-review.ts runs it): tables, and the read-only views every
3
3
  * surface shows. No schema version bump. Kept free of heavy imports so the
4
4
  * status readers can use it without an import cycle. */
5
+ import type { BuildFinding } from "./contracts/review-findings.js";
5
6
  import type { Store } from "./store.js";
6
- export type FindingSeverity = "HIGH" | "MEDIUM" | "LOW";
7
- /** One automatic-review finding: how bad, where, and how it fails. */
8
- export type BuildFinding = {
9
- severity: FindingSeverity;
10
- file: string;
11
- line: number;
12
- scenario: string;
13
- };
7
+ /** One automatic-review finding: how bad, where, and how it fails (src/contracts/review-findings.ts). */
8
+ export type { BuildFinding, FindingSeverity } from "./contracts/review-findings.js";
14
9
  export declare const REVIEW_SCHEMA = "\n-- A project's explicit review switch. No row: on while the project is under a hands-off mode, off otherwise.\nCREATE TABLE IF NOT EXISTS review_switch (\n repo TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),\n changed_by TEXT NOT NULL,\n changed_at TEXT NOT NULL\n);\n-- One row per reviewed build run: pending until its one reviewer run settles it.\nCREATE TABLE IF NOT EXISTS build_review (\n run INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n repo TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('pending', 'reviewed', 'not-reviewed')),\n request INTEGER,\n reviewer_run INTEGER,\n findings_json TEXT,\n reason TEXT,\n sent_back_as TEXT,\n queued_at TEXT NOT NULL,\n finished_at TEXT\n);\nCREATE INDEX IF NOT EXISTS build_review_pending ON build_review (repo) WHERE state = 'pending';\nCREATE INDEX IF NOT EXISTS build_review_sent_back ON build_review (sent_back_as) WHERE sent_back_as IS NOT NULL;\n";
15
10
  export type ReviewSwitch = {
16
11
  on: boolean;
@@ -2,7 +2,7 @@
2
2
  * automatic build review's findings (build-review.ts runs it). This module
3
3
  * cannot launch providers, create runs or retry work. */
4
4
  import type { CriterionJudgementWord } from "./proof.js";
5
- import type { BuildFinding } from "./review-switch.js";
5
+ import { type BuildFinding } from "./contracts/review-findings.js";
6
6
  /** The files the pass writes INTO the scratch directory for the agent —
7
7
  * the patch always; the rubric, re-serialized proof, check log, and
8
8
  * screenshots only when this run actually has one (v40) — a grandfathered
@@ -97,18 +97,14 @@ export declare function parseReview(raw: string, patchPaths: ReadonlySet<string>
97
97
  ok: false;
98
98
  problems: ReviewProblem[];
99
99
  };
100
- export type { BuildFinding, FindingSeverity } from "./review-switch.js";
101
- export declare const FINDING_LIMITS: {
102
- readonly findings: 40;
103
- readonly file: 300;
104
- readonly scenario: 500;
105
- };
100
+ export type { BuildFinding, FindingSeverity } from "./contracts/review-findings.js";
106
101
  /**
107
- * The automatic build review's reply, strict and wholesale like parseReview:
108
- * `{"version":1,"findings":[{"severity","file","line","scenario"}]}`. Any
109
- * invalid finding refuses the whole reply — a review that did not answer
110
- * what it was asked is "not reviewed", never a partial pass. Text is kept
111
- * to one line so it can travel in a revision note and a terminal.
102
+ * The automatic build review's reply, read through its contract (src/contracts/review-findings.ts), with all findings
103
+ * checked together: `{"version":1,"findings":[{"severity","file","line","scenario"}]}`. Any invalid
104
+ * finding refuses the whole reply — a review that did not answer what it was asked is "not reviewed", never a partial
105
+ * pass — and the refusal names each path (`findings[0].line: at least 1`). The reply shares Claude's review channel
106
+ * (provider.ts), so only its version and findings are read, as always. Text is kept to one line so it can travel in a
107
+ * revision note and a terminal; its limits are checked after whitespace collapses, as before.
112
108
  */
113
109
  export declare function parseBuildFindings(raw: string): {
114
110
  ok: true;