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
@@ -0,0 +1,128 @@
1
+ /**
2
+ * A sealed route (phase-routing.ts): which exact agent plans, builds, repairs and reviews a task, as a scope's
3
+ * `proposed_route_json` / `approved_route_json` and a standing order's route keep it. One schema covering both versions
4
+ * a route has been saved in — version 1 (sealed before sizing: no `size`, routine and strong tiers) and version 2 —
5
+ * plus the override list and the task size it carries. The rules JSON Schema cannot state run in plain code after
6
+ * parsing, each with a path-named error: the legs run plan, build, repair, review in that order; the posture is what
7
+ * the legs actually run; one override per phase.
8
+ *
9
+ * Reading never rewrites: a version 1 route stays version 1 (its digest domain and bytes are its own), and reads with
10
+ * no size. phase-routing.ts `canonicalRouteJson` / `routeDigestOf` encode the parsed value, so a sealed route's digest
11
+ * is what it always was.
12
+ */
13
+ import { z } from "zod";
14
+ import { EVIDENCE_KINDS } from "./acceptance-terms.js";
15
+ import { limited, parseContract, versioned } from "./contract.js";
16
+ /** The phases a route has a leg for, in the order its legs run. */
17
+ export const ROUTE_PHASES = ["plan", "build", "repair", "review"];
18
+ /** provider.ts's provider ids, restated (provider.ts sits above phase-routing.ts in the module graph) and pinned to
19
+ * them by the test suite. */
20
+ export const ROUTE_PROVIDERS = ["claude", "codex", "openrouter", "gemini"];
21
+ /** provider.ts's MODEL_ID, restated for the same reason: an exact model id is argv-safe — no leading dash, no
22
+ * whitespace or control bytes. */
23
+ export const MODEL_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
24
+ export const RISKS = ["routine", "elevated", "high"];
25
+ export const TASK_SIZE_WORDS = ["small", "medium", "large"];
26
+ /** The route version this code seals; version 1 (before sizing) still reads. */
27
+ export const ROUTE_VERSION = 2;
28
+ const phase = z.enum(ROUTE_PHASES);
29
+ const provider = z.enum(ROUTE_PROVIDERS);
30
+ const model = z.string().regex(MODEL_ID_SHAPE, { error: "must be an exact model id: letters, digits and . _ : / -, not starting with a dash" });
31
+ const text = z.string().min(1);
32
+ /** How big a change the task is, as it was sized (by the classifier, the description, or a person). */
33
+ export const taskSizingSchema = z.strictObject({
34
+ size: z.enum(TASK_SIZE_WORDS),
35
+ risky: z.boolean(),
36
+ source: z.enum(["classifier", "heuristic", "person"]),
37
+ reason: limited("size reason", "sizeReason"),
38
+ });
39
+ /** An approver's explicit per-phase choice, recorded with attribution. */
40
+ export const routeOverrideSchema = z.strictObject({ phase, provider, model, by: text, at: text });
41
+ function legSchema(tiers) {
42
+ const tier = z.enum(tiers);
43
+ return z.strictObject({
44
+ phase,
45
+ provider,
46
+ model,
47
+ tier,
48
+ chosen: z.enum(["recommended", "override", "pinned"]),
49
+ recommended: z.strictObject({ provider, model, tier }),
50
+ reasons: z.array(z.string()),
51
+ problem: text.nullable(),
52
+ });
53
+ }
54
+ /** One leg of a version 2 route: the light tier exists. */
55
+ export const routeLegSchema = legSchema(["light", "routine", "strong"]);
56
+ const terms = {
57
+ risk: z.enum(RISKS),
58
+ qualityMode: z.enum(["default", "strict"]),
59
+ publication: z.enum(["none", "notify", "automerge"]),
60
+ evidence: z.array(z.enum(EVIDENCE_KINDS)),
61
+ posture: z.enum(["economy", "strong"]),
62
+ demands: z.array(z.string()),
63
+ };
64
+ export const sealedRouteV1Schema = versioned(1, {
65
+ ...terms,
66
+ legs: z.array(legSchema(["routine", "strong"])).length(ROUTE_PHASES.length),
67
+ overrides: z.array(routeOverrideSchema),
68
+ });
69
+ export const sealedRouteV2Schema = versioned(ROUTE_VERSION, {
70
+ ...terms,
71
+ legs: z.array(routeLegSchema).length(ROUTE_PHASES.length),
72
+ overrides: z.array(routeOverrideSchema),
73
+ size: taskSizingSchema.nullable(),
74
+ });
75
+ /** A sealed route as saved, in either version. */
76
+ export const sealedRouteSchema = z.discriminatedUnion("version", [sealedRouteV1Schema, sealedRouteV2Schema]);
77
+ /** The posture legs actually run under: strong only when a leg runs a configured strong agent as recommended. */
78
+ export function postureOf(legs) {
79
+ return legs.some(leg => leg.chosen === "recommended" && leg.tier === "strong") ? "strong" : "economy";
80
+ }
81
+ const issue = (path, what) => ({ path, kind: "bad-value", line: `${path}: ${what}` });
82
+ /** Overrides as read: one per phase (a second is named by path), in phase order. */
83
+ function overridesOf(overrides, at) {
84
+ const issues = overrides.flatMap((one, index) => overrides.findIndex(other => other.phase === one.phase) === index ? [] : [issue(`${at}[${index}].phase`, `${JSON.stringify(one.phase)} appears twice`)]);
85
+ if (issues.length > 0)
86
+ return { ok: false, issues };
87
+ return { ok: true, value: [...overrides].sort((a, b) => ROUTE_PHASES.indexOf(a.phase) - ROUTE_PHASES.indexOf(b.phase)) };
88
+ }
89
+ /** Read a task's recorded override list (a list on its own, as `route_overrides_json` keeps it). */
90
+ export function readRouteOverrides(input) {
91
+ const parsed = parseContract(z.object({ overrides: z.array(routeOverrideSchema) }), { overrides: input });
92
+ return parsed.ok ? overridesOf(parsed.value.overrides, "overrides") : parsed;
93
+ }
94
+ /** Read a sealed route: either version as itself, every leg, override and the size proved, nothing unknown. */
95
+ export function readSealedRoute(input) {
96
+ const version = typeof input === "object" && input !== null ? input["version"] : undefined;
97
+ if (typeof version === "number" && Number.isInteger(version) && version > ROUTE_VERSION) {
98
+ return { ok: false, issues: [{ path: "version", kind: "newer-version", line: `version: made by a newer Toolroll (version ${version}; this one reads up to ${ROUTE_VERSION})` }] };
99
+ }
100
+ const parsed = parseContract(sealedRouteSchema, input);
101
+ if (!parsed.ok)
102
+ return parsed;
103
+ const route = parsed.value;
104
+ const issues = route.legs.flatMap((leg, index) => (leg.phase === ROUTE_PHASES[index] ? [] : [issue(`legs[${index}].phase`, `must be ${JSON.stringify(ROUTE_PHASES[index])} (legs run plan, build, repair, review)`)]));
105
+ const posture = postureOf(route.legs);
106
+ if (posture !== route.posture)
107
+ issues.push(issue("posture", `must be ${JSON.stringify(posture)}: that is what its legs run`));
108
+ const overrides = overridesOf(route.overrides, "overrides");
109
+ if (!overrides.ok)
110
+ issues.push(...overrides.issues);
111
+ if (issues.length > 0 || !overrides.ok)
112
+ return { ok: false, issues };
113
+ return {
114
+ ok: true,
115
+ value: {
116
+ version: route.version,
117
+ risk: route.risk,
118
+ qualityMode: route.qualityMode,
119
+ publication: route.publication,
120
+ evidence: [...route.evidence].sort(),
121
+ posture: route.posture,
122
+ demands: route.demands,
123
+ legs: route.legs,
124
+ overrides: overrides.value,
125
+ size: route.version === 1 ? null : route.size,
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * A task's scope: its goal, what is out of it, the paths it expects to touch and the acceptance rubric an operator
3
+ * signs. One schema for the terms and one for a criterion, both made from the plan contract's own fields
4
+ * (src/contracts/plan.ts) rather than restated; a scope may name 50 paths where a plan names 32. The rules JSON Schema
5
+ * cannot state — UTF-8 byte limits, control characters, duplicate criterion ids and evidence kinds — run in plain code
6
+ * after parsing, each with a path-named error.
7
+ *
8
+ * Reading keeps what every release accepted: a criterion's unknown keys are ignored, a null or empty `how` reads as
9
+ * null, and an absent or empty rubric reads as none (whether that is allowed is the caller's question). The digest
10
+ * (scope.ts `digestOf`) reads the parsed values, so a saved scope's digest is what it always was.
11
+ *
12
+ * The schemas are built lazily: plan.ts reads the evidence kinds through scope.ts, which reads this module, so in
13
+ * either load order plan.ts may still be loading when this module is.
14
+ */
15
+ import { z } from "zod";
16
+ import { type ContractProblem } from "./contract.js";
17
+ /** A criterion as saved and as every reader returns it: the plan's criterion with `how` always present (null for none). */
18
+ export declare const savedCriterionSchema: z.ZodLazy<z.ZodObject<{
19
+ id: z.ZodString;
20
+ statement: z.ZodString;
21
+ evidence: z.ZodArray<z.ZodEnum<{
22
+ "changed-path": "changed-path";
23
+ check: "check";
24
+ "manual-review": "manual-review";
25
+ screenshot: "screenshot";
26
+ }>>;
27
+ how: z.ZodNullable<z.ZodString>;
28
+ }, z.core.$strict>>;
29
+ export type AcceptanceCriterion = z.infer<typeof savedCriterionSchema>;
30
+ /** A rubric as saved: at most twelve criteria (an empty one is a scope filed before rubrics, or by a road with none). */
31
+ export declare const rubricSchema: z.ZodLazy<z.ZodArray<z.ZodLazy<z.ZodObject<{
32
+ id: z.ZodString;
33
+ statement: z.ZodString;
34
+ evidence: z.ZodArray<z.ZodEnum<{
35
+ "changed-path": "changed-path";
36
+ check: "check";
37
+ "manual-review": "manual-review";
38
+ screenshot: "screenshot";
39
+ }>>;
40
+ how: z.ZodNullable<z.ZodString>;
41
+ }, z.core.$strict>>>>;
42
+ /** A rubric as a row stores it: exactly the keys this code writes (`how` may be absent on an older row). */
43
+ export declare const storedRubricSchema: z.ZodLazy<z.ZodArray<z.ZodObject<{
44
+ id: z.ZodString;
45
+ statement: z.ZodString;
46
+ how: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ evidence: z.ZodArray<z.ZodEnum<{
48
+ "changed-path": "changed-path";
49
+ check: "check";
50
+ "manual-review": "manual-review";
51
+ screenshot: "screenshot";
52
+ }>>;
53
+ }, z.core.$strict>>>;
54
+ /** What a model drafts as a rubric (the lead's proposals): the plan's criteria, one to twelve. */
55
+ export declare const rubricInputSchema: z.ZodLazy<z.ZodArray<z.ZodObject<{
56
+ id: z.ZodString;
57
+ statement: z.ZodString;
58
+ how: z.ZodOptional<z.ZodNullable<z.ZodString>>;
59
+ evidence: z.ZodArray<z.ZodEnum<{
60
+ "changed-path": "changed-path";
61
+ check: "check";
62
+ "manual-review": "manual-review";
63
+ screenshot: "screenshot";
64
+ }>>;
65
+ }, z.core.$strict>>>;
66
+ /** A scope's terms as filed: the plan's goal, out-of-scope text and touch paths (up to 50), and its rubric. */
67
+ export declare const scopeTermsSchema: z.ZodLazy<z.ZodObject<{
68
+ goal: z.ZodString;
69
+ outOfScope: z.ZodOptional<z.ZodNullable<z.ZodString>>;
70
+ touches: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ acceptance: z.ZodOptional<z.ZodLazy<z.ZodArray<z.ZodLazy<z.ZodObject<{
72
+ id: z.ZodString;
73
+ statement: z.ZodString;
74
+ evidence: z.ZodArray<z.ZodEnum<{
75
+ "changed-path": "changed-path";
76
+ check: "check";
77
+ "manual-review": "manual-review";
78
+ screenshot: "screenshot";
79
+ }>>;
80
+ how: z.ZodNullable<z.ZodString>;
81
+ }, z.core.$strict>>>>>;
82
+ }, z.core.$strict>>;
83
+ export type ScopeTerms = z.infer<typeof scopeTermsSchema>;
84
+ export type AcceptanceProblem = ContractProblem;
85
+ /**
86
+ * Read a rubric from already-JSON-parsed input (a console form, the CLI, a routine or template, a model's proposal,
87
+ * a saved row): every problem at once, each naming its path. Absent or empty is `[]` with no problems. A criterion
88
+ * that reads cleanly is kept even when another does not, as before; any problem means the rubric is refused.
89
+ */
90
+ export declare function readAcceptance(value: unknown): {
91
+ criteria: AcceptanceCriterion[];
92
+ problems: AcceptanceProblem[];
93
+ };
94
+ export type ScopeTermsResult = {
95
+ ok: true;
96
+ terms: Omit<ScopeTerms, "acceptance"> & {
97
+ acceptance: AcceptanceCriterion[];
98
+ };
99
+ } | {
100
+ ok: false;
101
+ field: "goal" | "outOfScope" | "touches" | "acceptance" | "payload";
102
+ problems: AcceptanceProblem[];
103
+ };
104
+ /** Read a scope's terms as a person or a model files them; the first field refused names the refusal. */
105
+ export declare function readScopeTerms(input: unknown): ScopeTermsResult;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A task's scope: its goal, what is out of it, the paths it expects to touch and the acceptance rubric an operator
3
+ * signs. One schema for the terms and one for a criterion, both made from the plan contract's own fields
4
+ * (src/contracts/plan.ts) rather than restated; a scope may name 50 paths where a plan names 32. The rules JSON Schema
5
+ * cannot state — UTF-8 byte limits, control characters, duplicate criterion ids and evidence kinds — run in plain code
6
+ * after parsing, each with a path-named error.
7
+ *
8
+ * Reading keeps what every release accepted: a criterion's unknown keys are ignored, a null or empty `how` reads as
9
+ * null, and an absent or empty rubric reads as none (whether that is allowed is the caller's question). The digest
10
+ * (scope.ts `digestOf`) reads the parsed values, so a saved scope's digest is what it always was.
11
+ *
12
+ * The schemas are built lazily: plan.ts reads the evidence kinds through scope.ts, which reads this module, so in
13
+ * either load order plan.ts may still be loading when this module is.
14
+ */
15
+ import { z } from "zod";
16
+ import { hasForbiddenControls } from "../decision.js";
17
+ import { TEXT_LIMITS } from "../text-limits.js";
18
+ import { ACCEPTANCE_LIMITS } from "./acceptance-terms.js";
19
+ import { contractProblemOf, parseContract } from "./contract.js";
20
+ import { acceptanceCriterionSchema as planCriterionSchema, planSchema } from "./plan.js";
21
+ /** A criterion as saved and as every reader returns it: the plan's criterion with `how` always present (null for none). */
22
+ export const savedCriterionSchema = z.lazy(() => planCriterionSchema.extend({ how: planCriterionSchema.shape.how.unwrap() }));
23
+ /** A rubric as saved: at most twelve criteria (an empty one is a scope filed before rubrics, or by a road with none). */
24
+ export const rubricSchema = z.lazy(() => z.array(savedCriterionSchema).max(ACCEPTANCE_LIMITS.criteria));
25
+ /** A rubric as a row stores it: exactly the keys this code writes (`how` may be absent on an older row). */
26
+ export const storedRubricSchema = z.lazy(() => z.array(planCriterionSchema).max(ACCEPTANCE_LIMITS.criteria));
27
+ /** What a model drafts as a rubric (the lead's proposals): the plan's criteria, one to twelve. */
28
+ export const rubricInputSchema = z.lazy(() => z.array(planCriterionSchema).min(1).max(ACCEPTANCE_LIMITS.criteria));
29
+ /** A scope's terms as filed: the plan's goal, out-of-scope text and touch paths (up to 50), and its rubric. */
30
+ export const scopeTermsSchema = z.lazy(() => z.strictObject({
31
+ goal: planSchema.shape.goal,
32
+ outOfScope: planSchema.shape.outOfScope,
33
+ touches: z.array(planSchema.shape.touches.unwrap().element).max(TEXT_LIMITS.scopeTouches).optional(),
34
+ acceptance: rubricSchema.optional(),
35
+ }));
36
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
37
+ /** A criterion as the reader takes it: only the fields it knows, a null or empty `how` as null. Anything else as is. */
38
+ function criterionBody(entry) {
39
+ if (!isRecord(entry))
40
+ return entry;
41
+ const out = {};
42
+ for (const field of ["id", "statement", "evidence"])
43
+ if (entry[field] !== undefined)
44
+ out[field] = entry[field];
45
+ out["how"] = entry["how"] === undefined || entry["how"] === "" ? null : entry["how"];
46
+ return out;
47
+ }
48
+ /** The plain-code rules for one criterion JSON Schema cannot state: UTF-8 bytes and control characters. */
49
+ function criterionTextProblems(criterion, at) {
50
+ const out = [];
51
+ for (const [field, limit] of [["id", ACCEPTANCE_LIMITS.id], ["statement", ACCEPTANCE_LIMITS.statement], ["how", ACCEPTANCE_LIMITS.how]]) {
52
+ const value = criterion[field];
53
+ const path = `${at}.${field}`;
54
+ if (value === null)
55
+ continue;
56
+ if (Buffer.byteLength(value, "utf8") > limit)
57
+ out.push({ reason: `${path}-too-long`, message: `${path}: over ${limit.toLocaleString("en-US")} bytes` });
58
+ else if (hasForbiddenControls(value))
59
+ out.push({ reason: `${path}-controls`, message: `${path}: carries control characters that could become terminal escapes` });
60
+ }
61
+ return out;
62
+ }
63
+ const named = (problems, path) => problems.some(one => one.message.startsWith(`${path}:`));
64
+ /**
65
+ * Read a rubric from already-JSON-parsed input (a console form, the CLI, a routine or template, a model's proposal,
66
+ * a saved row): every problem at once, each naming its path. Absent or empty is `[]` with no problems. A criterion
67
+ * that reads cleanly is kept even when another does not, as before; any problem means the rubric is refused.
68
+ */
69
+ export function readAcceptance(value) {
70
+ if (value === undefined || value === null)
71
+ return { criteria: [], problems: [] };
72
+ const body = Array.isArray(value) ? value.map(criterionBody) : value;
73
+ const parsed = parseContract(z.object({ acceptance: rubricSchema }), { acceptance: body });
74
+ const issues = parsed.ok ? [] : parsed.issues;
75
+ // The list itself is wrong (not a list, too long): nothing in it is read.
76
+ if (!Array.isArray(body) || issues.some(one => one.path === "acceptance"))
77
+ return { criteria: [], problems: issues.map(contractProblemOf) };
78
+ const criteria = [];
79
+ const seen = new Set();
80
+ const problems = [];
81
+ body.forEach((entry, index) => {
82
+ const at = `acceptance[${index}]`;
83
+ const own = issues.filter(one => one.path === at || one.path.startsWith(`${at}.`) || one.path.startsWith(`${at}[`)).map(contractProblemOf);
84
+ const read = own.length === 0 ? savedCriterionSchema.safeParse(entry) : null;
85
+ const text = read?.success === true ? criterionTextProblems(read.data, at) : [];
86
+ const kinds = read?.success === true ? read.data.evidence : [];
87
+ const twice = kinds.flatMap((kind, k) => (kinds.indexOf(kind) === k ? [] : [{ reason: `${at}-duplicate-evidence-kind`, message: `${at}.evidence[${k}]: "${kind}" appears twice` }]));
88
+ problems.push(...own, ...text, ...twice);
89
+ // A duplicate id is a problem with the rubric, not with either criterion: both stay readable, as before.
90
+ const id = isRecord(entry) ? entry["id"] : undefined;
91
+ const idClean = typeof id === "string" && !named(own, `${at}.id`) && !named(text, `${at}.id`);
92
+ if (idClean && seen.has(id))
93
+ problems.push({ reason: `${at}-duplicate-id`, message: `${at}.id: "${id}" appears twice` });
94
+ if (idClean)
95
+ seen.add(id);
96
+ if (read?.success === true && text.length === 0 && twice.length === 0)
97
+ criteria.push(read.data);
98
+ });
99
+ return { criteria, problems };
100
+ }
101
+ /** Read a scope's terms as a person or a model files them; the first field refused names the refusal. */
102
+ export function readScopeTerms(input) {
103
+ if (!isRecord(input))
104
+ return { ok: false, field: "payload", problems: [{ reason: "bad-payload", message: "payload: must be an object" }] };
105
+ const acceptance = readAcceptance(input["acceptance"]);
106
+ const parsed = parseContract(scopeTermsSchema, { ...input, acceptance: acceptance.criteria });
107
+ if (!parsed.ok) {
108
+ const first = parsed.issues[0].path.replace(/[[.].*$/, "");
109
+ return { ok: false, field: first, problems: parsed.issues.filter(one => one.path.replace(/[[.].*$/, "") === first).map(contractProblemOf) };
110
+ }
111
+ if (acceptance.problems.length > 0)
112
+ return { ok: false, field: "acceptance", problems: acceptance.problems };
113
+ return { ok: true, terms: { ...parsed.value, acceptance: acceptance.criteria } };
114
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * The scout's report: one schema for the report a scout writes (its report file, or Claude's structured output), the
3
+ * JSON Schema a Claude scout is given (`--json-schema`, `SCOUT_OUTPUT_JSON_SCHEMA`) and the reader that takes it back
4
+ * (`parseReport` in scout-report.ts), which also reads every report already kept as evidence. The rules JSON Schema
5
+ * cannot state — UTF-8 byte limits, blank text, control characters, one-line titles, a one-paragraph summary, safe
6
+ * links, duplicate screenshots and items naming them — run in plain code after parsing, each with a path-named error.
7
+ */
8
+ import { z } from "zod";
9
+ import { type ContractResult } from "./contract.js";
10
+ /** How many follow-ups, items and screenshots a report may carry. */
11
+ export declare const REPORT_COUNTS: {
12
+ readonly followUps: 5;
13
+ readonly items: 6;
14
+ readonly images: 8;
15
+ };
16
+ /** Caps are BYTES of UTF-8 (v4 review, finding 11): a 64 KiB report is 64 KiB whatever script it is written in. They
17
+ * hold what a scout writes; a stored report is read without them (`storedReportSchema`). */
18
+ export declare const REPORT_LIMITS: {
19
+ readonly payload: number;
20
+ readonly title: 200;
21
+ readonly summary: 2500;
22
+ readonly document: number;
23
+ readonly followUps: 5;
24
+ readonly followUpTitle: 200;
25
+ /** A follow-up files as a task: its goal is held to the task goal limit, in bytes of the same count. */
26
+ readonly followUpGoal: 8000;
27
+ readonly items: 6;
28
+ readonly itemTitle: 200;
29
+ readonly itemWhy: 1000;
30
+ readonly url: 2000;
31
+ readonly images: 8;
32
+ readonly caption: 300;
33
+ };
34
+ /** A screenshot's name in the scout's output folder: one plain file name, PNG or JPEG, never a path. */
35
+ export declare const REPORT_IMAGE_FILE: RegExp;
36
+ /** Each text field, the limit it is written within, and whether it is one line or a web address. */
37
+ export declare const REPORT_TEXT_FIELDS: {
38
+ readonly title: {
39
+ readonly limit: "reportTitleBytes";
40
+ readonly oneLine: true;
41
+ };
42
+ readonly summary: {
43
+ readonly limit: "reportSummary";
44
+ };
45
+ readonly report: {
46
+ readonly limit: "reportDocumentBytes";
47
+ };
48
+ };
49
+ export declare const FOLLOW_UP_TEXT_FIELDS: {
50
+ readonly title: {
51
+ readonly limit: "reportTitleBytes";
52
+ readonly oneLine: true;
53
+ };
54
+ readonly goal: {
55
+ readonly limit: "goal";
56
+ };
57
+ };
58
+ export declare const ITEM_TEXT_FIELDS: {
59
+ readonly title: {
60
+ readonly limit: "reportTitleBytes";
61
+ readonly oneLine: true;
62
+ };
63
+ readonly why: {
64
+ readonly limit: "reportWhyBytes";
65
+ };
66
+ readonly url: {
67
+ readonly limit: "reportUrlBytes";
68
+ readonly link: true;
69
+ };
70
+ };
71
+ export declare const IMAGE_TEXT_FIELDS: {
72
+ readonly file: {
73
+ readonly limit: "reportCaptionBytes";
74
+ readonly oneLine: true;
75
+ };
76
+ readonly caption: {
77
+ readonly limit: "reportCaptionBytes";
78
+ readonly oneLine: true;
79
+ };
80
+ readonly url: {
81
+ readonly limit: "reportUrlBytes";
82
+ readonly link: true;
83
+ };
84
+ };
85
+ export declare const REPORT_VERSION = 1;
86
+ /** A report as a scout writes it now. */
87
+ export declare const scoutReportSchema: z.ZodObject<{
88
+ version: z.ZodLiteral<1>;
89
+ title: z.ZodString;
90
+ summary: z.ZodString;
91
+ report: z.ZodString;
92
+ followUps: z.ZodOptional<z.ZodArray<z.ZodObject<{
93
+ title: z.ZodString;
94
+ goal: z.ZodString;
95
+ }, z.core.$strict>>>;
96
+ items: z.ZodOptional<z.ZodArray<z.ZodObject<{
97
+ title: z.ZodString;
98
+ why: z.ZodString;
99
+ url: z.ZodString;
100
+ image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
101
+ }, z.core.$strict>>>;
102
+ images: z.ZodOptional<z.ZodArray<z.ZodObject<{
103
+ file: z.ZodString;
104
+ caption: z.ZodString;
105
+ url: z.ZodString;
106
+ sha256: z.ZodOptional<z.ZodString>;
107
+ artifact: z.ZodOptional<z.ZodInt>;
108
+ }, z.core.$strict>>>;
109
+ }, z.core.$strict>;
110
+ /** A report kept as evidence: the same, except text keeps the length it was stored with. */
111
+ export declare const storedReportSchema: z.ZodObject<{
112
+ version: z.ZodLiteral<1>;
113
+ title: z.ZodString;
114
+ summary: z.ZodString;
115
+ report: z.ZodString;
116
+ followUps: z.ZodOptional<z.ZodArray<z.ZodObject<{
117
+ title: z.ZodString;
118
+ goal: z.ZodString;
119
+ }, z.core.$strict>>>;
120
+ items: z.ZodOptional<z.ZodArray<z.ZodObject<{
121
+ title: z.ZodString;
122
+ why: z.ZodString;
123
+ url: z.ZodString;
124
+ image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
125
+ }, z.core.$strict>>>;
126
+ images: z.ZodOptional<z.ZodArray<z.ZodObject<{
127
+ file: z.ZodString;
128
+ caption: z.ZodString;
129
+ url: z.ZodString;
130
+ sha256: z.ZodOptional<z.ZodString>;
131
+ artifact: z.ZodOptional<z.ZodInt>;
132
+ }, z.core.$strict>>>;
133
+ }, z.core.$strict>;
134
+ export type ScoutReportPayload = z.infer<typeof scoutReportSchema>;
135
+ type FollowUp = NonNullable<ScoutReportPayload["followUps"]>[number];
136
+ type Item = NonNullable<ScoutReportPayload["items"]>[number];
137
+ type Image = NonNullable<ScoutReportPayload["images"]>[number];
138
+ /** One finding later steps read: it cites its URL, and may show one of the report's images (null when none). */
139
+ export type ReportItem = Omit<Item, "image"> & {
140
+ image: string | null;
141
+ };
142
+ /** A screenshot the scout saved. From the scout, `file` names a file in its output folder; once stored, the runner
143
+ * adds the bytes' sha256 and the evidence row that holds them. */
144
+ export type ReportImage = Image;
145
+ /** A report as read: the payload without its envelope, every list present (empty when the scout gave none). */
146
+ export type ParsedReport = {
147
+ title: string;
148
+ summary: string;
149
+ /** The report document, markdown, rendered fenced-inert everywhere. */
150
+ report: string;
151
+ /** Proposed follow-ups: each files as a task in the same repository. */
152
+ followUps: FollowUp[];
153
+ items: ReportItem[];
154
+ images: ReportImage[];
155
+ };
156
+ /** The park mailbox's decision as a scout hands it back: the same fields `parseDecision` reads, which stays its
157
+ * validator until the decision contract (docs/plans/zod-revamp.md, item 7) gives it one schema. */
158
+ export declare const scoutDecisionSchema: z.ZodObject<{
159
+ urgency: z.ZodLiteral<"blocking">;
160
+ recap: z.ZodString;
161
+ question: z.ZodString;
162
+ options: z.ZodArray<z.ZodObject<{
163
+ id: z.ZodString;
164
+ label: z.ZodString;
165
+ consequence: z.ZodString;
166
+ reversible: z.ZodBoolean;
167
+ }, z.core.$strict>>;
168
+ recommendation: z.ZodString;
169
+ }, z.core.$strict>;
170
+ /**
171
+ * The scout's handback for Claude's `--json-schema` (run 2334's fix): plan mode only lets a session write its own plan
172
+ * file, so a Claude scout returns a report — or a question for the operator — as structured output on the terminal
173
+ * result event. The report branch is the report schema itself; the byte limits and rules JSON Schema cannot state are
174
+ * `parseReport`'s (and `parseDecision`'s), applied to the branch as handed back.
175
+ */
176
+ export declare const scoutHandbackSchema: z.ZodObject<{
177
+ kind: z.ZodEnum<{
178
+ question: "question";
179
+ report: "report";
180
+ }>;
181
+ report: z.ZodOptional<z.ZodObject<{
182
+ version: z.ZodLiteral<1>;
183
+ title: z.ZodString;
184
+ summary: z.ZodString;
185
+ report: z.ZodString;
186
+ followUps: z.ZodOptional<z.ZodArray<z.ZodObject<{
187
+ title: z.ZodString;
188
+ goal: z.ZodString;
189
+ }, z.core.$strict>>>;
190
+ items: z.ZodOptional<z.ZodArray<z.ZodObject<{
191
+ title: z.ZodString;
192
+ why: z.ZodString;
193
+ url: z.ZodString;
194
+ image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
195
+ }, z.core.$strict>>>;
196
+ images: z.ZodOptional<z.ZodArray<z.ZodObject<{
197
+ file: z.ZodString;
198
+ caption: z.ZodString;
199
+ url: z.ZodString;
200
+ sha256: z.ZodOptional<z.ZodString>;
201
+ artifact: z.ZodOptional<z.ZodInt>;
202
+ }, z.core.$strict>>>;
203
+ }, z.core.$strict>>;
204
+ decision: z.ZodOptional<z.ZodObject<{
205
+ urgency: z.ZodLiteral<"blocking">;
206
+ recap: z.ZodString;
207
+ question: z.ZodString;
208
+ options: z.ZodArray<z.ZodObject<{
209
+ id: z.ZodString;
210
+ label: z.ZodString;
211
+ consequence: z.ZodString;
212
+ reversible: z.ZodBoolean;
213
+ }, z.core.$strict>>;
214
+ recommendation: z.ZodString;
215
+ }, z.core.$strict>>;
216
+ }, z.core.$strict>;
217
+ export declare const SCOUT_OUTPUT_JSON_SCHEMA: Readonly<Record<string, unknown>>;
218
+ /**
219
+ * Known reports keep the old parser's field-picking, whether unversioned (including `version: null`) or version 1:
220
+ * unknown fields in the report, follow-ups, items and images are ignored, and a null list means none. Known field
221
+ * values are never rewritten, and anything that is not an object is left for the schema to refuse by path.
222
+ */
223
+ declare function normalizeReportV1(body: Record<string, unknown>): Record<string, unknown>;
224
+ export declare const REPORT_UPGRADES: {
225
+ readonly 0: typeof normalizeReportV1;
226
+ };
227
+ /** Normalize current and unversioned reports; leave other versions for readVersioned to check. */
228
+ export declare function reportPayloadBody(input: unknown): Record<string, unknown> | null;
229
+ /** Read current and unversioned reports with the same field-picking, then the strict schema; refuse newer versions
230
+ * plainly. `stored`: one kept as evidence, read without the length limits. */
231
+ export declare function readReportPayload(input: unknown, options?: {
232
+ stored?: boolean;
233
+ }): ContractResult<ScoutReportPayload>;
234
+ export {};