@dtlabs/ai-toolkit 0.12.0 → 0.14.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 (50) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/bin/cli.js +851 -9
  3. package/docs/task-executor-bootstrap.md +195 -0
  4. package/docs/task-executor-cli.md +319 -0
  5. package/docs/workflow-control-plane.md +95 -0
  6. package/lib/agent-registry.js +1018 -0
  7. package/lib/execution-ledger.js +283 -7
  8. package/lib/task-executor/claude-process.js +563 -0
  9. package/lib/task-executor/git.js +1362 -0
  10. package/lib/task-executor/index.js +3729 -0
  11. package/lib/task-executor/ownership.js +590 -0
  12. package/lib/task-executor/plan.js +725 -0
  13. package/lib/task-executor/store.js +1164 -0
  14. package/lib/workflow-artifacts.js +102 -0
  15. package/lib/workflow-control.js +219 -0
  16. package/lib/workflow-runner.js +39 -0
  17. package/package.json +1 -1
  18. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  19. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +1 -1
  20. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  21. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  22. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  23. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  24. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  25. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  26. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  27. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  28. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  29. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  30. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  31. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  32. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  33. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +10 -10
  34. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  35. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  36. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  37. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  38. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  39. package/src/claude/commands/next-task.md +4 -4
  40. package/src/claude/skills/assess-codebase/SKILL.md +9 -103
  41. package/src/claude/skills/define-feature/SKILL.md +2 -2
  42. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  43. package/src/claude/skills/implement-feature/SKILL.md +131 -38
  44. package/src/claude/skills/init-agents/SKILL.md +2 -2
  45. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  46. package/src/claude/workflows/am-phase1.js +43 -294
  47. package/src/claude/workflows/am-phase2.js +10 -164
  48. package/src/claude/workflows/pm-phase1.js +45 -287
  49. package/src/claude/workflows/pm-phase2.js +51 -511
  50. package/src/claude/workflows/pm-phase3.js +5 -767
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
 
@@ -1695,7 +1876,9 @@ async function main() {
1695
1876
  return;
1696
1877
  }
1697
1878
 
1698
- if (argv[0] === '--local') {
1879
+ if (argv[0] === 'workflow') {
1880
+ await require('../lib/workflow-runner').main(argv.slice(1));
1881
+ } else if (argv[0] === '--local') {
1699
1882
  await installLocal(argv[1] || '.', force, dryRun);
1700
1883
  } else if (argv[0] === '--global') {
1701
1884
  const homeIdx = argv.indexOf('--home');
@@ -1764,6 +1947,14 @@ async function main() {
1764
1947
  const home = homeIdx !== -1 ? remaining[homeIdx + 1] : undefined;
1765
1948
  runDoctorResolution({ projectDir, home });
1766
1949
  process.exit(0);
1950
+ } else if (argv[0] === 'doctor' && argv[1] === 'agents') {
1951
+ const remaining = argv.slice(2);
1952
+ const projectIdx = remaining.indexOf('--project');
1953
+ const homeIdx = remaining.indexOf('--home');
1954
+ const projectDir = projectIdx !== -1 ? remaining[projectIdx + 1] : process.cwd();
1955
+ const home = homeIdx !== -1 ? remaining[homeIdx + 1] : undefined;
1956
+ runDoctorAgents({ projectDir, home });
1957
+ process.exit(0);
1767
1958
  } else if (argv[0] === 'validate-purity') {
1768
1959
  runValidatePurity(argv[1]);
1769
1960
  } else if (argv[0] === 'resolve-asset') {
@@ -1832,7 +2023,7 @@ async function main() {
1832
2023
  process.stderr.write(`Error spawning script: ${result.error.message}\n`);
1833
2024
  process.exit(1);
1834
2025
  }
1835
- process.exit(result.status || 0);
2026
+ process.exit(Number.isInteger(result.status) ? result.status : 1);
1836
2027
  } catch (err) {
1837
2028
  process.stderr.write(err.message + '\n');
1838
2029
  process.exit(1);
@@ -1918,8 +2109,12 @@ async function main() {
1918
2109
  process.stderr.write(err.message + '\n');
1919
2110
  process.exit(1);
1920
2111
  }
2112
+ } else if (argv[0] === 'agents') {
2113
+ await handleAgentsCommand(argv.slice(1));
1921
2114
  } else if (argv[0] === 'ledger') {
1922
2115
  handleLedgerCommand(argv.slice(1));
2116
+ } else if (argv[0] === 'executor') {
2117
+ await handleExecutorCommand(argv.slice(1));
1923
2118
  } else if (fs.existsSync(argv[0]) && fs.statSync(argv[0]).isDirectory()) {
1924
2119
  await installLocal(argv[0], force, dryRun);
1925
2120
  } else {
@@ -1938,7 +2133,7 @@ function shellQuotePosix(arg) {
1938
2133
 
1939
2134
  function parseLedgerArgs(argv) {
1940
2135
  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 };
2136
+ const result = { prefix: undefined, agent: undefined, attempt: 1, tokens: undefined, dir: undefined, phase: undefined, model: undefined, error: undefined, metadata: undefined };
1942
2137
 
1943
2138
  for (let i = 0; i < argv.length; i++) {
1944
2139
  const flag = argv[i];
@@ -1996,6 +2191,36 @@ function parseLedgerArgs(argv) {
1996
2191
  throw new Error('parseLedgerArgs: --error must be a non-empty string');
1997
2192
  }
1998
2193
  result.error = val;
2194
+ } else if (flag === '--metadata-json') {
2195
+ i++;
2196
+ if (!val) {
2197
+ throw new Error('parseLedgerArgs: --metadata-json must be a non-empty JSON string');
2198
+ }
2199
+ try {
2200
+ result.metadata = Object.assign(result.metadata || {}, JSON.parse(val));
2201
+ } catch (_) {
2202
+ throw new Error('parseLedgerArgs: --metadata-json is not valid JSON: ' + val);
2203
+ }
2204
+ } else if (flag === '--agent-id') {
2205
+ i++;
2206
+ if (!val) throw new Error('parseLedgerArgs: --agent-id must be a non-empty string');
2207
+ result.metadata = Object.assign(result.metadata || {}, { agentId: val });
2208
+ } else if (flag === '--native-name') {
2209
+ i++;
2210
+ if (!val) throw new Error('parseLedgerArgs: --native-name must be a non-empty string');
2211
+ result.metadata = Object.assign(result.metadata || {}, { nativeAgentName: val });
2212
+ } else if (flag === '--toolkit-version') {
2213
+ i++;
2214
+ if (!val) throw new Error('parseLedgerArgs: --toolkit-version must be a non-empty string');
2215
+ result.metadata = Object.assign(result.metadata || {}, { toolkitVersion: val });
2216
+ } else if (flag === '--scope') {
2217
+ i++;
2218
+ if (!val) throw new Error('parseLedgerArgs: --scope must be a non-empty string');
2219
+ result.metadata = Object.assign(result.metadata || {}, { resolutionScope: val });
2220
+ } else if (flag === '--hash') {
2221
+ i++;
2222
+ if (!val) throw new Error('parseLedgerArgs: --hash must be a non-empty string');
2223
+ result.metadata = Object.assign(result.metadata || {}, { definitionHash: val });
1999
2224
  }
2000
2225
  }
2001
2226
 
@@ -2040,7 +2265,7 @@ function handleLedgerCommand(argv) {
2040
2265
 
2041
2266
  if (subcommand === 'open') {
2042
2267
  try {
2043
- const result = executionLedger.open(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt);
2268
+ const result = executionLedger.open(args.dir, args.prefix, args.agent, args.phase, args.model, args.attempt, args.metadata);
2044
2269
  process.stdout.write(sortedJson(result) + '\n');
2045
2270
  process.exitCode = 0;
2046
2271
  } catch (err) {
@@ -2083,6 +2308,615 @@ function handleLedgerCommand(argv) {
2083
2308
  }
2084
2309
  }
2085
2310
 
2311
+ // ── agents dispatcher ─────────────────────────────────────────────────────────
2312
+ // Routes `ai-toolkit agents <subcommand> [flags]` to the appropriate
2313
+ // registry function. All domain logic lives in lib/agent-registry.js;
2314
+ // this function only parses CLI flags and invokes registry functions —
2315
+ // it is NOT a second registry.
2316
+ //
2317
+ // Subcommands:
2318
+ // list --project <dir> [--format json] [--home <dir>]
2319
+ // → calls listRegisteredAgents(); enumerates ONLY catalog toolkit
2320
+ // agents (never foreign/plugin); exits 0 on success.
2321
+ // resolve --project <dir> --id <canonicalId> [--home <dir>] [--require-verified]
2322
+ // → without --require-verified: diagnostic mode.
2323
+ // Exit-code contract:
2324
+ // exit 0 — verified | hash-unverifiable | hash-mismatch
2325
+ // exit 1 — not-found | conflict | manifest-missing |
2326
+ // not-installed | not-applicable
2327
+ // → with --require-verified: operational mode.
2328
+ // Exit-code contract:
2329
+ // exit 0 — verified ONLY (v0.13.0+ manifest with matching hash)
2330
+ // exit 1 — any other status; structured JSON error written to stderr
2331
+ // A v0.12.0 manifest (no fileHashes) is a HARD STOP under --require-verified.
2332
+ // preflight --project <dir> --pipeline <name> [--home <dir>]
2333
+ // → verifies every agent in the named pipeline using --require-verified semantics.
2334
+ // Required agent set is derived from CATALOG.allowedPipelines.
2335
+ // Exit-code contract:
2336
+ // exit 0 — all agents verified; JSON summary on stdout
2337
+ // exit 1 — any agent unverified or pipeline unknown; JSON error on stderr
2338
+ async function handleAgentsCommand(argv) {
2339
+ const os = require('os');
2340
+ const subcommand = argv[0];
2341
+ const remaining = argv.slice(1);
2342
+
2343
+ let projectDir = null;
2344
+ let home = undefined;
2345
+ let format = 'json';
2346
+ let agentId = null;
2347
+ let requireVerified = false;
2348
+ let pipeline = null;
2349
+
2350
+ for (let i = 0; i < remaining.length; i++) {
2351
+ if (remaining[i] === '--project' && remaining[i + 1]) { projectDir = remaining[++i]; }
2352
+ else if (remaining[i] === '--home' && remaining[i + 1]) { home = remaining[++i]; }
2353
+ else if (remaining[i] === '--format' && remaining[i + 1]) { format = remaining[++i]; }
2354
+ else if (remaining[i] === '--id' && remaining[i + 1]) { agentId = remaining[++i]; }
2355
+ else if (remaining[i] === '--pipeline' && remaining[i + 1]) { pipeline = remaining[++i]; }
2356
+ else if (remaining[i] === '--require-verified') { requireVerified = true; }
2357
+ }
2358
+
2359
+ if (subcommand === 'list') {
2360
+ if (!projectDir) {
2361
+ process.stderr.write('Error: agents list requires --project <dir>\n');
2362
+ process.exitCode = 1;
2363
+ return;
2364
+ }
2365
+ const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
2366
+ const effectiveProject = path.resolve(projectDir);
2367
+ const records = agentRegistry.listRegisteredAgents(effectiveProject, effectiveHome);
2368
+ if (format === 'json') {
2369
+ process.stdout.write(JSON.stringify(records, null, 2) + '\n');
2370
+ } else {
2371
+ for (const r of records) {
2372
+ process.stdout.write(r.agentId + '\t' + r.nativeName + '\t' + r.status + '\n');
2373
+ }
2374
+ }
2375
+ process.exitCode = 0;
2376
+
2377
+ } else if (subcommand === 'resolve') {
2378
+ if (!projectDir) {
2379
+ process.stderr.write('Error: agents resolve requires --project <dir>\n');
2380
+ process.exitCode = 1;
2381
+ return;
2382
+ }
2383
+ if (!agentId) {
2384
+ process.stderr.write('Error: agents resolve requires --id <canonicalId>\n');
2385
+ process.exitCode = 1;
2386
+ return;
2387
+ }
2388
+ const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
2389
+ const effectiveProject = path.resolve(projectDir);
2390
+ let record;
2391
+ try {
2392
+ record = await agentRegistry.resolveAgent({
2393
+ projectDir: effectiveProject,
2394
+ agentId: agentId,
2395
+ homeDir: effectiveHome,
2396
+ requireVerified: requireVerified,
2397
+ });
2398
+ } catch (err) {
2399
+ // requireVerified=true throws RESOLUTION_FAILED for non-verified agents;
2400
+ // the thrown error carries err.record with the full resolution record.
2401
+ const errorRecord = err.record
2402
+ ? Object.assign({}, err.record, { code: err.code })
2403
+ : { agentId: agentId, status: 'resolution-failed', error: err.message, code: err.code || 'UNKNOWN' };
2404
+ process.stderr.write(JSON.stringify(errorRecord, null, 2) + '\n');
2405
+ process.exitCode = 1;
2406
+ return;
2407
+ }
2408
+ process.stdout.write(JSON.stringify(record, null, 2) + '\n');
2409
+ if (requireVerified) {
2410
+ // Operational mode: exit 0 only for verified (non-verified would have thrown above)
2411
+ process.exitCode = record.status === 'verified' ? 0 : 1;
2412
+ } else {
2413
+ // Diagnostic mode: exit 0 for agent present in any integrity state
2414
+ const exitZeroStatuses = new Set(['verified', 'hash-unverifiable', 'hash-mismatch']);
2415
+ process.exitCode = exitZeroStatuses.has(record.status) ? 0 : 1;
2416
+ }
2417
+
2418
+ } else if (subcommand === 'preflight') {
2419
+ if (!projectDir) {
2420
+ process.stderr.write('Error: agents preflight requires --project <dir>\n');
2421
+ process.exitCode = 1;
2422
+ return;
2423
+ }
2424
+ if (!pipeline) {
2425
+ process.stderr.write('Error: agents preflight requires --pipeline <name>\n');
2426
+ process.exitCode = 1;
2427
+ return;
2428
+ }
2429
+ const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
2430
+ const effectiveProject = path.resolve(projectDir);
2431
+
2432
+ // Derive the required agent set from the CATALOG's allowedPipelines field.
2433
+ const pipelineAgents = agentRegistry.CATALOG.filter(function (e) {
2434
+ return Array.isArray(e.allowedPipelines) && e.allowedPipelines.indexOf(pipeline) !== -1;
2435
+ });
2436
+ if (pipelineAgents.length === 0) {
2437
+ process.stderr.write(
2438
+ JSON.stringify({ status: 'preflight-failed', error: 'Unknown pipeline: ' + pipeline }, null, 2) + '\n'
2439
+ );
2440
+ process.exitCode = 1;
2441
+ return;
2442
+ }
2443
+
2444
+ const results = [];
2445
+ let allPassed = true;
2446
+ for (let i = 0; i < pipelineAgents.length; i++) {
2447
+ const entry = pipelineAgents[i];
2448
+ try {
2449
+ const rec = await agentRegistry.resolveAgent({
2450
+ projectDir: effectiveProject,
2451
+ agentId: entry.agentId,
2452
+ homeDir: effectiveHome,
2453
+ requireVerified: true,
2454
+ });
2455
+ results.push({ agentId: rec.agentId, status: rec.status });
2456
+ } catch (err) {
2457
+ allPassed = false;
2458
+ const errRec = err.record
2459
+ ? Object.assign({}, err.record, { code: err.code })
2460
+ : { agentId: entry.agentId, status: 'resolution-failed', error: err.message, code: err.code || 'RESOLUTION_FAILED' };
2461
+ results.push(errRec);
2462
+ }
2463
+ }
2464
+
2465
+ if (allPassed) {
2466
+ process.stdout.write(
2467
+ JSON.stringify({ status: 'preflight-ok', pipeline: pipeline, agents: results }, null, 2) + '\n'
2468
+ );
2469
+ process.exitCode = 0;
2470
+ } else {
2471
+ process.stderr.write(
2472
+ JSON.stringify({ status: 'preflight-failed', pipeline: pipeline, agents: results }, null, 2) + '\n'
2473
+ );
2474
+ process.exitCode = 1;
2475
+ }
2476
+
2477
+ } else if (subcommand === 'cleanup') {
2478
+ // Reject any mutating flags up-front (AC-15).
2479
+ const hasDelete = remaining.indexOf('--delete') !== -1;
2480
+ const hasForce = remaining.indexOf('--force') !== -1;
2481
+ if (hasDelete || hasForce) {
2482
+ process.stderr.write(
2483
+ 'Error: agents cleanup does not support mutating flags (--delete, --force) in this version.\n' +
2484
+ ' Only --dry-run is supported; no files are ever modified or deleted.\n'
2485
+ );
2486
+ process.exitCode = 1;
2487
+ return;
2488
+ }
2489
+
2490
+ if (!projectDir) {
2491
+ process.stderr.write('Error: agents cleanup requires --project <dir>\n');
2492
+ process.exitCode = 1;
2493
+ return;
2494
+ }
2495
+ const effectiveHome = home !== undefined ? path.resolve(home) : os.homedir();
2496
+ const effectiveProject = path.resolve(projectDir);
2497
+
2498
+ // Locate the active manifest (project-scoped takes precedence over global).
2499
+ const projectManifestPath = path.join(effectiveProject, '.claude', MANIFEST_FILE);
2500
+ const globalManifestPath = path.join(effectiveHome, '.claude', MANIFEST_FILE);
2501
+
2502
+ let installRoot = null;
2503
+ let manifestData = null;
2504
+
2505
+ if (fs.existsSync(projectManifestPath)) {
2506
+ installRoot = effectiveProject;
2507
+ manifestData = readJsonSafe(projectManifestPath);
2508
+ } else if (fs.existsSync(globalManifestPath)) {
2509
+ installRoot = effectiveHome;
2510
+ manifestData = readJsonSafe(globalManifestPath);
2511
+ }
2512
+
2513
+ if (!installRoot || !manifestData || !Array.isArray(manifestData.files)) {
2514
+ process.stdout.write(
2515
+ JSON.stringify({ status: 'no-manifest', candidates: [], missing: [] }, null, 2) + '\n'
2516
+ );
2517
+ process.exitCode = 0;
2518
+ return;
2519
+ }
2520
+
2521
+ // Build the set of absolute paths that the current payload would install
2522
+ // into the same install root. Anything NOT in this set is a potential orphan.
2523
+ // buildExpectedPayload takes the .claude dir (same convention as the installer).
2524
+ const expectedPayload = buildExpectedPayload(path.join(installRoot, '.claude'));
2525
+
2526
+ const candidates = [];
2527
+ const missing = [];
2528
+
2529
+ for (const relPath of manifestData.files) {
2530
+ if (typeof relPath !== 'string') continue;
2531
+ const absPath = path.resolve(installRoot, relPath);
2532
+
2533
+ // If still in the current payload → not a cleanup candidate.
2534
+ if (expectedPayload.has(absPath)) continue;
2535
+
2536
+ // Not in current payload: check disk state.
2537
+ if (fs.existsSync(absPath)) {
2538
+ candidates.push({ path: relPath, absPath: absPath, status: 'candidate' });
2539
+ } else {
2540
+ missing.push({ path: relPath, absPath: absPath, status: 'missing' });
2541
+ }
2542
+ }
2543
+
2544
+ const output = {
2545
+ status: 'dry-run',
2546
+ installRoot: installRoot,
2547
+ candidates: candidates.map(function (c) { return { path: c.path, status: c.status }; }),
2548
+ missing: missing.map(function (m) { return { path: m.path, status: m.status }; }),
2549
+ };
2550
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
2551
+ process.exitCode = 0;
2552
+
2553
+ } else {
2554
+ process.stderr.write(
2555
+ 'Error: unknown agents subcommand: ' + (subcommand || '(none)') + '\n'
2556
+ );
2557
+ process.exitCode = 1;
2558
+ }
2559
+ }
2560
+
2561
+ // ── executor CLI git-context helpers ─────────────────────────────────────────
2562
+ // US-09-TASK-INFRA-01 (FTR-018): lib/task-executor/index.js's execute()/stop()
2563
+ // take only `project` and derive commonDir/executionRoot themselves via their
2564
+ // own internal _resolveCommonGitDir/_currentBranchRef helpers — but
2565
+ // reconcile()/resume()/replan() take `executionRoot`/`projectDir`/`taskRef` as
2566
+ // SEPARATE, already-derived arguments instead. Neither internal helper is
2567
+ // exported from that module (module.exports there is deliberately narrow —
2568
+ // see its own export list), and this task's brief is explicit: do not modify
2569
+ // lib/task-executor/index.js. These two helpers duplicate that exact same
2570
+ // git-command technique (same argv, same error codes: GIT_SPAWN_FAILED,
2571
+ // GIT_REF_RESOLUTION_FAILED, EXECUTE_VALIDATION_ERROR for a detached HEAD) so
2572
+ // the CLI can compute the values reconcile/resume/replan need from the single
2573
+ // --project flag the Tech-Spec's CLI contract actually exposes for them.
2574
+ function _executorResolveCommonGitDir(projectDir) {
2575
+ const { spawnSync } = require('child_process');
2576
+ const res = spawnSync('git', ['rev-parse', '--git-common-dir'], {
2577
+ cwd: projectDir, shell: false, encoding: 'utf8', windowsHide: true,
2578
+ });
2579
+ if (res.error || res.status !== 0) {
2580
+ const err = new Error(
2581
+ 'executor: failed to resolve the common Git directory for "' + projectDir + '": ' +
2582
+ (res.error ? res.error.message : (res.stderr || '').trim())
2583
+ );
2584
+ err.code = 'GIT_SPAWN_FAILED';
2585
+ throw err;
2586
+ }
2587
+ return path.resolve(projectDir, res.stdout.trim());
2588
+ }
2589
+
2590
+ function _executorExecutionRoot(projectDir) {
2591
+ return path.join(_executorResolveCommonGitDir(projectDir), 'ai-toolkit', 'execution');
2592
+ }
2593
+
2594
+ function _executorCurrentBranchRef(projectDir) {
2595
+ const { spawnSync } = require('child_process');
2596
+ const res = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
2597
+ cwd: projectDir, shell: false, encoding: 'utf8', windowsHide: true,
2598
+ });
2599
+ if (res.error || res.status !== 0) {
2600
+ const err = new Error(
2601
+ 'executor: failed to resolve the current branch for "' + projectDir + '": ' +
2602
+ (res.error ? res.error.message : (res.stderr || '').trim())
2603
+ );
2604
+ err.code = 'GIT_REF_RESOLUTION_FAILED';
2605
+ throw err;
2606
+ }
2607
+ const name = res.stdout.trim();
2608
+ if (name === 'HEAD') {
2609
+ const err = new Error(
2610
+ 'executor: "' + projectDir + '" has a detached HEAD — a task ref cannot be derived automatically'
2611
+ );
2612
+ err.code = 'EXECUTE_VALIDATION_ERROR';
2613
+ throw err;
2614
+ }
2615
+ return name;
2616
+ }
2617
+
2618
+ // ── executor exit-code mapping ───────────────────────────────────────────────
2619
+ // FTR-018-Tech-Spec.md section 10 defines a FIXED 9-value exit-code enum for
2620
+ // the executor CLI (0 success; 1 unexpected I/O; 2 invalid config/input;
2621
+ // 3 ownership/live worker conflict; 4 evidence/plan/approval mismatch;
2622
+ // 5 worker/verification/review failure; 6 integration conflict; 7 runtime not
2623
+ // qualified; 8 paused awaiting human decision) — but the lib/task-executor/*.js
2624
+ // + lib/execution-ledger.js + lib/agent-registry.js modules underneath it
2625
+ // collectively throw several dozen distinct `err.code` values. This table is
2626
+ // the single explicit, documented judgment call synthesizing all of them into
2627
+ // the fixed enum; it does not re-derive or duplicate the underlying modules'
2628
+ // own validation, it only classifies their already-thrown codes for exit
2629
+ // status purposes. Grouping rules, in the order applied:
2630
+ //
2631
+ // 3 (ownership/live worker conflict) — every lease/ownership-lock code, and
2632
+ // every code meaning "a competing coordinator or live worker exists":
2633
+ // LEASE_HELD, LEASE_NOT_OWNER, LEASE_STILL_LIVE, LEASE_LIVENESS_UNKNOWN,
2634
+ // LEASE_NOT_FOUND, GUARD_HELD, NO_LOCK_HELD, COMPETING_COORDINATOR_LIVE,
2635
+ // COMPETING_COORDINATOR_LIVENESS_UNKNOWN, LIVE_WORKER_BLOCKS_REPLAN.
2636
+ //
2637
+ // 4 (evidence/plan/approval mismatch) — everything meaning "the plan,
2638
+ // approval, or recorded evidence does not match what was expected",
2639
+ // including "expected evidence/record could not be found" (same bucket as
2640
+ // PLAN_NOT_FOUND, per this task's own brief) and immutable-record replay
2641
+ // conflicts (a conflicting second decision is itself an evidence
2642
+ // mismatch, not an I/O failure): SUCCESSOR_NOT_APPROVED,
2643
+ // SUCCESSOR_PLAN_NOT_FOUND, PLAN_NOT_FOUND, PLAN_PARSE_ERROR,
2644
+ // REPLAN_UNKNOWN_TASK_MAPPING, ATTEMPT_NUMBER_MISMATCH,
2645
+ // STATE_READBACK_MISMATCH, STATE_NOT_FOUND, STATE_TASK_NOT_FOUND,
2646
+ // STATE_ATTEMPT_NOT_FOUND, STATE_TASK_EXISTS, TREE_MISMATCH,
2647
+ // PARENT_MISMATCH, COMMIT_REINSPECTION_MISMATCH,
2648
+ // INTEGRATION_PARENT_MISMATCH, INTEGRATION_REINSPECTION_MISMATCH,
2649
+ // INTEGRATION_WORKTREE_STATE_MISMATCH, INTEGRATION_WORKTREE_NOT_CLEAN,
2650
+ // INTEGRATION_SOURCE_NOT_FOUND, INTEGRATION_SOURCE_NOT_ON_BRANCH,
2651
+ // UNEXPECTED_INDEX_CHANGE, STAGING_MISMATCH, DEFINITION_HASH_MISMATCH,
2652
+ // SHA_REGISTRATION_CONFLICT, SHA_REGISTRATION_STAGE_ERROR,
2653
+ // INTEGRATION_SHA_CONFLICT, INTEGRATION_SHA_STAGE_ERROR,
2654
+ // CHECKPOINT_INTENT_STAGE_ERROR, RECEIPT_CONFLICT, RECEIPT_NOT_FOUND,
2655
+ // INTENT_CONFLICT, INTENT_NOT_FOUND, METADATA_CONFLICT,
2656
+ // ACTIVITY_NOT_FOUND, TERMINAL_CONFLICT.
2657
+ //
2658
+ // 5 (worker/verification/review failure) — the dispatched agent/worker
2659
+ // process itself failed, could not be verified, or could not be spawned:
2660
+ // CLAUDE_SPAWN_FAILED, AGENT_NOT_VERIFIED, RESOLUTION_FAILED.
2661
+ //
2662
+ // 6 (integration conflict) — INTEGRATION_CONFLICT.
2663
+ //
2664
+ // 7 (runtime not qualified) — PLATFORM_NOT_QUALIFIED.
2665
+ //
2666
+ // 2 (invalid config/input) — every *_VALIDATION_ERROR code (there are many;
2667
+ // matched by suffix in mapExecutorErrorToExitCode below rather than
2668
+ // enumerated one-by-one), plus UNSUPPORTED_CONCURRENCY (Tech-Spec:
2669
+ // "reject, never downgrade"), CLAUDE_EXECUTABLE_NOT_FOUND ("unsupported
2670
+ // executable ... fail before mutation"), UNKNOWN_AGENT_TYPE,
2671
+ // METADATA_RESERVED_FIELD, METADATA_UNKNOWN_KEY, INVALID_STATUS.
2672
+ //
2673
+ // 1 (unexpected I/O) — the DEFAULT for everything else, including every
2674
+ // *_CORRUPTED/CORRUPT_* code, every GIT_*_FAILED/*_FAILED git-command-
2675
+ // spawn code, STATE_UNRECOVERABLE, and — explicitly, per this task's own
2676
+ // brief — NOT_IMPLEMENTED. `status`/`diagnose` are still NOT_IMPLEMENTED
2677
+ // stubs in lib/task-executor/index.js (attributed to US-06-TASK-BE-01/02,
2678
+ // neither of which has actually built their real body yet); this CLI
2679
+ // wires them through exactly like the other five commands and lets that
2680
+ // error surface through this same mapping table honestly — it is not
2681
+ // hidden or special-cased, it simply falls into the default bucket.
2682
+ //
2683
+ // 8 (paused awaiting human decision) has no throw site and is deliberately
2684
+ // absent from this table: execute()'s/resume()'s 'paused' run status is a
2685
+ // SUCCESS-path result field (returned, not thrown), so no error code maps to
2686
+ // exit 8 here. It is instead selected on the success path, in
2687
+ // handleExecutorCommand's own emitSuccess helper below, by inspecting
2688
+ // result.runStatus.
2689
+ const EXECUTOR_EXIT_CODE_BY_ERROR_CODE = {
2690
+ // 3 — ownership / live worker conflict
2691
+ LEASE_HELD: 3, LEASE_NOT_OWNER: 3, LEASE_STILL_LIVE: 3, LEASE_LIVENESS_UNKNOWN: 3,
2692
+ LEASE_NOT_FOUND: 3, GUARD_HELD: 3, NO_LOCK_HELD: 3, COMPETING_COORDINATOR_LIVE: 3,
2693
+ COMPETING_COORDINATOR_LIVENESS_UNKNOWN: 3, LIVE_WORKER_BLOCKS_REPLAN: 3,
2694
+
2695
+ // 4 — evidence / plan / approval mismatch
2696
+ SUCCESSOR_NOT_APPROVED: 4, SUCCESSOR_PLAN_NOT_FOUND: 4, PLAN_NOT_FOUND: 4,
2697
+ PLAN_PARSE_ERROR: 4, REPLAN_UNKNOWN_TASK_MAPPING: 4, ATTEMPT_NUMBER_MISMATCH: 4,
2698
+ STATE_READBACK_MISMATCH: 4, STATE_NOT_FOUND: 4, STATE_TASK_NOT_FOUND: 4,
2699
+ STATE_ATTEMPT_NOT_FOUND: 4, STATE_TASK_EXISTS: 4, TREE_MISMATCH: 4,
2700
+ PARENT_MISMATCH: 4, COMMIT_REINSPECTION_MISMATCH: 4, INTEGRATION_PARENT_MISMATCH: 4,
2701
+ INTEGRATION_REINSPECTION_MISMATCH: 4, INTEGRATION_WORKTREE_STATE_MISMATCH: 4,
2702
+ INTEGRATION_WORKTREE_NOT_CLEAN: 4, INTEGRATION_SOURCE_NOT_FOUND: 4,
2703
+ INTEGRATION_SOURCE_NOT_ON_BRANCH: 4, UNEXPECTED_INDEX_CHANGE: 4, STAGING_MISMATCH: 4,
2704
+ DEFINITION_HASH_MISMATCH: 4, SHA_REGISTRATION_CONFLICT: 4, SHA_REGISTRATION_STAGE_ERROR: 4,
2705
+ INTEGRATION_SHA_CONFLICT: 4, INTEGRATION_SHA_STAGE_ERROR: 4, CHECKPOINT_INTENT_STAGE_ERROR: 4,
2706
+ RECEIPT_CONFLICT: 4, RECEIPT_NOT_FOUND: 4, INTENT_CONFLICT: 4, INTENT_NOT_FOUND: 4,
2707
+ METADATA_CONFLICT: 4, ACTIVITY_NOT_FOUND: 4, TERMINAL_CONFLICT: 4,
2708
+
2709
+ // 5 — worker / verification / review failure
2710
+ CLAUDE_SPAWN_FAILED: 5, AGENT_NOT_VERIFIED: 5, RESOLUTION_FAILED: 5,
2711
+
2712
+ // 6 — integration conflict
2713
+ INTEGRATION_CONFLICT: 6,
2714
+
2715
+ // 7 — runtime not qualified
2716
+ PLATFORM_NOT_QUALIFIED: 7,
2717
+
2718
+ // 2 — invalid config/input (non-*_VALIDATION_ERROR entries; every
2719
+ // *_VALIDATION_ERROR code is matched by suffix in mapExecutorErrorToExitCode
2720
+ // below rather than listed here)
2721
+ UNSUPPORTED_CONCURRENCY: 2, CLAUDE_EXECUTABLE_NOT_FOUND: 2, UNKNOWN_AGENT_TYPE: 2,
2722
+ METADATA_RESERVED_FIELD: 2, METADATA_UNKNOWN_KEY: 2, INVALID_STATUS: 2,
2723
+
2724
+ // 1 — unexpected I/O (explicit entries kept for documentation clarity even
2725
+ // though 1 is also the default fallback below)
2726
+ STATE_CORRUPTED: 1, RECEIPT_CORRUPTED: 1, INTENT_CORRUPTED: 1, LEASE_CORRUPTED: 1,
2727
+ CORRUPT_LEDGER: 1, STATE_UNRECOVERABLE: 1, NOT_IMPLEMENTED: 1,
2728
+ GIT_SPAWN_FAILED: 1, GIT_STATUS_FAILED: 1, GIT_ADD_FAILED: 1, GIT_WRITE_TREE_FAILED: 1,
2729
+ GIT_DIFF_FAILED: 1, GIT_COMMIT_TREE_FAILED: 1, REF_UPDATE_FAILED: 1, GIT_CAT_FILE_FAILED: 1,
2730
+ GIT_MERGE_BASE_FAILED: 1, GIT_REV_PARSE_FAILED: 1, GIT_CHERRY_PICK_FAILED: 1,
2731
+ GIT_REF_RESOLUTION_FAILED: 1, INTEGRATION_CLEANUP_FAILED: 1,
2732
+ };
2733
+
2734
+ function mapExecutorErrorToExitCode(code) {
2735
+ if (typeof code === 'string' && Object.prototype.hasOwnProperty.call(EXECUTOR_EXIT_CODE_BY_ERROR_CODE, code)) {
2736
+ return EXECUTOR_EXIT_CODE_BY_ERROR_CODE[code];
2737
+ }
2738
+ // Suffix-based catch-all for the many *_VALIDATION_ERROR codes
2739
+ // (DISPATCH_VALIDATION_ERROR, LEASE_VALIDATION_ERROR,
2740
+ // CLAUDE_PROCESS_VALIDATION_ERROR, STATE_VALIDATION_ERROR,
2741
+ // STAGING_VALIDATION_ERROR, DETECT_CHANGED_PATHS_VALIDATION_ERROR,
2742
+ // COMMIT_VALIDATION_ERROR, INTEGRATION_VALIDATION_ERROR,
2743
+ // WORKTREE_VALIDATION_ERROR, SLOT_POOL_VALIDATION_ERROR,
2744
+ // SHA_REGISTRATION_VALIDATION_ERROR, INTEGRATION_SHA_VALIDATION_ERROR,
2745
+ // CHECKPOINT_INTENT_VALIDATION_ERROR, EXECUTE_VALIDATION_ERROR,
2746
+ // STOP_VALIDATION_ERROR, and any future addition following the same naming
2747
+ // convention) — every one of them means "malformed caller input", i.e.
2748
+ // exit 2, without needing its own table row.
2749
+ if (typeof code === 'string' && code.endsWith('_VALIDATION_ERROR')) {
2750
+ return 2;
2751
+ }
2752
+ // Unknown/unmapped code, including a missing .code entirely: safe default.
2753
+ return 1;
2754
+ }
2755
+
2756
+ // ── executor dispatcher ───────────────────────────────────────────────────────
2757
+ // US-09-TASK-INFRA-01 (FTR-018): Routes `ai-toolkit executor <subcommand>
2758
+ // [flags]` to lib/task-executor/index.js's execute/status/diagnose/stop/
2759
+ // reconcile/resume/replan (the seven commands of the CLI contract in
2760
+ // FTR-018-Tech-Spec.md section 10). All executor domain logic lives in
2761
+ // lib/task-executor/*.js; this function only parses CLI flags, derives the
2762
+ // couple of git-context values reconcile()/resume()/replan() require but do
2763
+ // not derive themselves (see _executorExecutionRoot/_executorCurrentBranchRef
2764
+ // above), and maps thrown error codes to the Tech-Spec's fixed exit-code enum
2765
+ // (see mapExecutorErrorToExitCode above) — it is NOT a second copy of any
2766
+ // executor logic.
2767
+ //
2768
+ // Flag-rejection mechanism for "no --force*, lock/state path overrides,
2769
+ // implicit --abandon or runtime concurrency adjustment" (Tech-Spec section
2770
+ // 10): this parser simply never recognizes such flags for any subcommand
2771
+ // below. An unrecognized flag is silently ignored rather than explicitly
2772
+ // rejected — there is nothing to reject because nothing here ever reads an
2773
+ // arbitrary flag by name and forwards it to a filesystem path or lock
2774
+ // primitive; only the whitelisted flags per subcommand are ever read via
2775
+ // getFlag()/hasFlag().
2776
+ async function handleExecutorCommand(argv) {
2777
+ const subcommand = argv[0];
2778
+ const remaining = argv.slice(1);
2779
+
2780
+ function getFlag(name) {
2781
+ const idx = remaining.indexOf(name);
2782
+ return idx !== -1 && remaining[idx + 1] !== undefined ? remaining[idx + 1] : undefined;
2783
+ }
2784
+
2785
+ function emitSuccess(result) {
2786
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
2787
+ // Tech-Spec section 10 exit code 8 ("paused awaiting human decision") is a
2788
+ // SUCCESS-path outcome, not an error — only execute()'s and resume()'s
2789
+ // result objects can legitimately carry `runStatus: 'paused'` (see the
2790
+ // exit-code mapping comment above EXECUTOR_EXIT_CODE_BY_ERROR_CODE for why
2791
+ // no *_ERROR code maps to 8). reconcile/stop/replan results never carry a
2792
+ // `runStatus` field at all, and status/diagnose never reach this success
2793
+ // path (still NOT_IMPLEMENTED) — so this check is a no-op for every other
2794
+ // subcommand's result shape and never needs to be scoped by subcommand.
2795
+ process.exitCode = result && result.runStatus === 'paused' ? 8 : 0;
2796
+ }
2797
+
2798
+ function emitError(err) {
2799
+ const code = err && err.code ? err.code : undefined;
2800
+ const exitCode = mapExecutorErrorToExitCode(code);
2801
+ process.stderr.write(JSON.stringify({
2802
+ status: 'error',
2803
+ code: code || 'UNKNOWN',
2804
+ message: err && err.message ? err.message : String(err),
2805
+ }, null, 2) + '\n');
2806
+ process.exitCode = exitCode;
2807
+ }
2808
+
2809
+ function usageError(message) {
2810
+ process.stderr.write('Error: ' + message + '\n');
2811
+ process.exitCode = 2;
2812
+ }
2813
+
2814
+ const projectDir = path.resolve(getFlag('--project') || process.cwd());
2815
+
2816
+ try {
2817
+ const taskExecutor = require('../lib/task-executor');
2818
+
2819
+ if (subcommand === 'start') {
2820
+ const feature = getFlag('--feature');
2821
+ if (!feature) { usageError('executor start requires --feature'); return; }
2822
+ const maxConcurrencyRaw = getFlag('--max-concurrency');
2823
+ const taskTimeoutMsRaw = getFlag('--task-timeout-ms');
2824
+ const agentBudgetUsdRaw = getFlag('--agent-budget-usd');
2825
+ const result = await taskExecutor.execute({
2826
+ project: projectDir,
2827
+ feature: feature,
2828
+ maxConcurrency: maxConcurrencyRaw === undefined ? undefined : Number(maxConcurrencyRaw),
2829
+ claudePath: getFlag('--claude-path'),
2830
+ taskTimeoutMs: taskTimeoutMsRaw === undefined ? undefined : Number(taskTimeoutMsRaw),
2831
+ agentBudgetUsd: agentBudgetUsdRaw === undefined ? undefined : Number(agentBudgetUsdRaw),
2832
+ });
2833
+ emitSuccess(result);
2834
+
2835
+ } else if (subcommand === 'status') {
2836
+ const runId = getFlag('--run-id');
2837
+ if (!runId) { usageError('executor status requires --run-id'); return; }
2838
+ const result = await taskExecutor.status({ project: projectDir, runId: runId });
2839
+ emitSuccess(result);
2840
+
2841
+ } else if (subcommand === 'diagnose') {
2842
+ const runId = getFlag('--run-id');
2843
+ if (!runId) { usageError('executor diagnose requires --run-id'); return; }
2844
+ const result = await taskExecutor.diagnose({ project: projectDir, runId: runId });
2845
+ emitSuccess(result);
2846
+
2847
+ } else if (subcommand === 'stop') {
2848
+ const runId = getFlag('--run-id');
2849
+ if (!runId) { usageError('executor stop requires --run-id'); return; }
2850
+ const result = await taskExecutor.stop({
2851
+ project: projectDir,
2852
+ runId: runId,
2853
+ mode: getFlag('--mode') || 'graceful',
2854
+ });
2855
+ emitSuccess(result);
2856
+
2857
+ } else if (subcommand === 'reconcile' || subcommand === 'resume') {
2858
+ const runId = getFlag('--run-id');
2859
+ if (!runId) { usageError('executor ' + subcommand + ' requires --run-id'); return; }
2860
+ const executionRoot = _executorExecutionRoot(projectDir);
2861
+ const taskRef = _executorCurrentBranchRef(projectDir);
2862
+ const fn = subcommand === 'reconcile' ? taskExecutor.reconcile : taskExecutor.resume;
2863
+ const result = await fn({
2864
+ executionRoot: executionRoot,
2865
+ runId: runId,
2866
+ projectDir: projectDir,
2867
+ taskRef: taskRef,
2868
+ });
2869
+ emitSuccess(result);
2870
+
2871
+ } else if (subcommand === 'replan') {
2872
+ const runId = getFlag('--run-id');
2873
+ const feature = getFlag('--feature');
2874
+ if (!runId) { usageError('executor replan requires --run-id'); return; }
2875
+ if (!feature) { usageError('executor replan requires --feature (the approved successor plan)'); return; }
2876
+
2877
+ // --task-mapping-json is NOT one of the Tech-Spec table's listed
2878
+ // arguments for replan (only --run-id/--feature are) — but
2879
+ // lib/task-executor/index.js's replan() requires args.taskMapping to be
2880
+ // an array (DISPATCH_VALIDATION_ERROR otherwise) and never invents one
2881
+ // itself (see its own doc comment: "taskMapping is an explicit,
2882
+ // caller-supplied old->new task ID mapping"). Following this session's
2883
+ // established pattern of flagging gaps rather than papering over them:
2884
+ // this flag is documented here as a necessary CLI-layer extension
2885
+ // beyond the literal table, using the same `--*-json` structured-input
2886
+ // convention the ledger subcommand already established
2887
+ // (--metadata-json). Omitting it defaults to an empty mapping (a valid
2888
+ // array — "no task carries forward" — never rejected by replan()).
2889
+ let taskMapping = [];
2890
+ const taskMappingJson = getFlag('--task-mapping-json');
2891
+ if (taskMappingJson !== undefined) {
2892
+ try {
2893
+ taskMapping = JSON.parse(taskMappingJson);
2894
+ } catch (parseErr) {
2895
+ usageError('--task-mapping-json is not valid JSON: ' + parseErr.message);
2896
+ return;
2897
+ }
2898
+ }
2899
+
2900
+ const executionRoot = _executorExecutionRoot(projectDir);
2901
+ const taskRef = _executorCurrentBranchRef(projectDir);
2902
+ const result = await taskExecutor.replan({
2903
+ executionRoot: executionRoot,
2904
+ runId: runId,
2905
+ projectDir: projectDir,
2906
+ taskRef: taskRef,
2907
+ feature: feature,
2908
+ taskMapping: taskMapping,
2909
+ });
2910
+ emitSuccess(result);
2911
+
2912
+ } else {
2913
+ usageError('unknown executor subcommand: ' + (subcommand || '(none)'));
2914
+ }
2915
+ } catch (err) {
2916
+ emitError(err);
2917
+ }
2918
+ }
2919
+
2086
2920
  // ── entry point guard ─────────────────────────────────────────────────────────
2087
2921
  // Run the CLI only when invoked directly (node bin/cli.js).
2088
2922
  // When required as a module (e.g., by Jest), skip main() and export pure
@@ -2127,5 +2961,13 @@ if (require.main === module) {
2127
2961
  handleLedgerCommand,
2128
2962
  sortedJson,
2129
2963
  resolveFeaturesRoot,
2964
+ computeFileSha256,
2965
+ handleAgentsCommand,
2966
+ handleExecutorCommand,
2967
+ mapExecutorErrorToExitCode,
2968
+ EXECUTOR_EXIT_CODE_BY_ERROR_CODE,
2969
+ _executorResolveCommonGitDir,
2970
+ _executorExecutionRoot,
2971
+ _executorCurrentBranchRef,
2130
2972
  };
2131
2973
  }