thoth-agents 0.5.1 → 0.7.0
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/README.md +88 -11
- package/dist/agents/prompt-dialects.d.ts +1 -0
- package/dist/{chunk-MRFCA3U2.js → chunk-3MHJGWS4.js} +2 -1
- package/dist/{chunk-V7HSGRMB.js → chunk-P3GDM5RH.js} +70 -174
- package/dist/{chunk-OW6I7ZUU.js → chunk-QOIZYJ5C.js} +322 -151
- package/dist/{chunk-VUNYH263.js → chunk-W5TSNGEE.js} +263 -44
- package/dist/cli/index.js +4 -4
- package/dist/cli/pi-install.d.ts +35 -67
- package/dist/cli/pi-tool-config.d.ts +19 -0
- package/dist/cli/tui/index.js +3 -3
- package/dist/config/constants.d.ts +30 -3
- package/dist/config/schema.d.ts +4 -0
- package/dist/harness/adapters/claude-code.d.ts +1 -1
- package/dist/harness/core/agent-pack.d.ts +6 -2
- package/dist/harness/core/result-contract.d.ts +366 -0
- package/dist/harness/core/sdd.d.ts +1 -1
- package/dist/harness/writers/pi-agent.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/pi.d.ts +6 -35
- package/dist/pi.js +37 -433
- package/package.json +5 -3
- package/pi/.thoth-agents-assets.json +5 -5
- package/pi/agents/thoth-designer.md +15 -20
- package/pi/agents/thoth-explorer.md +15 -19
- package/pi/agents/thoth-librarian.md +15 -19
- package/pi/agents/thoth-oracle.md +30 -22
- package/pi/agents/thoth-worker.md +13 -18
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/agents/designer.md +9 -14
- package/plugin/agents/explorer.md +10 -14
- package/plugin/agents/librarian.md +9 -13
- package/plugin/agents/oracle.md +24 -18
- package/plugin/agents/orchestrator.md +38 -61
- package/plugin/agents/worker.md +9 -14
- package/plugin/skills/plan-reviewer/SKILL.md +16 -8
- package/plugin/skills/thoth-archive/SKILL.md +2 -1
- package/plugin/skills/thoth-constitution/SKILL.md +1 -1
- package/plugin/skills/thoth-constitution/templates/constitution.md +1 -1
- package/plugin/skills/thoth-init/SKILL.md +1 -1
- package/plugin/skills/thoth-sdd/SKILL.md +67 -2
- package/plugin/skills/thoth-sdd/references/phases/verify.md +11 -1
- package/plugin/skills/thoth-sdd/scripts/result-contract.json +210 -0
- package/plugin/skills/thoth-sdd/scripts/validate-result.d.mts +47 -0
- package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +390 -0
- package/plugin/skills/thoth-sdd/scripts/validate.mjs +213 -8
- package/plugin/skills/thoth-sdd/templates/change.md +8 -0
- package/skills/plan-reviewer/SKILL.md +16 -8
- package/skills/thoth-archive/SKILL.md +2 -1
- package/skills/thoth-constitution/SKILL.md +1 -1
- package/skills/thoth-constitution/templates/constitution.md +1 -1
- package/skills/thoth-init/SKILL.md +1 -1
- package/skills/thoth-sdd/SKILL.md +67 -2
- package/skills/thoth-sdd/references/phases/verify.md +11 -1
- package/skills/thoth-sdd/scripts/result-contract.json +210 -0
- package/skills/thoth-sdd/scripts/validate-result.d.mts +47 -0
- package/skills/thoth-sdd/scripts/validate-result.mjs +390 -0
- package/skills/thoth-sdd/scripts/validate.mjs +213 -8
- package/skills/thoth-sdd/templates/change.md +8 -0
- package/thoth-agents.schema.json +14 -0
- package/dist/pi/tools-panel.d.ts +0 -51
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
parseRequirementDelta,
|
|
18
18
|
preflightRequirementDeltas,
|
|
19
19
|
} from './durable-deltas.mjs';
|
|
20
|
+
import { validateResult } from './validate-result.mjs';
|
|
20
21
|
|
|
21
22
|
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
22
23
|
const gates = [
|
|
@@ -261,10 +262,16 @@ function preflightDeltas(root, deltas, errors, warnings, recordName) {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
function settled(text) {
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
if (!text?.trim() || /\bTBD\b/.test(text)) return false;
|
|
266
|
+
return !text.split(/\r?\n/).some((line) => {
|
|
267
|
+
const content = line
|
|
268
|
+
.replace(/\*\*|__/g, '')
|
|
269
|
+
.trim()
|
|
270
|
+
.replace(/^(?:[-+*]|\d+[.)])\s+/, '')
|
|
271
|
+
.replace(/^\[[ xX]\]\s*/, '')
|
|
272
|
+
.replace(/^AC-\d+:\s*/, '');
|
|
273
|
+
return /^(?:TBD|PENDING)(?=:|\s*$)/i.test(content);
|
|
274
|
+
});
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
function coverageErrors(text, ids, errors, recordName) {
|
|
@@ -293,6 +300,184 @@ function coverageErrors(text, ids, errors, recordName) {
|
|
|
293
300
|
return rows;
|
|
294
301
|
}
|
|
295
302
|
|
|
303
|
+
// Whole-file textual binding: Markdown containers never hide candidates or markers.
|
|
304
|
+
function scanOracleEnvelope(text) {
|
|
305
|
+
let offset = 0;
|
|
306
|
+
const lines = text.split(/(?<=\n)/).map((raw) => {
|
|
307
|
+
const entry = { raw, offset, line: raw.replace(/\r?\n$/, '') };
|
|
308
|
+
if (offset === 0) entry.line = entry.line.replace(/^\uFEFF/, '');
|
|
309
|
+
offset += raw.length;
|
|
310
|
+
return entry;
|
|
311
|
+
});
|
|
312
|
+
const markers = [];
|
|
313
|
+
const fences = [];
|
|
314
|
+
const authorization = lines.findIndex(
|
|
315
|
+
({ line }) => line === '## Authorization',
|
|
316
|
+
);
|
|
317
|
+
const verification = lines.findIndex(
|
|
318
|
+
({ line }) => line === '## Verification',
|
|
319
|
+
);
|
|
320
|
+
const closeout = lines.findIndex(
|
|
321
|
+
({ line }, index) => index > verification && line === '## Closeout',
|
|
322
|
+
);
|
|
323
|
+
const verificationEnd = lines.findIndex(
|
|
324
|
+
({ line }, index) => index > verification && line.startsWith('## '),
|
|
325
|
+
);
|
|
326
|
+
for (const [index, { line }] of lines.entries()) {
|
|
327
|
+
if (line.trim() === '**Oracle envelope**:') markers.push(index);
|
|
328
|
+
const opening =
|
|
329
|
+
/^(\s*)(?:[>*+-]\s*|\d+[.)]\s*)*(`{3,}|~{3,})\s*thoth-result/i.exec(line);
|
|
330
|
+
if (!opening) continue;
|
|
331
|
+
let end = index + 1;
|
|
332
|
+
for (; end < lines.length; end++) {
|
|
333
|
+
const closing = /^(\s*)(`{3,}|~{3,})[ \t]*$/.exec(lines[end].line);
|
|
334
|
+
if (
|
|
335
|
+
closing &&
|
|
336
|
+
closing[1] === opening[1] &&
|
|
337
|
+
closing[2][0] === opening[2][0] &&
|
|
338
|
+
closing[2].length >= opening[2].length
|
|
339
|
+
)
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
fences.push({
|
|
343
|
+
index,
|
|
344
|
+
end,
|
|
345
|
+
closed: end < lines.length,
|
|
346
|
+
block: lines
|
|
347
|
+
.slice(index, end + 1)
|
|
348
|
+
.map(({ raw }) => raw)
|
|
349
|
+
.join(''),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
let next = markers[0] + 1;
|
|
353
|
+
while (next < lines.length && !lines[next].line.trim()) next++;
|
|
354
|
+
const positioned =
|
|
355
|
+
verification >= 0 &&
|
|
356
|
+
closeout > verification &&
|
|
357
|
+
markers[0] > verification &&
|
|
358
|
+
markers[0] < verificationEnd &&
|
|
359
|
+
fences[0]?.index === next &&
|
|
360
|
+
next < verificationEnd &&
|
|
361
|
+
(!fences[0]?.closed || fences[0].end < verificationEnd);
|
|
362
|
+
return {
|
|
363
|
+
markers,
|
|
364
|
+
fences,
|
|
365
|
+
positioned,
|
|
366
|
+
authorizationOffset: lines[authorization]?.offset,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function validateOracleEnvelope({
|
|
371
|
+
extraction,
|
|
372
|
+
verificationFields,
|
|
373
|
+
recordSha256,
|
|
374
|
+
reviewedDigest,
|
|
375
|
+
expectedRecord,
|
|
376
|
+
errors,
|
|
377
|
+
recordName,
|
|
378
|
+
}) {
|
|
379
|
+
const { markers, fences, positioned } = extraction;
|
|
380
|
+
if (markers.length > 1 || fences.length > 1) {
|
|
381
|
+
errors.push(
|
|
382
|
+
issue(
|
|
383
|
+
'SDD-ORACLE-ENVELOPE-DUPLICATE',
|
|
384
|
+
'Exactly one Oracle envelope marker and block are required',
|
|
385
|
+
recordName,
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!markers.length || !fences.length || !positioned) {
|
|
391
|
+
errors.push(
|
|
392
|
+
issue(
|
|
393
|
+
'SDD-ORACLE-ENVELOPE-MISSING',
|
|
394
|
+
'Oracle envelope marker followed by a final-verification block is required',
|
|
395
|
+
recordName,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!fences[0].closed) {
|
|
401
|
+
errors.push(
|
|
402
|
+
issue(
|
|
403
|
+
'SDD-ORACLE-ENVELOPE-INVALID',
|
|
404
|
+
'Oracle envelope fence is unclosed',
|
|
405
|
+
recordName,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Preserve the original wrapper and line endings for shared-contract validation.
|
|
411
|
+
const result = validateResult(fences[0].block, {
|
|
412
|
+
reviewKind: 'final-verification',
|
|
413
|
+
recordSha256: recordSha256,
|
|
414
|
+
});
|
|
415
|
+
if (!result.valid) {
|
|
416
|
+
errors.push(
|
|
417
|
+
issue(
|
|
418
|
+
'SDD-ORACLE-ENVELOPE-INVALID',
|
|
419
|
+
result.issues
|
|
420
|
+
.map((entry) => `${entry.code}: ${entry.message}`)
|
|
421
|
+
.join('; '),
|
|
422
|
+
recordName,
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
if (
|
|
426
|
+
result.issues.some((entry) => entry.code === 'RECORD_DIGEST_MISMATCH')
|
|
427
|
+
) {
|
|
428
|
+
errors.push(
|
|
429
|
+
issue(
|
|
430
|
+
'SDD-ORACLE-ENVELOPE-DIGEST-MISMATCH',
|
|
431
|
+
'Oracle envelope digest does not match the reviewed record prefix',
|
|
432
|
+
recordName,
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const { review } = result.envelope;
|
|
439
|
+
if (review.verdict !== 'PASS') {
|
|
440
|
+
errors.push(
|
|
441
|
+
issue(
|
|
442
|
+
'SDD-ORACLE-ENVELOPE-NOT-PASS',
|
|
443
|
+
'Oracle final-verification verdict must be PASS',
|
|
444
|
+
recordName,
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (review.reviewedRecord !== expectedRecord) {
|
|
449
|
+
errors.push(
|
|
450
|
+
issue(
|
|
451
|
+
'SDD-ORACLE-ENVELOPE-RECORD-MISMATCH',
|
|
452
|
+
`Oracle envelope must review ${expectedRecord}`,
|
|
453
|
+
recordName,
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (review.reviewedRecordSha256 !== reviewedDigest) {
|
|
458
|
+
errors.push(
|
|
459
|
+
issue(
|
|
460
|
+
'SDD-ORACLE-ENVELOPE-DIGEST-MISMATCH',
|
|
461
|
+
'Oracle envelope and Reviewed record SHA-256 field must agree',
|
|
462
|
+
recordName,
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const verdictLines = verificationFields.match(/^\*\*Verdict\*\*:.*$/gm) ?? [];
|
|
467
|
+
if (
|
|
468
|
+
verdictLines.length !== 1 ||
|
|
469
|
+
verdictLines[0] !== `**Verdict**: ${review.verdict}`
|
|
470
|
+
) {
|
|
471
|
+
errors.push(
|
|
472
|
+
issue(
|
|
473
|
+
'SDD-ORACLE-ENVELOPE-VERDICT-MISMATCH',
|
|
474
|
+
'Oracle envelope and Verdict field must agree',
|
|
475
|
+
recordName,
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
296
481
|
export function validate({ change, through }) {
|
|
297
482
|
if (!gates.includes(through)) throw new Error('Invalid SDD validation gate');
|
|
298
483
|
const errors = [];
|
|
@@ -358,7 +543,7 @@ export function validate({ change, through }) {
|
|
|
358
543
|
!new RegExp(
|
|
359
544
|
`^# Change: ${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`,
|
|
360
545
|
'm',
|
|
361
|
-
).test(text) ||
|
|
546
|
+
).test(text.replace(/^\uFEFF/, '')) ||
|
|
362
547
|
!/^\*\*Classification\*\*: substantial$/m.test(text) ||
|
|
363
548
|
!/^\*\*Scope\*\*: (?:local|coordinated|cross-cutting)$/m.test(text) ||
|
|
364
549
|
!/^\*\*Uncertainty\*\*: (?:low|medium|high)$/m.test(text) ||
|
|
@@ -534,9 +719,18 @@ export function validate({ change, through }) {
|
|
|
534
719
|
}
|
|
535
720
|
const verificationText =
|
|
536
721
|
section(text, 'Verification', { trim: false }) ?? '';
|
|
537
|
-
|
|
722
|
+
// Envelope contents are not record fields, AC rows, or reviewed source lines.
|
|
723
|
+
const extraction = scanOracleEnvelope(text);
|
|
724
|
+
const verificationFields =
|
|
725
|
+
through === 'closeout'
|
|
726
|
+
? verificationText.replace(extraction.fences[0]?.block ?? '', '')
|
|
727
|
+
: verificationText;
|
|
728
|
+
const verified = verificationFields.trim();
|
|
538
729
|
if (through === 'closeout') {
|
|
539
|
-
const prefix = text.slice(
|
|
730
|
+
const prefix = text.slice(
|
|
731
|
+
0,
|
|
732
|
+
extraction.authorizationOffset ?? text.length,
|
|
733
|
+
);
|
|
540
734
|
if (
|
|
541
735
|
!/^\*\*Reviewer\*\*: oracle$/m.test(verified) ||
|
|
542
736
|
!/^\*\*Independent from implementer\*\*: Yes$/m.test(verified) ||
|
|
@@ -551,7 +745,8 @@ export function validate({ change, through }) {
|
|
|
551
745
|
);
|
|
552
746
|
}
|
|
553
747
|
const digestLines =
|
|
554
|
-
|
|
748
|
+
verificationFields.match(/^\*\*Reviewed record SHA-256\*\*:.*$/gm) ??
|
|
749
|
+
[];
|
|
555
750
|
const reviewedDigest =
|
|
556
751
|
digestLines.length === 1
|
|
557
752
|
? /^\*\*Reviewed record SHA-256\*\*: ([a-f0-9]{64})$/.exec(
|
|
@@ -567,6 +762,16 @@ export function validate({ change, through }) {
|
|
|
567
762
|
),
|
|
568
763
|
);
|
|
569
764
|
}
|
|
765
|
+
validateOracleEnvelope({
|
|
766
|
+
extraction,
|
|
767
|
+
verificationFields,
|
|
768
|
+
recordSha256: sha(prefix),
|
|
769
|
+
reviewedDigest,
|
|
770
|
+
// Archived records retain the active path reviewed by the Oracle.
|
|
771
|
+
expectedRecord: `.thoth/changes/${id}/${id}.md`,
|
|
772
|
+
errors,
|
|
773
|
+
recordName,
|
|
774
|
+
});
|
|
570
775
|
const rows = verified
|
|
571
776
|
.split(/\r?\n/)
|
|
572
777
|
.filter((line) => /^- AC-/.test(line));
|
|
@@ -82,6 +82,14 @@ exactly one SHA field line; put notes on separate lines below the fields.
|
|
|
82
82
|
**Verdict**: PENDING
|
|
83
83
|
**Reviewed record SHA-256**: PENDING
|
|
84
84
|
|
|
85
|
+
**Oracle envelope**:
|
|
86
|
+
|
|
87
|
+
```thoth-result
|
|
88
|
+
PENDING
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
After PASS, root pastes the Oracle's unchanged final-verification envelope here.
|
|
92
|
+
|
|
85
93
|
- AC-1: PENDING | check | evidence
|
|
86
94
|
- Source: relative/path | sha256:<actual-source-digest>
|
|
87
95
|
- Source: .thoth/specs/<capability>/spec.md | sha256:<actual-source-digest>
|
package/thoth-agents.schema.json
CHANGED
|
@@ -207,6 +207,13 @@
|
|
|
207
207
|
}
|
|
208
208
|
]
|
|
209
209
|
},
|
|
210
|
+
"skills": {
|
|
211
|
+
"type": "array",
|
|
212
|
+
"items": {
|
|
213
|
+
"type": "string",
|
|
214
|
+
"minLength": 1
|
|
215
|
+
}
|
|
216
|
+
},
|
|
210
217
|
"temperature": {
|
|
211
218
|
"type": "number",
|
|
212
219
|
"minimum": 0,
|
|
@@ -262,6 +269,13 @@
|
|
|
262
269
|
}
|
|
263
270
|
]
|
|
264
271
|
},
|
|
272
|
+
"skills": {
|
|
273
|
+
"type": "array",
|
|
274
|
+
"items": {
|
|
275
|
+
"type": "string",
|
|
276
|
+
"minLength": 1
|
|
277
|
+
}
|
|
278
|
+
},
|
|
265
279
|
"temperature": {
|
|
266
280
|
"type": "number",
|
|
267
281
|
"minimum": 0,
|
package/dist/pi/tools-panel.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { type PiToolConfigSnapshot, type PiToolRoleInput, type PiToolRoleSnapshot, type PiToolSaveResult } from '../cli/pi-tool-config';
|
|
2
|
-
export type ToolsPanelResult = {
|
|
3
|
-
kind: 'cancelled';
|
|
4
|
-
} | {
|
|
5
|
-
kind: 'saved';
|
|
6
|
-
changedRoles: string[];
|
|
7
|
-
};
|
|
8
|
-
export interface ToolsPanelTheme {
|
|
9
|
-
fg(color: 'accent' | 'border' | 'muted' | 'dim' | 'text' | 'error' | 'warning', text: string): string;
|
|
10
|
-
bg(color: 'selectedBg', text: string): string;
|
|
11
|
-
}
|
|
12
|
-
export interface ToolsPanelDiscoveredTool {
|
|
13
|
-
name: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
active: boolean;
|
|
16
|
-
}
|
|
17
|
-
export type ToolsPanelKey = 'up' | 'down' | 'enter' | 'escape' | 'backspace' | 'space';
|
|
18
|
-
export interface ToolsPanelOptions {
|
|
19
|
-
snapshot: PiToolConfigSnapshot;
|
|
20
|
-
discoveredTools: readonly ToolsPanelDiscoveredTool[];
|
|
21
|
-
save(snapshot: PiToolConfigSnapshot, roles: readonly PiToolRoleInput[]): PiToolSaveResult;
|
|
22
|
-
onDone(result: ToolsPanelResult): void;
|
|
23
|
-
requestRender?(): void;
|
|
24
|
-
maxHeight?(): number;
|
|
25
|
-
matchesKey?: (data: string, key: ToolsPanelKey) => boolean;
|
|
26
|
-
truncate?: (text: string, width: number) => string;
|
|
27
|
-
visibleWidth?: (text: string) => number;
|
|
28
|
-
theme?: ToolsPanelTheme;
|
|
29
|
-
}
|
|
30
|
-
export type RoleToolsDraft = PiToolRoleSnapshot;
|
|
31
|
-
export type ToolsPanelScreen = 'overview' | 'tools' | 'discard';
|
|
32
|
-
export interface ToolsPanelState {
|
|
33
|
-
screen: ToolsPanelScreen;
|
|
34
|
-
selectedRole: number;
|
|
35
|
-
draft: RoleToolsDraft[];
|
|
36
|
-
error?: string;
|
|
37
|
-
changedRoles: string[];
|
|
38
|
-
}
|
|
39
|
-
export type ToolItemStatus = 'active' | 'inactive';
|
|
40
|
-
export interface ToolListItem {
|
|
41
|
-
name: string;
|
|
42
|
-
description?: string;
|
|
43
|
-
status: ToolItemStatus;
|
|
44
|
-
}
|
|
45
|
-
export declare function isEligibleTool(name: string): boolean;
|
|
46
|
-
export declare function createToolsPanel(options: ToolsPanelOptions): {
|
|
47
|
-
render: (width: number) => string[];
|
|
48
|
-
handleInput: (data: string) => void;
|
|
49
|
-
invalidate(): void;
|
|
50
|
-
getState: () => ToolsPanelState;
|
|
51
|
-
};
|