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.
- package/CHANGELOG.md +58 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +7 -2
- package/dist/contracts/contract.js +21 -12
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +5 -0
- package/dist/contracts/flow.js +5 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/desktop-update.d.ts +2 -2
- package/dist/discord-chat.js +31 -20
- package/dist/flow-engine.js +14 -17
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.js +7 -1
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-steps.js +3 -2
- package/dist/flows.d.ts +2 -4
- package/dist/flows.js +9 -5
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +108 -275
- package/dist/mcp-connect.d.ts +24 -4
- package/dist/mcp-connect.js +78 -15
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +9 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/task-review-brief.d.ts +1 -7
- package/dist/task-review-brief.js +6 -6
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +142 -1
- package/dist/text-limits.js +144 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/package.json +1 -1
package/dist/scout-report.d.ts
CHANGED
|
@@ -1,44 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The scout's terminal handoff (mate arc §10),
|
|
3
|
-
* fail closed, every problem reported at once,
|
|
4
|
-
* control-character rejection on every string. A report reaches the task
|
|
5
|
-
* page, the ledger, the terminal, and — through mateView — the mate; and
|
|
6
|
-
*
|
|
7
|
-
* the park discipline exactly as the plan does.
|
|
2
|
+
* The scout's terminal handoff (mate arc §10), read against the report contract (src/contracts/scout-report.ts) with
|
|
3
|
+
* the 422 rule: fail closed, every problem reported at once, each naming its path (`items[0].url: must be an http or
|
|
4
|
+
* https address`), stable reasons, caps and control-character rejection on every string. A report reaches the task
|
|
5
|
+
* page, the ledger, the terminal, and — through mateView — the mate; and each follow-up becomes a filing's title and
|
|
6
|
+
* goal at one tap, so it gets the park discipline exactly as the plan does.
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export type ParsedReport = {
|
|
14
|
-
title: string;
|
|
15
|
-
summary: string;
|
|
16
|
-
/** The report document, markdown, rendered fenced-inert everywhere. */
|
|
17
|
-
report: string;
|
|
18
|
-
/** Proposed follow-ups: each files as a task in the same repository. */
|
|
19
|
-
followUps: {
|
|
20
|
-
title: string;
|
|
21
|
-
goal: string;
|
|
22
|
-
}[];
|
|
23
|
-
/** What the scout found, as a short list later steps can read: each cites its URL, and may show one of `images`. */
|
|
24
|
-
items: ReportItem[];
|
|
25
|
-
/** Screenshots the scout saved during the run. From the scout, `file` names a file in its output folder; once
|
|
26
|
-
* stored, the runner adds the bytes' sha256 and the evidence row that holds them. */
|
|
27
|
-
images: ReportImage[];
|
|
28
|
-
};
|
|
29
|
-
export type ReportItem = {
|
|
30
|
-
title: string;
|
|
31
|
-
why: string;
|
|
32
|
-
url: string;
|
|
33
|
-
image: string | null;
|
|
34
|
-
};
|
|
35
|
-
export type ReportImage = {
|
|
36
|
-
file: string;
|
|
37
|
-
caption: string;
|
|
38
|
-
url: string;
|
|
39
|
-
sha256?: string;
|
|
40
|
-
artifact?: number;
|
|
41
|
-
};
|
|
8
|
+
import { type ContractProblem } from "./contracts/contract.js";
|
|
9
|
+
import { type ParsedReport } from "./contracts/scout-report.js";
|
|
10
|
+
export { REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA, type ParsedReport, type ReportImage, type ReportItem, } from "./contracts/scout-report.js";
|
|
11
|
+
export type ReportProblem = ContractProblem;
|
|
42
12
|
export type ReportParseResult = {
|
|
43
13
|
ok: true;
|
|
44
14
|
report: ParsedReport;
|
|
@@ -46,161 +16,6 @@ export type ReportParseResult = {
|
|
|
46
16
|
ok: false;
|
|
47
17
|
problems: ReportProblem[];
|
|
48
18
|
};
|
|
49
|
-
/** Caps are BYTES of UTF-8 (v4 review, finding 11): a 64 KiB report is
|
|
50
|
-
* 64 KiB whatever script it is written in. They hold what a scout writes;
|
|
51
|
-
* a stored report is read without them (parseReport's `stored`). */
|
|
52
|
-
export declare const REPORT_LIMITS: {
|
|
53
|
-
readonly payload: number;
|
|
54
|
-
readonly title: 200;
|
|
55
|
-
readonly summary: 2500;
|
|
56
|
-
readonly document: number;
|
|
57
|
-
readonly followUps: 5;
|
|
58
|
-
readonly followUpTitle: 200;
|
|
59
|
-
/** A follow-up files as a task: its goal is held to the task goal limit, in bytes of the same count. */
|
|
60
|
-
readonly followUpGoal: 8000;
|
|
61
|
-
readonly items: 6;
|
|
62
|
-
readonly itemTitle: 200;
|
|
63
|
-
readonly itemWhy: 1000;
|
|
64
|
-
readonly url: 2000;
|
|
65
|
-
readonly images: 8;
|
|
66
|
-
readonly caption: 300;
|
|
67
|
-
};
|
|
68
|
-
/** A screenshot's name in the scout's output folder: one plain file name, PNG or JPEG, never a path. */
|
|
69
|
-
export declare const REPORT_IMAGE_FILE: RegExp;
|
|
70
|
-
/**
|
|
71
|
-
* The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
|
|
72
|
-
* mode only lets a session write its own plan file, so a Claude scout
|
|
73
|
-
* returns a report — or a question for the operator — as structured output
|
|
74
|
-
* on the terminal result event. Shape only, never the validator: byte caps,
|
|
75
|
-
* one-line titles and control characters stay `parseReport`'s and
|
|
76
|
-
* `parseDecision`'s, applied to the re-serialized body.
|
|
77
|
-
*/
|
|
78
|
-
export declare const SCOUT_OUTPUT_JSON_SCHEMA: {
|
|
79
|
-
readonly type: "object";
|
|
80
|
-
readonly properties: {
|
|
81
|
-
readonly kind: {
|
|
82
|
-
readonly type: "string";
|
|
83
|
-
readonly enum: readonly ["report", "question"];
|
|
84
|
-
};
|
|
85
|
-
readonly report: {
|
|
86
|
-
readonly type: "object";
|
|
87
|
-
readonly properties: {
|
|
88
|
-
readonly title: {
|
|
89
|
-
readonly type: "string";
|
|
90
|
-
};
|
|
91
|
-
readonly summary: {
|
|
92
|
-
readonly type: "string";
|
|
93
|
-
};
|
|
94
|
-
readonly report: {
|
|
95
|
-
readonly type: "string";
|
|
96
|
-
};
|
|
97
|
-
readonly followUps: {
|
|
98
|
-
readonly type: "array";
|
|
99
|
-
readonly items: {
|
|
100
|
-
readonly type: "object";
|
|
101
|
-
readonly properties: {
|
|
102
|
-
readonly title: {
|
|
103
|
-
readonly type: "string";
|
|
104
|
-
};
|
|
105
|
-
readonly goal: {
|
|
106
|
-
readonly type: "string";
|
|
107
|
-
};
|
|
108
|
-
};
|
|
109
|
-
readonly required: readonly ["title", "goal"];
|
|
110
|
-
readonly additionalProperties: false;
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
readonly items: {
|
|
114
|
-
readonly type: "array";
|
|
115
|
-
readonly items: {
|
|
116
|
-
readonly type: "object";
|
|
117
|
-
readonly properties: {
|
|
118
|
-
readonly title: {
|
|
119
|
-
readonly type: "string";
|
|
120
|
-
};
|
|
121
|
-
readonly why: {
|
|
122
|
-
readonly type: "string";
|
|
123
|
-
};
|
|
124
|
-
readonly url: {
|
|
125
|
-
readonly type: "string";
|
|
126
|
-
};
|
|
127
|
-
readonly image: {
|
|
128
|
-
readonly type: "string";
|
|
129
|
-
};
|
|
130
|
-
};
|
|
131
|
-
readonly required: readonly ["title", "why", "url"];
|
|
132
|
-
readonly additionalProperties: false;
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
readonly images: {
|
|
136
|
-
readonly type: "array";
|
|
137
|
-
readonly items: {
|
|
138
|
-
readonly type: "object";
|
|
139
|
-
readonly properties: {
|
|
140
|
-
readonly file: {
|
|
141
|
-
readonly type: "string";
|
|
142
|
-
};
|
|
143
|
-
readonly caption: {
|
|
144
|
-
readonly type: "string";
|
|
145
|
-
};
|
|
146
|
-
readonly url: {
|
|
147
|
-
readonly type: "string";
|
|
148
|
-
};
|
|
149
|
-
};
|
|
150
|
-
readonly required: readonly ["file", "caption", "url"];
|
|
151
|
-
readonly additionalProperties: false;
|
|
152
|
-
};
|
|
153
|
-
};
|
|
154
|
-
};
|
|
155
|
-
readonly required: readonly ["title", "summary", "report"];
|
|
156
|
-
readonly additionalProperties: false;
|
|
157
|
-
};
|
|
158
|
-
readonly decision: {
|
|
159
|
-
readonly type: "object";
|
|
160
|
-
readonly properties: {
|
|
161
|
-
readonly urgency: {
|
|
162
|
-
readonly type: "string";
|
|
163
|
-
readonly enum: readonly ["blocking"];
|
|
164
|
-
};
|
|
165
|
-
readonly recap: {
|
|
166
|
-
readonly type: "string";
|
|
167
|
-
};
|
|
168
|
-
readonly question: {
|
|
169
|
-
readonly type: "string";
|
|
170
|
-
};
|
|
171
|
-
readonly options: {
|
|
172
|
-
readonly type: "array";
|
|
173
|
-
readonly items: {
|
|
174
|
-
readonly type: "object";
|
|
175
|
-
readonly properties: {
|
|
176
|
-
readonly id: {
|
|
177
|
-
readonly type: "string";
|
|
178
|
-
};
|
|
179
|
-
readonly label: {
|
|
180
|
-
readonly type: "string";
|
|
181
|
-
};
|
|
182
|
-
readonly consequence: {
|
|
183
|
-
readonly type: "string";
|
|
184
|
-
};
|
|
185
|
-
readonly reversible: {
|
|
186
|
-
readonly type: "boolean";
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
readonly required: readonly ["id", "label", "consequence", "reversible"];
|
|
190
|
-
readonly additionalProperties: false;
|
|
191
|
-
};
|
|
192
|
-
};
|
|
193
|
-
readonly recommendation: {
|
|
194
|
-
readonly type: "string";
|
|
195
|
-
};
|
|
196
|
-
};
|
|
197
|
-
readonly required: readonly ["urgency", "recap", "question", "options", "recommendation"];
|
|
198
|
-
readonly additionalProperties: false;
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
|
-
readonly required: readonly ["kind"];
|
|
202
|
-
readonly additionalProperties: false;
|
|
203
|
-
};
|
|
204
19
|
/** A report, checked. `stored`: one already kept as evidence, read without the length caps (a report written under
|
|
205
20
|
* higher limits stays readable); every other check still holds. */
|
|
206
21
|
export declare function parseReport(raw: string, options?: {
|
package/dist/scout-report.js
CHANGED
|
@@ -1,285 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The scout's terminal handoff (mate arc §10),
|
|
3
|
-
* fail closed, every problem reported at once,
|
|
4
|
-
* control-character rejection on every string. A report reaches the task
|
|
5
|
-
* page, the ledger, the terminal, and — through mateView — the mate; and
|
|
6
|
-
*
|
|
7
|
-
* the park discipline exactly as the plan does.
|
|
2
|
+
* The scout's terminal handoff (mate arc §10), read against the report contract (src/contracts/scout-report.ts) with
|
|
3
|
+
* the 422 rule: fail closed, every problem reported at once, each naming its path (`items[0].url: must be an http or
|
|
4
|
+
* https address`), stable reasons, caps and control-character rejection on every string. A report reaches the task
|
|
5
|
+
* page, the ledger, the terminal, and — through mateView — the mate; and each follow-up becomes a filing's title and
|
|
6
|
+
* goal at one tap, so it gets the park discipline exactly as the plan does.
|
|
8
7
|
*/
|
|
8
|
+
import { contractProblemOf } from "./contracts/contract.js";
|
|
9
|
+
import { FOLLOW_UP_TEXT_FIELDS, IMAGE_TEXT_FIELDS, ITEM_TEXT_FIELDS, readReportPayload, REPORT_LIMITS, REPORT_TEXT_FIELDS, reportPayloadBody, } from "./contracts/scout-report.js";
|
|
9
10
|
import { hasForbiddenControls } from "./decision.js";
|
|
10
|
-
import { TASK_TEXT_LIMITS } from "./task-text.js";
|
|
11
11
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
12
|
-
|
|
13
|
-
* 64 KiB whatever script it is written in. They hold what a scout writes;
|
|
14
|
-
* a stored report is read without them (parseReport's `stored`). */
|
|
15
|
-
export const REPORT_LIMITS = {
|
|
16
|
-
payload: 96 * 1024,
|
|
17
|
-
title: 200,
|
|
18
|
-
summary: TEXT_LIMITS.reportSummary,
|
|
19
|
-
document: 64 * 1024,
|
|
20
|
-
followUps: 5,
|
|
21
|
-
followUpTitle: 200,
|
|
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: TASK_TEXT_LIMITS.text,
|
|
24
|
-
items: 6,
|
|
25
|
-
itemTitle: 200,
|
|
26
|
-
itemWhy: 1_000,
|
|
27
|
-
url: 2_000,
|
|
28
|
-
images: 8,
|
|
29
|
-
caption: 300,
|
|
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
|
-
const REPORT_SHAPE = {
|
|
34
|
-
type: "object",
|
|
35
|
-
properties: {
|
|
36
|
-
title: { type: "string" },
|
|
37
|
-
summary: { type: "string" },
|
|
38
|
-
report: { type: "string" },
|
|
39
|
-
followUps: {
|
|
40
|
-
type: "array",
|
|
41
|
-
items: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: { title: { type: "string" }, goal: { type: "string" } },
|
|
44
|
-
required: ["title", "goal"],
|
|
45
|
-
additionalProperties: false,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
items: {
|
|
49
|
-
type: "array",
|
|
50
|
-
items: {
|
|
51
|
-
type: "object",
|
|
52
|
-
properties: { title: { type: "string" }, why: { type: "string" }, url: { type: "string" }, image: { type: "string" } },
|
|
53
|
-
required: ["title", "why", "url"],
|
|
54
|
-
additionalProperties: false,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
images: {
|
|
58
|
-
type: "array",
|
|
59
|
-
items: {
|
|
60
|
-
type: "object",
|
|
61
|
-
properties: { file: { type: "string" }, caption: { type: "string" }, url: { type: "string" } },
|
|
62
|
-
required: ["file", "caption", "url"],
|
|
63
|
-
additionalProperties: false,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
required: ["title", "summary", "report"],
|
|
68
|
-
additionalProperties: false,
|
|
69
|
-
};
|
|
70
|
-
/** The park mailbox's decision, the same fields `parseDecision` reads. */
|
|
71
|
-
const DECISION_SHAPE = {
|
|
72
|
-
type: "object",
|
|
73
|
-
properties: {
|
|
74
|
-
urgency: { type: "string", enum: ["blocking"] },
|
|
75
|
-
recap: { type: "string" },
|
|
76
|
-
question: { type: "string" },
|
|
77
|
-
options: {
|
|
78
|
-
type: "array",
|
|
79
|
-
items: {
|
|
80
|
-
type: "object",
|
|
81
|
-
properties: {
|
|
82
|
-
id: { type: "string" },
|
|
83
|
-
label: { type: "string" },
|
|
84
|
-
consequence: { type: "string" },
|
|
85
|
-
reversible: { type: "boolean" },
|
|
86
|
-
},
|
|
87
|
-
required: ["id", "label", "consequence", "reversible"],
|
|
88
|
-
additionalProperties: false,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
recommendation: { type: "string" },
|
|
92
|
-
},
|
|
93
|
-
required: ["urgency", "recap", "question", "options", "recommendation"],
|
|
94
|
-
additionalProperties: false,
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
|
|
98
|
-
* mode only lets a session write its own plan file, so a Claude scout
|
|
99
|
-
* returns a report — or a question for the operator — as structured output
|
|
100
|
-
* on the terminal result event. Shape only, never the validator: byte caps,
|
|
101
|
-
* one-line titles and control characters stay `parseReport`'s and
|
|
102
|
-
* `parseDecision`'s, applied to the re-serialized body.
|
|
103
|
-
*/
|
|
104
|
-
export const SCOUT_OUTPUT_JSON_SCHEMA = {
|
|
105
|
-
type: "object",
|
|
106
|
-
properties: {
|
|
107
|
-
kind: { type: "string", enum: ["report", "question"] },
|
|
108
|
-
report: REPORT_SHAPE,
|
|
109
|
-
decision: DECISION_SHAPE,
|
|
110
|
-
},
|
|
111
|
-
required: ["kind"],
|
|
112
|
-
additionalProperties: false,
|
|
113
|
-
};
|
|
12
|
+
export { REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA, } from "./contracts/scout-report.js";
|
|
114
13
|
function refuse(reason, message) {
|
|
115
14
|
return { ok: false, problems: [{ reason, message }] };
|
|
116
15
|
}
|
|
117
|
-
function describe(value) {
|
|
118
|
-
if (value === undefined)
|
|
119
|
-
return "nothing";
|
|
120
|
-
if (value === null)
|
|
121
|
-
return "null";
|
|
122
|
-
if (typeof value === "string")
|
|
123
|
-
return `a ${value.length}-char string`;
|
|
124
|
-
return `a ${Array.isArray(value) ? "array" : typeof value}`;
|
|
125
|
-
}
|
|
126
|
-
function prose(value, field, cap, problems) {
|
|
127
|
-
if (value === undefined || value === null || value === "" || (typeof value === "string" && value.trim() === "")) {
|
|
128
|
-
problems.push({ reason: `missing-${field}`, message: `${field} is required` });
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
if (typeof value !== "string") {
|
|
132
|
-
problems.push({ reason: `bad-${field}`, message: `${field} must be a string (got ${describe(value)})` });
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
if (!readingStored && Buffer.byteLength(value, "utf8") > cap) {
|
|
136
|
-
problems.push({ reason: `${field}-too-long`, message: `${field} is over ${cap} bytes` });
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
if (hasForbiddenControls(value)) {
|
|
140
|
-
problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
return value;
|
|
144
|
-
}
|
|
145
|
-
/** True while a stored report is read: a limit is for writing, and reading never re-checks a text's length. */
|
|
146
|
-
let readingStored = false;
|
|
147
16
|
/** A report, checked. `stored`: one already kept as evidence, read without the length caps (a report written under
|
|
148
17
|
* higher limits stays readable); every other check still holds. */
|
|
149
18
|
export function parseReport(raw, options = {}) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
finally {
|
|
155
|
-
readingStored = false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function parseReportBody(raw) {
|
|
159
|
-
if (!readingStored && Buffer.byteLength(raw, "utf8") > REPORT_LIMITS.payload) {
|
|
160
|
-
return refuse("too-large", `the payload is over ${REPORT_LIMITS.payload} bytes`);
|
|
19
|
+
const stored = options.stored === true;
|
|
20
|
+
if (!stored && Buffer.byteLength(raw, "utf8") > REPORT_LIMITS.payload) {
|
|
21
|
+
return refuse("too-large", `payload: over ${REPORT_LIMITS.payload.toLocaleString("en-US")} bytes`);
|
|
161
22
|
}
|
|
162
23
|
let parsed;
|
|
163
24
|
try {
|
|
164
25
|
parsed = JSON.parse(raw);
|
|
165
26
|
}
|
|
166
27
|
catch (error) {
|
|
167
|
-
return refuse("not-json", `
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// One paragraph: the summary is what the operator reads first, on a
|
|
181
|
-
// phone, in a digest line — a blank line inside it is a second paragraph.
|
|
182
|
-
if (summary !== null && /\n[ \t]*\n/.test(summary)) {
|
|
183
|
-
problems.push({ reason: "summary-paragraphs", message: "summary is one paragraph — no blank lines" });
|
|
184
|
-
}
|
|
185
|
-
const followUps = [];
|
|
186
|
-
if (body["followUps"] !== undefined && body["followUps"] !== null) {
|
|
187
|
-
if (!Array.isArray(body["followUps"])) {
|
|
188
|
-
problems.push({ reason: "bad-followUps", message: `followUps must be an array (got ${describe(body["followUps"])})` });
|
|
189
|
-
}
|
|
190
|
-
else if (body["followUps"].length > REPORT_LIMITS.followUps) {
|
|
191
|
-
problems.push({ reason: "followUps-too-many", message: `followUps lists ${body["followUps"].length} — cap is ${REPORT_LIMITS.followUps}` });
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
for (const [index, one] of body["followUps"].entries()) {
|
|
195
|
-
if (typeof one !== "object" || one === null || Array.isArray(one)) {
|
|
196
|
-
problems.push({ reason: `followUps[${index}]-shape`, message: `followUps[${index}] must be {title, goal}` });
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
const entry = one;
|
|
200
|
-
const followTitle = prose(entry["title"], `followUps[${index}].title`, REPORT_LIMITS.followUpTitle, problems);
|
|
201
|
-
const goal = prose(entry["goal"], `followUps[${index}].goal`, REPORT_LIMITS.followUpGoal, problems);
|
|
202
|
-
if (followTitle !== null && /[\n\r]/.test(followTitle)) {
|
|
203
|
-
problems.push({ reason: `followUps[${index}]-title-multiline`, message: `followUps[${index}].title must be one line` });
|
|
204
|
-
}
|
|
205
|
-
else if (followTitle !== null && goal !== null) {
|
|
206
|
-
followUps.push({ title: followTitle, goal });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const images = listOf(body["images"], "images", REPORT_LIMITS.images, problems, (entry, at) => {
|
|
212
|
-
// Any one line here: the runner refuses an image that isn't a plain file name in its output folder, not the report.
|
|
213
|
-
const file = prose(entry["file"], `${at}.file`, REPORT_LIMITS.caption, problems);
|
|
214
|
-
const caption = prose(entry["caption"], `${at}.caption`, REPORT_LIMITS.caption, problems);
|
|
215
|
-
const url = link(entry["url"], `${at}.url`, problems);
|
|
216
|
-
if (file !== null && /[\n\r]/.test(file))
|
|
217
|
-
problems.push({ reason: `${at}-file-multiline`, message: `${at}.file must be one line` });
|
|
218
|
-
if (caption !== null && /[\n\r]/.test(caption))
|
|
219
|
-
problems.push({ reason: `${at}-caption-multiline`, message: `${at}.caption must be one line` });
|
|
220
|
-
const sha256 = entry["sha256"];
|
|
221
|
-
if (sha256 !== undefined && (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/.test(sha256)))
|
|
222
|
-
problems.push({ reason: `${at}-sha256`, message: `${at}.sha256 must be a sha256 hex digest` });
|
|
223
|
-
const artifact = entry["artifact"];
|
|
224
|
-
if (artifact !== undefined && (typeof artifact !== "number" || !Number.isSafeInteger(artifact) || artifact <= 0))
|
|
225
|
-
problems.push({ reason: `${at}-artifact`, message: `${at}.artifact must be an evidence id` });
|
|
226
|
-
if (file === null || caption === null || url === null || /[\n\r]/.test(file) || /[\n\r]/.test(caption))
|
|
227
|
-
return null;
|
|
228
|
-
return { file, caption, url, ...(typeof sha256 === "string" ? { sha256 } : {}), ...(typeof artifact === "number" ? { artifact } : {}) };
|
|
229
|
-
});
|
|
230
|
-
const files = new Set();
|
|
231
|
-
for (const one of images) {
|
|
232
|
-
if (files.has(one.file))
|
|
233
|
-
problems.push({ reason: "images-duplicate", message: `images names ${one.file} twice` });
|
|
234
|
-
files.add(one.file);
|
|
235
|
-
}
|
|
236
|
-
const items = listOf(body["items"], "items", REPORT_LIMITS.items, problems, (entry, at) => {
|
|
237
|
-
const itemTitle = prose(entry["title"], `${at}.title`, REPORT_LIMITS.itemTitle, problems);
|
|
238
|
-
const why = prose(entry["why"], `${at}.why`, REPORT_LIMITS.itemWhy, problems);
|
|
239
|
-
const url = link(entry["url"], `${at}.url`, problems);
|
|
240
|
-
const image = entry["image"] === undefined || entry["image"] === null || entry["image"] === "" ? null : entry["image"];
|
|
241
|
-
if (itemTitle !== null && /[\n\r]/.test(itemTitle))
|
|
242
|
-
problems.push({ reason: `${at}-title-multiline`, message: `${at}.title must be one line` });
|
|
243
|
-
if (image !== null && (typeof image !== "string" || !files.has(image)))
|
|
244
|
-
problems.push({ reason: `${at}-image`, message: `${at}.image must name one of images by its file` });
|
|
245
|
-
if (itemTitle === null || why === null || url === null || /[\n\r]/.test(itemTitle) || (image !== null && (typeof image !== "string" || !files.has(image))))
|
|
246
|
-
return null;
|
|
247
|
-
return { title: itemTitle, why, url, image: image };
|
|
248
|
-
});
|
|
249
|
-
if (problems.length > 0)
|
|
28
|
+
return refuse("not-json", `payload: not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
const body = reportPayloadBody(parsed);
|
|
31
|
+
if (body === null)
|
|
32
|
+
return refuse("not-an-object", "payload: must be one JSON object");
|
|
33
|
+
const read = readReportPayload(parsed, { stored });
|
|
34
|
+
// Every report limit counts UTF-8 bytes, including those TEXT_LIMITS names without "Bytes" (a summary, a goal).
|
|
35
|
+
const issues = (read.ok ? [] : read.issues).map(issue => (issue.kind === "too-long" ? { ...issue, line: issue.line.replace(/ characters$/, " bytes") } : issue));
|
|
36
|
+
const contract = issues.map(contractProblemOf);
|
|
37
|
+
if (contract.some(problem => problem.reason === "newer-version" || problem.reason === "bad-version"))
|
|
38
|
+
return { ok: false, problems: contract };
|
|
39
|
+
const problems = [...contract, ...reportRuleProblems(body, issues, stored)];
|
|
40
|
+
if (!read.ok || problems.length > 0)
|
|
250
41
|
return { ok: false, problems };
|
|
251
|
-
|
|
42
|
+
const payload = read.value;
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
report: {
|
|
46
|
+
title: payload.title,
|
|
47
|
+
summary: payload.summary,
|
|
48
|
+
report: payload.report,
|
|
49
|
+
followUps: (payload.followUps ?? []).map(one => ({ title: one.title, goal: one.goal })),
|
|
50
|
+
items: (payload.items ?? []).map(one => ({ title: one.title, why: one.why, url: one.url, image: one.image === undefined || one.image === null || one.image === "" ? null : one.image })),
|
|
51
|
+
images: (payload.images ?? []).map(one => ({
|
|
52
|
+
file: one.file,
|
|
53
|
+
caption: one.caption,
|
|
54
|
+
url: one.url,
|
|
55
|
+
...(one.sha256 === undefined ? {} : { sha256: one.sha256 }),
|
|
56
|
+
...(one.artifact === undefined ? {} : { artifact: one.artifact }),
|
|
57
|
+
})),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
252
60
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The rules JSON Schema cannot state, in plain code with path-named errors: blank text, byte caps (on write only),
|
|
63
|
+
* control characters, one-line fields, a one-paragraph summary, safe links, a screenshot named twice and an item
|
|
64
|
+
* naming one the report doesn't have. A field the contract already refused (or one inside it) is skipped, so every
|
|
65
|
+
* problem is reported once and all at once.
|
|
66
|
+
*/
|
|
67
|
+
function reportRuleProblems(body, issues, stored) {
|
|
68
|
+
const problems = [];
|
|
69
|
+
// An unknown key is about its object, not the fields beside it.
|
|
70
|
+
const refused = issues.filter(issue => issue.kind !== "unknown-key").map(issue => issue.path);
|
|
71
|
+
const touched = (path) => refused.some(at => at === path || path.startsWith(`${at}.`) || path.startsWith(`${at}[`) || at.startsWith(`${path}.`) || at.startsWith(`${path}[`));
|
|
72
|
+
const say = (path, reason, what) => problems.push({ reason, message: `${path}: ${what}` });
|
|
73
|
+
/** One text field checked; true when it passed every rule. */
|
|
74
|
+
const text = (value, path, rule) => {
|
|
75
|
+
if (typeof value !== "string" || touched(path))
|
|
76
|
+
return false;
|
|
77
|
+
const limit = TEXT_LIMITS[rule.limit];
|
|
78
|
+
if (value.trim() === "")
|
|
79
|
+
say(path, `missing-${path}`, "must not be empty");
|
|
80
|
+
else if (!stored && Buffer.byteLength(value, "utf8") > limit)
|
|
81
|
+
say(path, `${path}-too-long`, `over ${limit.toLocaleString("en-US")} bytes`);
|
|
82
|
+
else if (hasForbiddenControls(value))
|
|
83
|
+
say(path, `${path}-controls`, "carries control characters that could become terminal escapes");
|
|
84
|
+
else if (rule.oneLine === true && /[\n\r]/.test(value))
|
|
85
|
+
say(path, `${path}-multiline`, "must be one line");
|
|
86
|
+
else if (rule.link === true && !isLink(value))
|
|
87
|
+
say(path, `${path}-not-a-link`, "must be an http or https address");
|
|
88
|
+
else
|
|
89
|
+
return true;
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
const entries = (list) => {
|
|
93
|
+
const value = body[list];
|
|
94
|
+
if (!Array.isArray(value) || touched(list))
|
|
95
|
+
return [];
|
|
96
|
+
return value.flatMap((entry, index) => typeof entry === "object" && entry !== null && !Array.isArray(entry) ? [[entry, `${list}[${index}]`]] : []);
|
|
97
|
+
};
|
|
98
|
+
const fields = (entry, at, rules) => {
|
|
99
|
+
for (const [field, rule] of Object.entries(rules))
|
|
100
|
+
text(entry[field], at === "" ? field : `${at}.${field}`, rule);
|
|
101
|
+
};
|
|
102
|
+
fields(body, "", REPORT_TEXT_FIELDS);
|
|
103
|
+
// One paragraph: the summary is what the operator reads first, on a phone, in a digest line — a blank line inside
|
|
104
|
+
// it is a second paragraph.
|
|
105
|
+
const summary = body["summary"];
|
|
106
|
+
if (typeof summary === "string" && !problems.some(problem => problem.message.startsWith("summary:")) && !touched("summary") && /\n[ \t]*\n/.test(summary)) {
|
|
107
|
+
say("summary", "summary-paragraphs", "one paragraph — no blank lines");
|
|
108
|
+
}
|
|
109
|
+
for (const [entry, at] of entries("followUps"))
|
|
110
|
+
fields(entry, at, FOLLOW_UP_TEXT_FIELDS);
|
|
111
|
+
const files = new Map();
|
|
112
|
+
for (const [entry, at] of entries("images")) {
|
|
113
|
+
fields(entry, at, IMAGE_TEXT_FIELDS);
|
|
114
|
+
const file = entry["file"];
|
|
115
|
+
if (typeof file !== "string" || touched(`${at}.file`))
|
|
270
116
|
continue;
|
|
117
|
+
const first = files.get(file);
|
|
118
|
+
if (first !== undefined)
|
|
119
|
+
say(`${at}.file`, "images-duplicate", `names ${file} again (also ${first})`);
|
|
120
|
+
else
|
|
121
|
+
files.set(file, at);
|
|
122
|
+
}
|
|
123
|
+
// An item's picture is checked against the images as written: one naming a screenshot the report doesn't list is refused.
|
|
124
|
+
const imagesRefused = touched("images");
|
|
125
|
+
for (const [entry, at] of entries("items")) {
|
|
126
|
+
fields(entry, at, ITEM_TEXT_FIELDS);
|
|
127
|
+
const image = entry["image"];
|
|
128
|
+
if (typeof image === "string" && image !== "" && !touched(`${at}.image`) && !imagesRefused && !files.has(image)) {
|
|
129
|
+
say(`${at}.image`, `bad-${at}.image`, "must name one of images by its file");
|
|
271
130
|
}
|
|
272
|
-
const read = one(entry, at);
|
|
273
|
-
if (read !== null)
|
|
274
|
-
kept.push(read);
|
|
275
131
|
}
|
|
276
|
-
return
|
|
132
|
+
return problems;
|
|
277
133
|
}
|
|
278
134
|
/** A cited web address: http or https, one line, no credentials. */
|
|
279
|
-
function
|
|
280
|
-
const text = prose(value, field, REPORT_LIMITS.url, problems);
|
|
281
|
-
if (text === null)
|
|
282
|
-
return null;
|
|
135
|
+
function isLink(text) {
|
|
283
136
|
let url = null;
|
|
284
137
|
try {
|
|
285
138
|
url = new URL(text);
|
|
@@ -287,9 +140,5 @@ function link(value, field, problems) {
|
|
|
287
140
|
catch {
|
|
288
141
|
url = null;
|
|
289
142
|
}
|
|
290
|
-
|
|
291
|
-
problems.push({ reason: `${field}-not-a-link`, message: `${field} must be an http or https address` });
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
return text;
|
|
143
|
+
return url !== null && (url.protocol === "http:" || url.protocol === "https:") && !/\s/.test(text) && url.username === "" && url.password === "";
|
|
295
144
|
}
|