toolroll 0.9.34 → 0.9.36
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 +44 -0
- package/README.md +7 -0
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/contracts/contract.d.ts +33 -5
- package/dist/contracts/contract.js +143 -16
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow.d.ts +19400 -0
- package/dist/contracts/flow.js +359 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/demo.js +28 -11
- package/dist/desktop-update.d.ts +2 -2
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +65 -83
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -218
- package/dist/flows.js +350 -385
- package/dist/mate-tools.js +14 -44
- package/dist/mcp-connect.d.ts +3 -0
- package/dist/mcp-connect.js +16 -4
- package/dist/operate.js +31 -2
- 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/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/serve.js +8 -99
- package/dist/setup-guide.d.ts +2 -2
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +75 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/text-limits.d.ts +70 -0
- package/dist/text-limits.js +70 -0
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/proof.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* so the review itself stays testable without a filesystem or a database.
|
|
20
20
|
*/
|
|
21
21
|
import { hasForbiddenControls } from "./decision.js";
|
|
22
|
-
import {
|
|
22
|
+
import { contractProblemOf } from "./contracts/contract.js";
|
|
23
|
+
import { PROOF_LIMITS, proofPayloadBody, readProofPayload } from "./contracts/proof.js";
|
|
24
|
+
export { PROOF_LIMITS } from "./contracts/proof.js";
|
|
23
25
|
/** Shared by preflight and final adjudication; presentation text is never a rubric. */
|
|
24
26
|
function criterionContractProblems(approved, answer) {
|
|
25
27
|
if (answer === undefined)
|
|
@@ -57,248 +59,96 @@ export function proofSubmissionProblems(proof, rubric) {
|
|
|
57
59
|
problems.push(...failedMetChecks(proof).map(failedCheckWords));
|
|
58
60
|
return problems;
|
|
59
61
|
}
|
|
60
|
-
/** Caps are BYTES of UTF-8, matching the scout report's rule: a payload is
|
|
61
|
-
* the same size whatever script it is written in. */
|
|
62
|
-
export const PROOF_LIMITS = {
|
|
63
|
-
payload: 64 * 1024,
|
|
64
|
-
criteria: 12,
|
|
65
|
-
criterionId: 40,
|
|
66
|
-
criterionStatement: 300,
|
|
67
|
-
criterionHow: 500,
|
|
68
|
-
evidencePerCriterion: 4,
|
|
69
|
-
evidenceRef: 300,
|
|
70
|
-
checks: 12,
|
|
71
|
-
checkCommand: 300,
|
|
72
|
-
checkSummary: 300,
|
|
73
|
-
changed: 64,
|
|
74
|
-
changedPath: 300,
|
|
75
|
-
caveats: 8,
|
|
76
|
-
caveat: 300,
|
|
77
|
-
screenshots: 8,
|
|
78
|
-
screenshotPath: 300,
|
|
79
|
-
screenshotCaption: 300,
|
|
80
|
-
};
|
|
81
62
|
function refuse(reason, message) {
|
|
82
63
|
return { ok: false, problems: [{ reason, message }] };
|
|
83
64
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
if (hasForbiddenControls(value)) {
|
|
107
|
-
problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
/** A repository-relative path: no leading slash, no drive letter, no `.`/`..`
|
|
113
|
-
* segment, no backslash, no control character. The same shape
|
|
114
|
-
* `readVerifiedArtifact` enforces on an evidence key, applied here to a
|
|
115
|
-
* path an agent claims rather than one the machine already wrote. */
|
|
116
|
-
function relativePath(value, field, cap, problems) {
|
|
117
|
-
const raw = prose(value, field, cap, problems);
|
|
118
|
-
if (raw === null)
|
|
119
|
-
return null;
|
|
120
|
-
const segments = raw.split("/");
|
|
121
|
-
const wellFormed = !raw.startsWith("/") &&
|
|
122
|
-
segments.every(segment => segment.length > 0 && segment !== "." && segment !== ".." && !segment.includes("\\"));
|
|
123
|
-
if (!wellFormed) {
|
|
124
|
-
problems.push({ reason: `${field}-not-relative`, message: `${field} must be a normalized repository-relative path (got ${describe(value)})` });
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
return raw;
|
|
128
|
-
}
|
|
129
|
-
/** `evidence` is optional on a criterion (absent → `[]`, exactly like every
|
|
130
|
-
* other list here) — the mandatory PRESENCE of an answer for a SIGNED
|
|
131
|
-
* criterion is `adjudicate`'s concern, not the parser's; this only proves
|
|
132
|
-
* the shape of what is there. */
|
|
133
|
-
function parseEvidenceRefs(value, field, problems) {
|
|
134
|
-
if (value === undefined || value === null)
|
|
135
|
-
return [];
|
|
136
|
-
if (!Array.isArray(value)) {
|
|
137
|
-
problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
if (value.length > PROOF_LIMITS.evidencePerCriterion) {
|
|
141
|
-
problems.push({ reason: `${field}-too-many`, message: `${field} lists ${value.length} — cap is ${PROOF_LIMITS.evidencePerCriterion}` });
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
const refs = [];
|
|
145
|
-
let bad = false;
|
|
146
|
-
for (const [index, entry] of value.entries()) {
|
|
147
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
148
|
-
problems.push({ reason: `${field}[${index}]-shape`, message: `${field}[${index}] must be an object` });
|
|
149
|
-
bad = true;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const one = entry;
|
|
153
|
-
const kind = one["kind"];
|
|
154
|
-
if (typeof kind !== "string" || !EVIDENCE_KINDS.includes(kind)) {
|
|
155
|
-
problems.push({
|
|
156
|
-
reason: `${field}[${index}]-bad-kind`,
|
|
157
|
-
message: `${field}[${index}].kind must draw from ${EVIDENCE_KINDS.join(", ")} (got ${describe(kind)})`,
|
|
158
|
-
});
|
|
159
|
-
bad = true;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const ref = prose(one["ref"], `${field}[${index}].ref`, PROOF_LIMITS.evidenceRef, problems);
|
|
163
|
-
if (ref === null) {
|
|
164
|
-
bad = true;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
refs.push({ kind: kind, ref });
|
|
168
|
-
}
|
|
169
|
-
return bad ? null : refs;
|
|
170
|
-
}
|
|
171
|
-
function parseCriteria(value, problems) {
|
|
172
|
-
if (value === undefined || value === null)
|
|
173
|
-
return [];
|
|
174
|
-
if (!Array.isArray(value)) {
|
|
175
|
-
problems.push({ reason: "bad-criteria", message: `criteria must be an array (got ${describe(value)})` });
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
if (value.length > PROOF_LIMITS.criteria) {
|
|
179
|
-
problems.push({ reason: "criteria-too-many", message: `criteria lists ${value.length} — cap is ${PROOF_LIMITS.criteria}` });
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
const criteria = [];
|
|
183
|
-
const seen = new Set();
|
|
184
|
-
for (const [index, entry] of value.entries()) {
|
|
185
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
186
|
-
problems.push({ reason: `criteria[${index}]-shape`, message: `criteria[${index}] must be an object` });
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const one = entry;
|
|
190
|
-
const id = prose(one["id"], `criteria[${index}].id`, PROOF_LIMITS.criterionId, problems);
|
|
191
|
-
if (id !== null && seen.has(id)) {
|
|
192
|
-
problems.push({ reason: `criteria[${index}]-duplicate-id`, message: `criterion id "${id}" appears twice` });
|
|
193
|
-
}
|
|
194
|
-
else if (id !== null) {
|
|
195
|
-
seen.add(id);
|
|
196
|
-
}
|
|
197
|
-
const statement = prose(one["statement"], `criteria[${index}].statement`, PROOF_LIMITS.criterionStatement, problems);
|
|
198
|
-
const how = prose(one["how"], `criteria[${index}].how`, PROOF_LIMITS.criterionHow, problems);
|
|
199
|
-
const verdict = one["verdict"];
|
|
200
|
-
if (verdict !== "met" && verdict !== "not-met" && verdict !== "not-checked" && verdict !== "pending-verification") {
|
|
201
|
-
problems.push({
|
|
202
|
-
reason: `criteria[${index}]-bad-verdict`,
|
|
203
|
-
message: `criteria[${index}].verdict must be "met", "not-met", "not-checked", or "pending-verification" (got ${describe(verdict)})`,
|
|
204
|
-
});
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const evidence = parseEvidenceRefs(one["evidence"], `criteria[${index}].evidence`, problems);
|
|
208
|
-
if (id === null || statement === null || how === null || evidence === null)
|
|
209
|
-
continue;
|
|
210
|
-
criteria.push({ id, statement, how, verdict, evidence });
|
|
211
|
-
}
|
|
212
|
-
return criteria;
|
|
213
|
-
}
|
|
214
|
-
function parseChecks(value, problems) {
|
|
215
|
-
if (value === undefined || value === null)
|
|
216
|
-
return [];
|
|
217
|
-
if (!Array.isArray(value)) {
|
|
218
|
-
problems.push({ reason: "bad-checks", message: `checks must be an array (got ${describe(value)})` });
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
if (value.length > PROOF_LIMITS.checks) {
|
|
222
|
-
problems.push({ reason: "checks-too-many", message: `checks lists ${value.length} — cap is ${PROOF_LIMITS.checks}` });
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
const checks = [];
|
|
226
|
-
for (const [index, entry] of value.entries()) {
|
|
227
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
228
|
-
problems.push({ reason: `checks[${index}]-shape`, message: `checks[${index}] must be an object` });
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const one = entry;
|
|
232
|
-
const command = prose(one["command"], `checks[${index}].command`, PROOF_LIMITS.checkCommand, problems);
|
|
233
|
-
const summary = prose(one["summary"], `checks[${index}].summary`, PROOF_LIMITS.checkSummary, problems);
|
|
234
|
-
const exitCode = one["exitCode"];
|
|
235
|
-
if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) {
|
|
236
|
-
problems.push({
|
|
237
|
-
reason: `checks[${index}]-bad-exit-code`,
|
|
238
|
-
message: `checks[${index}].exitCode must be an integer 0-255 (got ${describe(exitCode)})`,
|
|
65
|
+
/** Every text field of a proof body (version 2 shape) that the schema saw as a string. */
|
|
66
|
+
function textFields(body) {
|
|
67
|
+
const out = [];
|
|
68
|
+
const text = (path, value, bytes, relative) => {
|
|
69
|
+
if (typeof value === "string")
|
|
70
|
+
out.push({ path, value, bytes, ...(relative ? { relative } : {}) });
|
|
71
|
+
};
|
|
72
|
+
const records = (field) => {
|
|
73
|
+
const list = body[field];
|
|
74
|
+
return Array.isArray(list) ? list.flatMap((one, index) => (typeof one === "object" && one !== null && !Array.isArray(one) ? [[index, one]] : [])) : [];
|
|
75
|
+
};
|
|
76
|
+
for (const [index, one] of records("criteria")) {
|
|
77
|
+
text(`criteria[${index}].id`, one["id"], PROOF_LIMITS.criterionId);
|
|
78
|
+
text(`criteria[${index}].statement`, one["statement"], PROOF_LIMITS.criterionStatement);
|
|
79
|
+
text(`criteria[${index}].how`, one["how"], PROOF_LIMITS.criterionHow);
|
|
80
|
+
const evidence = one["evidence"];
|
|
81
|
+
if (Array.isArray(evidence)) {
|
|
82
|
+
evidence.forEach((ref, at) => {
|
|
83
|
+
if (typeof ref === "object" && ref !== null)
|
|
84
|
+
text(`criteria[${index}].evidence[${at}].ref`, ref["ref"], PROOF_LIMITS.evidenceRef);
|
|
239
85
|
});
|
|
240
|
-
continue;
|
|
241
86
|
}
|
|
242
|
-
if (command === null || summary === null)
|
|
243
|
-
continue;
|
|
244
|
-
checks.push({ command, summary, exitCode });
|
|
245
87
|
}
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (value === undefined || value === null)
|
|
250
|
-
return [];
|
|
251
|
-
if (!Array.isArray(value)) {
|
|
252
|
-
problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
|
|
253
|
-
return [];
|
|
88
|
+
for (const [index, one] of records("checks")) {
|
|
89
|
+
text(`checks[${index}].command`, one["command"], PROOF_LIMITS.checkCommand);
|
|
90
|
+
text(`checks[${index}].summary`, one["summary"], PROOF_LIMITS.checkSummary);
|
|
254
91
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
92
|
+
for (const field of ["changed", "caveats"]) {
|
|
93
|
+
const list = body[field];
|
|
94
|
+
if (Array.isArray(list))
|
|
95
|
+
list.forEach((one, index) => text(`${field}[${index}]`, one, field === "changed" ? PROOF_LIMITS.changedPath : PROOF_LIMITS.caveat));
|
|
258
96
|
}
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (item !== null)
|
|
263
|
-
out.push(item);
|
|
97
|
+
for (const [index, one] of records("screenshots")) {
|
|
98
|
+
text(`screenshots[${index}].path`, one["path"], PROOF_LIMITS.screenshotPath, true);
|
|
99
|
+
text(`screenshots[${index}].caption`, one["caption"], PROOF_LIMITS.screenshotCaption);
|
|
264
100
|
}
|
|
265
101
|
return out;
|
|
266
102
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const screenshots = [];
|
|
279
|
-
const seen = new Set();
|
|
280
|
-
for (const [index, entry] of value.entries()) {
|
|
281
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
282
|
-
problems.push({ reason: `screenshots[${index}]-shape`, message: `screenshots[${index}] must be an object` });
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
const one = entry;
|
|
286
|
-
const path = relativePath(one["path"], `screenshots[${index}].path`, PROOF_LIMITS.screenshotPath, problems);
|
|
287
|
-
const caption = prose(one["caption"], `screenshots[${index}].caption`, PROOF_LIMITS.screenshotCaption, problems);
|
|
288
|
-
if (path !== null && seen.has(path)) {
|
|
289
|
-
problems.push({ reason: `screenshots[${index}]-duplicate-path`, message: `screenshot path "${path}" appears twice` });
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (path === null || caption === null)
|
|
103
|
+
/**
|
|
104
|
+
* The rules JSON Schema cannot state, in plain code with path-named errors: blank text, UTF-8 byte limits, control
|
|
105
|
+
* characters, normalized repository-relative screenshot paths (no leading slash, `.`/`..` segment or backslash — the
|
|
106
|
+
* shape `readVerifiedArtifact` enforces on an evidence key), and unique criterion ids and screenshot paths. A field
|
|
107
|
+
* the schema already refused is not reported twice.
|
|
108
|
+
*/
|
|
109
|
+
function proofRuleProblems(body, contract) {
|
|
110
|
+
const named = (path) => contract.some(problem => problem.message.startsWith(`${path}:`));
|
|
111
|
+
const problems = [];
|
|
112
|
+
for (const field of textFields(body)) {
|
|
113
|
+
if (named(field.path))
|
|
293
114
|
continue;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
115
|
+
const { path, value } = field;
|
|
116
|
+
if (value.trim() === "")
|
|
117
|
+
problems.push({ reason: `missing-${path}`, message: `${path}: must not be blank` });
|
|
118
|
+
else if (Buffer.byteLength(value, "utf8") > field.bytes)
|
|
119
|
+
problems.push({ reason: `${path}-too-long`, message: `${path}: over ${field.bytes.toLocaleString("en-US")} bytes` });
|
|
120
|
+
else if (hasForbiddenControls(value))
|
|
121
|
+
problems.push({ reason: `${path}-controls`, message: `${path}: carries control characters that could become terminal escapes` });
|
|
122
|
+
else if (field.relative && (value.startsWith("/") || !value.split("/").every(segment => segment.length > 0 && segment !== "." && segment !== ".." && !segment.includes("\\")))) {
|
|
123
|
+
problems.push({ reason: `${path}-not-relative`, message: `${path}: must be a normalized repository-relative path` });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const unique = (field, key, what) => {
|
|
127
|
+
const list = body[field];
|
|
128
|
+
if (!Array.isArray(list) || named(field))
|
|
129
|
+
return;
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
list.forEach((one, index) => {
|
|
132
|
+
const value = typeof one === "object" && one !== null ? one[key] : undefined;
|
|
133
|
+
if (typeof value !== "string")
|
|
134
|
+
return;
|
|
135
|
+
if (seen.has(value))
|
|
136
|
+
problems.push({ reason: `${field}[${index}]-duplicate-${key}`, message: `${field}[${index}].${key}: ${what} ${JSON.stringify(value)} appears twice` });
|
|
137
|
+
seen.add(value);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
unique("criteria", "id", "criterion id");
|
|
141
|
+
unique("screenshots", "path", "screenshot path");
|
|
142
|
+
return problems;
|
|
298
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Read a proof: the payload byte cap and JSON first, then the proof schema (version 2 as itself, the version 1 a
|
|
146
|
+
* builder writes upgraded, a newer one refused plainly), then the plain-code rules. Every problem names its path and
|
|
147
|
+
* is reported at once; fail closed.
|
|
148
|
+
*/
|
|
299
149
|
export function parseProof(raw) {
|
|
300
150
|
if (Buffer.byteLength(raw, "utf8") > PROOF_LIMITS.payload) {
|
|
301
|
-
return refuse("too-large", `
|
|
151
|
+
return refuse("too-large", `payload: over ${PROOF_LIMITS.payload.toLocaleString("en-US")} bytes`);
|
|
302
152
|
}
|
|
303
153
|
let parsed;
|
|
304
154
|
try {
|
|
@@ -307,29 +157,20 @@ export function parseProof(raw) {
|
|
|
307
157
|
catch (error) {
|
|
308
158
|
return refuse("not-json", `the payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
159
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const checks = parseChecks(body["checks"], problems);
|
|
320
|
-
const changed = parseStringList(body["changed"], "changed", PROOF_LIMITS.changed, PROOF_LIMITS.changedPath, problems);
|
|
321
|
-
const caveats = parseStringList(body["caveats"], "caveats", PROOF_LIMITS.caveats, PROOF_LIMITS.caveat, problems);
|
|
322
|
-
const screenshots = parseScreenshots(body["screenshots"], problems);
|
|
323
|
-
if (problems.length > 0)
|
|
160
|
+
const body = proofPayloadBody(parsed);
|
|
161
|
+
if (body === null)
|
|
162
|
+
return refuse("not-an-object", "payload: must be one JSON object");
|
|
163
|
+
const read = readProofPayload(parsed);
|
|
164
|
+
const contract = read.ok ? [] : read.issues.map(contractProblemOf);
|
|
165
|
+
if (contract.some(problem => problem.reason === "newer-version" || problem.reason === "bad-version"))
|
|
166
|
+
return { ok: false, problems: contract };
|
|
167
|
+
const problems = [...contract, ...proofRuleProblems(body, contract)];
|
|
168
|
+
if (!read.ok || problems.length > 0)
|
|
324
169
|
return { ok: false, problems };
|
|
325
|
-
return {
|
|
326
|
-
ok: true,
|
|
327
|
-
proof: { version: 1, criteria, checks, changed, caveats, screenshots },
|
|
328
|
-
};
|
|
170
|
+
return { ok: true, proof: read.value };
|
|
329
171
|
}
|
|
330
|
-
/**
|
|
331
|
-
*
|
|
332
|
-
* is exactly what this parser admitted, key order and all. */
|
|
172
|
+
/** Serialize the validated shape, never the agent's raw bytes (the scout report's rule, `scout.ts`): what is stored
|
|
173
|
+
* and later hash-verified is exactly what the schema admitted, in its key order. */
|
|
333
174
|
export function serializeProof(proof) {
|
|
334
175
|
return JSON.stringify(proof, null, 2);
|
|
335
176
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Shared verified readers for result screens and the compact CLI review. */
|
|
2
|
+
import type { Artifact } from "./store.js";
|
|
3
|
+
export declare const SCREENSHOT_CAPTURE: RegExp;
|
|
4
|
+
/** What the run page shows of the terminal diff — verified bytes or a named problem, never silence. */
|
|
5
|
+
export type TerminalDiffView = {
|
|
6
|
+
patch: {
|
|
7
|
+
text: string;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
artifactId: number;
|
|
10
|
+
} | {
|
|
11
|
+
problem: string;
|
|
12
|
+
} | null;
|
|
13
|
+
stat: {
|
|
14
|
+
base: string;
|
|
15
|
+
head: string;
|
|
16
|
+
fileCount: number;
|
|
17
|
+
additions: number;
|
|
18
|
+
deletions: number;
|
|
19
|
+
binaryCount: number;
|
|
20
|
+
filesTruncated: boolean;
|
|
21
|
+
files: {
|
|
22
|
+
path: string;
|
|
23
|
+
additions: number | null;
|
|
24
|
+
deletions: number | null;
|
|
25
|
+
renamedFrom?: string;
|
|
26
|
+
}[];
|
|
27
|
+
} | {
|
|
28
|
+
problem: string;
|
|
29
|
+
} | null;
|
|
30
|
+
};
|
|
31
|
+
/** A compact, typed view of the agent-authored handoff. Older v1
|
|
32
|
+
* artifacts simply have no lists, while v2 can present the useful answer
|
|
33
|
+
* before the raw diff and evidence below it. */
|
|
34
|
+
export type StructuredHandoffView = {
|
|
35
|
+
conclusion: string;
|
|
36
|
+
changes: string[];
|
|
37
|
+
verification: string[];
|
|
38
|
+
followUps: string[];
|
|
39
|
+
};
|
|
40
|
+
export declare function structuredHandoffView(artifacts: Artifact[], root: string, displayText?: (text: string) => string): StructuredHandoffView | null;
|
|
41
|
+
/** A failed capture or unverifiable bytes keep their problem; absence returns null. */
|
|
42
|
+
export declare function terminalDiffView(artifacts: Artifact[], root: string): TerminalDiffView | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readVerifiedArtifact } from "./evidence.js";
|
|
2
|
+
import { hasForbiddenControls } from "./decision.js";
|
|
3
|
+
import { parseHandoffArtifact } from "./contracts/handoff.js";
|
|
4
|
+
const CAPTURE_EXIT = /\(exit ([0-9]{1,4})\)\s*$/;
|
|
5
|
+
export const SCREENSHOT_CAPTURE = /^agent-claimed screenshot at (.+) \(validated (?:png|jpeg)\)/;
|
|
6
|
+
const oneLineOf = (text, cap) => {
|
|
7
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
8
|
+
return flat.length <= cap ? flat : `${flat.slice(0, cap - 1)}…`;
|
|
9
|
+
};
|
|
10
|
+
export function structuredHandoffView(artifacts, root, displayText = text => text) {
|
|
11
|
+
const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
|
|
12
|
+
if (artifact === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
const read = readVerifiedArtifact(root, artifact);
|
|
15
|
+
if (!read.ok)
|
|
16
|
+
return null;
|
|
17
|
+
const parsed = parseHandoffArtifact(read.content.toString("utf8"));
|
|
18
|
+
if (!parsed.ok)
|
|
19
|
+
return null;
|
|
20
|
+
const handoff = parsed.value;
|
|
21
|
+
const conclusion = oneLineOf(displayText(handoff.conclusion), 600);
|
|
22
|
+
if (conclusion === "" || hasForbiddenControls(conclusion))
|
|
23
|
+
return null;
|
|
24
|
+
const list = (items) => (items ?? [])
|
|
25
|
+
.filter(one => one.trim() !== "" && !hasForbiddenControls(one))
|
|
26
|
+
.slice(0, 8)
|
|
27
|
+
.map(one => oneLineOf(one, 240));
|
|
28
|
+
return { conclusion, changes: list(handoff.changes), verification: list(handoff.verification), followUps: list(handoff.followUps) };
|
|
29
|
+
}
|
|
30
|
+
/** A failed capture or unverifiable bytes keep their problem; absence returns null. */
|
|
31
|
+
export function terminalDiffView(artifacts, root) {
|
|
32
|
+
const patchArtifact = artifacts.find(one => one.kind === "terminal-diff");
|
|
33
|
+
const statArtifact = artifacts.find(one => one.kind === "diff-stat");
|
|
34
|
+
if (patchArtifact === undefined && statArtifact === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const view = { patch: null, stat: null };
|
|
37
|
+
if (patchArtifact !== undefined) {
|
|
38
|
+
const exit = CAPTURE_EXIT.exec(patchArtifact.capture);
|
|
39
|
+
if (exit !== null && exit[1] !== "0") {
|
|
40
|
+
view.patch = { problem: `capture failed — ${patchArtifact.capture}` };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const read = readVerifiedArtifact(root, patchArtifact);
|
|
44
|
+
view.patch = read.ok
|
|
45
|
+
? { text: read.content.toString("utf8"), truncated: patchArtifact.truncated, artifactId: patchArtifact.id }
|
|
46
|
+
: { problem: `stored but unverifiable — ${read.problem}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (statArtifact !== undefined) {
|
|
50
|
+
const exit = CAPTURE_EXIT.exec(statArtifact.capture);
|
|
51
|
+
if (exit !== null && exit[1] !== "0") {
|
|
52
|
+
view.stat = { problem: `capture failed — ${statArtifact.capture}` };
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const read = readVerifiedArtifact(root, statArtifact);
|
|
56
|
+
if (!read.ok) {
|
|
57
|
+
view.stat = { problem: `stored but unverifiable — ${read.problem}` };
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(read.content.toString("utf8"));
|
|
62
|
+
// Every field this page renders is type-proved (arc 6, finding 7):
|
|
63
|
+
// "an object with a base key" was accepting any shape at all.
|
|
64
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value);
|
|
65
|
+
const wellFormed = parsed !== null &&
|
|
66
|
+
typeof parsed === "object" &&
|
|
67
|
+
typeof parsed["base"] === "string" &&
|
|
68
|
+
typeof parsed["head"] === "string" &&
|
|
69
|
+
count(parsed["fileCount"]) &&
|
|
70
|
+
count(parsed["additions"]) &&
|
|
71
|
+
count(parsed["deletions"]) &&
|
|
72
|
+
count(parsed["binaryCount"]) &&
|
|
73
|
+
typeof parsed["filesTruncated"] === "boolean" &&
|
|
74
|
+
Array.isArray(parsed["files"]) &&
|
|
75
|
+
parsed["files"].every(one => one !== null &&
|
|
76
|
+
typeof one === "object" &&
|
|
77
|
+
typeof one["path"] === "string" &&
|
|
78
|
+
(count(one["additions"]) || one["additions"] === null) &&
|
|
79
|
+
(count(one["deletions"]) || one["deletions"] === null));
|
|
80
|
+
view.stat = wellFormed
|
|
81
|
+
? parsed
|
|
82
|
+
: { problem: "stat is not the shape this page knows" };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
view.stat = { problem: "stat did not parse as JSON" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return view;
|
|
91
|
+
}
|