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.
- package/CHANGELOG.md +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- 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 +40 -7
- package/dist/contracts/contract.js +155 -19
- 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-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -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/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -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/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- 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.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- 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 +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- 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/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- 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 +17 -101
- 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/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- 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 +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
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
|
}
|
package/dist/scout.js
CHANGED
|
@@ -20,7 +20,8 @@ import { auditOf } from "./provider.js";
|
|
|
20
20
|
import { currentClaim, heartbeat } from "./claim.js";
|
|
21
21
|
import { heartbeat as runnerHeartbeat } from "./runner.js";
|
|
22
22
|
import { parseDecision } from "./decision.js";
|
|
23
|
-
import {
|
|
23
|
+
import { scoutHandbackSchema } from "./contracts/scout-report.js";
|
|
24
|
+
import { parseReport, REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA } from "./scout-report.js";
|
|
24
25
|
import { TOKEN_ENVS as TELEGRAM_TOKEN_ENVS } from "./telegram.js";
|
|
25
26
|
import { evidenceRoot, mailboxName, quarantineMailboxes, readMailbox, reportFileName, SCREENSHOT_BYTE_CAP, storeEvidence, validateScreenshotBytes, writeEvidenceFile, } from "./evidence.js";
|
|
26
27
|
import { MARKER as LEASE_MARKER } from "./worktree.js";
|
|
@@ -29,7 +30,7 @@ import { proveTreeUntouched, snapshotIgnored } from "./tree-proof.js";
|
|
|
29
30
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
30
31
|
import { CLAUDE_LIMITS } from "./scope.js";
|
|
31
32
|
import { catalogTool, projectToolsOf, toolLaunchFor } from "./project-tools.js";
|
|
32
|
-
import { researchToolsOf } from "./mcp-connect.js";
|
|
33
|
+
import { grantOf, researchToolsOf } from "./mcp-connect.js";
|
|
33
34
|
import { startScoutProxy } from "./scout-net.js";
|
|
34
35
|
import * as browserCheck from "./scout-browser.js";
|
|
35
36
|
import { invokeAgent } from "./invoke.js";
|
|
@@ -160,6 +161,7 @@ function scoutBrief(title, goal, outOfScope, mailbox, reportFile, answers, struc
|
|
|
160
161
|
`\`${reportFile}\`:`,
|
|
161
162
|
]),
|
|
162
163
|
"{",
|
|
164
|
+
` "version": ${REPORT_VERSION},`,
|
|
163
165
|
' "title": "one line",',
|
|
164
166
|
' "summary": "one paragraph the operator reads first",',
|
|
165
167
|
' "report": "the report as markdown: what you found, the evidence, the risks",',
|
|
@@ -183,7 +185,7 @@ function connectedResearch(store, taskRef, runId) {
|
|
|
183
185
|
const repo = store.refById(taskRef)?.repo ?? null;
|
|
184
186
|
if (repo === null)
|
|
185
187
|
return NO_RESEARCH;
|
|
186
|
-
return researchToolsOf(projectToolsOf(store, repo), new Set(toolLaunchFor(store, runId).tools.map(one => one.spec.name)));
|
|
188
|
+
return researchToolsOf(projectToolsOf(store, repo), new Set(toolLaunchFor(store, runId).tools.map(one => one.spec.name)), tool => grantOf(repo, tool));
|
|
187
189
|
}
|
|
188
190
|
catch {
|
|
189
191
|
return NO_RESEARCH;
|
|
@@ -325,6 +327,7 @@ async function scoutWith(store, request, outputDir) {
|
|
|
325
327
|
allowedTools: proxy === null ? [] : [...(browser === null ? SCOUT_RESEARCH_TOOLS : SCOUT_ALLOWED_TOOLS), ...research.allowed],
|
|
326
328
|
...(browser === null ? {} : { extraMcpServers: browser }),
|
|
327
329
|
readOnlyTools: research.reads,
|
|
330
|
+
...(research.withheld === undefined ? {} : { researchWithheld: research.withheld }),
|
|
328
331
|
skipPermissions: false,
|
|
329
332
|
resumeSession: null,
|
|
330
333
|
...(structured ? { jsonSchema: SCOUT_OUTPUT_JSON_SCHEMA } : {}),
|
|
@@ -484,7 +487,7 @@ async function scoutWith(store, request, outputDir) {
|
|
|
484
487
|
ok: false,
|
|
485
488
|
kind: "malformed",
|
|
486
489
|
reason: "malformed-report",
|
|
487
|
-
message: `the scout concluded, but the payload is not a report: ${parsed.problems.map(problem => problem.
|
|
490
|
+
message: `the scout concluded, but the payload is not a report: ${parsed.problems.map(problem => problem.message).join("; ")}`,
|
|
488
491
|
problems: parsed.problems,
|
|
489
492
|
};
|
|
490
493
|
}
|
|
@@ -544,7 +547,7 @@ async function scoutWith(store, request, outputDir) {
|
|
|
544
547
|
// the deliverable, and a task whose deliverable does not exist is not
|
|
545
548
|
// done.
|
|
546
549
|
try {
|
|
547
|
-
const content = Buffer.from(JSON.stringify(report, null, 2), "utf8");
|
|
550
|
+
const content = Buffer.from(JSON.stringify({ version: REPORT_VERSION, ...report }, null, 2), "utf8");
|
|
548
551
|
const key = writeEvidenceFile(root, request.runId, "report.json", content);
|
|
549
552
|
return {
|
|
550
553
|
ok: true,
|
|
@@ -630,17 +633,18 @@ function structuredHandback(structuredOutput) {
|
|
|
630
633
|
value = JSON.parse(structuredOutput);
|
|
631
634
|
}
|
|
632
635
|
catch {
|
|
633
|
-
return { kind: "invalid", problems: [{ reason: "structured-not-json", message: "the structured output is not JSON" }] };
|
|
636
|
+
return { kind: "invalid", problems: [{ reason: "structured-not-json", message: "payload: the structured output is not JSON" }] };
|
|
634
637
|
}
|
|
635
638
|
const body = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
return { kind: "invalid", problems: [{ reason: "structured-kind", message: 'the structured output must say kind "report" or "question"' }] };
|
|
639
|
+
const read = scoutHandbackSchema.shape.kind.safeParse(body["kind"]);
|
|
640
|
+
if (!read.success) {
|
|
641
|
+
return { kind: "invalid", problems: [{ reason: "structured-kind", message: `kind: must be ${scoutHandbackSchema.shape.kind.options.map(one => JSON.stringify(one)).join(" or ")}` }] };
|
|
640
642
|
}
|
|
643
|
+
const kind = read.data;
|
|
644
|
+
const inner = kind === "report" ? body["report"] : body["decision"];
|
|
641
645
|
if (inner === undefined || inner === null) {
|
|
642
646
|
const field = kind === "report" ? "report" : "decision";
|
|
643
|
-
return { kind: "invalid", problems: [{ reason: `structured-missing-${field}`, message:
|
|
647
|
+
return { kind: "invalid", problems: [{ reason: `structured-missing-${field}`, message: `${field}: required for kind "${kind}"` }] };
|
|
644
648
|
}
|
|
645
649
|
return { kind, raw: JSON.stringify(inner) };
|
|
646
650
|
}
|