toolroll 0.9.36 → 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 (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/assignment-adapters.d.ts +12 -48
  3. package/dist/assignment-adapters.js +33 -43
  4. package/dist/build-review.d.ts +1 -1
  5. package/dist/build-review.js +2 -1
  6. package/dist/chat-acceptance.d.ts +1 -1
  7. package/dist/chat-actions.d.ts +18 -12
  8. package/dist/chat-actions.js +15 -65
  9. package/dist/chat-ask.js +5 -5
  10. package/dist/chat-decide.d.ts +14 -1
  11. package/dist/chat-decide.js +12 -6
  12. package/dist/chat-delivery-state.d.ts +12 -70
  13. package/dist/chat-delivery-state.js +17 -135
  14. package/dist/chat-delivery.js +58 -30
  15. package/dist/chat-flow.js +8 -6
  16. package/dist/chat-inbox.js +1 -1
  17. package/dist/coding-context.d.ts +2 -39
  18. package/dist/coding-context.js +2 -1
  19. package/dist/coding-handoff.d.ts +6 -25
  20. package/dist/coding-handoff.js +11 -5
  21. package/dist/coding-types.d.ts +2 -26
  22. package/dist/coding-workspace.d.ts +2 -0
  23. package/dist/coding-workspace.js +50 -14
  24. package/dist/contracts/acceptance-terms.d.ts +14 -0
  25. package/dist/contracts/acceptance-terms.js +16 -0
  26. package/dist/contracts/chat-actions.d.ts +328 -0
  27. package/dist/contracts/chat-actions.js +133 -0
  28. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  29. package/dist/contracts/chat-callback-rows.js +46 -0
  30. package/dist/contracts/chat-content.d.ts +276 -0
  31. package/dist/contracts/chat-content.js +127 -0
  32. package/dist/contracts/chat-tables.d.ts +6 -0
  33. package/dist/contracts/chat-tables.js +136 -0
  34. package/dist/contracts/coding-context.d.ts +97 -0
  35. package/dist/contracts/coding-context.js +60 -0
  36. package/dist/contracts/coding-handoff.d.ts +44 -0
  37. package/dist/contracts/coding-handoff.js +47 -0
  38. package/dist/contracts/coding-workspace.d.ts +165 -0
  39. package/dist/contracts/coding-workspace.js +71 -0
  40. package/dist/contracts/contract.d.ts +7 -2
  41. package/dist/contracts/contract.js +21 -12
  42. package/dist/contracts/decision.d.ts +45 -0
  43. package/dist/contracts/decision.js +45 -0
  44. package/dist/contracts/discord-callback.d.ts +45 -0
  45. package/dist/contracts/discord-callback.js +34 -0
  46. package/dist/contracts/flow-card.d.ts +50 -0
  47. package/dist/contracts/flow-card.js +46 -0
  48. package/dist/contracts/flow-send.d.ts +119 -0
  49. package/dist/contracts/flow-send.js +112 -0
  50. package/dist/contracts/flow.d.ts +5 -0
  51. package/dist/contracts/flow.js +5 -0
  52. package/dist/contracts/gateway-tools.d.ts +374 -0
  53. package/dist/contracts/gateway-tools.js +94 -0
  54. package/dist/contracts/lead-context.d.ts +165 -0
  55. package/dist/contracts/lead-context.js +73 -0
  56. package/dist/contracts/lead-tools.d.ts +1905 -0
  57. package/dist/contracts/lead-tools.js +265 -0
  58. package/dist/contracts/memory-pass.d.ts +158 -0
  59. package/dist/contracts/memory-pass.js +102 -0
  60. package/dist/contracts/project-knowledge.d.ts +126 -0
  61. package/dist/contracts/project-knowledge.js +93 -0
  62. package/dist/contracts/project-memory.d.ts +117 -0
  63. package/dist/contracts/project-memory.js +51 -0
  64. package/dist/contracts/project-skills.d.ts +121 -0
  65. package/dist/contracts/project-skills.js +76 -0
  66. package/dist/contracts/review-findings.d.ts +46 -0
  67. package/dist/contracts/review-findings.js +48 -0
  68. package/dist/contracts/route.d.ts +484 -0
  69. package/dist/contracts/route.js +128 -0
  70. package/dist/contracts/scope.d.ts +105 -0
  71. package/dist/contracts/scope.js +114 -0
  72. package/dist/contracts/scout-report.d.ts +234 -0
  73. package/dist/contracts/scout-report.js +141 -0
  74. package/dist/contracts/slack-callback.d.ts +43 -0
  75. package/dist/contracts/slack-callback.js +35 -0
  76. package/dist/contracts/sort-answer.d.ts +83 -0
  77. package/dist/contracts/sort-answer.js +77 -0
  78. package/dist/contracts/stage-output.d.ts +127 -0
  79. package/dist/contracts/stage-output.js +166 -0
  80. package/dist/contracts/task-sizing.d.ts +24 -0
  81. package/dist/contracts/task-sizing.js +28 -0
  82. package/dist/contracts/teammate-turn.d.ts +43 -0
  83. package/dist/contracts/teammate-turn.js +51 -0
  84. package/dist/contracts/teams-callback.d.ts +14 -0
  85. package/dist/contracts/teams-callback.js +14 -0
  86. package/dist/contracts/telegram-callback.d.ts +120 -0
  87. package/dist/contracts/telegram-callback.js +97 -0
  88. package/dist/decision.d.ts +11 -20
  89. package/dist/decision.js +86 -167
  90. package/dist/desktop-update.d.ts +2 -2
  91. package/dist/discord-chat.js +31 -20
  92. package/dist/flow-engine.js +14 -17
  93. package/dist/flow-items.d.ts +4 -13
  94. package/dist/flow-items.js +1 -15
  95. package/dist/flow-send.d.ts +6 -37
  96. package/dist/flow-send.js +18 -14
  97. package/dist/flow-share.js +7 -1
  98. package/dist/flow-sort.d.ts +7 -26
  99. package/dist/flow-sort.js +25 -20
  100. package/dist/flow-steps.js +3 -2
  101. package/dist/flows.d.ts +2 -4
  102. package/dist/flows.js +9 -5
  103. package/dist/invoke.js +4 -4
  104. package/dist/lead-context.d.ts +4 -2
  105. package/dist/lead-context.js +24 -17
  106. package/dist/mate-tools.d.ts +4 -0
  107. package/dist/mate-tools.js +108 -275
  108. package/dist/mcp-connect.d.ts +24 -4
  109. package/dist/mcp-connect.js +78 -15
  110. package/dist/mcp.js +84 -216
  111. package/dist/memory-pass.d.ts +17 -42
  112. package/dist/memory-pass.js +54 -26
  113. package/dist/phase-routing.d.ts +19 -72
  114. package/dist/phase-routing.js +34 -164
  115. package/dist/project-knowledge.d.ts +16 -35
  116. package/dist/project-knowledge.js +42 -21
  117. package/dist/project-memory.d.ts +2 -28
  118. package/dist/project-memory.js +28 -13
  119. package/dist/project-skills.d.ts +16 -38
  120. package/dist/project-skills.js +41 -32
  121. package/dist/project-tools.d.ts +1 -0
  122. package/dist/project-tools.js +4 -4
  123. package/dist/provider.d.ts +2 -0
  124. package/dist/provider.js +4 -16
  125. package/dist/result-shots.d.ts +1 -1
  126. package/dist/result-shots.js +2 -1
  127. package/dist/review-switch.d.ts +3 -8
  128. package/dist/reviewer.d.ts +8 -12
  129. package/dist/reviewer.js +11 -35
  130. package/dist/scope.d.ts +21 -43
  131. package/dist/scope.js +42 -128
  132. package/dist/scout-report.d.ts +9 -194
  133. package/dist/scout-report.js +113 -264
  134. package/dist/scout.js +15 -11
  135. package/dist/serve.js +9 -2
  136. package/dist/setup-guide.d.ts +2 -2
  137. package/dist/slack-chat.js +26 -20
  138. package/dist/slack-state.js +2 -1
  139. package/dist/store.d.ts +9 -32
  140. package/dist/store.js +18 -15
  141. package/dist/structured-output.js +2 -1
  142. package/dist/subscription-chat.d.ts +4 -0
  143. package/dist/subscription-chat.js +4 -1
  144. package/dist/task-review-brief.d.ts +1 -7
  145. package/dist/task-review-brief.js +6 -6
  146. package/dist/task-sizing.d.ts +3 -27
  147. package/dist/task-sizing.js +11 -26
  148. package/dist/task-text.d.ts +0 -5
  149. package/dist/task-text.js +1 -3
  150. package/dist/teammate-question.js +7 -5
  151. package/dist/teammate-work.js +9 -5
  152. package/dist/teammates.d.ts +13 -64
  153. package/dist/teammates.js +32 -33
  154. package/dist/teams-chat.js +20 -11
  155. package/dist/telegram-flow.js +4 -3
  156. package/dist/telegram-mate.d.ts +4 -14
  157. package/dist/telegram-mate.js +8 -7
  158. package/dist/telegram-team.d.ts +2 -19
  159. package/dist/telegram.d.ts +1 -1
  160. package/dist/telegram.js +52 -42
  161. package/dist/text-limits.d.ts +142 -1
  162. package/dist/text-limits.js +144 -1
  163. package/dist/tools-ui.d.ts +2 -1
  164. package/dist/tools-ui.js +7 -4
  165. package/package.json +1 -1
@@ -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 {};
@@ -0,0 +1,141 @@
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 { limitRule, TEXT_LIMITS } from "../text-limits.js";
10
+ import { limited, readVersioned, toModelSchema, versioned } from "./contract.js";
11
+ /** How many follow-ups, items and screenshots a report may carry. */
12
+ export const REPORT_COUNTS = { followUps: 5, items: 6, images: 8 };
13
+ /** Caps are BYTES of UTF-8 (v4 review, finding 11): a 64 KiB report is 64 KiB whatever script it is written in. They
14
+ * hold what a scout writes; a stored report is read without them (`storedReportSchema`). */
15
+ export const REPORT_LIMITS = {
16
+ payload: TEXT_LIMITS.reportPayloadBytes,
17
+ title: TEXT_LIMITS.reportTitleBytes,
18
+ summary: TEXT_LIMITS.reportSummary,
19
+ document: TEXT_LIMITS.reportDocumentBytes,
20
+ followUps: REPORT_COUNTS.followUps,
21
+ followUpTitle: TEXT_LIMITS.reportTitleBytes,
22
+ /** A follow-up files as a task: its goal is held to the task goal limit, in bytes of the same count. */
23
+ followUpGoal: TEXT_LIMITS.goal,
24
+ items: REPORT_COUNTS.items,
25
+ itemTitle: TEXT_LIMITS.reportTitleBytes,
26
+ itemWhy: TEXT_LIMITS.reportWhyBytes,
27
+ url: TEXT_LIMITS.reportUrlBytes,
28
+ images: REPORT_COUNTS.images,
29
+ caption: TEXT_LIMITS.reportCaptionBytes,
30
+ };
31
+ /** A screenshot's name in the scout's output folder: one plain file name, PNG or JPEG, never a path. */
32
+ export const REPORT_IMAGE_FILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\.(?:png|jpe?g)$/i;
33
+ /** Each text field, the limit it is written within, and whether it is one line or a web address. */
34
+ export const REPORT_TEXT_FIELDS = {
35
+ title: { limit: "reportTitleBytes", oneLine: true },
36
+ summary: { limit: "reportSummary" },
37
+ report: { limit: "reportDocumentBytes" },
38
+ };
39
+ export const FOLLOW_UP_TEXT_FIELDS = { title: { limit: "reportTitleBytes", oneLine: true }, goal: { limit: "goal" } };
40
+ export const ITEM_TEXT_FIELDS = { title: { limit: "reportTitleBytes", oneLine: true }, why: { limit: "reportWhyBytes" }, url: { limit: "reportUrlBytes", link: true } };
41
+ // The image's file is any one line here: the runner refuses one that isn't a plain file name in its output folder, not the report.
42
+ export const IMAGE_TEXT_FIELDS = { file: { limit: "reportCaptionBytes", oneLine: true }, caption: { limit: "reportCaptionBytes", oneLine: true }, url: { limit: "reportUrlBytes", link: true } };
43
+ export const REPORT_VERSION = 1;
44
+ function reportSchemaOf(text) {
45
+ // Limits named without "Bytes" that a report still counts in bytes: the model is told so.
46
+ const bytes = (field, limit) => text(field, limit).describe(limitRule(field, TEXT_LIMITS[limit], "bytes"));
47
+ const followUp = z.strictObject({
48
+ title: text("follow-up title", "reportTitleBytes").min(1),
49
+ goal: bytes("follow-up goal", "goal").min(1),
50
+ });
51
+ const item = z.strictObject({
52
+ title: text("item title", "reportTitleBytes").min(1),
53
+ why: text("item why", "reportWhyBytes").min(1),
54
+ url: text("item url", "reportUrlBytes").min(1),
55
+ image: z.string().nullable().optional().describe("the file of the one of images this item shows, if any"),
56
+ });
57
+ const image = z.strictObject({
58
+ file: text("image file", "reportCaptionBytes").min(1),
59
+ caption: text("image caption", "reportCaptionBytes").min(1),
60
+ url: text("image url", "reportUrlBytes").min(1),
61
+ /** Added by Toolroll once the screenshot is stored: its bytes' sha256 and the evidence that holds them. */
62
+ sha256: z.string().regex(/^[0-9a-f]{64}$/, { error: "must be a sha256 hex digest" }).optional().describe("set by Toolroll when it keeps the screenshot; leave out"),
63
+ artifact: z.int().positive().optional().describe("set by Toolroll when it keeps the screenshot; leave out"),
64
+ });
65
+ return versioned(REPORT_VERSION, {
66
+ title: text("title", "reportTitleBytes").min(1),
67
+ summary: bytes("summary", "reportSummary").min(1),
68
+ report: text("report", "reportDocumentBytes").min(1),
69
+ followUps: z.array(followUp).max(REPORT_COUNTS.followUps).optional(),
70
+ items: z.array(item).max(REPORT_COUNTS.items).optional(),
71
+ images: z.array(image).max(REPORT_COUNTS.images).optional(),
72
+ });
73
+ }
74
+ /** A report as a scout writes it now. */
75
+ export const scoutReportSchema = reportSchemaOf(limited);
76
+ /** A report kept as evidence: the same, except text keeps the length it was stored with. */
77
+ export const storedReportSchema = reportSchemaOf(() => z.string());
78
+ /** The park mailbox's decision as a scout hands it back: the same fields `parseDecision` reads, which stays its
79
+ * validator until the decision contract (docs/plans/zod-revamp.md, item 7) gives it one schema. */
80
+ export const scoutDecisionSchema = z.strictObject({
81
+ urgency: z.literal("blocking"),
82
+ recap: z.string(),
83
+ question: z.string(),
84
+ options: z.array(z.strictObject({ id: z.string(), label: z.string(), consequence: z.string(), reversible: z.boolean() })),
85
+ recommendation: z.string(),
86
+ });
87
+ /**
88
+ * The scout's handback for Claude's `--json-schema` (run 2334's fix): plan mode only lets a session write its own plan
89
+ * file, so a Claude scout returns a report — or a question for the operator — as structured output on the terminal
90
+ * result event. The report branch is the report schema itself; the byte limits and rules JSON Schema cannot state are
91
+ * `parseReport`'s (and `parseDecision`'s), applied to the branch as handed back.
92
+ */
93
+ export const scoutHandbackSchema = z.strictObject({
94
+ kind: z.enum(["report", "question"]),
95
+ report: scoutReportSchema.optional(),
96
+ decision: scoutDecisionSchema.optional(),
97
+ });
98
+ export const SCOUT_OUTPUT_JSON_SCHEMA = toModelSchema(scoutHandbackSchema);
99
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
100
+ function pick(body, fields) {
101
+ const out = {};
102
+ for (const field of fields) {
103
+ if (Object.prototype.hasOwnProperty.call(body, field) && body[field] !== undefined)
104
+ out[field] = body[field];
105
+ }
106
+ return out;
107
+ }
108
+ const ENTRY_FIELDS = {
109
+ followUps: Object.keys(FOLLOW_UP_TEXT_FIELDS),
110
+ items: [...Object.keys(ITEM_TEXT_FIELDS), "image"],
111
+ images: [...Object.keys(IMAGE_TEXT_FIELDS), "sha256", "artifact"],
112
+ };
113
+ /**
114
+ * Known reports keep the old parser's field-picking, whether unversioned (including `version: null`) or version 1:
115
+ * unknown fields in the report, follow-ups, items and images are ignored, and a null list means none. Known field
116
+ * values are never rewritten, and anything that is not an object is left for the schema to refuse by path.
117
+ */
118
+ function normalizeReportV1(body) {
119
+ const out = { version: REPORT_VERSION, ...pick(body, [...Object.keys(REPORT_TEXT_FIELDS), "followUps", "items", "images"]) };
120
+ for (const list of ["followUps", "items", "images"]) {
121
+ if (out[list] === null)
122
+ delete out[list];
123
+ else if (Array.isArray(out[list]))
124
+ out[list] = out[list].map(entry => (isRecord(entry) ? pick(entry, ENTRY_FIELDS[list]) : entry));
125
+ }
126
+ return out;
127
+ }
128
+ export const REPORT_UPGRADES = { 0: normalizeReportV1 };
129
+ /** Normalize current and unversioned reports; leave other versions for readVersioned to check. */
130
+ export function reportPayloadBody(input) {
131
+ if (!isRecord(input))
132
+ return null;
133
+ return !Object.prototype.hasOwnProperty.call(input, "version") || input["version"] === null || input["version"] === REPORT_VERSION
134
+ ? normalizeReportV1(input)
135
+ : input;
136
+ }
137
+ /** Read current and unversioned reports with the same field-picking, then the strict schema; refuse newer versions
138
+ * plainly. `stored`: one kept as evidence, read without the length limits. */
139
+ export function readReportPayload(input, options = {}) {
140
+ return readVersioned(options.stored === true ? storedReportSchema : scoutReportSchema, reportPayloadBody(input) ?? input, REPORT_UPGRADES);
141
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A Slack button tap (slack-chat.ts `receiveSlack`): the `block_actions` interaction Slack sends, and the button data
3
+ * Toolroll put on it. The interaction is Slack's, so fields Toolroll doesn't read are ignored; the button (its
4
+ * `action_id` and one-time token `value`) is Toolroll's. A tap from the paired person whose button can't be read is
5
+ * answered with why (`actions[0].value: must be a Toolroll button token`), and nothing is done.
6
+ */
7
+ import { z } from "zod";
8
+ import { type ContractResult } from "./contract.js";
9
+ /** Toolroll's buttons: toolroll_* since the rename; standing_orders_* buttons on older messages still work. */
10
+ export declare const SLACK_BUTTON_ACTION: RegExp;
11
+ /** A link button opens its page in Slack's own client; Slack still reports the tap, and there is nothing to do. */
12
+ export declare const SLACK_LINK_ACTION: RegExp;
13
+ /** The interaction around a tap: who, and the message the button is on. */
14
+ export declare const slackBlockActionsSchema: z.ZodObject<{
15
+ type: z.ZodLiteral<"block_actions">;
16
+ user: z.ZodObject<{
17
+ id: z.ZodString;
18
+ }, z.core.$strip>;
19
+ container: z.ZodObject<{
20
+ type: z.ZodString;
21
+ channel_id: z.ZodOptional<z.ZodString>;
22
+ message_ts: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strip>;
24
+ message: z.ZodOptional<z.ZodObject<{
25
+ thread_ts: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>;
27
+ actions: z.ZodArray<z.ZodObject<{
28
+ action_id: z.ZodOptional<z.ZodString>;
29
+ value: z.ZodOptional<z.ZodString>;
30
+ action_ts: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>>;
32
+ }, z.core.$strip>;
33
+ export type SlackBlockActions = z.infer<typeof slackBlockActionsSchema>;
34
+ /** The one tapped button, as Toolroll made it. */
35
+ export declare const slackButtonSchema: z.ZodObject<{
36
+ action_id: z.ZodString;
37
+ value: z.ZodString;
38
+ action_ts: z.ZodString;
39
+ }, z.core.$strip>;
40
+ export type SlackButton = z.infer<typeof slackButtonSchema>;
41
+ export declare function readSlackBlockActions(body: unknown): ContractResult<SlackBlockActions>;
42
+ /** The tapped button: exactly one, Toolroll's (`actions[0].action_id: not a Toolroll button`). */
43
+ export declare function readSlackButton(interaction: SlackBlockActions): ContractResult<SlackButton>;