thoth-agents 0.9.7 → 0.9.9
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/dist/agents/prompt-dialects.d.ts +1 -0
- package/dist/{chunk-EDGQVTV3.js → chunk-2ER6FKVC.js} +52 -5
- package/dist/{chunk-V5GDCWFR.js → chunk-RN2ERZLZ.js} +1 -1
- package/dist/{chunk-ARUBELNT.js → chunk-RVQ6IIBR.js} +10 -4
- package/dist/chunk-RWQPMWEM.js +624 -0
- package/dist/{chunk-QQV5S3TW.js → chunk-VHQMTZ5O.js} +5 -24
- package/dist/cli/index.js +4 -4
- package/dist/cli/pi-tool-config.d.ts +0 -1
- package/dist/cli/tui/index.js +3 -3
- package/dist/harness/gate-hook-script.d.ts +2 -0
- package/dist/harness/writers/claude-code-plugin-package.d.ts +4 -4
- package/dist/harness/writers/pi-agent.d.ts +0 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/oracle-gate-verdict/hook.d.ts +11 -0
- package/dist/hooks/oracle-gate-verdict/index.d.ts +1 -0
- package/dist/index.js +42 -3
- package/dist/pi-gate-verdict.d.ts +4 -0
- package/dist/pi.d.ts +2 -1
- package/dist/pi.js +101 -2
- package/package.json +1 -1
- package/pi/.thoth-agents-assets.json +5 -5
- package/pi/agents/thoth-designer.md +0 -1
- package/pi/agents/thoth-explorer.md +0 -1
- package/pi/agents/thoth-librarian.md +0 -1
- package/pi/agents/thoth-oracle.md +0 -1
- package/pi/agents/thoth-worker.md +0 -1
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +6 -1
- package/plugin/.claude-plugin/plugin.json +3 -2
- package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/agents/orchestrator.md +5 -3
- package/plugin/claude-hooks/hooks.json +27 -0
- package/plugin/codex-hooks/hooks.json +14 -0
- package/plugin/codex-hooks/payloads/root.md +5 -3
- package/plugin/skills/plan-reviewer/SKILL.md +16 -5
- package/plugin/skills/thoth-sdd/SKILL.md +69 -23
- package/plugin/skills/thoth-sdd/references/phases/verify.md +6 -2
- package/plugin/skills/thoth-sdd/scripts/record-digest.mjs +15 -0
- package/plugin/skills/thoth-sdd/scripts/validate-result.d.mts +19 -0
- package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +110 -41
- package/plugin/skills/thoth-sdd/scripts/validate.mjs +7 -16
- package/plugin/thoth-hooks/gate-verdict.mjs +80 -0
- package/skills/plan-reviewer/SKILL.md +16 -5
- package/skills/thoth-sdd/SKILL.md +69 -23
- package/skills/thoth-sdd/references/phases/verify.md +6 -2
- package/skills/thoth-sdd/scripts/record-digest.mjs +15 -0
- package/skills/thoth-sdd/scripts/validate-result.d.mts +19 -0
- package/skills/thoth-sdd/scripts/validate-result.mjs +110 -41
- package/skills/thoth-sdd/scripts/validate.mjs +7 -16
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// skills/thoth-sdd/scripts/validate-result.mjs
|
|
2
|
+
import { readFileSync, realpathSync } from "fs";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
// skills/thoth-sdd/scripts/record-digest.mjs
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
function recordPrefixSha256(text) {
|
|
9
|
+
let offset = 0;
|
|
10
|
+
for (const raw of text.split(/(?<=\n)/)) {
|
|
11
|
+
let line = raw.replace(/\r?\n$/, "");
|
|
12
|
+
if (offset === 0) line = line.replace(/^\uFEFF/, "");
|
|
13
|
+
if (line === "## Authorization") break;
|
|
14
|
+
offset += raw.length;
|
|
15
|
+
}
|
|
16
|
+
return createHash("sha256").update(text.slice(0, offset), "utf8").digest("hex");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// skills/thoth-sdd/scripts/result-contract.json
|
|
20
|
+
var result_contract_default = {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
fence: "thoth-result",
|
|
23
|
+
common: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
schemaVersion: {
|
|
27
|
+
type: "number",
|
|
28
|
+
const: 1
|
|
29
|
+
},
|
|
30
|
+
role: {
|
|
31
|
+
type: "string",
|
|
32
|
+
enum: ["oracle"]
|
|
33
|
+
},
|
|
34
|
+
status: {
|
|
35
|
+
type: "string",
|
|
36
|
+
enum: ["complete", "partial", "blocked", "failed"]
|
|
37
|
+
},
|
|
38
|
+
conclusion: {
|
|
39
|
+
type: "string"
|
|
40
|
+
},
|
|
41
|
+
evidence: {
|
|
42
|
+
type: "array",
|
|
43
|
+
items: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
id: {
|
|
47
|
+
type: "string",
|
|
48
|
+
minLength: 1
|
|
49
|
+
},
|
|
50
|
+
kind: {
|
|
51
|
+
type: "string"
|
|
52
|
+
},
|
|
53
|
+
locator: {
|
|
54
|
+
type: "string"
|
|
55
|
+
},
|
|
56
|
+
claim: {
|
|
57
|
+
type: "string"
|
|
58
|
+
},
|
|
59
|
+
basis: {
|
|
60
|
+
type: "string",
|
|
61
|
+
enum: ["observed", "inferred"]
|
|
62
|
+
},
|
|
63
|
+
uncertainty: {
|
|
64
|
+
type: "string"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
required: [
|
|
68
|
+
"id",
|
|
69
|
+
"kind",
|
|
70
|
+
"locator",
|
|
71
|
+
"claim",
|
|
72
|
+
"basis",
|
|
73
|
+
"uncertainty"
|
|
74
|
+
],
|
|
75
|
+
additionalProperties: false
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
verification: {
|
|
79
|
+
type: "array",
|
|
80
|
+
items: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
check: {
|
|
84
|
+
type: "string"
|
|
85
|
+
},
|
|
86
|
+
method: {
|
|
87
|
+
type: "string"
|
|
88
|
+
},
|
|
89
|
+
outcome: {
|
|
90
|
+
type: "string",
|
|
91
|
+
enum: ["pass", "fail", "not-run"]
|
|
92
|
+
},
|
|
93
|
+
evidenceIds: {
|
|
94
|
+
type: "array",
|
|
95
|
+
items: {
|
|
96
|
+
type: "string"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
reason: {
|
|
100
|
+
type: "string"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
required: ["check", "method", "outcome", "evidenceIds", "reason"],
|
|
104
|
+
additionalProperties: false
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
risks: {
|
|
108
|
+
type: "array",
|
|
109
|
+
items: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
description: {
|
|
113
|
+
type: "string"
|
|
114
|
+
},
|
|
115
|
+
severity: {
|
|
116
|
+
type: "string",
|
|
117
|
+
enum: ["low", "medium", "high", "critical"]
|
|
118
|
+
},
|
|
119
|
+
blocking: {
|
|
120
|
+
type: "boolean"
|
|
121
|
+
},
|
|
122
|
+
evidenceIds: {
|
|
123
|
+
type: "array",
|
|
124
|
+
items: {
|
|
125
|
+
type: "string"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
required: ["description", "severity", "blocking", "evidenceIds"],
|
|
130
|
+
additionalProperties: false
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
openQuestions: {
|
|
134
|
+
type: "array",
|
|
135
|
+
items: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
question: {
|
|
139
|
+
type: "string"
|
|
140
|
+
},
|
|
141
|
+
owner: {
|
|
142
|
+
type: "string"
|
|
143
|
+
},
|
|
144
|
+
options: {
|
|
145
|
+
type: "array",
|
|
146
|
+
items: {
|
|
147
|
+
type: "string"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
facts: {
|
|
151
|
+
type: "array",
|
|
152
|
+
items: {
|
|
153
|
+
type: "string"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
required: ["question", "owner", "options", "facts"],
|
|
158
|
+
additionalProperties: false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
required: [
|
|
163
|
+
"schemaVersion",
|
|
164
|
+
"role",
|
|
165
|
+
"status",
|
|
166
|
+
"conclusion",
|
|
167
|
+
"evidence",
|
|
168
|
+
"verification",
|
|
169
|
+
"risks",
|
|
170
|
+
"openQuestions"
|
|
171
|
+
],
|
|
172
|
+
additionalProperties: false
|
|
173
|
+
},
|
|
174
|
+
roles: {
|
|
175
|
+
oracle: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
nextAction: {
|
|
179
|
+
type: ["string", "null"]
|
|
180
|
+
},
|
|
181
|
+
review: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
kind: {
|
|
185
|
+
type: "string",
|
|
186
|
+
enum: ["plan-review", "final-verification"]
|
|
187
|
+
},
|
|
188
|
+
verdict: {
|
|
189
|
+
type: "string"
|
|
190
|
+
},
|
|
191
|
+
reviewedRecord: {
|
|
192
|
+
type: "string",
|
|
193
|
+
minLength: 1
|
|
194
|
+
},
|
|
195
|
+
reviewedRecordSha256: {
|
|
196
|
+
type: "string",
|
|
197
|
+
pattern: "^[a-f0-9]{64}$"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
required: ["kind", "verdict"],
|
|
201
|
+
additionalProperties: false
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
required: ["nextAction", "review"],
|
|
205
|
+
additionalProperties: false
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
reviewKinds: {
|
|
209
|
+
"plan-review": {
|
|
210
|
+
verdicts: ["OKAY", "REJECT"]
|
|
211
|
+
},
|
|
212
|
+
"final-verification": {
|
|
213
|
+
verdicts: ["PASS", "FAIL", "BLOCKED"]
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
rules: {
|
|
217
|
+
positiveVerdicts: ["OKAY", "PASS"],
|
|
218
|
+
positiveStatus: "complete",
|
|
219
|
+
positiveAllowsBlockingRisks: false,
|
|
220
|
+
evidenceIdUnique: true,
|
|
221
|
+
evidenceReferences: ["verification", "risks"],
|
|
222
|
+
recordBindingReviewKind: "final-verification",
|
|
223
|
+
recordBindingRequiredFields: ["reviewedRecord", "reviewedRecordSha256"],
|
|
224
|
+
recordBindingForbiddenUnderNoRecord: [
|
|
225
|
+
"reviewedRecord",
|
|
226
|
+
"reviewedRecordSha256"
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// skills/thoth-sdd/scripts/validate-result.mjs
|
|
232
|
+
var childPath = (path, key) => `${path}/${String(key).replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
233
|
+
function parseStrictJSON(source) {
|
|
234
|
+
let cursor = 0;
|
|
235
|
+
const fail = (code, path, message) => {
|
|
236
|
+
throw { code, path, message };
|
|
237
|
+
};
|
|
238
|
+
const whitespace = () => {
|
|
239
|
+
while (/[\t\n\r ]/.test(source[cursor] ?? "\0")) cursor++;
|
|
240
|
+
};
|
|
241
|
+
const syntax = (path) => fail("JSON_SYNTAX", path, `Invalid JSON at offset ${cursor}.`);
|
|
242
|
+
function string(path) {
|
|
243
|
+
const start = cursor++;
|
|
244
|
+
while (cursor < source.length) {
|
|
245
|
+
const char = source[cursor++];
|
|
246
|
+
if (char === "\\") cursor++;
|
|
247
|
+
else if (char === '"') {
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(source.slice(start, cursor));
|
|
250
|
+
} catch {
|
|
251
|
+
syntax(path);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
syntax(path);
|
|
256
|
+
}
|
|
257
|
+
function value(path, depth) {
|
|
258
|
+
if (depth > 256)
|
|
259
|
+
fail("JSON_SYNTAX", path, "JSON nesting exceeds 256 levels.");
|
|
260
|
+
whitespace();
|
|
261
|
+
const char = source[cursor];
|
|
262
|
+
if (char === '"') return string(path);
|
|
263
|
+
if (char === "{" || char === "[") {
|
|
264
|
+
const isObject = char === "{";
|
|
265
|
+
const close = isObject ? "}" : "]";
|
|
266
|
+
const result3 = isObject ? /* @__PURE__ */ Object.create(null) : [];
|
|
267
|
+
const keys = /* @__PURE__ */ new Set();
|
|
268
|
+
cursor++;
|
|
269
|
+
whitespace();
|
|
270
|
+
if (source[cursor] === close) {
|
|
271
|
+
cursor++;
|
|
272
|
+
return result3;
|
|
273
|
+
}
|
|
274
|
+
for (; ; ) {
|
|
275
|
+
let key = result3.length;
|
|
276
|
+
if (isObject) {
|
|
277
|
+
if (source[cursor] !== '"') syntax(path);
|
|
278
|
+
key = string(path);
|
|
279
|
+
if (keys.has(key))
|
|
280
|
+
fail(
|
|
281
|
+
"JSON_DUPLICATE_KEY",
|
|
282
|
+
childPath(path, key),
|
|
283
|
+
`Duplicate key ${JSON.stringify(key)}.`
|
|
284
|
+
);
|
|
285
|
+
keys.add(key);
|
|
286
|
+
whitespace();
|
|
287
|
+
if (source[cursor++] !== ":") syntax(path);
|
|
288
|
+
}
|
|
289
|
+
result3[key] = value(childPath(path, key), depth + 1);
|
|
290
|
+
whitespace();
|
|
291
|
+
if (source[cursor] === close) {
|
|
292
|
+
cursor++;
|
|
293
|
+
return result3;
|
|
294
|
+
}
|
|
295
|
+
if (source[cursor++] !== ",") syntax(path);
|
|
296
|
+
whitespace();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const token = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(
|
|
300
|
+
source.slice(cursor)
|
|
301
|
+
);
|
|
302
|
+
if (!token) syntax(path);
|
|
303
|
+
cursor += token[0].length;
|
|
304
|
+
const result2 = JSON.parse(token[0]);
|
|
305
|
+
if (typeof result2 === "number" && !Number.isFinite(result2)) syntax(path);
|
|
306
|
+
return result2;
|
|
307
|
+
}
|
|
308
|
+
const result = value("", 0);
|
|
309
|
+
whitespace();
|
|
310
|
+
if (cursor !== source.length) syntax("");
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
function checkExpectations(options, issue) {
|
|
314
|
+
if (options.noRecord && (options.recordSha256 !== void 0 || options.reviewKind !== void 0 && options.reviewKind !== result_contract_default.rules.recordBindingReviewKind))
|
|
315
|
+
issue(
|
|
316
|
+
"EXPECTATION_INVALID",
|
|
317
|
+
"",
|
|
318
|
+
"No-record mode requires final-verification without an expected digest."
|
|
319
|
+
);
|
|
320
|
+
const expectations = [
|
|
321
|
+
["role", Object.keys(result_contract_default.roles)],
|
|
322
|
+
["reviewKind", Object.keys(result_contract_default.reviewKinds)]
|
|
323
|
+
];
|
|
324
|
+
for (const [key, values] of expectations) {
|
|
325
|
+
if (options[key] !== void 0 && !values.includes(options[key]))
|
|
326
|
+
issue("EXPECTATION_INVALID", "", `Unknown expected ${key}.`);
|
|
327
|
+
}
|
|
328
|
+
if (options.recordSha256 !== void 0 && !/^[a-f0-9]{64}$/.test(options.recordSha256))
|
|
329
|
+
issue(
|
|
330
|
+
"EXPECTATION_INVALID",
|
|
331
|
+
"",
|
|
332
|
+
"Expected digest must be 64 lowercase hex characters."
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
function parseEnvelope(message) {
|
|
336
|
+
const trimmed = typeof message === "string" ? message.trim() : "";
|
|
337
|
+
const block = new RegExp(
|
|
338
|
+
`^\`\`\`${result_contract_default.fence}\\r?\\n([\\s\\S]*?)\\r?\\n\`\`\`$`
|
|
339
|
+
).exec(trimmed);
|
|
340
|
+
if (!block || (trimmed.match(/^```.*$/gm) ?? []).length !== 2) {
|
|
341
|
+
return {
|
|
342
|
+
issues: [
|
|
343
|
+
{
|
|
344
|
+
code: "ENVELOPE_FORMAT",
|
|
345
|
+
path: "",
|
|
346
|
+
message: "Expected exactly one thoth-result fence and no surrounding prose."
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
return { envelope: parseStrictJSON(block[1]), issues: [] };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return { issues: [error] };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function validateEnvelope(parsed, options) {
|
|
358
|
+
const issues = [];
|
|
359
|
+
const issue = (code, path, text) => issues.push({ code, path, message: text });
|
|
360
|
+
const invalid = () => ({ valid: false, issues });
|
|
361
|
+
checkExpectations(options, issue);
|
|
362
|
+
if (issues.length) return invalid();
|
|
363
|
+
if (parsed.issues.length) return { valid: false, issues: parsed.issues };
|
|
364
|
+
const { envelope } = parsed;
|
|
365
|
+
function check(value, schema, path) {
|
|
366
|
+
const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
367
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
368
|
+
if (!types.includes(type)) {
|
|
369
|
+
issue("TYPE_MISMATCH", path, `Expected ${types.join(" or ")}.`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if ("const" in schema && value !== schema.const)
|
|
373
|
+
issue("INVALID_VALUE", path, "Value differs from required constant.");
|
|
374
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
375
|
+
issue("INVALID_VALUE", path, "Value is not in the allowed enumeration.");
|
|
376
|
+
if (type === "string") {
|
|
377
|
+
if (schema.minLength && value.length < schema.minLength)
|
|
378
|
+
issue("INVALID_VALUE", path, "String must not be empty.");
|
|
379
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value))
|
|
380
|
+
issue(
|
|
381
|
+
"INVALID_VALUE",
|
|
382
|
+
path,
|
|
383
|
+
"String does not match the required pattern."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (type === "array") {
|
|
387
|
+
value.forEach((item, index) => {
|
|
388
|
+
check(item, schema.items, childPath(path, index));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (type === "object") {
|
|
392
|
+
for (const key of schema.required)
|
|
393
|
+
if (!Object.hasOwn(value, key))
|
|
394
|
+
issue(
|
|
395
|
+
"REQUIRED_FIELD",
|
|
396
|
+
childPath(path, key),
|
|
397
|
+
"Required field is missing."
|
|
398
|
+
);
|
|
399
|
+
for (const key of Object.keys(value)) {
|
|
400
|
+
if (!Object.hasOwn(schema.properties, key))
|
|
401
|
+
issue(
|
|
402
|
+
"UNDECLARED_FIELD",
|
|
403
|
+
childPath(path, key),
|
|
404
|
+
"Undeclared field is forbidden."
|
|
405
|
+
);
|
|
406
|
+
else check(value[key], schema.properties[key], childPath(path, key));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const roleSchema = envelope && typeof envelope.role === "string" && Object.hasOwn(result_contract_default.roles, envelope.role) ? result_contract_default.roles[envelope.role] : { properties: {}, required: [] };
|
|
411
|
+
check(
|
|
412
|
+
envelope,
|
|
413
|
+
{
|
|
414
|
+
...result_contract_default.common,
|
|
415
|
+
properties: { ...result_contract_default.common.properties, ...roleSchema.properties },
|
|
416
|
+
required: [...result_contract_default.common.required, ...roleSchema.required]
|
|
417
|
+
},
|
|
418
|
+
""
|
|
419
|
+
);
|
|
420
|
+
if (issues.length) return invalid();
|
|
421
|
+
if (options.noRecord && envelope.review.kind !== result_contract_default.rules.recordBindingReviewKind) {
|
|
422
|
+
issue(
|
|
423
|
+
"EXPECTATION_INVALID",
|
|
424
|
+
"/review/kind",
|
|
425
|
+
"No-record mode requires final-verification."
|
|
426
|
+
);
|
|
427
|
+
return invalid();
|
|
428
|
+
}
|
|
429
|
+
if (options.role && envelope.role !== options.role)
|
|
430
|
+
issue("ROLE_MISMATCH", "/role", "Role does not match assignment.");
|
|
431
|
+
if (options.reviewKind && envelope.review.kind !== options.reviewKind)
|
|
432
|
+
issue(
|
|
433
|
+
"REVIEW_KIND_MISMATCH",
|
|
434
|
+
"/review/kind",
|
|
435
|
+
"Review kind does not match assignment."
|
|
436
|
+
);
|
|
437
|
+
const ids = /* @__PURE__ */ new Set();
|
|
438
|
+
envelope.evidence.forEach((entry, index) => {
|
|
439
|
+
if (result_contract_default.rules.evidenceIdUnique && ids.has(entry.id))
|
|
440
|
+
issue(
|
|
441
|
+
"EVIDENCE_ID_DUPLICATE",
|
|
442
|
+
`/evidence/${index}/id`,
|
|
443
|
+
"Evidence ids must be unique."
|
|
444
|
+
);
|
|
445
|
+
ids.add(entry.id);
|
|
446
|
+
});
|
|
447
|
+
for (const field of result_contract_default.rules.evidenceReferences) {
|
|
448
|
+
envelope[field].forEach((entry, index) => {
|
|
449
|
+
entry.evidenceIds.forEach((id, ref) => {
|
|
450
|
+
if (!ids.has(id))
|
|
451
|
+
issue(
|
|
452
|
+
"EVIDENCE_REFERENCE",
|
|
453
|
+
`/${field}/${index}/evidenceIds/${ref}`,
|
|
454
|
+
"Reference does not identify supplied evidence."
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const { kind, verdict } = envelope.review;
|
|
460
|
+
if (!result_contract_default.reviewKinds[kind].verdicts.includes(verdict))
|
|
461
|
+
issue(
|
|
462
|
+
"VERDICT_MISMATCH",
|
|
463
|
+
"/review/verdict",
|
|
464
|
+
"Verdict is not allowed for this review kind."
|
|
465
|
+
);
|
|
466
|
+
if (result_contract_default.rules.positiveVerdicts.includes(verdict)) {
|
|
467
|
+
if (envelope.status !== result_contract_default.rules.positiveStatus)
|
|
468
|
+
issue(
|
|
469
|
+
"VERDICT_STATUS",
|
|
470
|
+
"/status",
|
|
471
|
+
"Positive verdict requires complete status."
|
|
472
|
+
);
|
|
473
|
+
envelope.risks.forEach((risk, index) => {
|
|
474
|
+
if (!result_contract_default.rules.positiveAllowsBlockingRisks && risk.blocking)
|
|
475
|
+
issue(
|
|
476
|
+
"BLOCKING_RISK",
|
|
477
|
+
`/risks/${index}/blocking`,
|
|
478
|
+
"Positive verdict forbids blocking risks."
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (options.noRecord) {
|
|
483
|
+
for (const key of result_contract_default.rules.recordBindingForbiddenUnderNoRecord) {
|
|
484
|
+
if (Object.hasOwn(envelope.review, key))
|
|
485
|
+
issue(
|
|
486
|
+
"RECORD_BINDING_FORBIDDEN",
|
|
487
|
+
`/review/${key}`,
|
|
488
|
+
"No-record mode forbids reviewed record binding."
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
} else if (envelope.review.kind === result_contract_default.rules.recordBindingReviewKind) {
|
|
492
|
+
for (const key of result_contract_default.rules.recordBindingRequiredFields) {
|
|
493
|
+
if (!envelope.review[key])
|
|
494
|
+
issue(
|
|
495
|
+
"RECORD_BINDING_REQUIRED",
|
|
496
|
+
`/review/${key}`,
|
|
497
|
+
"Final verification requires reviewed record binding."
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (options.recordSha256 !== void 0) {
|
|
502
|
+
if (envelope.review.kind !== result_contract_default.rules.recordBindingReviewKind)
|
|
503
|
+
issue(
|
|
504
|
+
"REVIEW_KIND_MISMATCH",
|
|
505
|
+
"/review/kind",
|
|
506
|
+
"Record binding requires final-verification."
|
|
507
|
+
);
|
|
508
|
+
if (envelope.review.reviewedRecordSha256 && envelope.review.reviewedRecordSha256 !== options.recordSha256)
|
|
509
|
+
issue(
|
|
510
|
+
"RECORD_DIGEST_MISMATCH",
|
|
511
|
+
"/review/reviewedRecordSha256",
|
|
512
|
+
"Digest does not match assignment."
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return issues.length ? invalid() : { valid: true, envelope, issues };
|
|
516
|
+
}
|
|
517
|
+
var stringOrNull = (value) => typeof value === "string" ? value : null;
|
|
518
|
+
function gateMode(envelope) {
|
|
519
|
+
const review = envelope?.review;
|
|
520
|
+
return review?.kind === result_contract_default.rules.recordBindingReviewKind && result_contract_default.rules.recordBindingRequiredFields.every(
|
|
521
|
+
(key) => !Object.hasOwn(review, key)
|
|
522
|
+
) ? "no-record" : "record";
|
|
523
|
+
}
|
|
524
|
+
function recordDigest(review, cwd) {
|
|
525
|
+
if (!review || !Object.hasOwn(review, "reviewedRecordSha256")) return "none";
|
|
526
|
+
if (!cwd || typeof review.reviewedRecord !== "string") return "unavailable";
|
|
527
|
+
const inside = (root, path) => {
|
|
528
|
+
const fromRoot = relative(root, path);
|
|
529
|
+
return fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
|
|
530
|
+
};
|
|
531
|
+
try {
|
|
532
|
+
const root = resolve(cwd);
|
|
533
|
+
const path = resolve(root, review.reviewedRecord);
|
|
534
|
+
if (!inside(root, path) || !inside(realpathSync(root), realpathSync(path)))
|
|
535
|
+
return "unavailable";
|
|
536
|
+
return recordPrefixSha256(readFileSync(path, "utf8")) === review.reviewedRecordSha256 ? "match" : "mismatch";
|
|
537
|
+
} catch {
|
|
538
|
+
return "unavailable";
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function summarizeValidation(envelope, result, cwd) {
|
|
542
|
+
const review = envelope?.review;
|
|
543
|
+
return {
|
|
544
|
+
valid: result.valid,
|
|
545
|
+
role: stringOrNull(envelope?.role),
|
|
546
|
+
kind: stringOrNull(review?.kind),
|
|
547
|
+
verdict: stringOrNull(review?.verdict),
|
|
548
|
+
mode: gateMode(envelope),
|
|
549
|
+
record: stringOrNull(review?.reviewedRecord),
|
|
550
|
+
recordSha256: stringOrNull(review?.reviewedRecordSha256),
|
|
551
|
+
recordDigest: recordDigest(review, cwd),
|
|
552
|
+
issues: result.issues.map(({ code, path }) => ({ code, path }))
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function summarizeGateResult(message, { cwd } = {}) {
|
|
556
|
+
const parsed = parseEnvelope(message);
|
|
557
|
+
return summarizeValidation(
|
|
558
|
+
parsed.envelope,
|
|
559
|
+
validateEnvelope(parsed, {
|
|
560
|
+
noRecord: gateMode(parsed.envelope) === "no-record"
|
|
561
|
+
}),
|
|
562
|
+
cwd
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
function formatGateVerdict(summary) {
|
|
566
|
+
return `thoth-gate-verdict ${JSON.stringify(summary)}`;
|
|
567
|
+
}
|
|
568
|
+
function main(args) {
|
|
569
|
+
const print = (result2, envelope) => console.log(
|
|
570
|
+
JSON.stringify(summarizeValidation(envelope, result2, process.cwd()))
|
|
571
|
+
);
|
|
572
|
+
const options = {};
|
|
573
|
+
let input;
|
|
574
|
+
const names = {
|
|
575
|
+
"--role": "role",
|
|
576
|
+
"--review-kind": "reviewKind",
|
|
577
|
+
"--record-sha256": "recordSha256",
|
|
578
|
+
"--no-record": "noRecord",
|
|
579
|
+
"--input": "input"
|
|
580
|
+
};
|
|
581
|
+
const seen = /* @__PURE__ */ new Set();
|
|
582
|
+
for (let index = 0; index < args.length; index++) {
|
|
583
|
+
const flag = args[index];
|
|
584
|
+
if (!Object.hasOwn(names, flag) || seen.has(flag) || flag !== "--no-record" && (!args[index + 1] || args[index + 1].startsWith("--"))) {
|
|
585
|
+
print({ valid: false, issues: [{ code: "CLI_USAGE", path: "" }] });
|
|
586
|
+
return 2;
|
|
587
|
+
}
|
|
588
|
+
seen.add(flag);
|
|
589
|
+
if (flag === "--no-record") {
|
|
590
|
+
options.noRecord = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const value = args[++index];
|
|
594
|
+
if (flag === "--input") input = value;
|
|
595
|
+
else options[names[flag]] = value;
|
|
596
|
+
}
|
|
597
|
+
const expectationIssues = [];
|
|
598
|
+
checkExpectations(options, (code, path, message2) => {
|
|
599
|
+
expectationIssues.push({ code, path, message: message2 });
|
|
600
|
+
});
|
|
601
|
+
if (expectationIssues.length) {
|
|
602
|
+
print({ valid: false, issues: expectationIssues });
|
|
603
|
+
return 2;
|
|
604
|
+
}
|
|
605
|
+
let message;
|
|
606
|
+
try {
|
|
607
|
+
message = readFileSync(input ?? 0, "utf8");
|
|
608
|
+
} catch {
|
|
609
|
+
print({ valid: false, issues: [{ code: "CLI_INPUT", path: "" }] });
|
|
610
|
+
return 2;
|
|
611
|
+
}
|
|
612
|
+
const parsed = parseEnvelope(message);
|
|
613
|
+
const result = validateEnvelope(parsed, options);
|
|
614
|
+
print(result, parsed.envelope);
|
|
615
|
+
if (result.valid) return 0;
|
|
616
|
+
return result.issues.some((issue) => issue.code === "EXPECTATION_INVALID") ? 2 : 1;
|
|
617
|
+
}
|
|
618
|
+
if (import.meta.url.endsWith("/validate-result.mjs") && process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
619
|
+
process.exitCode = main(process.argv.slice(2));
|
|
620
|
+
|
|
621
|
+
export {
|
|
622
|
+
summarizeGateResult,
|
|
623
|
+
formatGateVerdict
|
|
624
|
+
};
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
renderAgentRoutingDescription,
|
|
20
20
|
renderPromptSection,
|
|
21
21
|
renderRolePrompt
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-RVQ6IIBR.js";
|
|
23
23
|
|
|
24
24
|
// src/harness/writers/pi-agent.ts
|
|
25
25
|
var PI_MANAGED_OWNER = "thoth-agents";
|
|
@@ -63,7 +63,6 @@ function renderPiAgentDefinition(input) {
|
|
|
63
63
|
...input.model ? [`model: ${yamlScalar(input.model)}`] : [],
|
|
64
64
|
...input.effort ? [`effort: ${yamlScalar(input.effort)}`] : [],
|
|
65
65
|
...input.skills !== void 0 ? [`skills: ${JSON.stringify(input.skills)}`] : [],
|
|
66
|
-
`subagent_mode: ${yamlScalar(input.subagentMode)}`,
|
|
67
66
|
`managed-by: ${PI_MANAGED_OWNER}`,
|
|
68
67
|
"---",
|
|
69
68
|
"",
|
|
@@ -263,8 +262,7 @@ function readOwnedPiSpecialistDefinition(piRoot, role) {
|
|
|
263
262
|
"model",
|
|
264
263
|
"effort",
|
|
265
264
|
"thinking",
|
|
266
|
-
"tools"
|
|
267
|
-
"subagent_mode"
|
|
265
|
+
"tools"
|
|
268
266
|
]) {
|
|
269
267
|
if ([...frontmatter(content).matchAll(new RegExp(`^${name}:`, "gm"))].length > 1) {
|
|
270
268
|
throw new Error(`Duplicate ${name} in managed Pi definition: ${path}`);
|
|
@@ -380,7 +378,7 @@ function parseFrontmatter(content) {
|
|
|
380
378
|
});
|
|
381
379
|
const mapping = document.contents;
|
|
382
380
|
if (isMap(mapping)) {
|
|
383
|
-
for (const name of ["tools", "disallowed_tools"
|
|
381
|
+
for (const name of ["tools", "disallowed_tools"]) {
|
|
384
382
|
const occurrences = mapping.items.filter(
|
|
385
383
|
({ key }) => isScalar(key) && key.value === name
|
|
386
384
|
);
|
|
@@ -517,23 +515,13 @@ function parseDisallowedToolsField(mapping, source) {
|
|
|
517
515
|
}
|
|
518
516
|
return [...new Set(tools)];
|
|
519
517
|
}
|
|
520
|
-
function parseModeField(mapping, source) {
|
|
521
|
-
const pair = fieldPair(mapping, "subagent_mode");
|
|
522
|
-
if (!pair) return void 0;
|
|
523
|
-
const mode = scalarString(pair.value, "subagent_mode", source);
|
|
524
|
-
if (mode !== "task" && mode !== "background")
|
|
525
|
-
throw new Error('Invalid subagent_mode; expected "task" or "background".');
|
|
526
|
-
return mode;
|
|
527
|
-
}
|
|
528
518
|
function readPiSpecialistToolOverrides(content) {
|
|
529
519
|
const { mapping, source } = parseFrontmatter(content);
|
|
530
520
|
const parsedTools = parseToolsField(mapping, source);
|
|
531
521
|
const disallowedTools = parseDisallowedToolsField(mapping, source);
|
|
532
|
-
const subagentMode = parseModeField(mapping, source);
|
|
533
522
|
return {
|
|
534
523
|
...parsedTools.present ? { tools: parsedTools.tools } : {},
|
|
535
|
-
...disallowedTools !== void 0 ? { disallowedTools } : {}
|
|
536
|
-
...subagentMode ? { subagentMode } : {}
|
|
524
|
+
...disallowedTools !== void 0 ? { disallowedTools } : {}
|
|
537
525
|
};
|
|
538
526
|
}
|
|
539
527
|
function createPiToolsPanelAdapter(piRoot) {
|
|
@@ -641,12 +629,6 @@ function preserveOverrides(next, current, role) {
|
|
|
641
629
|
"disallowed_tools",
|
|
642
630
|
JSON.stringify(toolOverrides.disallowedTools.join(", "))
|
|
643
631
|
);
|
|
644
|
-
if (toolOverrides.subagentMode)
|
|
645
|
-
frontmatter2 = replaceField2(
|
|
646
|
-
frontmatter2,
|
|
647
|
-
"subagent_mode",
|
|
648
|
-
JSON.stringify(toolOverrides.subagentMode)
|
|
649
|
-
);
|
|
650
632
|
return `${frontmatter2}${next.slice(end)}`;
|
|
651
633
|
}
|
|
652
634
|
function syncPiSpecialists(options) {
|
|
@@ -702,7 +684,7 @@ function syncPiSpecialists(options) {
|
|
|
702
684
|
content = preserveOverrides(content, current, role);
|
|
703
685
|
} catch (error) {
|
|
704
686
|
diagnostics2.push(
|
|
705
|
-
`Preserved ${name} unchanged because its tool, disallowed_tools,
|
|
687
|
+
`Preserved ${name} unchanged because its tool, disallowed_tools, or skills override is malformed: ${error instanceof Error ? error.message : String(error)}. Fix the frontmatter explicitly before synchronizing this role.`
|
|
706
688
|
);
|
|
707
689
|
continue;
|
|
708
690
|
}
|
|
@@ -818,7 +800,6 @@ function roleArtifacts(config) {
|
|
|
818
800
|
role: { ...role, name: role.name },
|
|
819
801
|
model,
|
|
820
802
|
effort: override ? void 0 : preset.effort,
|
|
821
|
-
subagentMode: "background",
|
|
822
803
|
skills: config?.agents?.[role.name]?.skills,
|
|
823
804
|
description: renderAgentRoutingDescription(role),
|
|
824
805
|
instructions: [
|