@dtlabs/ai-toolkit 0.11.0 → 0.13.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/CHANGELOG.md +65 -0
- package/bin/cli.js +757 -68
- package/lib/agent-registry.js +1018 -0
- package/lib/execution-ledger.js +609 -0
- package/package.json +1 -1
- package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
- package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +37 -61
- package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
- package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
- package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
- package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
- package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
- package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
- package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
- package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
- package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
- package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
- package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
- package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
- package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
- package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +1 -1
- package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
- package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
- package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
- package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
- package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
- package/src/claude/commands/next-task.md +4 -4
- package/src/claude/scripts/wb-render.js +101 -2
- package/src/claude/scripts/wb-validate.js +32 -2
- package/src/claude/skills/assess-codebase/SKILL.md +1 -1
- package/src/claude/skills/define-feature/SKILL.md +2 -2
- package/src/claude/skills/hi-gaia/SKILL.md +1 -1
- package/src/claude/skills/implement-feature/SKILL.md +190 -10
- package/src/claude/skills/init-agents/SKILL.md +2 -2
- package/src/claude/skills/install-toolkit/SKILL.md +2 -2
- package/src/claude/workflows/pm-phase1.js +94 -73
- package/src/claude/workflows/pm-phase2.js +118 -108
- package/src/claude/workflows/pm-phase3.js +219 -75
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,8 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const executionLedger = require('../lib/execution-ledger');
|
|
7
|
+
const agentRegistry = require('../lib/agent-registry');
|
|
6
8
|
const packageRoot = path.join(__dirname, '..');
|
|
7
9
|
|
|
8
10
|
// ── colors ────────────────────────────────────────────────────────────────────
|
|
@@ -45,6 +47,10 @@ function fileHash(filePath) {
|
|
|
45
47
|
return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
function computeFileSha256(filePath) {
|
|
51
|
+
return 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
function readJsonSafe(filePath) {
|
|
49
55
|
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
50
56
|
}
|
|
@@ -288,7 +294,7 @@ function moveToTrash(destRoot, relativePath) {
|
|
|
288
294
|
}
|
|
289
295
|
}
|
|
290
296
|
|
|
291
|
-
function writeManifest(destRoot, fileList, installationMode) {
|
|
297
|
+
function writeManifest(destRoot, fileList, installationMode, fileHashes) {
|
|
292
298
|
const manifestPath = path.join(destRoot, '.claude', MANIFEST_FILE);
|
|
293
299
|
const trashDir = path.join(destRoot, '.claude', '.ai-toolkit-trash');
|
|
294
300
|
const filtered = fileList.filter(rel => {
|
|
@@ -301,6 +307,18 @@ function writeManifest(destRoot, fileList, installationMode) {
|
|
|
301
307
|
installationMode: installationMode,
|
|
302
308
|
files: filtered.map(f => f.replace(/\\/g, '/')),
|
|
303
309
|
};
|
|
310
|
+
if (fileHashes !== undefined && fileHashes !== null) {
|
|
311
|
+
// Enforce set(files) == set(keys(fileHashes)) invariant (AC-40).
|
|
312
|
+
const filesSet = new Set(manifest.files);
|
|
313
|
+
const hashesSet = new Set(Object.keys(fileHashes).map(k => k.replace(/\\/g, '/')));
|
|
314
|
+
if (filesSet.size !== hashesSet.size || [...filesSet].some(f => !hashesSet.has(f))) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
'writeManifest: set(files) ≠ set(keys(fileHashes)). ' +
|
|
317
|
+
'Every installed file must have exactly one hash entry.'
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
manifest.fileHashes = fileHashes;
|
|
321
|
+
}
|
|
304
322
|
try {
|
|
305
323
|
ensureDir(manifestPath);
|
|
306
324
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
@@ -414,7 +432,7 @@ async function runInstall(label, mappings, force, destRoot, dryRun = false, inst
|
|
|
414
432
|
|
|
415
433
|
if (modified.length === 0) {
|
|
416
434
|
console.log(` ${clr('green', '✔')} All new files copied. No conflicts.\n`);
|
|
417
|
-
writeManifest(destRoot, newFileSet, installationMode);
|
|
435
|
+
writeManifest(destRoot, newFileSet, installationMode, _buildFileHashes(destRoot, newFileSet));
|
|
418
436
|
return;
|
|
419
437
|
}
|
|
420
438
|
|
|
@@ -425,7 +443,7 @@ async function runInstall(label, mappings, force, destRoot, dryRun = false, inst
|
|
|
425
443
|
fs.copyFileSync(e.src, e.dest);
|
|
426
444
|
console.log(` ${clr('yellow', '↺')} ${dim(path.relative(process.cwd(), e.dest))}`);
|
|
427
445
|
}
|
|
428
|
-
writeManifest(destRoot, newFileSet, installationMode);
|
|
446
|
+
writeManifest(destRoot, newFileSet, installationMode, _buildFileHashes(destRoot, newFileSet));
|
|
429
447
|
return;
|
|
430
448
|
}
|
|
431
449
|
|
|
@@ -454,7 +472,20 @@ async function runInstall(label, mappings, force, destRoot, dryRun = false, inst
|
|
|
454
472
|
` ${clr('gray', `✖ Kept as-is: ${skipped}`)}\n`
|
|
455
473
|
);
|
|
456
474
|
|
|
457
|
-
writeManifest(destRoot, newFileSet, installationMode);
|
|
475
|
+
writeManifest(destRoot, newFileSet, installationMode, _buildFileHashes(destRoot, newFileSet));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Compute SHA-256 for every relative path in relPaths under destRoot.
|
|
479
|
+
// Files that cannot be read are silently skipped (they won't appear in fileHashes).
|
|
480
|
+
function _buildFileHashes(destRoot, relPaths) {
|
|
481
|
+
const fileHashes = {};
|
|
482
|
+
for (const relPath of relPaths) {
|
|
483
|
+
const absPath = path.join(destRoot, relPath);
|
|
484
|
+
try {
|
|
485
|
+
fileHashes[relPath] = computeFileSha256(absPath);
|
|
486
|
+
} catch (_) { /* ignore unreadable files */ }
|
|
487
|
+
}
|
|
488
|
+
return fileHashes;
|
|
458
489
|
}
|
|
459
490
|
|
|
460
491
|
// ── subagent spawn-depth check (verify & advise only — never write) ────────────
|
|
@@ -580,67 +611,6 @@ function mergeAllowlist(destDir) {
|
|
|
580
611
|
return { status: 'merged', preserved };
|
|
581
612
|
}
|
|
582
613
|
|
|
583
|
-
// INFRA-T01 (FTR-013):
|
|
584
|
-
// Append a new entry to {featureDir}/{prefix}-token-ledger.json atomically.
|
|
585
|
-
// If the file does not exist it is created. If JSON is malformed the file is
|
|
586
|
-
// overwritten with a single-element array containing the new entry.
|
|
587
|
-
//
|
|
588
|
-
// Algorithm:
|
|
589
|
-
// 1. Read and parse the existing ledger (or start with [])
|
|
590
|
-
// 2. Push the new entry
|
|
591
|
-
// 3. Write the full array back in one synchronous write (atomic)
|
|
592
|
-
function appendLedgerEntry(featureDir, prefix, entry) {
|
|
593
|
-
const filePath = path.join(featureDir, `${prefix}-token-ledger.json`);
|
|
594
|
-
let ledger = [];
|
|
595
|
-
if (fs.existsSync(filePath)) {
|
|
596
|
-
try {
|
|
597
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
598
|
-
ledger = Array.isArray(parsed) ? parsed : [];
|
|
599
|
-
} catch (_) {
|
|
600
|
-
console.log(`Warning: could not parse token ledger at ${filePath} — starting fresh`);
|
|
601
|
-
ledger = [];
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
ledger.push(entry);
|
|
605
|
-
const dir = path.dirname(filePath);
|
|
606
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
607
|
-
fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2), 'utf8');
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// INFRA-T02 (FTR-013):
|
|
611
|
-
// Find and update an existing entry in {featureDir}/{prefix}-token-ledger.json
|
|
612
|
-
// by agent key, atomically. Searches from the end of the array so that the most
|
|
613
|
-
// recent entry for a given key is updated (handles any accidental duplicates).
|
|
614
|
-
// If the file does not exist or the key is not found the call is a silent no-op.
|
|
615
|
-
//
|
|
616
|
-
// Algorithm:
|
|
617
|
-
// 1. Read and parse the existing ledger (silent return on missing/malformed)
|
|
618
|
-
// 2. Find the last entry where entry.agent === agentKey
|
|
619
|
-
// 3. Object.assign the updates onto that entry
|
|
620
|
-
// 4. Write the full array back in one synchronous write (atomic)
|
|
621
|
-
function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
|
|
622
|
-
const filePath = path.join(featureDir, `${prefix}-token-ledger.json`);
|
|
623
|
-
if (!fs.existsSync(filePath)) return;
|
|
624
|
-
let ledger;
|
|
625
|
-
try {
|
|
626
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
627
|
-
ledger = Array.isArray(parsed) ? parsed : [];
|
|
628
|
-
} catch (_) {
|
|
629
|
-
console.log(`Warning: could not parse token ledger at ${filePath} — skipping update for "${agentKey}"`);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
let idx = -1;
|
|
633
|
-
for (let i = ledger.length - 1; i >= 0; i--) {
|
|
634
|
-
if (ledger[i] && ledger[i].agent === agentKey) { idx = i; break; }
|
|
635
|
-
}
|
|
636
|
-
if (idx === -1) {
|
|
637
|
-
console.log(`Warning: ledger entry not found for agent key "${agentKey}"`);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
Object.assign(ledger[idx], updates);
|
|
641
|
-
fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2), 'utf8');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
614
|
// US-05-T01:
|
|
645
615
|
// Idempotently append `.claude/settings.local.json` to {destDir}/.gitignore.
|
|
646
616
|
// Creates .gitignore if it does not exist (AC-06, AC-07).
|
|
@@ -672,6 +642,113 @@ function updateGitignore(destDir) {
|
|
|
672
642
|
}
|
|
673
643
|
}
|
|
674
644
|
|
|
645
|
+
// ── resolveFeaturesRoot ───────────────────────────────────────────────────────
|
|
646
|
+
|
|
647
|
+
// US-05-TASK-BE-01 (FTR-016):
|
|
648
|
+
// Resolve the features root directory for a given working directory.
|
|
649
|
+
//
|
|
650
|
+
// Ordered precedence — ALL candidates are gathered before deciding so that
|
|
651
|
+
// ambiguity (multiply-declared AGENTS.md entries, multiple inconsistent
|
|
652
|
+
// defaults) can be detected rather than silently swallowed:
|
|
653
|
+
// 1. Explicit override via options.featuresRoot (highest precedence)
|
|
654
|
+
// 2. features_root: convention parsed from <cwd>/AGENTS.md
|
|
655
|
+
// 3. A single existing conventional default directory
|
|
656
|
+
//
|
|
657
|
+
// AGENTS.md grammar parser (deterministic):
|
|
658
|
+
// - Ignores HTML-comment lines of the form <!-- ... --> (same input line)
|
|
659
|
+
// - Strips inline # comments from the value and trims trailing whitespace
|
|
660
|
+
// - Counts only active (non-commented) declarations when detecting the
|
|
661
|
+
// multiply-declared error; throws when two or more are found
|
|
662
|
+
//
|
|
663
|
+
// Returns an absolute path.
|
|
664
|
+
// Throws a clear structured Error on ambiguous or multiply-declared roots.
|
|
665
|
+
//
|
|
666
|
+
// Parameters:
|
|
667
|
+
// cwd (string): project root directory
|
|
668
|
+
// options (object): { featuresRoot? } — explicit override, highest precedence
|
|
669
|
+
function resolveFeaturesRoot(cwd, options) {
|
|
670
|
+
const opts = options || {};
|
|
671
|
+
const resolvedCwd = path.resolve(cwd);
|
|
672
|
+
|
|
673
|
+
// Conventional default directories tried when no higher-precedence source is found.
|
|
674
|
+
const CONVENTIONAL_DEFAULTS = [
|
|
675
|
+
'internal_docs/features',
|
|
676
|
+
'docs/features',
|
|
677
|
+
];
|
|
678
|
+
|
|
679
|
+
// ── AGENTS.md grammar parser ──────────────────────────────────────────────
|
|
680
|
+
// Returns an array of trimmed value strings for each ACTIVE features_root:
|
|
681
|
+
// declaration found in <cwdDir>/AGENTS.md. Deterministic: same input →
|
|
682
|
+
// same result; does not rely on filesystem ordering.
|
|
683
|
+
function parseAgentsMdDeclarations(cwdDir) {
|
|
684
|
+
const agentsMdPath = path.join(cwdDir, 'AGENTS.md');
|
|
685
|
+
if (!fs.existsSync(agentsMdPath)) return [];
|
|
686
|
+
let content;
|
|
687
|
+
try { content = fs.readFileSync(agentsMdPath, 'utf8'); } catch (_) { return []; }
|
|
688
|
+
|
|
689
|
+
const declarations = [];
|
|
690
|
+
for (const line of content.split('\n')) {
|
|
691
|
+
const trimmed = line.trim();
|
|
692
|
+
|
|
693
|
+
// Ignore HTML comment lines: <!-- anything -->
|
|
694
|
+
if (/^<!--.*-->$/.test(trimmed)) continue;
|
|
695
|
+
|
|
696
|
+
// Match active features_root: declarations
|
|
697
|
+
const match = trimmed.match(/^features_root:\s*(.*)$/);
|
|
698
|
+
if (!match) continue;
|
|
699
|
+
|
|
700
|
+
// Strip inline # comment, then trim
|
|
701
|
+
let value = match[1];
|
|
702
|
+
const hashIdx = value.indexOf('#');
|
|
703
|
+
if (hashIdx !== -1) value = value.substring(0, hashIdx);
|
|
704
|
+
value = value.trim();
|
|
705
|
+
|
|
706
|
+
if (value) declarations.push(value);
|
|
707
|
+
}
|
|
708
|
+
return declarations;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── Source 1: Explicit override ───────────────────────────────────────────
|
|
712
|
+
if (opts.featuresRoot !== undefined && opts.featuresRoot !== null && opts.featuresRoot !== '') {
|
|
713
|
+
return path.resolve(resolvedCwd, opts.featuresRoot);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ── Source 2: AGENTS.md features_root: convention ────────────────────────
|
|
717
|
+
const agentsDeclarations = parseAgentsMdDeclarations(resolvedCwd);
|
|
718
|
+
if (agentsDeclarations.length > 1) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`resolveFeaturesRoot: AGENTS.md declares features_root: ${agentsDeclarations.length} times ` +
|
|
721
|
+
`(values: ${agentsDeclarations.map(v => JSON.stringify(v)).join(', ')}); ` +
|
|
722
|
+
'only one active declaration is allowed'
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
if (agentsDeclarations.length === 1) {
|
|
726
|
+
return path.resolve(resolvedCwd, agentsDeclarations[0]);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ── Source 3: Conventional defaults — use only when exactly one exists ────
|
|
730
|
+
const existingDefaults = CONVENTIONAL_DEFAULTS
|
|
731
|
+
.map(d => path.resolve(resolvedCwd, d))
|
|
732
|
+
.filter(d => { try { return fs.statSync(d).isDirectory(); } catch (_) { return false; } });
|
|
733
|
+
|
|
734
|
+
if (existingDefaults.length === 1) {
|
|
735
|
+
return existingDefaults[0];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (existingDefaults.length > 1) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`resolveFeaturesRoot: multiple conventional default directories exist ` +
|
|
741
|
+
`(${existingDefaults.join(', ')}); declare features_root: in AGENTS.md to disambiguate`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
throw new Error(
|
|
746
|
+
`resolveFeaturesRoot: no features root found under ${resolvedCwd}. ` +
|
|
747
|
+
'Declare features_root: in AGENTS.md or create one of the conventional default directories ' +
|
|
748
|
+
`(${CONVENTIONAL_DEFAULTS.join(', ')})`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
675
752
|
// ── payload & detection ───────────────────────────────────────────────────────
|
|
676
753
|
|
|
677
754
|
// Returns file-level {src, dest} mappings (absolute paths) for ALL distributable
|
|
@@ -1439,6 +1516,157 @@ function runDoctorResolution(options) {
|
|
|
1439
1516
|
L('');
|
|
1440
1517
|
}
|
|
1441
1518
|
|
|
1519
|
+
// ── runDoctorAgents ───────────────────────────────────────────────────────────
|
|
1520
|
+
|
|
1521
|
+
// US-08-TASK-BE-01 (FTR-017):
|
|
1522
|
+
// Read-only diagnostic report for all registered toolkit agents.
|
|
1523
|
+
// Never modifies, creates, or deletes any file (AC-14).
|
|
1524
|
+
//
|
|
1525
|
+
// Parameters (via options object):
|
|
1526
|
+
// projectDir (string): target project directory (defaults to process.cwd())
|
|
1527
|
+
// home (string): override for os.homedir() to enable test isolation
|
|
1528
|
+
//
|
|
1529
|
+
// Output: human-readable report on stdout; always exits 0.
|
|
1530
|
+
function runDoctorAgents(options) {
|
|
1531
|
+
const os = require('os');
|
|
1532
|
+
|
|
1533
|
+
const opts = options || {};
|
|
1534
|
+
const effectiveHome = opts.home !== undefined ? path.resolve(opts.home) : os.homedir();
|
|
1535
|
+
const effectiveProject = opts.projectDir !== undefined ? path.resolve(opts.projectDir) : process.cwd();
|
|
1536
|
+
|
|
1537
|
+
const records = agentRegistry.listRegisteredAgents(effectiveProject, effectiveHome);
|
|
1538
|
+
|
|
1539
|
+
const STATUS_ICON = {
|
|
1540
|
+
'verified': '[OK]',
|
|
1541
|
+
'conflict': '[!!]',
|
|
1542
|
+
'hash-unverifiable': '[??]',
|
|
1543
|
+
'unobservable': '[??]',
|
|
1544
|
+
'not-installed': '[--]',
|
|
1545
|
+
'not-applicable': '[NA]',
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
const lines = [];
|
|
1549
|
+
lines.push('');
|
|
1550
|
+
lines.push('ai-toolkit doctor agents');
|
|
1551
|
+
lines.push('─'.repeat(72));
|
|
1552
|
+
lines.push('');
|
|
1553
|
+
|
|
1554
|
+
const statusCounts = {};
|
|
1555
|
+
const unobservable = [];
|
|
1556
|
+
const conflicts = [];
|
|
1557
|
+
const hashUnverifiable = [];
|
|
1558
|
+
|
|
1559
|
+
for (const rec of records) {
|
|
1560
|
+
const status = rec.status;
|
|
1561
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
1562
|
+
|
|
1563
|
+
const icon = STATUS_ICON[status] || '[??]';
|
|
1564
|
+
lines.push(' ' + icon + ' ' + rec.agentId);
|
|
1565
|
+
lines.push(' native: ' + rec.nativeName);
|
|
1566
|
+
lines.push(' scope: ' + (rec.scope || '—'));
|
|
1567
|
+
lines.push(' version: ' + (rec.toolkitVersion || '—'));
|
|
1568
|
+
if (rec.path) lines.push(' path: ' + rec.path);
|
|
1569
|
+
if (rec.sha256) lines.push(' sha256: ' + rec.sha256);
|
|
1570
|
+
if (rec.integrityDetail) lines.push(' detail: ' + rec.integrityDetail);
|
|
1571
|
+
if (rec.error) lines.push(' error: ' + rec.error);
|
|
1572
|
+
if (rec.deprecated) lines.push(' DEPRECATED — target: ' + (rec.deprecationTarget || '(unspecified)'));
|
|
1573
|
+
lines.push('');
|
|
1574
|
+
|
|
1575
|
+
if (status === 'unobservable') unobservable.push(rec);
|
|
1576
|
+
else if (status === 'conflict') conflicts.push(rec);
|
|
1577
|
+
else if (status === 'hash-unverifiable') hashUnverifiable.push(rec);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
1581
|
+
lines.push('─'.repeat(72));
|
|
1582
|
+
lines.push('Summary:');
|
|
1583
|
+
for (const [status, count] of Object.entries(statusCounts)) {
|
|
1584
|
+
lines.push(' ' + (STATUS_ICON[status] || '[??]') + ' ' + status + ': ' + count);
|
|
1585
|
+
}
|
|
1586
|
+
lines.push('');
|
|
1587
|
+
|
|
1588
|
+
// ── Unobservable WARNING (AC-33) ──────────────────────────────────────────────
|
|
1589
|
+
if (unobservable.length > 0) {
|
|
1590
|
+
lines.push('WARNING: unobservable scope(s) detected.');
|
|
1591
|
+
lines.push(' The following agents exist in plugin or session scopes that cannot be');
|
|
1592
|
+
lines.push(' enumerated from the filesystem. Dispatch is NOT blocked. The mandatory');
|
|
1593
|
+
lines.push(' mitigation is gaia-* namespacing — a uniquely prefixed native name is');
|
|
1594
|
+
lines.push(' extremely unlikely to collide with a plugin or session agent.');
|
|
1595
|
+
for (const rec of unobservable) {
|
|
1596
|
+
lines.push(' - ' + rec.agentId + ' (' + rec.nativeName + ')');
|
|
1597
|
+
}
|
|
1598
|
+
lines.push('');
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// ── hash-unverifiable remediation ────────────────────────────────────────────
|
|
1602
|
+
if (hashUnverifiable.length > 0) {
|
|
1603
|
+
lines.push('Remediation for hash-unverifiable agent(s):');
|
|
1604
|
+
lines.push(' Reinstall or upgrade the toolkit runtime to generate integrity hashes.');
|
|
1605
|
+
lines.push(' Command: ai-toolkit --local <project-dir> --force');
|
|
1606
|
+
lines.push('');
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// ── Conflict detail ───────────────────────────────────────────────────────────
|
|
1610
|
+
if (conflicts.length > 0) {
|
|
1611
|
+
lines.push('Conflicts:');
|
|
1612
|
+
for (const rec of conflicts) {
|
|
1613
|
+
const detail = rec.integrityDetail || rec.error || 'ambiguous installation';
|
|
1614
|
+
lines.push(' ' + rec.agentId + ': ' + detail);
|
|
1615
|
+
if (rec.path) lines.push(' path: ' + rec.path);
|
|
1616
|
+
}
|
|
1617
|
+
lines.push('');
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// ── Foreign observable agents (AC-09) ────────────────────────────────────────
|
|
1621
|
+
const catalogNativeNames = new Set(agentRegistry.CATALOG.map(function (e) { return e.nativeNames.claude; }));
|
|
1622
|
+
const agentsDirs = [];
|
|
1623
|
+
var projectAgentsDir = path.join(effectiveProject, '.claude', 'agents');
|
|
1624
|
+
var globalAgentsDir = path.join(effectiveHome, '.claude', 'agents');
|
|
1625
|
+
if (fs.existsSync(projectAgentsDir)) agentsDirs.push({ dir: projectAgentsDir, scope: 'project' });
|
|
1626
|
+
if (fs.existsSync(globalAgentsDir)) agentsDirs.push({ dir: globalAgentsDir, scope: 'global' });
|
|
1627
|
+
|
|
1628
|
+
var foreignAgents = [];
|
|
1629
|
+
for (var di = 0; di < agentsDirs.length; di++) {
|
|
1630
|
+
var entry = agentsDirs[di];
|
|
1631
|
+
try {
|
|
1632
|
+
var files = fs.readdirSync(entry.dir).filter(function (f) { return f.endsWith('.md'); });
|
|
1633
|
+
for (var fi = 0; fi < files.length; fi++) {
|
|
1634
|
+
var nativeName = files[fi].replace(/\.md$/, '');
|
|
1635
|
+
if (!catalogNativeNames.has(nativeName)) {
|
|
1636
|
+
foreignAgents.push({ nativeName: nativeName, path: path.join(entry.dir, files[fi]), scope: entry.scope });
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
} catch (_) { /* ignore unreadable dirs */ }
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (foreignAgents.length > 0) {
|
|
1643
|
+
lines.push('Foreign observable agents (not in toolkit catalog):');
|
|
1644
|
+
lines.push(' These agents are present in an observable scope but are NOT registered');
|
|
1645
|
+
lines.push(' toolkit agents. They do NOT block dispatch. Review and remove if stale.');
|
|
1646
|
+
for (var fai = 0; fai < foreignAgents.length; fai++) {
|
|
1647
|
+
var fa = foreignAgents[fai];
|
|
1648
|
+
lines.push(' [foreign/' + fa.scope + '] ' + fa.nativeName);
|
|
1649
|
+
lines.push(' path: ' + fa.path);
|
|
1650
|
+
}
|
|
1651
|
+
lines.push('');
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// ── Deprecated / legacy agents ───────────────────────────────────────────────
|
|
1655
|
+
var deprecated = records.filter(function (r) { return r.deprecated; });
|
|
1656
|
+
if (deprecated.length > 0) {
|
|
1657
|
+
lines.push('Deprecated agents (Phase A legacy names):');
|
|
1658
|
+
for (var dpi = 0; dpi < deprecated.length; dpi++) {
|
|
1659
|
+
var drec = deprecated[dpi];
|
|
1660
|
+
lines.push(' ' + drec.agentId + ' (' + drec.nativeName + ')');
|
|
1661
|
+
if (drec.deprecationTarget) lines.push(' rename target: ' + drec.deprecationTarget);
|
|
1662
|
+
}
|
|
1663
|
+
lines.push(' Remediation: reinstall the toolkit after Phase B renames are complete.');
|
|
1664
|
+
lines.push('');
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1442
1670
|
// ── validatePurityGuard ───────────────────────────────────────────────────────
|
|
1443
1671
|
|
|
1444
1672
|
// US-05-TASK-BE-03 (FTR-015):
|
|
@@ -1507,7 +1735,7 @@ function runValidatePurity(sourceDir) {
|
|
|
1507
1735
|
// projects. Keyed by category name; value is a Set of item names to skip.
|
|
1508
1736
|
// See AGENTS.md hard constraints.
|
|
1509
1737
|
const TOOLKIT_INTERNAL_ASSETS = {
|
|
1510
|
-
agents: new Set(['install-toolkit.md']),
|
|
1738
|
+
agents: new Set(['gaia-install-toolkit.md']),
|
|
1511
1739
|
skills: new Set(['install-toolkit']),
|
|
1512
1740
|
};
|
|
1513
1741
|
|
|
@@ -1717,6 +1945,14 @@ async function main() {
|
|
|
1717
1945
|
const home = homeIdx !== -1 ? remaining[homeIdx + 1] : undefined;
|
|
1718
1946
|
runDoctorResolution({ projectDir, home });
|
|
1719
1947
|
process.exit(0);
|
|
1948
|
+
} else if (argv[0] === 'doctor' && argv[1] === 'agents') {
|
|
1949
|
+
const remaining = argv.slice(2);
|
|
1950
|
+
const projectIdx = remaining.indexOf('--project');
|
|
1951
|
+
const homeIdx = remaining.indexOf('--home');
|
|
1952
|
+
const projectDir = projectIdx !== -1 ? remaining[projectIdx + 1] : process.cwd();
|
|
1953
|
+
const home = homeIdx !== -1 ? remaining[homeIdx + 1] : undefined;
|
|
1954
|
+
runDoctorAgents({ projectDir, home });
|
|
1955
|
+
process.exit(0);
|
|
1720
1956
|
} else if (argv[0] === 'validate-purity') {
|
|
1721
1957
|
runValidatePurity(argv[1]);
|
|
1722
1958
|
} else if (argv[0] === 'resolve-asset') {
|
|
@@ -1862,6 +2098,19 @@ async function main() {
|
|
|
1862
2098
|
for (const r of results) process.stdout.write(r + '\n');
|
|
1863
2099
|
}
|
|
1864
2100
|
process.exit(0);
|
|
2101
|
+
} else if (argv[0] === 'resolve-features-root') {
|
|
2102
|
+
try {
|
|
2103
|
+
const resolved = resolveFeaturesRoot(process.cwd());
|
|
2104
|
+
process.stdout.write(resolved + '\n');
|
|
2105
|
+
process.exit(0);
|
|
2106
|
+
} catch (err) {
|
|
2107
|
+
process.stderr.write(err.message + '\n');
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
}
|
|
2110
|
+
} else if (argv[0] === 'agents') {
|
|
2111
|
+
await handleAgentsCommand(argv.slice(1));
|
|
2112
|
+
} else if (argv[0] === 'ledger') {
|
|
2113
|
+
handleLedgerCommand(argv.slice(1));
|
|
1865
2114
|
} else if (fs.existsSync(argv[0]) && fs.statSync(argv[0]).isDirectory()) {
|
|
1866
2115
|
await installLocal(argv[0], force, dryRun);
|
|
1867
2116
|
} else {
|
|
@@ -1870,6 +2119,441 @@ async function main() {
|
|
|
1870
2119
|
}
|
|
1871
2120
|
}
|
|
1872
2121
|
|
|
2122
|
+
// ── shell quoting helper ──────────────────────────────────────────────────────
|
|
2123
|
+
function shellQuotePosix(arg) {
|
|
2124
|
+
const s = String(arg);
|
|
2125
|
+
if (s.indexOf('\0') !== -1) throw new Error('shellQuotePosix: argument contains a NUL byte');
|
|
2126
|
+
if (s.indexOf('\n') !== -1) throw new Error('shellQuotePosix: argument contains a newline');
|
|
2127
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
function parseLedgerArgs(argv) {
|
|
2131
|
+
const PREFIX_RE = /^[A-Za-z]+-\d+$/;
|
|
2132
|
+
const result = { prefix: undefined, agent: undefined, attempt: 1, tokens: undefined, dir: undefined, phase: undefined, model: undefined, error: undefined, metadata: undefined };
|
|
2133
|
+
|
|
2134
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2135
|
+
const flag = argv[i];
|
|
2136
|
+
const val = argv[i + 1];
|
|
2137
|
+
|
|
2138
|
+
if (flag === '--prefix') {
|
|
2139
|
+
i++;
|
|
2140
|
+
if (!val || !PREFIX_RE.test(val)) {
|
|
2141
|
+
throw new Error(
|
|
2142
|
+
'parseLedgerArgs: --prefix must match /^[A-Za-z]+-\\d+$/ (e.g. FTR-016), got: ' + val
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
result.prefix = val;
|
|
2146
|
+
} else if (flag === '--agent') {
|
|
2147
|
+
i++;
|
|
2148
|
+
if (!val) {
|
|
2149
|
+
throw new Error('parseLedgerArgs: --agent must be a non-empty string');
|
|
2150
|
+
}
|
|
2151
|
+
result.agent = val;
|
|
2152
|
+
} else if (flag === '--attempt') {
|
|
2153
|
+
i++;
|
|
2154
|
+
const n = Number(val);
|
|
2155
|
+
if (!Number.isInteger(n)) {
|
|
2156
|
+
throw new Error('parseLedgerArgs: --attempt must be an integer, got: ' + val);
|
|
2157
|
+
}
|
|
2158
|
+
result.attempt = n;
|
|
2159
|
+
} else if (flag === '--tokens') {
|
|
2160
|
+
i++;
|
|
2161
|
+
const n = Number(val);
|
|
2162
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
2163
|
+
throw new Error('parseLedgerArgs: --tokens must be an integer >= 1, got: ' + val);
|
|
2164
|
+
}
|
|
2165
|
+
result.tokens = n;
|
|
2166
|
+
} else if (flag === '--dir') {
|
|
2167
|
+
i++;
|
|
2168
|
+
if (!val) {
|
|
2169
|
+
throw new Error('parseLedgerArgs: --dir must be a non-empty string');
|
|
2170
|
+
}
|
|
2171
|
+
result.dir = val;
|
|
2172
|
+
} else if (flag === '--phase') {
|
|
2173
|
+
i++;
|
|
2174
|
+
if (!val) {
|
|
2175
|
+
throw new Error('parseLedgerArgs: --phase must be a non-empty string');
|
|
2176
|
+
}
|
|
2177
|
+
result.phase = val;
|
|
2178
|
+
} else if (flag === '--model') {
|
|
2179
|
+
i++;
|
|
2180
|
+
if (!val) {
|
|
2181
|
+
throw new Error('parseLedgerArgs: --model must be a non-empty string');
|
|
2182
|
+
}
|
|
2183
|
+
result.model = val;
|
|
2184
|
+
} else if (flag === '--error') {
|
|
2185
|
+
i++;
|
|
2186
|
+
if (!val) {
|
|
2187
|
+
throw new Error('parseLedgerArgs: --error must be a non-empty string');
|
|
2188
|
+
}
|
|
2189
|
+
result.error = val;
|
|
2190
|
+
} else if (flag === '--metadata-json') {
|
|
2191
|
+
i++;
|
|
2192
|
+
if (!val) {
|
|
2193
|
+
throw new Error('parseLedgerArgs: --metadata-json must be a non-empty JSON string');
|
|
2194
|
+
}
|
|
2195
|
+
try {
|
|
2196
|
+
result.metadata = Object.assign(result.metadata || {}, JSON.parse(val));
|
|
2197
|
+
} catch (_) {
|
|
2198
|
+
throw new Error('parseLedgerArgs: --metadata-json is not valid JSON: ' + val);
|
|
2199
|
+
}
|
|
2200
|
+
} else if (flag === '--agent-id') {
|
|
2201
|
+
i++;
|
|
2202
|
+
if (!val) throw new Error('parseLedgerArgs: --agent-id must be a non-empty string');
|
|
2203
|
+
result.metadata = Object.assign(result.metadata || {}, { agentId: val });
|
|
2204
|
+
} else if (flag === '--native-name') {
|
|
2205
|
+
i++;
|
|
2206
|
+
if (!val) throw new Error('parseLedgerArgs: --native-name must be a non-empty string');
|
|
2207
|
+
result.metadata = Object.assign(result.metadata || {}, { nativeAgentName: val });
|
|
2208
|
+
} else if (flag === '--toolkit-version') {
|
|
2209
|
+
i++;
|
|
2210
|
+
if (!val) throw new Error('parseLedgerArgs: --toolkit-version must be a non-empty string');
|
|
2211
|
+
result.metadata = Object.assign(result.metadata || {}, { toolkitVersion: val });
|
|
2212
|
+
} else if (flag === '--scope') {
|
|
2213
|
+
i++;
|
|
2214
|
+
if (!val) throw new Error('parseLedgerArgs: --scope must be a non-empty string');
|
|
2215
|
+
result.metadata = Object.assign(result.metadata || {}, { resolutionScope: val });
|
|
2216
|
+
} else if (flag === '--hash') {
|
|
2217
|
+
i++;
|
|
2218
|
+
if (!val) throw new Error('parseLedgerArgs: --hash must be a non-empty string');
|
|
2219
|
+
result.metadata = Object.assign(result.metadata || {}, { definitionHash: val });
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
if (!result.prefix) {
|
|
2224
|
+
throw new Error('parseLedgerArgs: --prefix is required');
|
|
2225
|
+
}
|
|
2226
|
+
if (!result.agent) {
|
|
2227
|
+
throw new Error('parseLedgerArgs: --agent is required and must be non-empty');
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return result;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Serialize obj to JSON with all object keys sorted recursively.
|
|
2234
|
+
// Used by the ledger subcommand to produce deterministic stdout output.
|
|
2235
|
+
function sortedJson(obj) {
|
|
2236
|
+
return JSON.stringify(obj, (key, val) => {
|
|
2237
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
|
2238
|
+
const sorted = {};
|
|
2239
|
+
for (const k of Object.keys(val).sort()) sorted[k] = val[k];
|
|
2240
|
+
return sorted;
|
|
2241
|
+
}
|
|
2242
|
+
return val;
|
|
2243
|
+
}, 2);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// ── ledger dispatcher ─────────────────────────────────────────────────────────
|
|
2247
|
+
// Routes `ai-toolkit ledger <subcommand> [flags]` to the per-operation
|
|
2248
|
+
// handlers in lib/execution-ledger.js. All operation logic lives in that
|
|
2249
|
+
// module; this function only parses shared flags and routes.
|
|
2250
|
+
function handleLedgerCommand(argv) {
|
|
2251
|
+
const subcommand = argv[0];
|
|
2252
|
+
const flags = argv.slice(1);
|
|
2253
|
+
let args;
|
|
2254
|
+
try {
|
|
2255
|
+
args = parseLedgerArgs(flags);
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2258
|
+
process.exitCode = 1;
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (subcommand === 'open') {
|
|
2263
|
+
try {
|
|
2264
|
+
const result = executionLedger.open(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt, args.metadata);
|
|
2265
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2266
|
+
process.exitCode = 0;
|
|
2267
|
+
} catch (err) {
|
|
2268
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2269
|
+
process.exitCode = 1;
|
|
2270
|
+
}
|
|
2271
|
+
return;
|
|
2272
|
+
} else if (subcommand === 'close') {
|
|
2273
|
+
try {
|
|
2274
|
+
const result = executionLedger.close(args.dir, args.prefix, args.agent, args.tokens, args.attempt);
|
|
2275
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2276
|
+
process.exitCode = 0;
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2279
|
+
process.exitCode = 1;
|
|
2280
|
+
}
|
|
2281
|
+
return;
|
|
2282
|
+
} else if (subcommand === 'fail') {
|
|
2283
|
+
try {
|
|
2284
|
+
const result = executionLedger.fail(args.dir, args.prefix, args.agent, args.error, args.attempt);
|
|
2285
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2286
|
+
process.exitCode = 0;
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2289
|
+
process.exitCode = 1;
|
|
2290
|
+
}
|
|
2291
|
+
return;
|
|
2292
|
+
} else if (subcommand === 'skip') {
|
|
2293
|
+
try {
|
|
2294
|
+
const result = executionLedger.skip(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt);
|
|
2295
|
+
process.stdout.write(sortedJson(result) + '\n');
|
|
2296
|
+
process.exitCode = 0;
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
process.stderr.write(sortedJson({ message: err.message, status: 'error' }) + '\n');
|
|
2299
|
+
process.exitCode = 1;
|
|
2300
|
+
}
|
|
2301
|
+
return;
|
|
2302
|
+
} else {
|
|
2303
|
+
throw new Error('handleLedgerCommand: unknown subcommand: ' + subcommand);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// ── agents dispatcher ─────────────────────────────────────────────────────────
|
|
2308
|
+
// Routes `ai-toolkit agents <subcommand> [flags]` to the appropriate
|
|
2309
|
+
// registry function. All domain logic lives in lib/agent-registry.js;
|
|
2310
|
+
// this function only parses CLI flags and invokes registry functions —
|
|
2311
|
+
// it is NOT a second registry.
|
|
2312
|
+
//
|
|
2313
|
+
// Subcommands:
|
|
2314
|
+
// list --project <dir> [--format json] [--home <dir>]
|
|
2315
|
+
// → calls listRegisteredAgents(); enumerates ONLY catalog toolkit
|
|
2316
|
+
// agents (never foreign/plugin); exits 0 on success.
|
|
2317
|
+
// resolve --project <dir> --id <canonicalId> [--home <dir>] [--require-verified]
|
|
2318
|
+
// → without --require-verified: diagnostic mode.
|
|
2319
|
+
// Exit-code contract:
|
|
2320
|
+
// exit 0 — verified | hash-unverifiable | hash-mismatch
|
|
2321
|
+
// exit 1 — not-found | conflict | manifest-missing |
|
|
2322
|
+
// not-installed | not-applicable
|
|
2323
|
+
// → with --require-verified: operational mode.
|
|
2324
|
+
// Exit-code contract:
|
|
2325
|
+
// exit 0 — verified ONLY (v0.13.0+ manifest with matching hash)
|
|
2326
|
+
// exit 1 — any other status; structured JSON error written to stderr
|
|
2327
|
+
// A v0.12.0 manifest (no fileHashes) is a HARD STOP under --require-verified.
|
|
2328
|
+
// preflight --project <dir> --pipeline <name> [--home <dir>]
|
|
2329
|
+
// → verifies every agent in the named pipeline using --require-verified semantics.
|
|
2330
|
+
// Required agent set is derived from CATALOG.allowedPipelines.
|
|
2331
|
+
// Exit-code contract:
|
|
2332
|
+
// exit 0 — all agents verified; JSON summary on stdout
|
|
2333
|
+
// exit 1 — any agent unverified or pipeline unknown; JSON error on stderr
|
|
2334
|
+
async function handleAgentsCommand(argv) {
|
|
2335
|
+
const os = require('os');
|
|
2336
|
+
const subcommand = argv[0];
|
|
2337
|
+
const remaining = argv.slice(1);
|
|
2338
|
+
|
|
2339
|
+
let projectDir = null;
|
|
2340
|
+
let home = undefined;
|
|
2341
|
+
let format = 'json';
|
|
2342
|
+
let agentId = null;
|
|
2343
|
+
let requireVerified = false;
|
|
2344
|
+
let pipeline = null;
|
|
2345
|
+
|
|
2346
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
2347
|
+
if (remaining[i] === '--project' && remaining[i + 1]) { projectDir = remaining[++i]; }
|
|
2348
|
+
else if (remaining[i] === '--home' && remaining[i + 1]) { home = remaining[++i]; }
|
|
2349
|
+
else if (remaining[i] === '--format' && remaining[i + 1]) { format = remaining[++i]; }
|
|
2350
|
+
else if (remaining[i] === '--id' && remaining[i + 1]) { agentId = remaining[++i]; }
|
|
2351
|
+
else if (remaining[i] === '--pipeline' && remaining[i + 1]) { pipeline = remaining[++i]; }
|
|
2352
|
+
else if (remaining[i] === '--require-verified') { requireVerified = true; }
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (subcommand === 'list') {
|
|
2356
|
+
if (!projectDir) {
|
|
2357
|
+
process.stderr.write('Error: agents list requires --project <dir>\n');
|
|
2358
|
+
process.exitCode = 1;
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
|
|
2362
|
+
const effectiveProject = path.resolve(projectDir);
|
|
2363
|
+
const records = agentRegistry.listRegisteredAgents(effectiveProject, effectiveHome);
|
|
2364
|
+
if (format === 'json') {
|
|
2365
|
+
process.stdout.write(JSON.stringify(records, null, 2) + '\n');
|
|
2366
|
+
} else {
|
|
2367
|
+
for (const r of records) {
|
|
2368
|
+
process.stdout.write(r.agentId + '\t' + r.nativeName + '\t' + r.status + '\n');
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
process.exitCode = 0;
|
|
2372
|
+
|
|
2373
|
+
} else if (subcommand === 'resolve') {
|
|
2374
|
+
if (!projectDir) {
|
|
2375
|
+
process.stderr.write('Error: agents resolve requires --project <dir>\n');
|
|
2376
|
+
process.exitCode = 1;
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (!agentId) {
|
|
2380
|
+
process.stderr.write('Error: agents resolve requires --id <canonicalId>\n');
|
|
2381
|
+
process.exitCode = 1;
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
|
|
2385
|
+
const effectiveProject = path.resolve(projectDir);
|
|
2386
|
+
let record;
|
|
2387
|
+
try {
|
|
2388
|
+
record = await agentRegistry.resolveAgent({
|
|
2389
|
+
projectDir: effectiveProject,
|
|
2390
|
+
agentId: agentId,
|
|
2391
|
+
homeDir: effectiveHome,
|
|
2392
|
+
requireVerified: requireVerified,
|
|
2393
|
+
});
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
// requireVerified=true throws RESOLUTION_FAILED for non-verified agents;
|
|
2396
|
+
// the thrown error carries err.record with the full resolution record.
|
|
2397
|
+
const errorRecord = err.record
|
|
2398
|
+
? Object.assign({}, err.record, { code: err.code })
|
|
2399
|
+
: { agentId: agentId, status: 'resolution-failed', error: err.message, code: err.code || 'UNKNOWN' };
|
|
2400
|
+
process.stderr.write(JSON.stringify(errorRecord, null, 2) + '\n');
|
|
2401
|
+
process.exitCode = 1;
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
process.stdout.write(JSON.stringify(record, null, 2) + '\n');
|
|
2405
|
+
if (requireVerified) {
|
|
2406
|
+
// Operational mode: exit 0 only for verified (non-verified would have thrown above)
|
|
2407
|
+
process.exitCode = record.status === 'verified' ? 0 : 1;
|
|
2408
|
+
} else {
|
|
2409
|
+
// Diagnostic mode: exit 0 for agent present in any integrity state
|
|
2410
|
+
const exitZeroStatuses = new Set(['verified', 'hash-unverifiable', 'hash-mismatch']);
|
|
2411
|
+
process.exitCode = exitZeroStatuses.has(record.status) ? 0 : 1;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
} else if (subcommand === 'preflight') {
|
|
2415
|
+
if (!projectDir) {
|
|
2416
|
+
process.stderr.write('Error: agents preflight requires --project <dir>\n');
|
|
2417
|
+
process.exitCode = 1;
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (!pipeline) {
|
|
2421
|
+
process.stderr.write('Error: agents preflight requires --pipeline <name>\n');
|
|
2422
|
+
process.exitCode = 1;
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
|
|
2426
|
+
const effectiveProject = path.resolve(projectDir);
|
|
2427
|
+
|
|
2428
|
+
// Derive the required agent set from the CATALOG's allowedPipelines field.
|
|
2429
|
+
const pipelineAgents = agentRegistry.CATALOG.filter(function (e) {
|
|
2430
|
+
return Array.isArray(e.allowedPipelines) && e.allowedPipelines.indexOf(pipeline) !== -1;
|
|
2431
|
+
});
|
|
2432
|
+
if (pipelineAgents.length === 0) {
|
|
2433
|
+
process.stderr.write(
|
|
2434
|
+
JSON.stringify({ status: 'preflight-failed', error: 'Unknown pipeline: ' + pipeline }, null, 2) + '\n'
|
|
2435
|
+
);
|
|
2436
|
+
process.exitCode = 1;
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
const results = [];
|
|
2441
|
+
let allPassed = true;
|
|
2442
|
+
for (let i = 0; i < pipelineAgents.length; i++) {
|
|
2443
|
+
const entry = pipelineAgents[i];
|
|
2444
|
+
try {
|
|
2445
|
+
const rec = await agentRegistry.resolveAgent({
|
|
2446
|
+
projectDir: effectiveProject,
|
|
2447
|
+
agentId: entry.agentId,
|
|
2448
|
+
homeDir: effectiveHome,
|
|
2449
|
+
requireVerified: true,
|
|
2450
|
+
});
|
|
2451
|
+
results.push({ agentId: rec.agentId, status: rec.status });
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
allPassed = false;
|
|
2454
|
+
const errRec = err.record
|
|
2455
|
+
? Object.assign({}, err.record, { code: err.code })
|
|
2456
|
+
: { agentId: entry.agentId, status: 'resolution-failed', error: err.message, code: err.code || 'RESOLUTION_FAILED' };
|
|
2457
|
+
results.push(errRec);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
if (allPassed) {
|
|
2462
|
+
process.stdout.write(
|
|
2463
|
+
JSON.stringify({ status: 'preflight-ok', pipeline: pipeline, agents: results }, null, 2) + '\n'
|
|
2464
|
+
);
|
|
2465
|
+
process.exitCode = 0;
|
|
2466
|
+
} else {
|
|
2467
|
+
process.stderr.write(
|
|
2468
|
+
JSON.stringify({ status: 'preflight-failed', pipeline: pipeline, agents: results }, null, 2) + '\n'
|
|
2469
|
+
);
|
|
2470
|
+
process.exitCode = 1;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
} else if (subcommand === 'cleanup') {
|
|
2474
|
+
// Reject any mutating flags up-front (AC-15).
|
|
2475
|
+
const hasDelete = remaining.indexOf('--delete') !== -1;
|
|
2476
|
+
const hasForce = remaining.indexOf('--force') !== -1;
|
|
2477
|
+
if (hasDelete || hasForce) {
|
|
2478
|
+
process.stderr.write(
|
|
2479
|
+
'Error: agents cleanup does not support mutating flags (--delete, --force) in this version.\n' +
|
|
2480
|
+
' Only --dry-run is supported; no files are ever modified or deleted.\n'
|
|
2481
|
+
);
|
|
2482
|
+
process.exitCode = 1;
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
if (!projectDir) {
|
|
2487
|
+
process.stderr.write('Error: agents cleanup requires --project <dir>\n');
|
|
2488
|
+
process.exitCode = 1;
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
|
|
2492
|
+
const effectiveProject = path.resolve(projectDir);
|
|
2493
|
+
|
|
2494
|
+
// Locate the active manifest (project-scoped takes precedence over global).
|
|
2495
|
+
const projectManifestPath = path.join(effectiveProject, '.claude', MANIFEST_FILE);
|
|
2496
|
+
const globalManifestPath = path.join(effectiveHome, '.claude', MANIFEST_FILE);
|
|
2497
|
+
|
|
2498
|
+
let installRoot = null;
|
|
2499
|
+
let manifestData = null;
|
|
2500
|
+
|
|
2501
|
+
if (fs.existsSync(projectManifestPath)) {
|
|
2502
|
+
installRoot = effectiveProject;
|
|
2503
|
+
manifestData = readJsonSafe(projectManifestPath);
|
|
2504
|
+
} else if (fs.existsSync(globalManifestPath)) {
|
|
2505
|
+
installRoot = effectiveHome;
|
|
2506
|
+
manifestData = readJsonSafe(globalManifestPath);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
if (!installRoot || !manifestData || !Array.isArray(manifestData.files)) {
|
|
2510
|
+
process.stdout.write(
|
|
2511
|
+
JSON.stringify({ status: 'no-manifest', candidates: [], missing: [] }, null, 2) + '\n'
|
|
2512
|
+
);
|
|
2513
|
+
process.exitCode = 0;
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// Build the set of absolute paths that the current payload would install
|
|
2518
|
+
// into the same install root. Anything NOT in this set is a potential orphan.
|
|
2519
|
+
// buildExpectedPayload takes the .claude dir (same convention as the installer).
|
|
2520
|
+
const expectedPayload = buildExpectedPayload(path.join(installRoot, '.claude'));
|
|
2521
|
+
|
|
2522
|
+
const candidates = [];
|
|
2523
|
+
const missing = [];
|
|
2524
|
+
|
|
2525
|
+
for (const relPath of manifestData.files) {
|
|
2526
|
+
if (typeof relPath !== 'string') continue;
|
|
2527
|
+
const absPath = path.resolve(installRoot, relPath);
|
|
2528
|
+
|
|
2529
|
+
// If still in the current payload → not a cleanup candidate.
|
|
2530
|
+
if (expectedPayload.has(absPath)) continue;
|
|
2531
|
+
|
|
2532
|
+
// Not in current payload: check disk state.
|
|
2533
|
+
if (fs.existsSync(absPath)) {
|
|
2534
|
+
candidates.push({ path: relPath, absPath: absPath, status: 'candidate' });
|
|
2535
|
+
} else {
|
|
2536
|
+
missing.push({ path: relPath, absPath: absPath, status: 'missing' });
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
const output = {
|
|
2541
|
+
status: 'dry-run',
|
|
2542
|
+
installRoot: installRoot,
|
|
2543
|
+
candidates: candidates.map(function (c) { return { path: c.path, status: c.status }; }),
|
|
2544
|
+
missing: missing.map(function (m) { return { path: m.path, status: m.status }; }),
|
|
2545
|
+
};
|
|
2546
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
2547
|
+
process.exitCode = 0;
|
|
2548
|
+
|
|
2549
|
+
} else {
|
|
2550
|
+
process.stderr.write(
|
|
2551
|
+
'Error: unknown agents subcommand: ' + (subcommand || '(none)') + '\n'
|
|
2552
|
+
);
|
|
2553
|
+
process.exitCode = 1;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
1873
2557
|
// ── entry point guard ─────────────────────────────────────────────────────────
|
|
1874
2558
|
// Run the CLI only when invoked directly (node bin/cli.js).
|
|
1875
2559
|
// When required as a module (e.g., by Jest), skip main() and export pure
|
|
@@ -1898,8 +2582,6 @@ if (require.main === module) {
|
|
|
1898
2582
|
writeSettings,
|
|
1899
2583
|
mergeAllowlist,
|
|
1900
2584
|
updateGitignore,
|
|
1901
|
-
appendLedgerEntry,
|
|
1902
|
-
updateLedgerEntry,
|
|
1903
2585
|
resolveClaudeRuntimeAsset,
|
|
1904
2586
|
runDoctorResolution,
|
|
1905
2587
|
validatePurityGuard,
|
|
@@ -1911,5 +2593,12 @@ if (require.main === module) {
|
|
|
1911
2593
|
hasToolkitPayloadFiles,
|
|
1912
2594
|
isToolkitInstalled,
|
|
1913
2595
|
runVerifyInstall,
|
|
2596
|
+
shellQuotePosix,
|
|
2597
|
+
parseLedgerArgs,
|
|
2598
|
+
handleLedgerCommand,
|
|
2599
|
+
sortedJson,
|
|
2600
|
+
resolveFeaturesRoot,
|
|
2601
|
+
computeFileSha256,
|
|
2602
|
+
handleAgentsCommand,
|
|
1914
2603
|
};
|
|
1915
2604
|
}
|