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/reviewer.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { citesSuppliedProvenance } from "./review-context.js";
2
2
  import { normalizeStructuredJson, REVIEW_OUTPUT_LIMITS } from "./structured-output.js";
3
+ import { readBuildFindings } from "./contracts/review-findings.js";
3
4
  /** The files the pass writes INTO the scratch directory for the agent —
4
5
  * the patch always; the rubric, re-serialized proof, check log, and
5
6
  * screenshots only when this run actually has one (v40) — a grandfathered
@@ -200,13 +201,13 @@ export function parseReview(raw, patchPaths, approvedCriteriaIds = new Set(), pr
200
201
  return { ok: false, problems };
201
202
  return { ok: true, comments, criteria, ...(payload["learning"] === undefined ? {} : { learning: payload["learning"] }), ...(payload["learningAssessment"] === undefined ? {} : { learningAssessment: payload["learningAssessment"] }) };
202
203
  }
203
- export const FINDING_LIMITS = { findings: 40, file: REVIEW_OUTPUT_LIMITS.path, scenario: REVIEW_OUTPUT_LIMITS.note };
204
204
  /**
205
- * The automatic build review's reply, strict and wholesale like parseReview:
206
- * `{"version":1,"findings":[{"severity","file","line","scenario"}]}`. Any
207
- * invalid finding refuses the whole reply — a review that did not answer
208
- * what it was asked is "not reviewed", never a partial pass. Text is kept
209
- * to one line so it can travel in a revision note and a terminal.
205
+ * The automatic build review's reply, read through its contract (src/contracts/review-findings.ts), with all findings
206
+ * checked together: `{"version":1,"findings":[{"severity","file","line","scenario"}]}`. Any invalid
207
+ * finding refuses the whole reply — a review that did not answer what it was asked is "not reviewed", never a partial
208
+ * pass — and the refusal names each path (`findings[0].line: at least 1`). The reply shares Claude's review channel
209
+ * (provider.ts), so only its version and findings are read, as always. Text is kept to one line so it can travel in a
210
+ * revision note and a terminal; its limits are checked after whitespace collapses, as before.
210
211
  */
211
212
  export function parseBuildFindings(raw) {
212
213
  let parsed;
@@ -218,33 +219,8 @@ export function parseBuildFindings(raw) {
218
219
  }
219
220
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
220
221
  return { ok: false, problem: "the reply is not an object" };
221
- const payload = parsed;
222
- if (payload["version"] !== 1)
223
- return { ok: false, problem: "version must be 1" };
224
- const list = payload["findings"];
225
- if (!Array.isArray(list))
226
- return { ok: false, problem: "findings must be an array" };
227
- if (list.length > FINDING_LIMITS.findings)
228
- return { ok: false, problem: `at most ${FINDING_LIMITS.findings} findings` };
229
- const findings = [];
230
- const oneLine = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g, " ").replace(/\s+/g, " ").trim();
231
- for (const [index, one] of list.entries()) {
232
- if (one === null || typeof one !== "object" || Array.isArray(one))
233
- return { ok: false, problem: `finding ${index}: not an object` };
234
- const finding = one;
235
- const severity = finding["severity"];
236
- if (severity !== "HIGH" && severity !== "MEDIUM" && severity !== "LOW")
237
- return { ok: false, problem: `finding ${index}: severity must be HIGH, MEDIUM or LOW` };
238
- const file = typeof finding["file"] === "string" ? oneLine(finding["file"]) : "";
239
- if (file.length === 0 || file.length > FINDING_LIMITS.file)
240
- return { ok: false, problem: `finding ${index}: file must be a path of 1..${FINDING_LIMITS.file} characters` };
241
- const line = finding["line"];
242
- if (typeof line !== "number" || !Number.isInteger(line) || line < 1)
243
- return { ok: false, problem: `finding ${index}: line must be a positive whole number` };
244
- const scenario = typeof finding["scenario"] === "string" ? oneLine(finding["scenario"]) : "";
245
- if (scenario.length === 0 || scenario.length > FINDING_LIMITS.scenario)
246
- return { ok: false, problem: `finding ${index}: scenario must be one sentence of 1..${FINDING_LIMITS.scenario} characters` };
247
- findings.push({ severity, file, line, scenario });
248
- }
249
- return { ok: true, findings };
222
+ const read = readBuildFindings(parsed);
223
+ if (!read.ok)
224
+ return { ok: false, problem: read.issues.map(issue => issue.line).join("; ") };
225
+ return { ok: true, findings: read.value.findings };
250
226
  }
package/dist/scope.d.ts CHANGED
@@ -1,4 +1,7 @@
1
+ export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS, type EvidenceKind } from "./contracts/acceptance-terms.js";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { type EvidenceKind } from "./contracts/acceptance-terms.js";
4
+ import { type AcceptanceCriterion, type AcceptanceProblem, type ScopeTerms } from "./contracts/scope.js";
2
5
  import type { Store, Mutation } from "./store.js";
3
6
  import type { QualityMode } from "./quality.js";
4
7
  import type { AuthMode } from "./keys.js";
@@ -176,40 +179,14 @@ export declare function exactKeys(value: object, required: readonly string[], op
176
179
  /** Strict re-hydration of a stored snapshot — every field type-proved;
177
180
  * anything unexpected is null, never a guess. */
178
181
  export declare function profileFromJson(json: string | null): ExecutionProfile | null;
179
- export type EvidenceKind = "check" | "screenshot" | "changed-path" | "manual-review";
180
- export declare const EVIDENCE_KINDS: readonly EvidenceKind[];
181
- export type AcceptanceCriterion = {
182
- id: string;
183
- /** The signed outcome statement. Restating it differently in a proof is
184
- * refuted, not short — the same severity as any other altered term. */
185
- statement: string;
186
- /** Advisory guidance for HOW to satisfy the statement. Never enters the
187
- * digest and is never a term the approval binds — an operator can leave
188
- * it, change it, or ignore it without touching what was signed. */
189
- how: string | null;
190
- /** SIGNED: the evidence kinds a proof must reference, by exact id, to
191
- * answer this criterion. Never empty. */
192
- evidence: EvidenceKind[];
193
- };
194
- export declare const ACCEPTANCE_LIMITS: {
195
- readonly criteria: 12;
196
- readonly id: 40;
197
- readonly statement: 1000;
198
- readonly how: 500;
199
- readonly evidenceKinds: number;
200
- };
201
- export type AcceptanceProblem = {
202
- reason: string;
203
- message: string;
204
- };
182
+ export type { AcceptanceCriterion, AcceptanceProblem };
205
183
  /**
206
184
  * Parse a rubric from already-JSON-parsed input (the planner's handoff, a
207
185
  * console form, a routine or template definition): fail closed, every
208
- * problem reported at once, stable reasons — the same discipline as every
209
- * other quoted-data parser in this codebase (`plan.ts`, `proof.ts`). An
210
- * absent or empty `value` parses to `[]` with no problems: whether that is
211
- * ALLOWED is a question for the caller (`propose` says yes; every authoring
212
- * road says no), never for this parser.
186
+ * problem reported at once, each naming its path — the acceptance contract
187
+ * (contracts/scope.ts) reads it. An absent or empty `value` parses to `[]`
188
+ * with no problems: whether that is ALLOWED is a question for the caller
189
+ * (`propose` says yes; every authoring road says no), never for this parser.
213
190
  */
214
191
  export declare function parseAcceptanceCriteria(value: unknown): {
215
192
  criteria: AcceptanceCriterion[];
@@ -257,19 +234,20 @@ export declare function canonicalAcceptance(criteria: readonly AcceptanceCriteri
257
234
  statement: string;
258
235
  evidence: EvidenceKind[];
259
236
  }[];
260
- export type Scope = {
237
+ /**
238
+ * A scope's terms, from the scope contract (contracts/scope.ts): what
239
+ * success looks like in the operator's words (`goal`), what the task is
240
+ * explicitly not allowed to turn into (`outOfScope`), the paths it expects
241
+ * to touch (advisory, and worth stating), and the signed acceptance rubric
242
+ * — `[]` on every scope proposed before v39 and on any legacy road that
243
+ * still calls `propose` directly: a scope-producing ROAD enforces
244
+ * non-emptiness, never this type, never `propose`, never the digest.
245
+ */
246
+ export type SavedScopeTerms = {
247
+ [K in keyof ScopeTerms]-?: Exclude<ScopeTerms[K], undefined>;
248
+ };
249
+ export type Scope = SavedScopeTerms & {
261
250
  taskId: string;
262
- /** What success looks like, in the operator's words. */
263
- goal: string;
264
- /** What this task is explicitly not allowed to turn into. */
265
- outOfScope: string | null;
266
- /** Paths the work is expected to touch. Advisory, and worth stating. */
267
- touches: string[];
268
- /** v39: the signed acceptance rubric. `[]` on every scope proposed before
269
- * this migration and on any legacy road that still calls `propose`
270
- * directly — a scope-producing ROAD enforces non-emptiness, never this
271
- * type, never `propose`, never the digest. */
272
- acceptance: AcceptanceCriterion[];
273
251
  /** v41: evidence policy, signed with the scope. Default is omitted from
274
252
  * the digest so every pre-v41 approval remains byte-for-byte valid. */
275
253
  qualityMode?: QualityMode;
package/dist/scope.js CHANGED
@@ -1,3 +1,5 @@
1
+ // The rubric's vocabulary loads before anything that reaches the plan contract (see contracts/acceptance-terms.ts).
2
+ export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
1
3
  import { passwordGuardOf } from "./sign-in-guard.js";
2
4
  import { validateScopeText } from "./task-text.js";
3
5
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -29,9 +31,11 @@ import { claimActor, currentActor, parseLeadToken } from "./actor.js";
29
31
  * glance is the whole safety property.
30
32
  */
31
33
  import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
32
- import { hasForbiddenControls } from "./decision.js";
33
- import { TEXT_LIMITS } from "./text-limits.js";
34
- import { NO_READINESS, exactModelId, isRiskLevel, legOf, projectRoute, routeDigestOf, routeFromJson, routeWords, } from "./phase-routing.js";
34
+ import { z } from "zod";
35
+ import {} from "./contracts/acceptance-terms.js";
36
+ import { parseContract } from "./contracts/contract.js";
37
+ import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
38
+ import { NO_READINESS, exactModelId, isRiskLevel, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
35
39
  /** Same shape as a runner's credential, for the same reasons. */
36
40
  function mintToken() {
37
41
  return randomBytes(32).toString("base64url");
@@ -248,13 +252,18 @@ export function exactAcceptance(value) {
248
252
  }
249
253
  if (!Array.isArray(parsed))
250
254
  return { ok: false, problem: "the rubric is not a JSON list" };
251
- for (const [index, entry] of parsed.entries()) {
252
- if (entry === null || typeof entry !== "object" || Array.isArray(entry))
253
- return { ok: false, problem: `rubric entry ${index + 1} is not an object` };
254
- if (!exactKeys(entry, ["id", "statement", "evidence"], ["how"]))
255
- return { ok: false, problem: `rubric entry ${index + 1} carries a key this code never writes` };
255
+ // Stored, a criterion carries exactly the keys this code writes: the saved rubric schema is strict about them.
256
+ const strict = parseContract(z.object({ acceptance: storedRubricSchema }), { acceptance: parsed });
257
+ for (const issue of strict.ok ? [] : strict.issues) {
258
+ const entry = /^acceptance\[(\d+)\]$/.exec(issue.path);
259
+ if (entry === null)
260
+ continue;
261
+ if (issue.kind === "wrong-type" || issue.kind === "required")
262
+ return { ok: false, problem: `rubric entry ${Number(entry[1]) + 1} is not an object` };
263
+ if (issue.kind === "unknown-key")
264
+ return { ok: false, problem: `rubric entry ${Number(entry[1]) + 1} carries a key this code never writes` };
256
265
  }
257
- const read = parseAcceptanceCriteria(parsed);
266
+ const read = readAcceptance(parsed);
258
267
  if (read.problems.length > 0)
259
268
  return { ok: false, problem: `the rubric does not parse: ${read.problems.map(one => one.message).join("; ")}` };
260
269
  return { ok: true, criteria: read.criteria };
@@ -397,126 +406,28 @@ export function profileFromJson(json) {
397
406
  }
398
407
  return null;
399
408
  }
400
- export const EVIDENCE_KINDS = ["check", "screenshot", "changed-path", "manual-review"];
401
- export const ACCEPTANCE_LIMITS = {
402
- criteria: 12,
403
- id: TEXT_LIMITS.acceptanceIdBytes,
404
- // A statement is one testable outcome; three hundred bytes forced people to
405
- // drop the qualifying clause that made it testable.
406
- statement: TEXT_LIMITS.acceptanceStatementBytes,
407
- how: TEXT_LIMITS.acceptanceHowBytes,
408
- evidenceKinds: EVIDENCE_KINDS.length,
409
- };
410
- function acceptanceDescribe(value) {
411
- if (value === undefined)
412
- return "nothing";
413
- if (value === null)
414
- return "null";
415
- if (typeof value === "string")
416
- return `a ${value.length}-char string`;
417
- return `a ${Array.isArray(value) ? "array" : typeof value}`;
418
- }
419
- function acceptanceProse(value, field, cap, required, problems) {
420
- if (value === undefined || value === null || value === "") {
421
- if (required)
422
- problems.push({ reason: `missing-${field}`, message: `${field} is required` });
423
- return null;
424
- }
425
- if (typeof value !== "string") {
426
- problems.push({ reason: `bad-${field}`, message: `${field} must be a string (got ${acceptanceDescribe(value)})` });
427
- return null;
428
- }
429
- if (Buffer.byteLength(value, "utf8") > cap) {
430
- problems.push({ reason: `${field}-too-long`, message: `${field} is over ${cap} bytes` });
431
- return null;
432
- }
433
- if (hasForbiddenControls(value)) {
434
- problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
435
- return null;
436
- }
437
- return value;
438
- }
439
409
  /**
440
410
  * Parse a rubric from already-JSON-parsed input (the planner's handoff, a
441
411
  * console form, a routine or template definition): fail closed, every
442
- * problem reported at once, stable reasons — the same discipline as every
443
- * other quoted-data parser in this codebase (`plan.ts`, `proof.ts`). An
444
- * absent or empty `value` parses to `[]` with no problems: whether that is
445
- * ALLOWED is a question for the caller (`propose` says yes; every authoring
446
- * road says no), never for this parser.
412
+ * problem reported at once, each naming its path — the acceptance contract
413
+ * (contracts/scope.ts) reads it. An absent or empty `value` parses to `[]`
414
+ * with no problems: whether that is ALLOWED is a question for the caller
415
+ * (`propose` says yes; every authoring road says no), never for this parser.
447
416
  */
448
417
  export function parseAcceptanceCriteria(value) {
449
- const problems = [];
450
- if (value === undefined || value === null)
451
- return { criteria: [], problems };
452
- if (!Array.isArray(value)) {
453
- problems.push({ reason: "bad-acceptance", message: `acceptance must be an array (got ${acceptanceDescribe(value)})` });
454
- return { criteria: [], problems };
455
- }
456
- if (value.length > ACCEPTANCE_LIMITS.criteria) {
457
- problems.push({ reason: "acceptance-too-many", message: `acceptance lists ${value.length} — cap is ${ACCEPTANCE_LIMITS.criteria}` });
458
- return { criteria: [], problems };
459
- }
460
- const criteria = [];
461
- const seen = new Set();
462
- for (const [index, entry] of value.entries()) {
463
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
464
- problems.push({ reason: `acceptance[${index}]-shape`, message: `acceptance[${index}] must be an object` });
465
- continue;
466
- }
467
- const one = entry;
468
- const id = acceptanceProse(one["id"], `acceptance[${index}].id`, ACCEPTANCE_LIMITS.id, true, problems);
469
- if (id !== null && seen.has(id)) {
470
- problems.push({ reason: `acceptance[${index}]-duplicate-id`, message: `criterion id "${id}" appears twice` });
471
- }
472
- else if (id !== null) {
473
- seen.add(id);
474
- }
475
- const statement = acceptanceProse(one["statement"], `acceptance[${index}].statement`, ACCEPTANCE_LIMITS.statement, true, problems);
476
- const how = acceptanceProse(one["how"], `acceptance[${index}].how`, ACCEPTANCE_LIMITS.how, false, problems);
477
- const rawEvidence = one["evidence"];
478
- let evidence = null;
479
- if (!Array.isArray(rawEvidence)) {
480
- problems.push({
481
- reason: `acceptance[${index}]-bad-evidence`,
482
- message: `acceptance[${index}].evidence must be a non-empty array of evidence kinds (got ${acceptanceDescribe(rawEvidence)})`,
483
- });
484
- }
485
- else if (rawEvidence.length === 0 || rawEvidence.length > ACCEPTANCE_LIMITS.evidenceKinds) {
486
- problems.push({
487
- reason: `acceptance[${index}]-evidence-count`,
488
- message: `acceptance[${index}].evidence must name 1-${ACCEPTANCE_LIMITS.evidenceKinds} evidence kinds`,
489
- });
490
- }
491
- else {
492
- const kinds = [];
493
- const seenKinds = new Set();
494
- let bad = false;
495
- for (const k of rawEvidence) {
496
- if (typeof k !== "string" || !EVIDENCE_KINDS.includes(k)) {
497
- problems.push({
498
- reason: `acceptance[${index}]-bad-evidence-kind`,
499
- message: `acceptance[${index}].evidence must draw from ${EVIDENCE_KINDS.join(", ")} (got ${acceptanceDescribe(k)})`,
500
- });
501
- bad = true;
502
- continue;
503
- }
504
- if (seenKinds.has(k)) {
505
- problems.push({ reason: `acceptance[${index}]-duplicate-evidence-kind`, message: `evidence kind "${k}" appears twice` });
506
- bad = true;
507
- continue;
508
- }
509
- seenKinds.add(k);
510
- kinds.push(k);
511
- }
512
- if (!bad)
513
- evidence = kinds;
514
- }
515
- if (id === null || statement === null || evidence === null)
516
- continue;
517
- criteria.push({ id, statement, how, evidence });
518
- }
519
- return { criteria, problems };
418
+ const read = readAcceptance(value);
419
+ // Keep the prose shape plan.ts's existing acceptanceLine consumes. Validation stays in the contract; only this
420
+ // compatibility boundary formats messages. That mapping takes a field followed by a space, not a colon, and
421
+ // evidence-array indices belong in the detail because it recognizes only the criterion's index.
422
+ return {
423
+ ...read,
424
+ problems: read.problems.map(problem => ({
425
+ ...problem,
426
+ message: problem.message
427
+ .replace(/^(acceptance\[\d+\]\.evidence)\[(\d+)\]: /, (_, path, index) => `${path} entry ${Number(index) + 1}: `)
428
+ .replace(/^(acceptance(?:\[\d+\])?(?:\.[A-Za-z]+)?): /, "$1 "),
429
+ })),
430
+ };
520
431
  }
521
432
  /** The exact bytes a rubric's SIGNED terms reduce to for the digest: sorted
522
433
  * by id, `how` dropped (advisory, never signed), evidence kinds sorted so
@@ -710,15 +621,18 @@ export function propose(store, input) {
710
621
  */
711
622
  export function proposeGuarded(store, input) {
712
623
  const touches = [...(input.touches ?? [])].map(one => one.trim()).filter(one => one !== "");
624
+ // Plain words first for what a person typed (lengths, bytes, hidden text), then the scope contract.
713
625
  const badText = validateScopeText({ goal: input.goal, outOfScope: input.outOfScope ?? null, touches });
714
626
  if (badText !== null)
715
627
  return badText;
716
628
  const goal = input.goal.trim();
717
629
  const outOfScope = input.outOfScope?.trim() || null;
718
- const acceptanceParse = parseAcceptanceCriteria(input.acceptance);
719
- if (acceptanceParse.problems.length > 0) {
720
- return { ok: false, reason: "bad-acceptance" };
630
+ const terms = readScopeTerms({ goal, outOfScope, touches, acceptance: input.acceptance });
631
+ if (!terms.ok) {
632
+ const reason = terms.field === "acceptance" ? "bad-acceptance" : terms.field === "touches" ? "bad-touches" : terms.field === "outOfScope" ? "bad-out-of-scope" : "bad-goal";
633
+ return { ok: false, reason, message: terms.problems.map(one => one.message).join("; ") };
721
634
  }
635
+ const acceptanceParse = { criteria: terms.terms.acceptance };
722
636
  if (acceptanceParse.criteria.length === 0) {
723
637
  return { ok: false, reason: "acceptance-required" };
724
638
  }
@@ -1070,7 +984,7 @@ export function scopeAuthorityOf(scope, env = {}) {
1070
984
  const routeJson = scope.proposedRouteJson ?? null;
1071
985
  const route = routeFromJson(routeJson);
1072
986
  if (route === null) {
1073
- return { ok: false, reason: "route", problem: routeJson === null ? "the scope was filed under agent routing but carries no route" : "the scope's agent route cannot be read exactly" };
987
+ return { ok: false, reason: "route", problem: routeJson === null ? "the scope was filed under agent routing but carries no route" : `the scope's agent route cannot be read exactly (${routeJsonProblem(routeJson)})` };
1074
988
  }
1075
989
  const parity = routeParityProblem(route, profile, { riskLevel: scope.riskLevel ?? "routine", qualityMode: scope.qualityMode ?? "default" });
1076
990
  if (parity !== null)
@@ -1,44 +1,14 @@
1
1
  /**
2
- * The scout's terminal handoff (mate arc §10), parsed with the 422 rule:
3
- * fail closed, every problem reported at once, stable reasons, caps and
4
- * control-character rejection on every string. A report reaches the task
5
- * page, the ledger, the terminal, and — through mateView — the mate; and
6
- * each follow-up becomes a filing's title and goal at one tap, so it gets
7
- * the park discipline exactly as the plan does.
2
+ * The scout's terminal handoff (mate arc §10), read against the report contract (src/contracts/scout-report.ts) with
3
+ * the 422 rule: fail closed, every problem reported at once, each naming its path (`items[0].url: must be an http or
4
+ * https address`), stable reasons, caps and control-character rejection on every string. A report reaches the task
5
+ * page, the ledger, the terminal, and — through mateView — the mate; and each follow-up becomes a filing's title and
6
+ * goal at one tap, so it gets the park discipline exactly as the plan does.
8
7
  */
9
- export type ReportProblem = {
10
- reason: string;
11
- message: string;
12
- };
13
- export type ParsedReport = {
14
- title: string;
15
- summary: string;
16
- /** The report document, markdown, rendered fenced-inert everywhere. */
17
- report: string;
18
- /** Proposed follow-ups: each files as a task in the same repository. */
19
- followUps: {
20
- title: string;
21
- goal: string;
22
- }[];
23
- /** What the scout found, as a short list later steps can read: each cites its URL, and may show one of `images`. */
24
- items: ReportItem[];
25
- /** Screenshots the scout saved during the run. From the scout, `file` names a file in its output folder; once
26
- * stored, the runner adds the bytes' sha256 and the evidence row that holds them. */
27
- images: ReportImage[];
28
- };
29
- export type ReportItem = {
30
- title: string;
31
- why: string;
32
- url: string;
33
- image: string | null;
34
- };
35
- export type ReportImage = {
36
- file: string;
37
- caption: string;
38
- url: string;
39
- sha256?: string;
40
- artifact?: number;
41
- };
8
+ import { type ContractProblem } from "./contracts/contract.js";
9
+ import { type ParsedReport } from "./contracts/scout-report.js";
10
+ export { REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA, type ParsedReport, type ReportImage, type ReportItem, } from "./contracts/scout-report.js";
11
+ export type ReportProblem = ContractProblem;
42
12
  export type ReportParseResult = {
43
13
  ok: true;
44
14
  report: ParsedReport;
@@ -46,161 +16,6 @@ export type ReportParseResult = {
46
16
  ok: false;
47
17
  problems: ReportProblem[];
48
18
  };
49
- /** Caps are BYTES of UTF-8 (v4 review, finding 11): a 64 KiB report is
50
- * 64 KiB whatever script it is written in. They hold what a scout writes;
51
- * a stored report is read without them (parseReport's `stored`). */
52
- export declare const REPORT_LIMITS: {
53
- readonly payload: number;
54
- readonly title: 200;
55
- readonly summary: 2500;
56
- readonly document: number;
57
- readonly followUps: 5;
58
- readonly followUpTitle: 200;
59
- /** A follow-up files as a task: its goal is held to the task goal limit, in bytes of the same count. */
60
- readonly followUpGoal: 8000;
61
- readonly items: 6;
62
- readonly itemTitle: 200;
63
- readonly itemWhy: 1000;
64
- readonly url: 2000;
65
- readonly images: 8;
66
- readonly caption: 300;
67
- };
68
- /** A screenshot's name in the scout's output folder: one plain file name, PNG or JPEG, never a path. */
69
- export declare const REPORT_IMAGE_FILE: RegExp;
70
- /**
71
- * The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
72
- * mode only lets a session write its own plan file, so a Claude scout
73
- * returns a report — or a question for the operator — as structured output
74
- * on the terminal result event. Shape only, never the validator: byte caps,
75
- * one-line titles and control characters stay `parseReport`'s and
76
- * `parseDecision`'s, applied to the re-serialized body.
77
- */
78
- export declare const SCOUT_OUTPUT_JSON_SCHEMA: {
79
- readonly type: "object";
80
- readonly properties: {
81
- readonly kind: {
82
- readonly type: "string";
83
- readonly enum: readonly ["report", "question"];
84
- };
85
- readonly report: {
86
- readonly type: "object";
87
- readonly properties: {
88
- readonly title: {
89
- readonly type: "string";
90
- };
91
- readonly summary: {
92
- readonly type: "string";
93
- };
94
- readonly report: {
95
- readonly type: "string";
96
- };
97
- readonly followUps: {
98
- readonly type: "array";
99
- readonly items: {
100
- readonly type: "object";
101
- readonly properties: {
102
- readonly title: {
103
- readonly type: "string";
104
- };
105
- readonly goal: {
106
- readonly type: "string";
107
- };
108
- };
109
- readonly required: readonly ["title", "goal"];
110
- readonly additionalProperties: false;
111
- };
112
- };
113
- readonly items: {
114
- readonly type: "array";
115
- readonly items: {
116
- readonly type: "object";
117
- readonly properties: {
118
- readonly title: {
119
- readonly type: "string";
120
- };
121
- readonly why: {
122
- readonly type: "string";
123
- };
124
- readonly url: {
125
- readonly type: "string";
126
- };
127
- readonly image: {
128
- readonly type: "string";
129
- };
130
- };
131
- readonly required: readonly ["title", "why", "url"];
132
- readonly additionalProperties: false;
133
- };
134
- };
135
- readonly images: {
136
- readonly type: "array";
137
- readonly items: {
138
- readonly type: "object";
139
- readonly properties: {
140
- readonly file: {
141
- readonly type: "string";
142
- };
143
- readonly caption: {
144
- readonly type: "string";
145
- };
146
- readonly url: {
147
- readonly type: "string";
148
- };
149
- };
150
- readonly required: readonly ["file", "caption", "url"];
151
- readonly additionalProperties: false;
152
- };
153
- };
154
- };
155
- readonly required: readonly ["title", "summary", "report"];
156
- readonly additionalProperties: false;
157
- };
158
- readonly decision: {
159
- readonly type: "object";
160
- readonly properties: {
161
- readonly urgency: {
162
- readonly type: "string";
163
- readonly enum: readonly ["blocking"];
164
- };
165
- readonly recap: {
166
- readonly type: "string";
167
- };
168
- readonly question: {
169
- readonly type: "string";
170
- };
171
- readonly options: {
172
- readonly type: "array";
173
- readonly items: {
174
- readonly type: "object";
175
- readonly properties: {
176
- readonly id: {
177
- readonly type: "string";
178
- };
179
- readonly label: {
180
- readonly type: "string";
181
- };
182
- readonly consequence: {
183
- readonly type: "string";
184
- };
185
- readonly reversible: {
186
- readonly type: "boolean";
187
- };
188
- };
189
- readonly required: readonly ["id", "label", "consequence", "reversible"];
190
- readonly additionalProperties: false;
191
- };
192
- };
193
- readonly recommendation: {
194
- readonly type: "string";
195
- };
196
- };
197
- readonly required: readonly ["urgency", "recap", "question", "options", "recommendation"];
198
- readonly additionalProperties: false;
199
- };
200
- };
201
- readonly required: readonly ["kind"];
202
- readonly additionalProperties: false;
203
- };
204
19
  /** A report, checked. `stored`: one already kept as evidence, read without the length caps (a report written under
205
20
  * higher limits stays readable); every other check still holds. */
206
21
  export declare function parseReport(raw: string, options?: {