toolroll 0.9.36 → 0.9.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/assignment-adapters.d.ts +12 -48
  3. package/dist/assignment-adapters.js +33 -43
  4. package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +26 -26
  7. package/dist/browser-crew.js +5 -0
  8. package/dist/browser-workspace.d.ts +2 -0
  9. package/dist/build-review.d.ts +1 -1
  10. package/dist/build-review.js +2 -1
  11. package/dist/chat-acceptance.d.ts +1 -1
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-channel.js +20 -9
  16. package/dist/chat-decide.d.ts +18 -4
  17. package/dist/chat-decide.js +31 -11
  18. package/dist/chat-delivery-state.d.ts +12 -70
  19. package/dist/chat-delivery-state.js +17 -135
  20. package/dist/chat-delivery.js +58 -30
  21. package/dist/chat-flow.js +8 -6
  22. package/dist/chat-inbox.js +1 -1
  23. package/dist/coding-context.d.ts +2 -39
  24. package/dist/coding-context.js +2 -1
  25. package/dist/coding-handoff.d.ts +6 -25
  26. package/dist/coding-handoff.js +11 -5
  27. package/dist/coding-types.d.ts +2 -26
  28. package/dist/coding-workspace.d.ts +2 -0
  29. package/dist/coding-workspace.js +50 -14
  30. package/dist/contracts/acceptance-terms.d.ts +14 -0
  31. package/dist/contracts/acceptance-terms.js +16 -0
  32. package/dist/contracts/chat-actions.d.ts +328 -0
  33. package/dist/contracts/chat-actions.js +133 -0
  34. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  35. package/dist/contracts/chat-callback-rows.js +46 -0
  36. package/dist/contracts/chat-content.d.ts +276 -0
  37. package/dist/contracts/chat-content.js +127 -0
  38. package/dist/contracts/chat-tables.d.ts +6 -0
  39. package/dist/contracts/chat-tables.js +136 -0
  40. package/dist/contracts/coding-context.d.ts +97 -0
  41. package/dist/contracts/coding-context.js +60 -0
  42. package/dist/contracts/coding-handoff.d.ts +44 -0
  43. package/dist/contracts/coding-handoff.js +47 -0
  44. package/dist/contracts/coding-workspace.d.ts +165 -0
  45. package/dist/contracts/coding-workspace.js +71 -0
  46. package/dist/contracts/contract.d.ts +7 -2
  47. package/dist/contracts/contract.js +21 -12
  48. package/dist/contracts/decision.d.ts +45 -0
  49. package/dist/contracts/decision.js +45 -0
  50. package/dist/contracts/discord-callback.d.ts +45 -0
  51. package/dist/contracts/discord-callback.js +34 -0
  52. package/dist/contracts/flow-card.d.ts +50 -0
  53. package/dist/contracts/flow-card.js +46 -0
  54. package/dist/contracts/flow-send.d.ts +119 -0
  55. package/dist/contracts/flow-send.js +112 -0
  56. package/dist/contracts/flow.d.ts +5 -0
  57. package/dist/contracts/flow.js +5 -0
  58. package/dist/contracts/gateway-tools.d.ts +374 -0
  59. package/dist/contracts/gateway-tools.js +94 -0
  60. package/dist/contracts/lead-context.d.ts +165 -0
  61. package/dist/contracts/lead-context.js +73 -0
  62. package/dist/contracts/lead-tools.d.ts +1905 -0
  63. package/dist/contracts/lead-tools.js +265 -0
  64. package/dist/contracts/memory-pass.d.ts +158 -0
  65. package/dist/contracts/memory-pass.js +102 -0
  66. package/dist/contracts/project-knowledge.d.ts +126 -0
  67. package/dist/contracts/project-knowledge.js +93 -0
  68. package/dist/contracts/project-memory.d.ts +117 -0
  69. package/dist/contracts/project-memory.js +51 -0
  70. package/dist/contracts/project-skills.d.ts +121 -0
  71. package/dist/contracts/project-skills.js +76 -0
  72. package/dist/contracts/review-findings.d.ts +46 -0
  73. package/dist/contracts/review-findings.js +48 -0
  74. package/dist/contracts/route.d.ts +484 -0
  75. package/dist/contracts/route.js +128 -0
  76. package/dist/contracts/scope.d.ts +105 -0
  77. package/dist/contracts/scope.js +114 -0
  78. package/dist/contracts/scout-report.d.ts +234 -0
  79. package/dist/contracts/scout-report.js +141 -0
  80. package/dist/contracts/slack-callback.d.ts +43 -0
  81. package/dist/contracts/slack-callback.js +35 -0
  82. package/dist/contracts/sort-answer.d.ts +83 -0
  83. package/dist/contracts/sort-answer.js +77 -0
  84. package/dist/contracts/stage-output.d.ts +127 -0
  85. package/dist/contracts/stage-output.js +166 -0
  86. package/dist/contracts/task-sizing.d.ts +24 -0
  87. package/dist/contracts/task-sizing.js +28 -0
  88. package/dist/contracts/teammate-turn.d.ts +43 -0
  89. package/dist/contracts/teammate-turn.js +51 -0
  90. package/dist/contracts/teams-callback.d.ts +14 -0
  91. package/dist/contracts/teams-callback.js +14 -0
  92. package/dist/contracts/telegram-callback.d.ts +120 -0
  93. package/dist/contracts/telegram-callback.js +97 -0
  94. package/dist/decision.d.ts +11 -20
  95. package/dist/decision.js +86 -167
  96. package/dist/desktop-update.d.ts +1 -1
  97. package/dist/discord-chat.js +31 -20
  98. package/dist/flow-engine.js +14 -17
  99. package/dist/flow-items.d.ts +4 -13
  100. package/dist/flow-items.js +1 -15
  101. package/dist/flow-send.d.ts +6 -37
  102. package/dist/flow-send.js +18 -14
  103. package/dist/flow-share.js +7 -1
  104. package/dist/flow-sort.d.ts +7 -26
  105. package/dist/flow-sort.js +25 -20
  106. package/dist/flow-steps.js +3 -2
  107. package/dist/flows.d.ts +2 -4
  108. package/dist/flows.js +9 -5
  109. package/dist/invoke.js +4 -4
  110. package/dist/lead-context.d.ts +4 -2
  111. package/dist/lead-context.js +24 -17
  112. package/dist/mate-progress.d.ts +26 -0
  113. package/dist/mate-progress.js +24 -0
  114. package/dist/mate-tools.d.ts +4 -0
  115. package/dist/mate-tools.js +108 -275
  116. package/dist/mate.js +11 -2
  117. package/dist/mcp-connect.d.ts +24 -4
  118. package/dist/mcp-connect.js +78 -15
  119. package/dist/mcp.js +84 -216
  120. package/dist/memory-pass.d.ts +17 -42
  121. package/dist/memory-pass.js +54 -26
  122. package/dist/phase-routing.d.ts +19 -72
  123. package/dist/phase-routing.js +34 -164
  124. package/dist/project-knowledge.d.ts +16 -35
  125. package/dist/project-knowledge.js +42 -21
  126. package/dist/project-memory.d.ts +2 -28
  127. package/dist/project-memory.js +28 -13
  128. package/dist/project-skills.d.ts +16 -38
  129. package/dist/project-skills.js +41 -32
  130. package/dist/project-tools.d.ts +1 -0
  131. package/dist/project-tools.js +4 -4
  132. package/dist/provider.d.ts +2 -0
  133. package/dist/provider.js +4 -16
  134. package/dist/result-shots.d.ts +1 -1
  135. package/dist/result-shots.js +2 -1
  136. package/dist/review-switch.d.ts +3 -8
  137. package/dist/reviewer.d.ts +8 -12
  138. package/dist/reviewer.js +11 -35
  139. package/dist/scope.d.ts +21 -43
  140. package/dist/scope.js +42 -128
  141. package/dist/scout-report.d.ts +9 -194
  142. package/dist/scout-report.js +113 -264
  143. package/dist/scout.js +15 -11
  144. package/dist/serve.js +23 -5
  145. package/dist/setup-guide.d.ts +2 -2
  146. package/dist/slack-chat.js +26 -20
  147. package/dist/slack-state.js +2 -1
  148. package/dist/store.d.ts +9 -32
  149. package/dist/store.js +18 -15
  150. package/dist/structured-output.js +2 -1
  151. package/dist/subscription-chat.d.ts +4 -0
  152. package/dist/subscription-chat.js +4 -1
  153. package/dist/task-review-brief.d.ts +1 -7
  154. package/dist/task-review-brief.js +6 -6
  155. package/dist/task-sizing.d.ts +3 -27
  156. package/dist/task-sizing.js +11 -26
  157. package/dist/task-text.d.ts +0 -5
  158. package/dist/task-text.js +1 -3
  159. package/dist/teammate-question.js +7 -5
  160. package/dist/teammate-work.js +9 -5
  161. package/dist/teammates.d.ts +13 -64
  162. package/dist/teammates.js +32 -33
  163. package/dist/teams-chat.js +20 -11
  164. package/dist/telegram-flow.d.ts +9 -0
  165. package/dist/telegram-flow.js +44 -8
  166. package/dist/telegram-mate.d.ts +4 -14
  167. package/dist/telegram-mate.js +9 -8
  168. package/dist/telegram-team.d.ts +2 -19
  169. package/dist/telegram.d.ts +1 -1
  170. package/dist/telegram.js +94 -68
  171. package/dist/text-limits.d.ts +142 -1
  172. package/dist/text-limits.js +144 -1
  173. package/dist/tools-ui.d.ts +2 -1
  174. package/dist/tools-ui.js +7 -4
  175. package/package.json +17 -12
@@ -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.
@@ -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;
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)