@dtlabs/ai-toolkit 0.12.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/cli.js +480 -7
  3. package/lib/agent-registry.js +1018 -0
  4. package/lib/execution-ledger.js +97 -3
  5. package/package.json +1 -1
  6. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  7. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +1 -1
  8. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  9. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  10. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  11. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  12. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  13. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  14. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  15. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  16. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  17. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  18. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  19. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  20. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  21. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +1 -1
  22. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  23. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  24. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  25. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  26. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  27. package/src/claude/commands/next-task.md +4 -4
  28. package/src/claude/skills/assess-codebase/SKILL.md +1 -1
  29. package/src/claude/skills/define-feature/SKILL.md +2 -2
  30. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  31. package/src/claude/skills/implement-feature/SKILL.md +186 -8
  32. package/src/claude/skills/init-agents/SKILL.md +2 -2
  33. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  34. package/src/claude/workflows/pm-phase1.js +76 -5
  35. package/src/claude/workflows/pm-phase2.js +63 -2
  36. package/src/claude/workflows/pm-phase3.js +66 -9
package/CHANGELOG.md CHANGED
@@ -1,4 +1,32 @@
1
1
  ## 0.1.4 (2026-07-21)
2
+ ## [0.13.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.12.0...v0.13.0) (2026-09-09)
3
+
4
+
5
+ ### Features
6
+
7
+ * add --require-verified operational mode to agents resolve ([c8cf8c1](https://github.com/dtlab-tech/ai-toolkit/commit/c8cf8c1d3db4ccbcf109408a4029381db1b21479))
8
+ * add agents cleanup dry-run command ([d773ce2](https://github.com/dtlab-tech/ai-toolkit/commit/d773ce257eb00c988f6218c603a88a9c9df91b86))
9
+ * add agents list and diagnostic resolve CLI commands ([0cd71bd](https://github.com/dtlab-tech/ai-toolkit/commit/0cd71bd037ff11a576c69476ebfd22b1d9025711))
10
+ * add agents preflight command with fail-closed ledger tracking ([3c16268](https://github.com/dtlab-tech/ai-toolkit/commit/3c1626873997ce205cd68f7f7314431d39cc8e9e))
11
+ * add canonical agent registry module with deterministic resolution ([21d5c02](https://github.com/dtlab-tech/ai-toolkit/commit/21d5c028d9c1978952dde234c4241db254383477))
12
+ * add computeFileSha256() helper for manifest integrity ([7fdf938](https://github.com/dtlab-tech/ai-toolkit/commit/7fdf9386d189954eafba8d24d229016510a0f127))
13
+ * add doctor agents command with comprehensive diagnostics ([ce47a78](https://github.com/dtlab-tech/ai-toolkit/commit/ce47a78cfa67ac44209dfa6cb4d2b8aba36ebf51))
14
+ * add ledger CLI facade with whitelisted metadata flags ([f26c6a6](https://github.com/dtlab-tech/ai-toolkit/commit/f26c6a61c83bdca154e4600041025a59a95ce1ff))
15
+ * add pm-phaseN:self ledger self-registration with fail-closed lifecycle ([8a04899](https://github.com/dtlab-tech/ai-toolkit/commit/8a048999943bf6b28e9bca7a4be0f659a11d1fec))
16
+ * add Tier 1 dispatch guard to implement-feature skill ([5326f5f](https://github.com/dtlab-tech/ai-toolkit/commit/5326f5fd6159a8fce67a01e2b41c1a60e2ad4258))
17
+ * add Tier 2 resolution guard to pm-phase workflows ([4f2e4e5](https://github.com/dtlab-tech/ai-toolkit/commit/4f2e4e5d72a57d211b86bc04c44102e1d6d4ef68))
18
+ * add WB agent_type inventory, legacy mapping, and rejection guard ([fc4228b](https://github.com/dtlab-tech/ai-toolkit/commit/fc4228b732fbb3e1d3332f3e24750b86108207e0))
19
+ * detect observable-scope collisions and add collision tests ([9857004](https://github.com/dtlab-tech/ai-toolkit/commit/9857004118aa0b77a39a9f9f7b1961fb2abcaa4f))
20
+ * extend ledger open() with whitelisted metadata and safety invariants ([cbc8cdc](https://github.com/dtlab-tech/ai-toolkit/commit/cbc8cdc600614b8e91daacc737b0e7f8493447f7))
21
+ * extend writeManifest() with optional fileHashes parameter ([9583aff](https://github.com/dtlab-tech/ai-toolkit/commit/9583affb26b97348def78b6973f645533c9cdbbe))
22
+ * **FTR-017:** deterministic agent resolution and orchestrator guard ([57e1e9b](https://github.com/dtlab-tech/ai-toolkit/commit/57e1e9b42a27937cd06a6cf2d1449f37cfeea4a7))
23
+ * **FTR-017:** Phase B completion gate (BE-24) — all 15 agents renamed to gaia-* prefix ([118f6e3](https://github.com/dtlab-tech/ai-toolkit/commit/118f6e3278457f81aea77d0e4a2b95bb661f6b7c))
24
+ * **FTR-017:** Phase B rename developer-* and review-solution agents to gaia-* (BE-09..12) ([2ff27e8](https://github.com/dtlab-tech/ai-toolkit/commit/2ff27e86519cfbdb57404c36860ab2ce5e1af7c3))
25
+ * **FTR-017:** Phase B rename remaining 11 agents to gaia-* prefix (BE-13..23) ([23ceb1f](https://github.com/dtlab-tech/ai-toolkit/commit/23ceb1f4ab5163c1e05ccdd67f7cd0e771800410))
26
+ * hard-stop on missing pm-phase3 and post-return ledger failure ([43e095c](https://github.com/dtlab-tech/ai-toolkit/commit/43e095cfc2f65ea984ae73f687313f9d298502a9))
27
+ * record per-file SHA-256 hashes in installer manifest ([26c9d74](https://github.com/dtlab-tech/ai-toolkit/commit/26c9d7448adc05585b2181bf14f5afadca6c72dd))
28
+ * remove legacy orchestrator references from runtime assets ([76177e3](https://github.com/dtlab-tech/ai-toolkit/commit/76177e3b7f0d78e470a9898ca37cb1a060575bff))
29
+
2
30
  ## [0.12.0](https://github.com/dtlab-tech/ai-toolkit/compare/v0.11.0...v0.12.0) (2026-09-04)
3
31
 
4
32
 
package/bin/cli.js CHANGED
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const crypto = require('crypto');
5
5
  const readline = require('readline');
6
6
  const executionLedger = require('../lib/execution-ledger');
7
+ const agentRegistry = require('../lib/agent-registry');
7
8
  const packageRoot = path.join(__dirname, '..');
8
9
 
9
10
  // ── colors ────────────────────────────────────────────────────────────────────
@@ -46,6 +47,10 @@ function fileHash(filePath) {
46
47
  return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
47
48
  }
48
49
 
50
+ function computeFileSha256(filePath) {
51
+ return 'sha256:' + crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
52
+ }
53
+
49
54
  function readJsonSafe(filePath) {
50
55
  try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
51
56
  }
@@ -289,7 +294,7 @@ function moveToTrash(destRoot, relativePath) {
289
294
  }
290
295
  }
291
296
 
292
- function writeManifest(destRoot, fileList, installationMode) {
297
+ function writeManifest(destRoot, fileList, installationMode, fileHashes) {
293
298
  const manifestPath = path.join(destRoot, '.claude', MANIFEST_FILE);
294
299
  const trashDir = path.join(destRoot, '.claude', '.ai-toolkit-trash');
295
300
  const filtered = fileList.filter(rel => {
@@ -302,6 +307,18 @@ function writeManifest(destRoot, fileList, installationMode) {
302
307
  installationMode: installationMode,
303
308
  files: filtered.map(f => f.replace(/\\/g, '/')),
304
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
+ }
305
322
  try {
306
323
  ensureDir(manifestPath);
307
324
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
@@ -415,7 +432,7 @@ async function runInstall(label, mappings, force, destRoot, dryRun = false, inst
415
432
 
416
433
  if (modified.length === 0) {
417
434
  console.log(` ${clr('green', '✔')} All new files copied. No conflicts.\n`);
418
- writeManifest(destRoot, newFileSet, installationMode);
435
+ writeManifest(destRoot, newFileSet, installationMode, _buildFileHashes(destRoot, newFileSet));
419
436
  return;
420
437
  }
421
438
 
@@ -426,7 +443,7 @@ async function runInstall(label, mappings, force, destRoot, dryRun = false, inst
426
443
  fs.copyFileSync(e.src, e.dest);
427
444
  console.log(` ${clr('yellow', '↺')} ${dim(path.relative(process.cwd(), e.dest))}`);
428
445
  }
429
- writeManifest(destRoot, newFileSet, installationMode);
446
+ writeManifest(destRoot, newFileSet, installationMode, _buildFileHashes(destRoot, newFileSet));
430
447
  return;
431
448
  }
432
449
 
@@ -455,7 +472,20 @@ async function runInstall(label, mappings, force, destRoot, dryRun = false, inst
455
472
  ` ${clr('gray', `✖ Kept as-is: ${skipped}`)}\n`
456
473
  );
457
474
 
458
- 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;
459
489
  }
460
490
 
461
491
  // ── subagent spawn-depth check (verify & advise only — never write) ────────────
@@ -1486,6 +1516,157 @@ function runDoctorResolution(options) {
1486
1516
  L('');
1487
1517
  }
1488
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
+
1489
1670
  // ── validatePurityGuard ───────────────────────────────────────────────────────
1490
1671
 
1491
1672
  // US-05-TASK-BE-03 (FTR-015):
@@ -1554,7 +1735,7 @@ function runValidatePurity(sourceDir) {
1554
1735
  // projects. Keyed by category name; value is a Set of item names to skip.
1555
1736
  // See AGENTS.md hard constraints.
1556
1737
  const TOOLKIT_INTERNAL_ASSETS = {
1557
- agents: new Set(['install-toolkit.md']),
1738
+ agents: new Set(['gaia-install-toolkit.md']),
1558
1739
  skills: new Set(['install-toolkit']),
1559
1740
  };
1560
1741
 
@@ -1764,6 +1945,14 @@ async function main() {
1764
1945
  const home = homeIdx !== -1 ? remaining[homeIdx + 1] : undefined;
1765
1946
  runDoctorResolution({ projectDir, home });
1766
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);
1767
1956
  } else if (argv[0] === 'validate-purity') {
1768
1957
  runValidatePurity(argv[1]);
1769
1958
  } else if (argv[0] === 'resolve-asset') {
@@ -1918,6 +2107,8 @@ async function main() {
1918
2107
  process.stderr.write(err.message + '\n');
1919
2108
  process.exit(1);
1920
2109
  }
2110
+ } else if (argv[0] === 'agents') {
2111
+ await handleAgentsCommand(argv.slice(1));
1921
2112
  } else if (argv[0] === 'ledger') {
1922
2113
  handleLedgerCommand(argv.slice(1));
1923
2114
  } else if (fs.existsSync(argv[0]) && fs.statSync(argv[0]).isDirectory()) {
@@ -1938,7 +2129,7 @@ function shellQuotePosix(arg) {
1938
2129
 
1939
2130
  function parseLedgerArgs(argv) {
1940
2131
  const PREFIX_RE = /^[A-Za-z]+-\d+$/;
1941
- const result = { prefix: undefined, agent: undefined, attempt: 1, tokens: undefined, dir: undefined, phase: undefined, model: undefined, error: undefined };
2132
+ const result = { prefix: undefined, agent: undefined, attempt: 1, tokens: undefined, dir: undefined, phase: undefined, model: undefined, error: undefined, metadata: undefined };
1942
2133
 
1943
2134
  for (let i = 0; i < argv.length; i++) {
1944
2135
  const flag = argv[i];
@@ -1996,6 +2187,36 @@ function parseLedgerArgs(argv) {
1996
2187
  throw new Error('parseLedgerArgs: --error must be a non-empty string');
1997
2188
  }
1998
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 });
1999
2220
  }
2000
2221
  }
2001
2222
 
@@ -2040,7 +2261,7 @@ function handleLedgerCommand(argv) {
2040
2261
 
2041
2262
  if (subcommand === 'open') {
2042
2263
  try {
2043
- const result = executionLedger.open(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt);
2264
+ const result = executionLedger.open(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt, args.metadata);
2044
2265
  process.stdout.write(sortedJson(result) + '\n');
2045
2266
  process.exitCode = 0;
2046
2267
  } catch (err) {
@@ -2083,6 +2304,256 @@ function handleLedgerCommand(argv) {
2083
2304
  }
2084
2305
  }
2085
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
+
2086
2557
  // ── entry point guard ─────────────────────────────────────────────────────────
2087
2558
  // Run the CLI only when invoked directly (node bin/cli.js).
2088
2559
  // When required as a module (e.g., by Jest), skip main() and export pure
@@ -2127,5 +2598,7 @@ if (require.main === module) {
2127
2598
  handleLedgerCommand,
2128
2599
  sortedJson,
2129
2600
  resolveFeaturesRoot,
2601
+ computeFileSha256,
2602
+ handleAgentsCommand,
2130
2603
  };
2131
2604
  }