hatch3r 1.7.1 → 1.7.5

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 (150) hide show
  1. package/README.md +37 -11
  2. package/agents/hatch3r-a11y-auditor.md +4 -0
  3. package/agents/hatch3r-architect.md +4 -0
  4. package/agents/hatch3r-ci-watcher.md +4 -0
  5. package/agents/hatch3r-context-rules.md +4 -0
  6. package/agents/hatch3r-creator.md +4 -0
  7. package/agents/hatch3r-dependency-auditor.md +4 -0
  8. package/agents/hatch3r-devops.md +4 -0
  9. package/agents/hatch3r-docs-writer.md +4 -0
  10. package/agents/hatch3r-fixer.md +4 -0
  11. package/agents/hatch3r-handoff-loader.md +243 -0
  12. package/agents/hatch3r-handoff-preparer.md +134 -0
  13. package/agents/hatch3r-implementer.md +4 -0
  14. package/agents/hatch3r-learnings-loader.md +4 -0
  15. package/agents/hatch3r-lint-fixer.md +4 -0
  16. package/agents/hatch3r-perf-profiler.md +8 -0
  17. package/agents/hatch3r-researcher.md +4 -0
  18. package/agents/hatch3r-reviewer.md +92 -0
  19. package/agents/hatch3r-security-auditor.md +24 -0
  20. package/agents/hatch3r-test-writer.md +4 -0
  21. package/agents/modes/requirements-elicitation.md +4 -1
  22. package/agents/modes/similar-implementation.md +6 -0
  23. package/agents/modes/user-flows.md +76 -0
  24. package/agents/shared/quality-charter.md +128 -0
  25. package/commands/hatch3r-board-fill.md +1 -0
  26. package/commands/hatch3r-create.md +2 -0
  27. package/commands/hatch3r-handoff.md +126 -0
  28. package/commands/hatch3r-pr-resolve.md +4 -0
  29. package/commands/hatch3r-quick-change.md +4 -2
  30. package/commands/hatch3r-workflow.md +2 -0
  31. package/dist/cli/index.js +2321 -460
  32. package/dist/cli/index.js.map +1 -1
  33. package/package.json +4 -2
  34. package/rules/hatch3r-accessibility-standards.md +21 -0
  35. package/rules/hatch3r-accessibility-standards.mdc +21 -0
  36. package/rules/hatch3r-agent-orchestration.md +9 -1
  37. package/rules/hatch3r-agent-orchestration.mdc +9 -1
  38. package/rules/hatch3r-ai-evals.md +158 -0
  39. package/rules/hatch3r-ai-evals.mdc +154 -0
  40. package/rules/hatch3r-ai-ux-patterns.md +131 -0
  41. package/rules/hatch3r-ai-ux-patterns.mdc +127 -0
  42. package/rules/hatch3r-api-design.md +67 -9
  43. package/rules/hatch3r-api-design.mdc +67 -9
  44. package/rules/hatch3r-api-versioning.md +119 -0
  45. package/rules/hatch3r-api-versioning.mdc +115 -0
  46. package/rules/hatch3r-auth-patterns.md +170 -0
  47. package/rules/hatch3r-auth-patterns.mdc +166 -0
  48. package/rules/hatch3r-component-conventions.md +30 -0
  49. package/rules/hatch3r-component-conventions.mdc +30 -0
  50. package/rules/hatch3r-container-hardening.md +131 -0
  51. package/rules/hatch3r-container-hardening.mdc +127 -0
  52. package/rules/hatch3r-contract-testing.md +117 -0
  53. package/rules/hatch3r-contract-testing.mdc +113 -0
  54. package/rules/hatch3r-deep-context.md +2 -0
  55. package/rules/hatch3r-deep-context.mdc +2 -0
  56. package/rules/hatch3r-dependency-management.md +73 -1
  57. package/rules/hatch3r-dependency-management.mdc +72 -0
  58. package/rules/hatch3r-design-system-detection.md +142 -0
  59. package/rules/hatch3r-design-system-detection.mdc +138 -0
  60. package/rules/hatch3r-event-schema-evolution.md +90 -0
  61. package/rules/hatch3r-event-schema-evolution.mdc +86 -0
  62. package/rules/hatch3r-handoff-readiness.md +45 -0
  63. package/rules/hatch3r-handoff-readiness.mdc +40 -0
  64. package/rules/hatch3r-i18n.md +13 -0
  65. package/rules/hatch3r-i18n.mdc +13 -0
  66. package/rules/hatch3r-migrations.md +61 -16
  67. package/rules/hatch3r-migrations.mdc +61 -16
  68. package/rules/hatch3r-observability-logging.md +1 -1
  69. package/rules/hatch3r-observability-logging.mdc +1 -1
  70. package/rules/hatch3r-observability-metrics.md +1 -1
  71. package/rules/hatch3r-observability-metrics.mdc +1 -1
  72. package/rules/hatch3r-observability-tracing-detail.md +1 -1
  73. package/rules/hatch3r-observability-tracing-detail.mdc +1 -1
  74. package/rules/hatch3r-observability-tracing.md +1 -1
  75. package/rules/hatch3r-observability-tracing.mdc +1 -1
  76. package/rules/hatch3r-observability.md +1 -0
  77. package/rules/hatch3r-observability.mdc +1 -0
  78. package/rules/hatch3r-operability.md +149 -0
  79. package/rules/hatch3r-operability.mdc +145 -0
  80. package/rules/hatch3r-passkey-server.md +181 -0
  81. package/rules/hatch3r-passkey-server.mdc +177 -0
  82. package/rules/hatch3r-progressive-delivery.md +120 -0
  83. package/rules/hatch3r-progressive-delivery.mdc +116 -0
  84. package/rules/hatch3r-resilience-patterns.md +154 -0
  85. package/rules/hatch3r-resilience-patterns.mdc +150 -0
  86. package/rules/hatch3r-secrets-management.md +29 -0
  87. package/rules/hatch3r-secrets-management.mdc +29 -0
  88. package/rules/hatch3r-testing.md +139 -43
  89. package/rules/hatch3r-testing.mdc +139 -43
  90. package/rules/hatch3r-ux-states-and-flows.md +149 -0
  91. package/rules/hatch3r-ux-states-and-flows.mdc +145 -0
  92. package/skills/hatch3r-a11y-audit/SKILL.md +14 -0
  93. package/skills/hatch3r-ai-feature/SKILL.md +134 -0
  94. package/skills/hatch3r-api-spec/SKILL.md +5 -0
  95. package/skills/hatch3r-architecture-review/SKILL.md +14 -0
  96. package/skills/hatch3r-bug-fix/SKILL.md +5 -0
  97. package/skills/hatch3r-ci-pipeline/SKILL.md +14 -0
  98. package/skills/hatch3r-cli-aichat/SKILL.md +84 -0
  99. package/skills/hatch3r-cli-ast-grep/SKILL.md +85 -0
  100. package/skills/hatch3r-cli-az-devops/SKILL.md +89 -0
  101. package/skills/hatch3r-cli-bat/SKILL.md +85 -0
  102. package/skills/hatch3r-cli-comby/SKILL.md +85 -0
  103. package/skills/hatch3r-cli-csvkit/SKILL.md +84 -0
  104. package/skills/hatch3r-cli-delta/SKILL.md +86 -0
  105. package/skills/hatch3r-cli-difftastic/SKILL.md +84 -0
  106. package/skills/hatch3r-cli-docker/SKILL.md +89 -0
  107. package/skills/hatch3r-cli-duckdb/SKILL.md +84 -0
  108. package/skills/hatch3r-cli-fd/SKILL.md +85 -0
  109. package/skills/hatch3r-cli-fzf/SKILL.md +84 -0
  110. package/skills/hatch3r-cli-gh/SKILL.md +90 -0
  111. package/skills/hatch3r-cli-glab/SKILL.md +89 -0
  112. package/skills/hatch3r-cli-jq/SKILL.md +85 -0
  113. package/skills/hatch3r-cli-lazygit/SKILL.md +78 -0
  114. package/skills/hatch3r-cli-llm/SKILL.md +84 -0
  115. package/skills/hatch3r-cli-miller/SKILL.md +84 -0
  116. package/skills/hatch3r-cli-mods/SKILL.md +84 -0
  117. package/skills/hatch3r-cli-overview/SKILL.md +60 -0
  118. package/skills/hatch3r-cli-playwright/SKILL.md +89 -0
  119. package/skills/hatch3r-cli-podman/SKILL.md +84 -0
  120. package/skills/hatch3r-cli-ripgrep/SKILL.md +85 -0
  121. package/skills/hatch3r-cli-rtk/SKILL.md +91 -0
  122. package/skills/hatch3r-cli-sd/SKILL.md +85 -0
  123. package/skills/hatch3r-cli-stagehand/SKILL.md +79 -0
  124. package/skills/hatch3r-cli-taplo/SKILL.md +84 -0
  125. package/skills/hatch3r-cli-xsv/SKILL.md +89 -0
  126. package/skills/hatch3r-cli-yq/SKILL.md +85 -0
  127. package/skills/hatch3r-cli-zstd/SKILL.md +85 -0
  128. package/skills/hatch3r-context-health/SKILL.md +14 -0
  129. package/skills/hatch3r-cost-tracking/SKILL.md +14 -0
  130. package/skills/hatch3r-customize/SKILL.md +14 -0
  131. package/skills/hatch3r-dep-audit/SKILL.md +14 -0
  132. package/skills/hatch3r-design-system-detect/SKILL.md +162 -0
  133. package/skills/hatch3r-feature/SKILL.md +2 -0
  134. package/skills/hatch3r-gh-agentic-workflows/SKILL.md +13 -0
  135. package/skills/hatch3r-handoff-prepare/SKILL.md +160 -0
  136. package/skills/hatch3r-handoff-resume/SKILL.md +171 -0
  137. package/skills/hatch3r-incident-response/SKILL.md +14 -0
  138. package/skills/hatch3r-issue-workflow/SKILL.md +5 -0
  139. package/skills/hatch3r-logical-refactor/SKILL.md +14 -0
  140. package/skills/hatch3r-migration/SKILL.md +14 -0
  141. package/skills/hatch3r-observability-verify/SKILL.md +133 -0
  142. package/skills/hatch3r-perf-audit/SKILL.md +14 -0
  143. package/skills/hatch3r-pr-creation/SKILL.md +14 -0
  144. package/skills/hatch3r-qa-validation/SKILL.md +18 -0
  145. package/skills/hatch3r-recipe/SKILL.md +14 -0
  146. package/skills/hatch3r-refactor/SKILL.md +14 -0
  147. package/skills/hatch3r-release/SKILL.md +14 -0
  148. package/skills/hatch3r-reliability-verify/SKILL.md +144 -0
  149. package/skills/hatch3r-ui-ux-verify/SKILL.md +136 -0
  150. package/skills/hatch3r-visual-refactor/SKILL.md +15 -1
package/dist/cli/index.js CHANGED
@@ -14,7 +14,7 @@ var HATCH3R_VERSION;
14
14
  var init_version = __esm({
15
15
  "src/version.ts"() {
16
16
  "use strict";
17
- HATCH3R_VERSION = "1.7.1";
17
+ HATCH3R_VERSION = "1.7.5";
18
18
  }
19
19
  });
20
20
 
@@ -79,7 +79,8 @@ function printBox(title, lines, style = "info") {
79
79
  const colors = {
80
80
  success: "#10b981",
81
81
  info: "#06b6d4",
82
- error: "#ef4444"
82
+ error: "#ef4444",
83
+ warning: "#f59e0b"
83
84
  };
84
85
  const content = lines.join("\n");
85
86
  console.log(
@@ -138,6 +139,15 @@ var init_ui = __esm({
138
139
  });
139
140
 
140
141
  // src/types.ts
142
+ function getMarkersForPath(filePath) {
143
+ if (filePath) {
144
+ const lower = filePath.toLowerCase();
145
+ if (lower.endsWith(".yml") || lower.endsWith(".yaml")) {
146
+ return { start: MANAGED_BLOCK_START_YAML, end: MANAGED_BLOCK_END_YAML };
147
+ }
148
+ }
149
+ return { start: MANAGED_BLOCK_START, end: MANAGED_BLOCK_END };
150
+ }
141
151
  function sanitizeId(id) {
142
152
  return id.replace(/[^a-zA-Z0-9._-]/g, "");
143
153
  }
@@ -145,7 +155,7 @@ function toPrefixedId(id, prefix = HATCH3R_PREFIX) {
145
155
  const base = id.replace(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "");
146
156
  return `${prefix}${base}`;
147
157
  }
148
- var TOOLS, VALID_TOOLS, TOOL_CHOICES, WORKTREE_CAPABLE_TOOLS, MANAGED_BLOCK_START, MANAGED_BLOCK_END, HATCH3R_PREFIX, AGENTS_DIR, ARCHIVE_DIR, CUSTOMIZE_DIR, ERROR_CODE_TO_EXIT_CODE, HatchError, MANIFEST_FILE, WORKTREE_INCLUDE_FILE, DEFAULT_FEATURES, ENV_VAR_HELP, AVAILABLE_MCP_SERVERS;
158
+ var TOOLS, VALID_TOOLS, TOOL_CHOICES, WORKTREE_CAPABLE_TOOLS, MANAGED_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START_YAML, MANAGED_BLOCK_END_YAML, MANAGED_BLOCK_VARIANTS, HATCH3R_PREFIX, AGENTS_DIR, ARCHIVE_DIR, CUSTOMIZE_DIR, ERROR_CODE_TO_EXIT_CODE, HatchError, MANIFEST_FILE, WORKTREE_INCLUDE_FILE, DEFAULT_FEATURES, ENV_VAR_HELP, AVAILABLE_MCP_SERVERS;
149
159
  var init_types = __esm({
150
160
  "src/types.ts"() {
151
161
  "use strict";
@@ -155,6 +165,12 @@ var init_types = __esm({
155
165
  WORKTREE_CAPABLE_TOOLS = /* @__PURE__ */ new Set(["claude"]);
156
166
  MANAGED_BLOCK_START = "<!-- HATCH3R:BEGIN -->";
157
167
  MANAGED_BLOCK_END = "<!-- HATCH3R:END -->";
168
+ MANAGED_BLOCK_START_YAML = "# HATCH3R:BEGIN";
169
+ MANAGED_BLOCK_END_YAML = "# HATCH3R:END";
170
+ MANAGED_BLOCK_VARIANTS = [
171
+ { start: MANAGED_BLOCK_START, end: MANAGED_BLOCK_END },
172
+ { start: MANAGED_BLOCK_START_YAML, end: MANAGED_BLOCK_END_YAML }
173
+ ];
158
174
  HATCH3R_PREFIX = "hatch3r-";
159
175
  AGENTS_DIR = ".agents";
160
176
  ARCHIVE_DIR = ".hatch3r-archive";
@@ -190,7 +206,8 @@ var init_types = __esm({
190
206
  commands: true,
191
207
  mcp: true,
192
208
  githubAgents: true,
193
- hooks: true
209
+ hooks: true,
210
+ handoffs: true
194
211
  };
195
212
  ENV_VAR_HELP = {
196
213
  GITHUB_PAT: {
@@ -269,21 +286,29 @@ var init_types = __esm({
269
286
  });
270
287
 
271
288
  // src/merge/managedBlocks.ts
272
- function insertManagedBlock(existingContent, managedContent) {
273
- const startIdx = existingContent.indexOf(MANAGED_BLOCK_START);
274
- const endIdx = existingContent.indexOf(MANAGED_BLOCK_END);
275
- const block = `${MANAGED_BLOCK_START}
276
- ${managedContent.trim()}
277
- ${MANAGED_BLOCK_END}`;
278
- if (startIdx === -1 || endIdx === -1) {
289
+ function detectMarkers(content) {
290
+ for (const variant of MANAGED_BLOCK_VARIANTS) {
291
+ const startIdx = content.indexOf(variant.start);
292
+ const endIdx = content.indexOf(variant.end);
293
+ if (startIdx !== -1 && endIdx !== -1) {
294
+ return { variant, startIdx, endIdx };
295
+ }
296
+ }
297
+ return null;
298
+ }
299
+ function insertManagedBlock(existingContent, managedContent, filePath) {
300
+ const outputMarkers = getMarkersForPath(filePath);
301
+ const detected = detectMarkers(existingContent);
302
+ if (!detected) {
279
303
  throw new HatchError(
280
304
  "Content must contain managed block markers (HATCH3R:BEGIN and HATCH3R:END)",
281
305
  1,
282
306
  "VALIDATION_ERROR"
283
307
  );
284
308
  }
285
- const secondStart = existingContent.indexOf(MANAGED_BLOCK_START, startIdx + 1);
286
- const secondEnd = existingContent.indexOf(MANAGED_BLOCK_END, endIdx + 1);
309
+ const { variant, startIdx, endIdx } = detected;
310
+ const secondStart = existingContent.indexOf(variant.start, startIdx + 1);
311
+ const secondEnd = existingContent.indexOf(variant.end, endIdx + 1);
287
312
  if (secondStart !== -1) {
288
313
  throw new HatchError(
289
314
  "Corrupted managed block: duplicate start marker found. Remove the duplicate before syncing.",
@@ -305,37 +330,35 @@ ${MANAGED_BLOCK_END}`;
305
330
  "VALIDATION_ERROR"
306
331
  );
307
332
  }
333
+ const block = `${outputMarkers.start}
334
+ ${managedContent.trim()}
335
+ ${outputMarkers.end}`;
308
336
  const before = existingContent.substring(0, startIdx);
309
- const after = existingContent.substring(endIdx + MANAGED_BLOCK_END.length);
337
+ const after = existingContent.substring(endIdx + variant.end.length);
310
338
  const result = `${before}${block}${after}`;
311
339
  return result.endsWith("\n") ? result : result + "\n";
312
340
  }
313
341
  function extractManagedBlock(content) {
314
- const startIdx = content.indexOf(MANAGED_BLOCK_START);
315
- const endIdx = content.indexOf(MANAGED_BLOCK_END);
316
- if (startIdx === -1 || endIdx === -1) {
317
- return null;
318
- }
319
- return content.substring(startIdx + MANAGED_BLOCK_START.length, endIdx).trim();
342
+ const detected = detectMarkers(content);
343
+ if (!detected) return null;
344
+ return content.substring(detected.startIdx + detected.variant.start.length, detected.endIdx).trim();
320
345
  }
321
346
  function extractCustomContent(content) {
322
- const startIdx = content.indexOf(MANAGED_BLOCK_START);
323
- const endIdx = content.indexOf(MANAGED_BLOCK_END);
324
- if (startIdx === -1 || endIdx === -1) {
325
- return content;
326
- }
327
- const before = content.substring(0, startIdx).trim();
328
- const after = content.substring(endIdx + MANAGED_BLOCK_END.length).trim();
347
+ const detected = detectMarkers(content);
348
+ if (!detected) return content;
349
+ const before = content.substring(0, detected.startIdx).trim();
350
+ const after = content.substring(detected.endIdx + detected.variant.end.length).trim();
329
351
  return [before, after].filter(Boolean).join("\n\n");
330
352
  }
331
- function wrapInManagedBlock(content) {
332
- return `${MANAGED_BLOCK_START}
353
+ function wrapInManagedBlock(content, filePath) {
354
+ const markers = getMarkersForPath(filePath);
355
+ return `${markers.start}
333
356
  ${content.trim()}
334
- ${MANAGED_BLOCK_END}
357
+ ${markers.end}
335
358
  `;
336
359
  }
337
360
  function hasManagedBlock(content) {
338
- return content.includes(MANAGED_BLOCK_START) && content.includes(MANAGED_BLOCK_END);
361
+ return detectMarkers(content) !== null;
339
362
  }
340
363
  var init_managedBlocks = __esm({
341
364
  "src/merge/managedBlocks.ts"() {
@@ -1327,7 +1350,7 @@ async function safeWriteFile(filePath, content, options = {}) {
1327
1350
  const deniedFindings = customContent ? scanForDeniedPatterns(customContent) : [];
1328
1351
  let merged;
1329
1352
  try {
1330
- merged = insertManagedBlock(existingContent, options.managedContent);
1353
+ merged = insertManagedBlock(existingContent, options.managedContent, filePath);
1331
1354
  } catch {
1332
1355
  const bakPath = filePath + ".bak";
1333
1356
  await copyFile(filePath, bakPath);
@@ -1619,8 +1642,8 @@ async function resolvePatterns(rootDir, patterns) {
1619
1642
  }
1620
1643
  function isInsideWorktree(dir) {
1621
1644
  try {
1622
- const stat11 = statSync(join5(dir, ".git"));
1623
- return stat11.isFile();
1645
+ const stat13 = statSync(join5(dir, ".git"));
1646
+ return stat13.isFile();
1624
1647
  } catch {
1625
1648
  return false;
1626
1649
  }
@@ -1922,10 +1945,10 @@ async function cleanupWorktree(worktreeRoot) {
1922
1945
  for (const entry of entries) {
1923
1946
  const targetPath = join6(worktreeRoot, entry.pattern.replace(/\/$/, ""));
1924
1947
  try {
1925
- const stat11 = await lstat(targetPath);
1926
- if (stat11.isSymbolicLink()) {
1948
+ const stat13 = await lstat(targetPath);
1949
+ if (stat13.isSymbolicLink()) {
1927
1950
  await unlink2(targetPath);
1928
- } else if (entry.strategy === "copy" && stat11.isFile() && mainRoot) {
1951
+ } else if (entry.strategy === "copy" && stat13.isFile() && mainRoot) {
1929
1952
  const sourcePath = join6(mainRoot, entry.pattern.replace(/\/$/, ""));
1930
1953
  try {
1931
1954
  const sourceContent = await readFile4(sourcePath, "utf-8");
@@ -2327,6 +2350,7 @@ __export(hatchJson_exports, {
2327
2350
  extractPreservedManifestFields: () => extractPreservedManifestFields,
2328
2351
  isValidGitBranchName: () => isValidGitBranchName,
2329
2352
  migrateManifest: () => migrateManifest,
2353
+ readCliToolsConfig: () => readCliToolsConfig,
2330
2354
  readManifest: () => readManifest,
2331
2355
  removeManagedFile: () => removeManagedFile,
2332
2356
  writeManifest: () => writeManifest
@@ -2394,6 +2418,9 @@ function createManifest(options) {
2394
2418
  if (options.customization) {
2395
2419
  manifest.customization = options.customization;
2396
2420
  }
2421
+ if (options.cliTools) {
2422
+ manifest.cliTools = options.cliTools;
2423
+ }
2397
2424
  if (options.languages && options.languages.length > 0 && options.languages[0] !== "unknown") {
2398
2425
  manifest.languages = options.languages;
2399
2426
  }
@@ -2607,6 +2634,7 @@ function extractPreservedManifestFields(manifest) {
2607
2634
  if (manifest.repos) out.repos = manifest.repos;
2608
2635
  if (manifest.packages) out.packages = manifest.packages;
2609
2636
  if (manifest.workspace) out.workspace = manifest.workspace;
2637
+ if (manifest.cliTools) out.cliTools = manifest.cliTools;
2610
2638
  if (manifest.worktree?.extraPatterns !== void 0 || manifest.worktree?.nodeModules !== void 0) {
2611
2639
  out.worktreeExtras = {};
2612
2640
  if (manifest.worktree.extraPatterns !== void 0) {
@@ -2644,6 +2672,9 @@ function applyPreservedManifestFields(manifest, preserved) {
2644
2672
  if (preserved.repos) manifest.repos = preserved.repos;
2645
2673
  if (preserved.packages) manifest.packages = preserved.packages;
2646
2674
  if (preserved.workspace) manifest.workspace = preserved.workspace;
2675
+ if (preserved.cliTools && manifest.cliTools === void 0) {
2676
+ manifest.cliTools = preserved.cliTools;
2677
+ }
2647
2678
  if (preserved.worktreeExtras && manifest.worktree?.enabled) {
2648
2679
  if (preserved.worktreeExtras.extraPatterns !== void 0) {
2649
2680
  manifest.worktree.extraPatterns = preserved.worktreeExtras.extraPatterns;
@@ -2653,6 +2684,9 @@ function applyPreservedManifestFields(manifest, preserved) {
2653
2684
  }
2654
2685
  }
2655
2686
  }
2687
+ function readCliToolsConfig(m) {
2688
+ return m.cliTools ?? { enabled: false, selected: [] };
2689
+ }
2656
2690
  var init_hatchJson = __esm({
2657
2691
  "src/manifest/hatchJson.ts"() {
2658
2692
  "use strict";
@@ -3101,6 +3135,16 @@ var init_agentToolAllowlist = __esm({
3101
3135
  allowedTools: ["read", "search", "write", "execute"],
3102
3136
  description: "Code implementation: file read/write, code search, command execution (tests, linters). No git, board, or web."
3103
3137
  },
3138
+ {
3139
+ agentId: "hatch3r-handoff-preparer",
3140
+ allowedTools: ["read", "search", "write"],
3141
+ description: "Handoff preparation: read session state, search git/files for context, write canonical handoff to .agents/handoffs/active/. No execute (filesystem-only)."
3142
+ },
3143
+ {
3144
+ agentId: "hatch3r-handoff-loader",
3145
+ allowedTools: ["read", "search"],
3146
+ description: "Session-start loader: read .agents/handoffs/active/ and search git for branch context to surface active handoffs. No write, execute, or external IO."
3147
+ },
3104
3148
  {
3105
3149
  agentId: "hatch3r-reviewer",
3106
3150
  allowedTools: ["read", "search"],
@@ -3332,7 +3376,7 @@ function filterByLanguages(items, projectLanguages) {
3332
3376
  return itemLangTags.some((t) => relevant.has(t));
3333
3377
  });
3334
3378
  }
3335
- var TAG_CORE, TAG_PLANNING, TAG_IMPLEMENTATION, TAG_REVIEW, TAG_DEVOPS, TAG_MAINTENANCE, TAG_BOARD, TAG_SECURITY, TAG_A11Y, TAG_PERFORMANCE, TAG_CUSTOMIZE, TAG_LANG_TYPESCRIPT, TAG_LANG_PYTHON, TAG_LANG_GO, TAG_LANG_RUST, TAG_LANG_JAVA, TAG_LANG_RUBY, WORKFLOW_TAGS, DOMAIN_TAGS, LANGUAGE_TO_TAG;
3379
+ var TAG_CORE, TAG_PLANNING, TAG_IMPLEMENTATION, TAG_REVIEW, TAG_DEVOPS, TAG_MAINTENANCE, TAG_BOARD, TAG_SECURITY, TAG_A11Y, TAG_PERFORMANCE, TAG_CUSTOMIZE, TAG_FRONTEND, TAG_UI, TAG_UX, TAG_DESIGN_SYSTEM, TAG_LANG_TYPESCRIPT, TAG_LANG_PYTHON, TAG_LANG_GO, TAG_LANG_RUST, TAG_LANG_JAVA, TAG_LANG_RUBY, WORKFLOW_TAGS, DOMAIN_TAGS, LANGUAGE_TO_TAG;
3336
3380
  var init_tags = __esm({
3337
3381
  "src/content/tags.ts"() {
3338
3382
  "use strict";
@@ -3347,6 +3391,10 @@ var init_tags = __esm({
3347
3391
  TAG_A11Y = "a11y";
3348
3392
  TAG_PERFORMANCE = "performance";
3349
3393
  TAG_CUSTOMIZE = "customize";
3394
+ TAG_FRONTEND = "frontend";
3395
+ TAG_UI = "ui";
3396
+ TAG_UX = "ux";
3397
+ TAG_DESIGN_SYSTEM = "design-system";
3350
3398
  TAG_LANG_TYPESCRIPT = "lang:typescript";
3351
3399
  TAG_LANG_PYTHON = "lang:python";
3352
3400
  TAG_LANG_GO = "lang:go";
@@ -3366,7 +3414,11 @@ var init_tags = __esm({
3366
3414
  TAG_SECURITY,
3367
3415
  TAG_A11Y,
3368
3416
  TAG_PERFORMANCE,
3369
- TAG_CUSTOMIZE
3417
+ TAG_CUSTOMIZE,
3418
+ TAG_FRONTEND,
3419
+ TAG_UI,
3420
+ TAG_UX,
3421
+ TAG_DESIGN_SYSTEM
3370
3422
  ];
3371
3423
  LANGUAGE_TO_TAG = {
3372
3424
  typescript: TAG_LANG_TYPESCRIPT,
@@ -5249,6 +5301,77 @@ description: ${desc}
5249
5301
  ---`;
5250
5302
  results.push(output(pathFn(skill.id), `${fm}
5251
5303
 
5304
+ ${wrapInManagedBlock(content)}`, content));
5305
+ }
5306
+ return results;
5307
+ }
5308
+ /**
5309
+ * Read canonical skills with the CLI-tooling pivot filter applied.
5310
+ *
5311
+ * Filter rules (plan §4.6):
5312
+ * - Skills whose id does NOT start with `hatch3r-cli-` pass through
5313
+ * unchanged (every adapter still emits the non-CLI skill catalogue).
5314
+ * - When `manifest.cliTools` is absent or `enabled: false`, drop every
5315
+ * `hatch3r-cli-*` skill (master switch off).
5316
+ * - When `cliTools.enabled` is true, keep only those whose suffix
5317
+ * (after stripping `hatch3r-cli-`) appears in `cliTools.selected`.
5318
+ *
5319
+ * Wave 3 swaps each adapter's `processSkillsWithFm` /
5320
+ * `processSkillsRaw` call to the `*CliFiltered` variants below; the
5321
+ * filter helper is exposed protected so adapters with custom skill
5322
+ * pipelines can reuse it directly.
5323
+ */
5324
+ async readCliFilteredSkills(ctx) {
5325
+ const all = await this.readTrackedCanonicalFiles(ctx.agentsDir, "skills");
5326
+ const cliCfg = ctx.manifest.cliTools ?? { enabled: false, selected: [] };
5327
+ const selected = new Set(cliCfg.selected ?? []);
5328
+ return all.filter((skill) => {
5329
+ if (!skill.id.startsWith("hatch3r-cli-")) return true;
5330
+ if (!cliCfg.enabled) return false;
5331
+ const cliId = skill.id.replace(/^hatch3r-cli-/, "");
5332
+ return selected.has(cliId);
5333
+ });
5334
+ }
5335
+ /**
5336
+ * CLI-filtered twin of {@link processSkillsRaw}. Adapters that emit
5337
+ * skills as raw managed-block files (no YAML frontmatter) call this
5338
+ * after Wave 3 instead of `processSkillsRaw` to honour
5339
+ * `manifest.cliTools.selected`.
5340
+ */
5341
+ async processSkillsRawCliFiltered(ctx, pathFn) {
5342
+ if (!ctx.features.skills) return [];
5343
+ const results = [];
5344
+ const skills = await this.readCliFilteredSkills(ctx);
5345
+ for (const skill of skills) {
5346
+ const { content: raw, skip, warnings } = await applyCustomizationRaw(ctx.projectRoot, skill);
5347
+ this.warnings.push(...warnings);
5348
+ if (skip) continue;
5349
+ const content = this.substituteAskUserMarker(raw);
5350
+ results.push(output(pathFn(skill.id), wrapInManagedBlock(content), content));
5351
+ }
5352
+ return results;
5353
+ }
5354
+ /**
5355
+ * CLI-filtered twin of {@link processSkillsWithFm}. Adapters that emit
5356
+ * skills as managed-block files prefixed with a `name: + description:`
5357
+ * YAML stub call this after Wave 3 instead of `processSkillsWithFm`.
5358
+ */
5359
+ async processSkillsWithFmCliFiltered(ctx, pathFn) {
5360
+ if (!ctx.features.skills) return [];
5361
+ const results = [];
5362
+ const skills = await this.readCliFilteredSkills(ctx);
5363
+ for (const skill of skills) {
5364
+ const { content: raw, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, skill);
5365
+ this.warnings.push(...warnings);
5366
+ if (skip) continue;
5367
+ const content = this.substituteAskUserMarker(raw);
5368
+ const desc = overrides.description ?? skill.description;
5369
+ const fm = `---
5370
+ name: ${skill.id}
5371
+ description: ${desc}
5372
+ ---`;
5373
+ results.push(output(pathFn(skill.id), `${fm}
5374
+
5252
5375
  ${wrapInManagedBlock(content)}`, content));
5253
5376
  }
5254
5377
  return results;
@@ -5364,7 +5487,7 @@ var init_aider = __esm({
5364
5487
  output("CONVENTIONS.md", wrapInManagedBlock(inner), inner)
5365
5488
  ];
5366
5489
  results.push(
5367
- ...await this.processSkillsRaw(ctx, (id) => `.aider/skills/${toPrefixedId(id)}/SKILL.md`)
5490
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.aider/skills/${toPrefixedId(id)}/SKILL.md`)
5368
5491
  );
5369
5492
  results.push(output(".aider.conf.yml", [
5370
5493
  "# Managed by hatch3r \u2014 do not edit manually",
@@ -5410,7 +5533,7 @@ var init_amazonq = __esm({
5410
5533
  ].join("\n").trim();
5411
5534
  results.push(output(".amazonq/rules/hatch3r-agents.md", wrapInManagedBlock(inner), inner));
5412
5535
  results.push(
5413
- ...await this.processSkillsRaw(ctx, (id) => `.amazonq/rules/hatch3r-skill-${id}.md`)
5536
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.amazonq/rules/hatch3r-skill-${id}.md`)
5414
5537
  );
5415
5538
  const mcp = await this.readFilteredMcp(ctx);
5416
5539
  if (mcp && Object.keys(mcp).length > 0) {
@@ -5538,7 +5661,7 @@ var init_antigravity = __esm({
5538
5661
  ].join("\n").trim();
5539
5662
  results.push(output(".antigravity/rules.md", wrapInManagedBlock(inner), inner));
5540
5663
  results.push(
5541
- ...await this.processSkillsRaw(ctx, (id) => `.agent/skills/${toPrefixedId(id)}/SKILL.md`)
5664
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.agent/skills/${toPrefixedId(id)}/SKILL.md`)
5542
5665
  );
5543
5666
  const mcp = await this.readFilteredMcp(ctx);
5544
5667
  if (mcp && Object.keys(mcp).length > 0) {
@@ -5916,7 +6039,7 @@ ${wrapInManagedBlock(body)}`, body));
5916
6039
  results.push(output(".claude/hooks/hatch3r-hooks.json", JSON.stringify(pluginHooksObj, null, 2)));
5917
6040
  }
5918
6041
  results.push(
5919
- ...await this.processSkillsRaw(ctx, (id) => `.claude/skills/${toPrefixedId(id)}/SKILL.md`)
6042
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.claude/skills/${toPrefixedId(id)}/SKILL.md`)
5920
6043
  );
5921
6044
  results.push(
5922
6045
  ...await this.processCommandsRaw(ctx, (id) => `.claude/commands/${toPrefixedId(id)}.md`)
@@ -5986,7 +6109,7 @@ Recommended model: ${model}. Select this model in the Roo Code model dropdown wh
5986
6109
  }, null, 2)));
5987
6110
  }
5988
6111
  results.push(
5989
- ...await this.processSkillsRaw(ctx, (id) => `.cline/skills/${toPrefixedId(id)}/SKILL.md`)
6112
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.cline/skills/${toPrefixedId(id)}/SKILL.md`)
5990
6113
  );
5991
6114
  if (ctx.features.rules) {
5992
6115
  const rules = await readCanonicalFiles(ctx.agentsDir, "rules", this.warnings);
@@ -6229,7 +6352,7 @@ var init_codex = __esm({
6229
6352
  }
6230
6353
  results.push(output(".codex/config.toml", configLines.join("\n")));
6231
6354
  results.push(
6232
- ...await this.processSkillsRaw(ctx, (id) => `.codex/skills/${toPrefixedId(id)}/SKILL.md`)
6355
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.codex/skills/${toPrefixedId(id)}/SKILL.md`)
6233
6356
  );
6234
6357
  return results;
6235
6358
  }
@@ -6374,9 +6497,10 @@ jobs:
6374
6497
  run: ${install}
6375
6498
  - name: Build
6376
6499
  run: ${build}`;
6500
+ const copilotSetupStepsPath = ".github/workflows/copilot-setup-steps.yml";
6377
6501
  results.push(output(
6378
- ".github/workflows/copilot-setup-steps.yml",
6379
- wrapInManagedBlock(copilotSetupStepsInner),
6502
+ copilotSetupStepsPath,
6503
+ wrapInManagedBlock(copilotSetupStepsInner, copilotSetupStepsPath),
6380
6504
  copilotSetupStepsInner
6381
6505
  ));
6382
6506
  for (const { rule, content, scope } of scopedRules) {
@@ -6443,7 +6567,7 @@ ${wrapInManagedBlock(content)}`, content));
6443
6567
  }
6444
6568
  }
6445
6569
  results.push(
6446
- ...await this.processSkillsWithFm(ctx, (id) => `.github/skills/${toPrefixedId(id)}/SKILL.md`)
6570
+ ...await this.processSkillsWithFmCliFiltered(ctx, (id) => `.github/skills/${toPrefixedId(id)}/SKILL.md`)
6447
6571
  );
6448
6572
  const mcp = await this.readFilteredMcp(ctx);
6449
6573
  if (mcp && Object.keys(mcp).length > 0) {
@@ -6529,7 +6653,7 @@ ${lines.join("\n")}
6529
6653
  }
6530
6654
  }
6531
6655
  results.push(
6532
- ...await this.processSkillsWithFm(ctx, (id) => `.cursor/skills/${toPrefixedId(id)}/SKILL.md`)
6656
+ ...await this.processSkillsWithFmCliFiltered(ctx, (id) => `.cursor/skills/${toPrefixedId(id)}/SKILL.md`)
6533
6657
  );
6534
6658
  results.push(
6535
6659
  ...await this.processCommandsRaw(ctx, (id) => `.cursor/commands/${toPrefixedId(id)}.md`)
@@ -6682,7 +6806,7 @@ var init_gemini = __esm({
6682
6806
  }
6683
6807
  results.push(output(".gemini/settings.json", JSON.stringify(settings, null, 2)));
6684
6808
  results.push(
6685
- ...await this.processSkillsRaw(ctx, (id) => `.gemini/skills/${toPrefixedId(id)}/SKILL.md`)
6809
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.gemini/skills/${toPrefixedId(id)}/SKILL.md`)
6686
6810
  );
6687
6811
  if (ctx.features.commands) {
6688
6812
  const commandsRaw = await readCanonicalFiles(ctx.agentsDir, "commands", this.warnings);
@@ -6714,7 +6838,6 @@ var init_goose = __esm({
6714
6838
  init_types();
6715
6839
  init_managedBlocks();
6716
6840
  init_base();
6717
- init_canonical();
6718
6841
  init_customization();
6719
6842
  init_mcp_utils();
6720
6843
  GooseAdapter = class extends BaseAdapter {
@@ -6727,7 +6850,7 @@ var init_goose = __esm({
6727
6850
  ...await this.inlineAgents(ctx)
6728
6851
  ];
6729
6852
  if (ctx.features.skills) {
6730
- const skills = await readCanonicalFiles(ctx.agentsDir, "skills", this.warnings);
6853
+ const skills = await this.readCliFilteredSkills(ctx);
6731
6854
  for (const skill of skills) {
6732
6855
  const { content, skip, warnings } = await applyCustomizationRaw(ctx.projectRoot, skill);
6733
6856
  this.warnings.push(...warnings);
@@ -6887,7 +7010,7 @@ ${content}`;
6887
7010
  const inner = lines.join("\n").trim();
6888
7011
  results.push(output(".kiro/steering/hatch3r-agents.md", wrapInManagedBlock(inner), inner));
6889
7012
  results.push(
6890
- ...await this.processSkillsRaw(ctx, (id) => `.kiro/steering/hatch3r-skill-${id}.md`)
7013
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.kiro/steering/hatch3r-skill-${id}.md`)
6891
7014
  );
6892
7015
  const hooks = await this.readHooks(ctx);
6893
7016
  for (const hook of hooks) {
@@ -7018,7 +7141,7 @@ ${wrapInManagedBlock(content)}`, content));
7018
7141
  }
7019
7142
  }
7020
7143
  results.push(
7021
- ...await this.processSkillsRaw(ctx, (id) => `.opencode/skills/${toPrefixedId(id)}/SKILL.md`)
7144
+ ...await this.processSkillsRawCliFiltered(ctx, (id) => `.opencode/skills/${toPrefixedId(id)}/SKILL.md`)
7022
7145
  );
7023
7146
  results.push(
7024
7147
  ...await this.processCommandsRaw(ctx, (id) => `.opencode/commands/${toPrefixedId(id)}.md`)
@@ -7159,7 +7282,7 @@ ${wrapInManagedBlock(body)}`, body));
7159
7282
  }
7160
7283
  }
7161
7284
  results.push(
7162
- ...await this.processSkillsWithFm(ctx, (id) => `.windsurf/skills/${toPrefixedId(id)}/SKILL.md`)
7285
+ ...await this.processSkillsWithFmCliFiltered(ctx, (id) => `.windsurf/skills/${toPrefixedId(id)}/SKILL.md`)
7163
7286
  );
7164
7287
  results.push(
7165
7288
  ...await this.processCommandsRaw(ctx, (id) => `.windsurf/workflows/${toPrefixedId(id)}.md`)
@@ -7284,6 +7407,9 @@ function getUnsupportedFeatureWarnings(tool, manifest) {
7284
7407
  unsupported.push(label2);
7285
7408
  }
7286
7409
  }
7410
+ if (manifest.cliTools?.enabled && (manifest.cliTools.selected?.length ?? 0) > 0 && !caps.cliTools) {
7411
+ unsupported.push("CLI tool skills");
7412
+ }
7287
7413
  if (unsupported.length === 0) return [];
7288
7414
  const noun = unsupported.length === 1 ? "feature" : "features";
7289
7415
  return [`${tool}: ${noun} enabled but not supported by this adapter: ${unsupported.join(", ")}`];
@@ -7344,28 +7470,32 @@ var init_adapters = __esm({
7344
7470
  };
7345
7471
  adapterCache = /* @__PURE__ */ new Map();
7346
7472
  ADAPTER_CAPABILITIES = {
7347
- cursor: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7348
- claude: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: true },
7349
- gemini: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7350
- cline: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7351
- codex: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7352
- "amazon-q": { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7353
- copilot: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: true, githubAgents: true, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7354
- opencode: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7473
+ cursor: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7474
+ claude: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: true, cliTools: true },
7475
+ gemini: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7476
+ cline: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7477
+ codex: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7478
+ "amazon-q": { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7479
+ copilot: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: true, githubAgents: true, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7480
+ opencode: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7355
7481
  // C7.5-W2B2-H31 (D9-SA9.7.1): Windsurf shipped Cascade Hooks in v1.13.12 (2026-01-25).
7356
7482
  // Hatch3r emits `.windsurf/hooks.json` per docs.windsurf.com/windsurf/cascade/hooks.md.
7357
- windsurf: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false },
7483
+ windsurf: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: true, prompts: false, githubAgents: false, worktree: true, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7358
7484
  // Amp reads AGENTS.md natively; the root file is written by generateRootAgentsMd()
7359
7485
  // in init/update, not by this adapter. Amp also reads skills natively from
7360
7486
  // `.agents/skills/` — populated by copyHatch3rFiles, not re-emitted by this
7361
7487
  // adapter (re-emission corrupts SKILL.md frontmatter via managed-block wrap).
7362
- // doGenerate() emits MCP settings only.
7363
- amp: { agents: false, skills: false, rules: false, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7364
- kiro: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7365
- aider: { agents: true, skills: true, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7366
- goose: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false },
7367
- zed: { agents: true, skills: false, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: false, modelOverride: false, nativeQuestionTool: false },
7368
- antigravity: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false }
7488
+ // doGenerate() emits MCP settings only. cliTools: false — Amp reads
7489
+ // `hatch3r-cli-*` skills from the canonical `.agents/skills/` tree directly.
7490
+ amp: { agents: false, skills: false, rules: false, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: false },
7491
+ kiro: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7492
+ aider: { agents: true, skills: true, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7493
+ goose: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true },
7494
+ // Zed has no skills surface (skills: false). cliTools: false Wave 3 will
7495
+ // emit a one-line "Available CLI tool guides: ..." reference inside the
7496
+ // rules output instead of per-tool skill files.
7497
+ zed: { agents: true, skills: false, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: false, modelOverride: false, nativeQuestionTool: false, cliTools: false },
7498
+ antigravity: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false, worktree: false, customization: true, modelOverride: true, nativeQuestionTool: false, cliTools: true }
7369
7499
  };
7370
7500
  }
7371
7501
  });
@@ -7682,13 +7812,8 @@ async function fileIsUserWrapped(absPath) {
7682
7812
  if (!hasManagedBlock(content)) {
7683
7813
  return { wrapped: false };
7684
7814
  }
7685
- const before = content.slice(0, content.indexOf("<!-- HATCH3R:BEGIN -->"));
7686
- const endMarker = "<!-- HATCH3R:END -->";
7687
- const endIdx = content.indexOf(endMarker);
7688
- const after = endIdx === -1 ? "" : content.slice(endIdx + endMarker.length);
7689
- const userBefore = before.trim();
7690
- const userAfter = after.trim();
7691
- return { wrapped: userBefore.length > 0 || userAfter.length > 0 };
7815
+ const userOutside = extractCustomContent(content).trim();
7816
+ return { wrapped: userOutside.length > 0 };
7692
7817
  } catch (err) {
7693
7818
  return { wrapped: false, error: err instanceof Error ? err.message : String(err) };
7694
7819
  }
@@ -8319,7 +8444,7 @@ var init_retryWithBackoff = __esm({
8319
8444
 
8320
8445
  // src/detect/installContext.ts
8321
8446
  import { execFileSync as execFileSync5 } from "child_process";
8322
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
8447
+ import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync3 } from "fs";
8323
8448
  import { dirname as dirname14, join as join28, normalize as normalize3, resolve as resolve4, sep as sep3 } from "path";
8324
8449
  import { fileURLToPath as fileURLToPath4 } from "url";
8325
8450
  function isNpxPath(p) {
@@ -8365,12 +8490,25 @@ function npmGlobalRoot() {
8365
8490
  }
8366
8491
  return cachedNpmGlobalRoot;
8367
8492
  }
8493
+ function isLinkedPackageDir(packageRoot) {
8494
+ try {
8495
+ return lstatSync(packageRoot).isSymbolicLink();
8496
+ } catch {
8497
+ return false;
8498
+ }
8499
+ }
8368
8500
  function classifyInvocation(binPath, projectRoot) {
8369
8501
  if (isNpxPath(binPath)) return "npx";
8370
8502
  const globalRoot = npmGlobalRoot();
8371
- if (globalRoot && binPath.startsWith(globalRoot)) return "global";
8503
+ if (globalRoot && binPath.startsWith(globalRoot)) {
8504
+ if (isLinkedPackageDir(join28(globalRoot, "hatch3r"))) return "dev-source";
8505
+ return "global";
8506
+ }
8372
8507
  const projectNodeModules = join28(projectRoot, "node_modules") + sep3;
8373
- if (binPath.startsWith(projectNodeModules)) return "project-local";
8508
+ if (binPath.startsWith(projectNodeModules)) {
8509
+ if (isLinkedPackageDir(join28(projectRoot, "node_modules", "hatch3r"))) return "dev-source";
8510
+ return "project-local";
8511
+ }
8374
8512
  return "dev-source";
8375
8513
  }
8376
8514
  async function buildLocation(kind, binPath, packageRoot, cwdForPmDetection) {
@@ -8405,7 +8543,7 @@ async function surveyInstalls(rootDir) {
8405
8543
  const alsoPresent = [];
8406
8544
  if (invokedKind !== "project-local") {
8407
8545
  const projectPkgRoot = join28(rootDir, "node_modules", "hatch3r");
8408
- if (existsSync3(join28(projectPkgRoot, "package.json"))) {
8546
+ if (existsSync3(join28(projectPkgRoot, "package.json")) && !isLinkedPackageDir(projectPkgRoot)) {
8409
8547
  const projectBin = join28(projectPkgRoot, "dist", "cli", "index.js");
8410
8548
  alsoPresent.push(
8411
8549
  await buildLocation("project-local", projectBin, projectPkgRoot, rootDir)
@@ -8416,7 +8554,7 @@ async function surveyInstalls(rootDir) {
8416
8554
  const globalRoot = npmGlobalRoot();
8417
8555
  if (globalRoot) {
8418
8556
  const globalPkgRoot = join28(globalRoot, "hatch3r");
8419
- if (existsSync3(join28(globalPkgRoot, "package.json"))) {
8557
+ if (existsSync3(join28(globalPkgRoot, "package.json")) && !isLinkedPackageDir(globalPkgRoot)) {
8420
8558
  const globalBin = join28(globalPkgRoot, "dist", "cli", "index.js");
8421
8559
  alsoPresent.push(
8422
8560
  await buildLocation("global", globalBin, globalPkgRoot, rootDir)
@@ -8582,8 +8720,8 @@ import { appendFile as appendFile2, cp as cp4, mkdir as mkdir9, readFile as read
8582
8720
  import { spawnSync as spawnSync2 } from "child_process";
8583
8721
  import { fileURLToPath as fileURLToPath5 } from "url";
8584
8722
  import { dirname as dirname15, join as join29, sep as sep4 } from "path";
8585
- import chalk7 from "chalk";
8586
- import inquirer6 from "inquirer";
8723
+ import chalk9 from "chalk";
8724
+ import inquirer8 from "inquirer";
8587
8725
  function buildReExecPassThroughArgs(opts) {
8588
8726
  const args = [];
8589
8727
  if (opts?.yes) args.push("--yes");
@@ -8894,23 +9032,23 @@ async function runUpdateDryRun(rootDir, manifest, options = {}) {
8894
9032
  }
8895
9033
  }
8896
9034
  const summaryLines = [];
8897
- summaryLines.push(chalk7.dim(`Offline: ${options.offline ? "yes" : "no"}`));
8898
- summaryLines.push(chalk7.dim(`Canonical candidate files: ${canonicalCandidates.length}`));
9035
+ summaryLines.push(chalk9.dim(`Offline: ${options.offline ? "yes" : "no"}`));
9036
+ summaryLines.push(chalk9.dim(`Canonical candidate files: ${canonicalCandidates.length}`));
8899
9037
  for (const [tool, bucket] of adapterChanges) {
8900
9038
  if (bucket.error) {
8901
- summaryLines.push(`${chalk7.red("x")} ${tool}: ${bucket.error}`);
9039
+ summaryLines.push(`${chalk9.red("x")} ${tool}: ${bucket.error}`);
8902
9040
  continue;
8903
9041
  }
8904
9042
  const lines = [
8905
- ...bucket.added.map((p) => `${chalk7.green("+ added")} ${p}`),
8906
- ...bucket.modified.map((p) => `${chalk7.yellow("~ modified")} ${p}`),
8907
- ...bucket.unchanged.map((p) => `${chalk7.dim("= unchanged")} ${p}`)
9043
+ ...bucket.added.map((p) => `${chalk9.green("+ added")} ${p}`),
9044
+ ...bucket.modified.map((p) => `${chalk9.yellow("~ modified")} ${p}`),
9045
+ ...bucket.unchanged.map((p) => `${chalk9.dim("= unchanged")} ${p}`)
8908
9046
  ];
8909
- summaryLines.push(chalk7.bold(tool));
9047
+ summaryLines.push(chalk9.bold(tool));
8910
9048
  summaryLines.push(...lines.map((l) => ` ${l}`));
8911
9049
  }
8912
9050
  console.log();
8913
- printBox("Update dry run (no writes)", summaryLines.length > 0 ? summaryLines : [chalk7.dim("No adapters configured.")], "info");
9051
+ printBox("Update dry run (no writes)", summaryLines.length > 0 ? summaryLines : [chalk9.dim("No adapters configured.")], "info");
8914
9052
  return { canonicalCandidates, adapterChanges };
8915
9053
  }
8916
9054
  async function enumerateHatch3rFiles(srcDir, insideHatch3rDir, selectedIds) {
@@ -8976,7 +9114,7 @@ async function updateCommand(_opts) {
8976
9114
  const manifest = await readManifest(rootDir);
8977
9115
  if (!manifest) {
8978
9116
  error("No .agents/hatch.json found.");
8979
- console.log(chalk7.dim(" Run `npx hatch3r init` to set up your project first.\n"));
9117
+ console.log(chalk9.dim(" Run `npx hatch3r init` to set up your project first.\n"));
8980
9118
  throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
8981
9119
  }
8982
9120
  const headless = !!_opts?.yes;
@@ -9067,14 +9205,14 @@ async function updateCommand(_opts) {
9067
9205
  console.log();
9068
9206
  warn("A clean reinit is recommended for this version update:");
9069
9207
  for (const advisory of reinitAdvisories) {
9070
- console.log(chalk7.dim(` - ${advisory.reason}`));
9208
+ console.log(chalk9.dim(` - ${advisory.reason}`));
9071
9209
  for (const change of advisory.changes ?? []) {
9072
- console.log(chalk7.dim(` \u2022 ${change}`));
9210
+ console.log(chalk9.dim(` \u2022 ${change}`));
9073
9211
  }
9074
9212
  }
9075
9213
  console.log();
9076
- info(`Run ${chalk7.bold("hatch3r clean")} and choose to reinitialize when prompted.`);
9077
- console.log(chalk7.dim(" Your customizations and learnings will be preserved.\n"));
9214
+ info(`Run ${chalk9.bold("hatch3r clean")} and choose to reinitialize when prompted.`);
9215
+ console.log(chalk9.dim(" Your customizations and learnings will be preserved.\n"));
9078
9216
  }
9079
9217
  if (_opts?.diff && result.diffBefore && result.diffAfter) {
9080
9218
  const diffLines = [];
@@ -9082,11 +9220,11 @@ async function updateCommand(_opts) {
9082
9220
  const before = result.diffBefore.get(filePath) ?? null;
9083
9221
  const after = result.diffAfter.get(filePath) ?? null;
9084
9222
  if (before === null && after !== null) {
9085
- diffLines.push(`${chalk7.green("+ added")} ${filePath}`);
9223
+ diffLines.push(`${chalk9.green("+ added")} ${filePath}`);
9086
9224
  } else if (before !== null && after !== null && before !== after) {
9087
- diffLines.push(`${chalk7.yellow("~ modified")} ${filePath}`);
9225
+ diffLines.push(`${chalk9.yellow("~ modified")} ${filePath}`);
9088
9226
  } else if (before !== null && after !== null && before === after) {
9089
- diffLines.push(`${chalk7.dim("= unchanged")} ${filePath}`);
9227
+ diffLines.push(`${chalk9.dim("= unchanged")} ${filePath}`);
9090
9228
  }
9091
9229
  }
9092
9230
  if (diffLines.length > 0) {
@@ -9106,6 +9244,9 @@ async function updateCommand(_opts) {
9106
9244
  label("Tools", `${compactedResult.syncedTools} tool(s) re-synced`),
9107
9245
  label("Version", `v${compactedResult.version}`)
9108
9246
  ], "success");
9247
+ if (!m.cliTools || m.cliTools.selected.length === 0) {
9248
+ info("CLI tooling available as a token-efficient alternative to MCP \u2014 run `npx hatch3r cli-tools` to opt in.");
9249
+ }
9109
9250
  if (isPipelineTimedOut(pipelineState)) {
9110
9251
  const { report } = terminatePipeline(pipelineState);
9111
9252
  warn(report.summary);
@@ -9153,7 +9294,7 @@ var init_update = __esm({
9153
9294
  content.projectType = "brownfield";
9154
9295
  content.teamSize = "team";
9155
9296
  } else {
9156
- const { projectType } = await inquirer6.prompt([
9297
+ const { projectType } = await inquirer8.prompt([
9157
9298
  {
9158
9299
  type: "select",
9159
9300
  name: "projectType",
@@ -9165,7 +9306,7 @@ var init_update = __esm({
9165
9306
  default: "brownfield"
9166
9307
  }
9167
9308
  ]);
9168
- const { teamSize } = await inquirer6.prompt([
9309
+ const { teamSize } = await inquirer8.prompt([
9169
9310
  {
9170
9311
  type: "select",
9171
9312
  name: "teamSize",
@@ -9194,7 +9335,7 @@ var init_update = __esm({
9194
9335
  if (headless) {
9195
9336
  platform = "github";
9196
9337
  } else {
9197
- const answer = await inquirer6.prompt([
9338
+ const answer = await inquirer8.prompt([
9198
9339
  {
9199
9340
  type: "select",
9200
9341
  name: "platform",
@@ -9216,7 +9357,7 @@ var init_update = __esm({
9216
9357
  updated.project = updated.project || updated.repo;
9217
9358
  notices.push("Migrated to GitHub platform (auto-detected from existing config)");
9218
9359
  } else {
9219
- const answers = await inquirer6.prompt([
9360
+ const answers = await inquirer8.prompt([
9220
9361
  { type: "input", name: "namespace", message: platform === "azure-devops" ? "Azure DevOps organization:" : "GitLab namespace (group or username):", default: updated.owner || void 0 },
9221
9362
  { type: "input", name: "project", message: platform === "azure-devops" ? "Azure DevOps project:" : "Project name:", default: updated.repo || void 0 },
9222
9363
  { type: "input", name: "repo", message: "Repository name:", default: updated.repo || void 0 }
@@ -9280,7 +9421,7 @@ var init_update = __esm({
9280
9421
  if (headless) {
9281
9422
  enabled = true;
9282
9423
  } else {
9283
- const answer = await inquirer6.prompt([{
9424
+ const answer = await inquirer8.prompt([{
9284
9425
  type: "confirm",
9285
9426
  name: "enabled",
9286
9427
  message: "hatch3r now supports worktree file isolation for parallel agent sessions. Enable it?",
@@ -9900,8 +10041,8 @@ async function worktreeCleanupCommand(opts = {}) {
9900
10041
  init_ui();
9901
10042
  init_types();
9902
10043
  import { rm as rm4 } from "fs/promises";
9903
- import chalk6 from "chalk";
9904
- import inquirer5 from "inquirer";
10044
+ import chalk8 from "chalk";
10045
+ import inquirer7 from "inquirer";
9905
10046
 
9906
10047
  // src/clean/index.ts
9907
10048
  init_archive();
@@ -10281,8 +10422,8 @@ init_types();
10281
10422
  import { access as access9, mkdir as mkdir8 } from "fs/promises";
10282
10423
  import { fileURLToPath as fileURLToPath3 } from "url";
10283
10424
  import { basename as basename2, dirname as dirname12, join as join27 } from "path";
10284
- import chalk5 from "chalk";
10285
- import inquirer4 from "inquirer";
10425
+ import chalk7 from "chalk";
10426
+ import inquirer6 from "inquirer";
10286
10427
 
10287
10428
  // src/detect/repoAnalyzer.ts
10288
10429
  init_packageManager();
@@ -10738,6 +10879,813 @@ function isWSL() {
10738
10879
  }
10739
10880
  }
10740
10881
 
10882
+ // src/cli/shared/pickers.ts
10883
+ import inquirer4 from "inquirer";
10884
+ import chalk5 from "chalk";
10885
+
10886
+ // src/cliTools/registry.ts
10887
+ var AVAILABLE_CLI_TOOLS = {
10888
+ // ── Tier 1 (10 tools) ───────────────────────────────────────────
10889
+ ripgrep: {
10890
+ id: "ripgrep",
10891
+ probe: "rg",
10892
+ description: "Fast recursive grep with sane defaults and gitignore awareness",
10893
+ category: "search",
10894
+ tier: 1,
10895
+ install: {
10896
+ mac: [{ manager: "brew", command: "brew install ripgrep" }],
10897
+ linux: [{ manager: "apt", command: "sudo apt install ripgrep" }],
10898
+ win: [{ manager: "scoop", command: "scoop install ripgrep" }]
10899
+ },
10900
+ homepage: "https://github.com/BurntSushi/ripgrep"
10901
+ },
10902
+ fd: {
10903
+ id: "fd",
10904
+ probe: "fd",
10905
+ description: "User-friendly find replacement, gitignore-aware",
10906
+ category: "search",
10907
+ tier: 1,
10908
+ install: {
10909
+ mac: [{ manager: "brew", command: "brew install fd" }],
10910
+ linux: [{ manager: "apt", command: "sudo apt install fd-find" }],
10911
+ win: [{ manager: "scoop", command: "scoop install fd" }]
10912
+ },
10913
+ homepage: "https://github.com/sharkdp/fd"
10914
+ },
10915
+ jq: {
10916
+ id: "jq",
10917
+ probe: "jq",
10918
+ description: "JSON processor and query language",
10919
+ category: "json",
10920
+ tier: 1,
10921
+ install: {
10922
+ mac: [{ manager: "brew", command: "brew install jq" }],
10923
+ linux: [{ manager: "apt", command: "sudo apt install jq" }],
10924
+ win: [{ manager: "scoop", command: "scoop install jq" }]
10925
+ },
10926
+ homepage: "https://github.com/jqlang/jq"
10927
+ },
10928
+ yq: {
10929
+ id: "yq",
10930
+ probe: "yq",
10931
+ description: "YAML processor (mikefarah Go implementation)",
10932
+ category: "yaml",
10933
+ tier: 1,
10934
+ install: {
10935
+ mac: [{ manager: "brew", command: "brew install yq" }],
10936
+ linux: [{ manager: "snap", command: "sudo snap install yq" }],
10937
+ win: [{ manager: "scoop", command: "scoop install yq" }]
10938
+ },
10939
+ homepage: "https://github.com/mikefarah/yq"
10940
+ },
10941
+ gh: {
10942
+ id: "gh",
10943
+ probe: "gh",
10944
+ description: "GitHub CLI \u2014 repos, issues, PRs, releases, gists",
10945
+ category: "forge",
10946
+ tier: 1,
10947
+ install: {
10948
+ mac: [{ manager: "brew", command: "brew install gh" }],
10949
+ linux: [{ manager: "apt", command: "sudo apt install gh" }],
10950
+ win: [{ manager: "winget", command: "winget install GitHub.cli" }]
10951
+ },
10952
+ requiresEnv: ["GH_TOKEN"],
10953
+ homepage: "https://cli.github.com/"
10954
+ },
10955
+ delta: {
10956
+ id: "delta",
10957
+ probe: "delta",
10958
+ description: "Syntax-highlighting git diff pager",
10959
+ category: "git",
10960
+ tier: 1,
10961
+ install: {
10962
+ mac: [{ manager: "brew", command: "brew install git-delta" }],
10963
+ linux: [{ manager: "apt", command: "sudo apt install git-delta" }],
10964
+ win: [{ manager: "scoop", command: "scoop install delta" }]
10965
+ },
10966
+ homepage: "https://github.com/dandavison/delta"
10967
+ },
10968
+ bat: {
10969
+ id: "bat",
10970
+ probe: "bat",
10971
+ description: "cat clone with syntax highlighting and git integration",
10972
+ category: "view",
10973
+ tier: 1,
10974
+ install: {
10975
+ mac: [{ manager: "brew", command: "brew install bat" }],
10976
+ linux: [{ manager: "apt", command: "sudo apt install bat" }],
10977
+ win: [{ manager: "winget", command: "winget install sharkdp.bat" }]
10978
+ },
10979
+ homepage: "https://github.com/sharkdp/bat"
10980
+ },
10981
+ sd: {
10982
+ id: "sd",
10983
+ probe: "sd",
10984
+ description: "Intuitive sed replacement with literal string patterns",
10985
+ category: "edit",
10986
+ tier: 1,
10987
+ install: {
10988
+ mac: [{ manager: "brew", command: "brew install sd" }],
10989
+ linux: [{ manager: "cargo", command: "cargo install sd" }],
10990
+ win: [{ manager: "scoop", command: "scoop install sd" }]
10991
+ },
10992
+ homepage: "https://github.com/chmln/sd"
10993
+ },
10994
+ "ast-grep": {
10995
+ id: "ast-grep",
10996
+ probe: "sg",
10997
+ description: "Structural search and rewrite for code via AST patterns",
10998
+ category: "search",
10999
+ tier: 1,
11000
+ install: {
11001
+ mac: [{ manager: "brew", command: "brew install ast-grep" }],
11002
+ linux: [{ manager: "cargo", command: "cargo install ast-grep" }],
11003
+ win: [{ manager: "scoop", command: "scoop install ast-grep" }]
11004
+ },
11005
+ homepage: "https://ast-grep.github.io/"
11006
+ },
11007
+ zstd: {
11008
+ id: "zstd",
11009
+ probe: "zstd",
11010
+ description: "Fast lossless compression with high ratio",
11011
+ category: "archive",
11012
+ tier: 1,
11013
+ install: {
11014
+ mac: [{ manager: "brew", command: "brew install zstd" }],
11015
+ linux: [{ manager: "apt", command: "sudo apt install zstd" }],
11016
+ win: [{ manager: "winget", command: "winget install Facebook.Zstandard" }]
11017
+ },
11018
+ homepage: "https://github.com/facebook/zstd"
11019
+ },
11020
+ // ── Tier 2 (11 tools, conditional) ──────────────────────────────
11021
+ playwright: {
11022
+ id: "playwright",
11023
+ probe: "playwright",
11024
+ description: "Browser automation, web testing, and UI interaction",
11025
+ category: "browser",
11026
+ tier: 2,
11027
+ trigger: "web-project",
11028
+ install: {
11029
+ mac: [{ manager: "npm", command: "npm install -D @playwright/test && npx playwright install" }],
11030
+ linux: [{ manager: "npm", command: "npm install -D @playwright/test && npx playwright install --with-deps" }],
11031
+ win: [{ manager: "npm", command: "npm install -D @playwright/test && npx playwright install" }]
11032
+ },
11033
+ homepage: "https://playwright.dev/"
11034
+ },
11035
+ duckdb: {
11036
+ id: "duckdb",
11037
+ probe: "duckdb",
11038
+ description: "Embedded analytical database with first-class CSV/Parquet support",
11039
+ category: "data",
11040
+ tier: 2,
11041
+ trigger: "data-project",
11042
+ install: {
11043
+ mac: [{ manager: "brew", command: "brew install duckdb" }],
11044
+ linux: [{ manager: "curl", command: "curl https://install.duckdb.org | sh" }],
11045
+ win: [{ manager: "winget", command: "winget install DuckDB.cli" }]
11046
+ },
11047
+ homepage: "https://duckdb.org/"
11048
+ },
11049
+ xsv: {
11050
+ id: "xsv",
11051
+ probe: "xsv",
11052
+ description: "Fast CSV toolkit (slice, search, join, stats)",
11053
+ category: "data",
11054
+ tier: 2,
11055
+ trigger: "data-project",
11056
+ install: {
11057
+ mac: [{ manager: "brew", command: "brew install xsv" }],
11058
+ linux: [{ manager: "cargo", command: "cargo install xsv" }],
11059
+ win: [{ manager: "scoop", command: "scoop install xsv" }]
11060
+ },
11061
+ homepage: "https://github.com/BurntSushi/xsv"
11062
+ },
11063
+ taplo: {
11064
+ id: "taplo",
11065
+ probe: "taplo",
11066
+ description: "TOML toolkit (format, lint, query) for pyproject.toml / Cargo.toml",
11067
+ category: "yaml",
11068
+ tier: 2,
11069
+ trigger: "rust-project",
11070
+ install: {
11071
+ mac: [{ manager: "brew", command: "brew install taplo" }],
11072
+ linux: [{ manager: "cargo", command: "cargo install taplo-cli --locked" }],
11073
+ win: [{ manager: "scoop", command: "scoop install taplo" }]
11074
+ },
11075
+ homepage: "https://taplo.tamasfe.dev/"
11076
+ },
11077
+ glab: {
11078
+ id: "glab",
11079
+ probe: "glab",
11080
+ description: "GitLab CLI \u2014 merge requests, issues, pipelines",
11081
+ category: "forge",
11082
+ tier: 2,
11083
+ trigger: "gitlab-remote",
11084
+ install: {
11085
+ mac: [{ manager: "brew", command: "brew install glab" }],
11086
+ linux: [{ manager: "apt", command: "sudo apt install glab" }],
11087
+ win: [{ manager: "winget", command: "winget install GitLab.GLab" }]
11088
+ },
11089
+ requiresEnv: ["GITLAB_TOKEN"],
11090
+ homepage: "https://gitlab.com/gitlab-org/cli"
11091
+ },
11092
+ "az-devops": {
11093
+ id: "az-devops",
11094
+ probe: "az",
11095
+ description: "Azure DevOps work items, repos, pipelines via az CLI extension",
11096
+ category: "forge",
11097
+ tier: 2,
11098
+ trigger: "azure-remote",
11099
+ install: {
11100
+ mac: [{ manager: "brew", command: "brew install azure-cli && az extension add --name azure-devops" }],
11101
+ linux: [{ manager: "curl", command: "curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash && az extension add --name azure-devops" }],
11102
+ win: [{ manager: "winget", command: "winget install Microsoft.AzureCLI && az extension add --name azure-devops" }]
11103
+ },
11104
+ requiresEnv: ["AZURE_DEVOPS_PAT", "AZURE_DEVOPS_ORG"],
11105
+ homepage: "https://learn.microsoft.com/en-us/cli/azure/azure-devops"
11106
+ },
11107
+ docker: {
11108
+ id: "docker",
11109
+ probe: "docker",
11110
+ description: "Container runtime and CLI",
11111
+ category: "container",
11112
+ tier: 2,
11113
+ trigger: "docker-detected",
11114
+ install: {
11115
+ mac: [{ manager: "brew", command: "brew install --cask docker" }],
11116
+ linux: [{ manager: "curl", command: "curl -fsSL https://get.docker.com | sudo sh" }],
11117
+ win: [{ manager: "winget", command: "winget install Docker.DockerDesktop" }]
11118
+ },
11119
+ homepage: "https://docs.docker.com/get-docker/"
11120
+ },
11121
+ llm: {
11122
+ id: "llm",
11123
+ probe: "llm",
11124
+ description: "simonw/llm \u2014 invoke LLMs from the command line with prompt templates",
11125
+ category: "ai",
11126
+ tier: 2,
11127
+ trigger: "ci-llm-project",
11128
+ install: {
11129
+ mac: [{ manager: "brew", command: "brew install llm" }],
11130
+ linux: [{ manager: "pipx", command: "pipx install llm" }],
11131
+ win: [{ manager: "pipx", command: "pipx install llm" }]
11132
+ },
11133
+ homepage: "https://llm.datasette.io/"
11134
+ },
11135
+ fzf: {
11136
+ id: "fzf",
11137
+ probe: "fzf",
11138
+ description: "Interactive fuzzy finder for TTY pickers",
11139
+ category: "interactive",
11140
+ tier: 2,
11141
+ trigger: "interactive-tty",
11142
+ install: {
11143
+ mac: [{ manager: "brew", command: "brew install fzf" }],
11144
+ linux: [{ manager: "apt", command: "sudo apt install fzf" }],
11145
+ win: [{ manager: "scoop", command: "scoop install fzf" }]
11146
+ },
11147
+ homepage: "https://github.com/junegunn/fzf"
11148
+ },
11149
+ lazygit: {
11150
+ id: "lazygit",
11151
+ probe: "lazygit",
11152
+ description: "Terminal UI for git with keyboard-driven workflows",
11153
+ category: "git",
11154
+ tier: 2,
11155
+ trigger: "interactive-tty",
11156
+ install: {
11157
+ mac: [{ manager: "brew", command: "brew install lazygit" }],
11158
+ linux: [{ manager: "apt", command: "sudo apt install lazygit" }],
11159
+ win: [{ manager: "scoop", command: "scoop install lazygit" }]
11160
+ },
11161
+ homepage: "https://github.com/jesseduffield/lazygit"
11162
+ },
11163
+ difftastic: {
11164
+ id: "difftastic",
11165
+ probe: "difft",
11166
+ description: "Structural diff that understands syntax",
11167
+ category: "git",
11168
+ tier: 2,
11169
+ trigger: "interactive-tty",
11170
+ install: {
11171
+ mac: [{ manager: "brew", command: "brew install difftastic" }],
11172
+ linux: [{ manager: "cargo", command: "cargo install --locked difftastic" }],
11173
+ win: [{ manager: "scoop", command: "scoop install difftastic" }]
11174
+ },
11175
+ homepage: "https://difftastic.wilfred.me.uk/"
11176
+ },
11177
+ // ── Tier 3 (8 tools, opt-in advanced) ───────────────────────────
11178
+ rtk: {
11179
+ id: "rtk",
11180
+ probe: "rtk",
11181
+ description: "CLI output-compression proxy (see \u26A0 caveat)",
11182
+ category: "ai",
11183
+ tier: 3,
11184
+ caveat: "pipe-output-corruption",
11185
+ install: {
11186
+ mac: [{ manager: "brew", command: "brew install rtk-ai/tap/rtk" }],
11187
+ linux: [{ manager: "curl", command: "curl -fsSL https://rtk.dev/install.sh | sh" }],
11188
+ win: [{ manager: "scoop", command: "scoop install rtk" }]
11189
+ },
11190
+ homepage: "https://github.com/rtk-ai/rtk"
11191
+ },
11192
+ stagehand: {
11193
+ id: "stagehand",
11194
+ probe: "stagehand",
11195
+ description: "Browserbase Stagehand \u2014 AI-driven browser automation",
11196
+ category: "browser",
11197
+ tier: 3,
11198
+ install: {
11199
+ mac: [{ manager: "npm", command: "npm install -g @browserbasehq/stagehand" }],
11200
+ linux: [{ manager: "npm", command: "npm install -g @browserbasehq/stagehand" }],
11201
+ win: [{ manager: "npm", command: "npm install -g @browserbasehq/stagehand" }]
11202
+ },
11203
+ homepage: "https://github.com/browserbase/stagehand"
11204
+ },
11205
+ aichat: {
11206
+ id: "aichat",
11207
+ probe: "aichat",
11208
+ description: "Multi-provider LLM chat CLI with RAG and session memory",
11209
+ category: "ai",
11210
+ tier: 3,
11211
+ install: {
11212
+ mac: [{ manager: "brew", command: "brew install aichat" }],
11213
+ linux: [{ manager: "cargo", command: "cargo install aichat" }],
11214
+ win: [{ manager: "scoop", command: "scoop install aichat" }]
11215
+ },
11216
+ homepage: "https://github.com/sigoden/aichat"
11217
+ },
11218
+ mods: {
11219
+ id: "mods",
11220
+ probe: "mods",
11221
+ description: "Charm mods \u2014 Unix-friendly LLM pipeline tool",
11222
+ category: "ai",
11223
+ tier: 3,
11224
+ install: {
11225
+ mac: [{ manager: "brew", command: "brew install charmbracelet/tap/mods" }],
11226
+ linux: [{ manager: "apt", command: "sudo apt install mods" }],
11227
+ win: [{ manager: "scoop", command: "scoop install mods" }]
11228
+ },
11229
+ homepage: "https://github.com/charmbracelet/mods"
11230
+ },
11231
+ comby: {
11232
+ id: "comby",
11233
+ probe: "comby",
11234
+ description: "Structural search and replace across languages with declarative patterns",
11235
+ category: "search",
11236
+ tier: 3,
11237
+ install: {
11238
+ mac: [{ manager: "brew", command: "brew install comby" }],
11239
+ linux: [{ manager: "curl", command: "bash <(curl -sL get.comby.dev)" }],
11240
+ win: [{ manager: "scoop", command: "scoop install comby" }]
11241
+ },
11242
+ homepage: "https://comby.dev/"
11243
+ },
11244
+ miller: {
11245
+ id: "miller",
11246
+ probe: "mlr",
11247
+ description: "awk/sed/cut/join for CSV/TSV/JSON/Parquet streams",
11248
+ category: "data",
11249
+ tier: 3,
11250
+ install: {
11251
+ mac: [{ manager: "brew", command: "brew install miller" }],
11252
+ linux: [{ manager: "apt", command: "sudo apt install miller" }],
11253
+ win: [{ manager: "scoop", command: "scoop install miller" }]
11254
+ },
11255
+ homepage: "https://miller.readthedocs.io/"
11256
+ },
11257
+ csvkit: {
11258
+ id: "csvkit",
11259
+ probe: "csvlook",
11260
+ description: "csvkit \u2014 Python CSV toolkit (csvlook, csvsql, csvjoin, csvstat)",
11261
+ category: "data",
11262
+ tier: 3,
11263
+ install: {
11264
+ mac: [{ manager: "pipx", command: "pipx install csvkit" }],
11265
+ linux: [{ manager: "pipx", command: "pipx install csvkit" }],
11266
+ win: [{ manager: "pipx", command: "pipx install csvkit" }]
11267
+ },
11268
+ homepage: "https://csvkit.readthedocs.io/"
11269
+ },
11270
+ podman: {
11271
+ id: "podman",
11272
+ probe: "podman",
11273
+ description: "Daemonless container engine, rootless by default (Docker alternative)",
11274
+ category: "container",
11275
+ tier: 3,
11276
+ install: {
11277
+ mac: [{ manager: "brew", command: "brew install podman" }],
11278
+ linux: [{ manager: "apt", command: "sudo apt install podman" }],
11279
+ win: [{ manager: "winget", command: "winget install RedHat.Podman" }]
11280
+ },
11281
+ homepage: "https://podman.io/"
11282
+ }
11283
+ };
11284
+ var TIER1_CLI_TOOLS = [
11285
+ "ripgrep",
11286
+ "fd",
11287
+ "jq",
11288
+ "yq",
11289
+ "gh",
11290
+ "delta",
11291
+ "bat",
11292
+ "sd",
11293
+ "ast-grep",
11294
+ "zstd"
11295
+ ];
11296
+ var TIER2_CLI_TOOLS_BY_TRIGGER = {
11297
+ "web-project": ["playwright"],
11298
+ "data-project": ["duckdb", "xsv"],
11299
+ "rust-project": ["taplo"],
11300
+ "python-project": ["taplo"],
11301
+ "docker-detected": ["docker"],
11302
+ "ci-llm-project": ["llm"],
11303
+ "interactive-tty": ["fzf", "lazygit", "difftastic"],
11304
+ "gitlab-remote": ["glab"],
11305
+ "azure-remote": ["az-devops"]
11306
+ };
11307
+ var TIER3_CLI_TOOLS = [
11308
+ "rtk",
11309
+ "stagehand",
11310
+ "aichat",
11311
+ "mods",
11312
+ "comby",
11313
+ "miller",
11314
+ "csvkit",
11315
+ "podman"
11316
+ ];
11317
+ var DEFAULT_CLI_TOOLS = TIER1_CLI_TOOLS;
11318
+ var CLI_TOOL_SECRET_NOTES = Object.freeze(
11319
+ (() => {
11320
+ const out = {};
11321
+ for (const tool of Object.values(AVAILABLE_CLI_TOOLS)) {
11322
+ const env = tool.requiresEnv;
11323
+ if (env && env.length > 0) {
11324
+ out[tool.id] = [...env];
11325
+ }
11326
+ }
11327
+ return out;
11328
+ })()
11329
+ );
11330
+
11331
+ // src/cli/shared/pickers.ts
11332
+ function tierSeparator(tier) {
11333
+ const label2 = tier === 1 ? "\u2500\u2500 Tier 1 \u2014 default-on \u2500\u2500" : tier === 2 ? "\u2500\u2500 Tier 2 \u2014 conditional \u2500\u2500" : "\u2500\u2500 Tier 3 \u2014 opt-in advanced \u2500\u2500";
11334
+ return new inquirer4.Separator(label2);
11335
+ }
11336
+ function toolChoice(meta, preChecked) {
11337
+ const caveat = meta.caveat ? `${chalk5.yellow("\u26A0")} ` : "";
11338
+ const description = meta.description;
11339
+ return {
11340
+ name: `${caveat}${meta.id} \u2014 ${description}`,
11341
+ value: meta.id,
11342
+ checked: preChecked
11343
+ };
11344
+ }
11345
+ async function pickCliTools(opts = {}) {
11346
+ const existingSet = new Set(opts.existing ?? []);
11347
+ const suggestedSet = new Set(opts.tier2Suggested ?? []);
11348
+ const hasExistingSelection = (opts.existing?.length ?? 0) > 0;
11349
+ const tier1Choices = [];
11350
+ const tier2Choices = [];
11351
+ const tier3Choices = [];
11352
+ for (const id of TIER1_CLI_TOOLS) {
11353
+ const meta = AVAILABLE_CLI_TOOLS[id];
11354
+ if (!meta) continue;
11355
+ const checked = hasExistingSelection ? existingSet.has(id) : true;
11356
+ tier1Choices.push(toolChoice(meta, checked));
11357
+ }
11358
+ const seenTier2 = /* @__PURE__ */ new Set();
11359
+ for (const ids of Object.values(TIER2_CLI_TOOLS_BY_TRIGGER)) {
11360
+ for (const id of ids) {
11361
+ if (seenTier2.has(id)) continue;
11362
+ seenTier2.add(id);
11363
+ const meta = AVAILABLE_CLI_TOOLS[id];
11364
+ if (!meta) continue;
11365
+ const checked = hasExistingSelection ? existingSet.has(id) : suggestedSet.has(id);
11366
+ tier2Choices.push(toolChoice(meta, checked));
11367
+ }
11368
+ }
11369
+ for (const id of TIER3_CLI_TOOLS) {
11370
+ const meta = AVAILABLE_CLI_TOOLS[id];
11371
+ if (!meta) continue;
11372
+ const checked = hasExistingSelection && existingSet.has(id);
11373
+ tier3Choices.push(toolChoice(meta, checked));
11374
+ }
11375
+ const choices = [
11376
+ tierSeparator(1),
11377
+ ...tier1Choices,
11378
+ tierSeparator(2),
11379
+ ...tier2Choices,
11380
+ tierSeparator(3),
11381
+ ...tier3Choices
11382
+ ];
11383
+ const themeOption = opts.wslTheme ? { theme: opts.wslTheme } : {};
11384
+ const { tools } = await inquirer4.prompt([
11385
+ {
11386
+ type: "checkbox",
11387
+ name: "tools",
11388
+ message: "Select CLI tools (default tier-1 + project-triggered tier-2):",
11389
+ choices,
11390
+ ...themeOption
11391
+ }
11392
+ ]);
11393
+ return tools ?? [];
11394
+ }
11395
+ async function pickMcpServers(opts) {
11396
+ const platformMcp = PLATFORM_MCP_SERVER[opts.platform];
11397
+ const defaultSelection = opts.existing && opts.existing.length > 0 ? opts.existing : Array.from(/* @__PURE__ */ new Set([platformMcp, "playwright", "context7"]));
11398
+ const themeOption = opts.wslTheme ? { theme: opts.wslTheme } : {};
11399
+ const { mcp } = await inquirer4.prompt([
11400
+ {
11401
+ type: "checkbox",
11402
+ name: "mcp",
11403
+ message: "Select MCP servers:",
11404
+ choices: MCP_CHOICES,
11405
+ default: defaultSelection,
11406
+ ...themeOption
11407
+ }
11408
+ ]);
11409
+ const servers = mcp ?? [];
11410
+ if (!servers.includes(platformMcp)) {
11411
+ servers.unshift(platformMcp);
11412
+ }
11413
+ return servers;
11414
+ }
11415
+ async function confirmMcpGate(opts) {
11416
+ const defaultYes = opts.defaultYes ?? opts.hasExisting;
11417
+ const { proceed } = await inquirer4.prompt([
11418
+ {
11419
+ type: "confirm",
11420
+ name: "proceed",
11421
+ message: "Configure MCP servers? (CLI tools are recommended as the default)",
11422
+ default: defaultYes
11423
+ }
11424
+ ]);
11425
+ return proceed;
11426
+ }
11427
+
11428
+ // src/cliTools/detect.ts
11429
+ import { spawn } from "child_process";
11430
+ var PROBE_TIMEOUT_MS = 2e3;
11431
+ function isSafeProbeName(name) {
11432
+ return /^[A-Za-z0-9._\-+]+$/.test(name);
11433
+ }
11434
+ async function probeBin(name) {
11435
+ if (!isSafeProbeName(name)) return "";
11436
+ const isWindows = process.platform === "win32";
11437
+ const [cmd, args] = isWindows ? ["where", [name]] : ["/bin/sh", ["-c", `command -v -- "${name}"`]];
11438
+ return new Promise((resolve5) => {
11439
+ let settled = false;
11440
+ let stdout = "";
11441
+ const child = spawn(cmd, args, {
11442
+ stdio: ["ignore", "pipe", "ignore"],
11443
+ windowsHide: true
11444
+ });
11445
+ const timer = setTimeout(() => {
11446
+ if (settled) return;
11447
+ settled = true;
11448
+ try {
11449
+ child.kill("SIGKILL");
11450
+ } catch {
11451
+ }
11452
+ resolve5("");
11453
+ }, PROBE_TIMEOUT_MS);
11454
+ child.stdout?.on("data", (chunk) => {
11455
+ stdout += typeof chunk === "string" ? chunk : chunk.toString("utf8");
11456
+ });
11457
+ child.on("error", () => {
11458
+ if (settled) return;
11459
+ settled = true;
11460
+ clearTimeout(timer);
11461
+ resolve5("");
11462
+ });
11463
+ child.on("close", (code) => {
11464
+ if (settled) return;
11465
+ settled = true;
11466
+ clearTimeout(timer);
11467
+ if (code !== 0) {
11468
+ resolve5("");
11469
+ return;
11470
+ }
11471
+ const first = stdout.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0) ?? "";
11472
+ resolve5(first);
11473
+ });
11474
+ });
11475
+ }
11476
+ async function detectCliTool(id) {
11477
+ const meta = AVAILABLE_CLI_TOOLS[id];
11478
+ const probe = meta?.probe ?? id;
11479
+ const path = await probeBin(probe);
11480
+ return {
11481
+ id,
11482
+ probe,
11483
+ installed: path.length > 0,
11484
+ path
11485
+ };
11486
+ }
11487
+ async function detectCliTools(ids) {
11488
+ return Promise.all(ids.map((id) => detectCliTool(id)));
11489
+ }
11490
+ async function findMissingCliTools(ids) {
11491
+ const results = await detectCliTools(ids);
11492
+ return results.filter((r) => !r.installed).map((r) => r.id);
11493
+ }
11494
+
11495
+ // src/cliTools/install.ts
11496
+ init_ui();
11497
+ import chalk6 from "chalk";
11498
+ import inquirer5 from "inquirer";
11499
+
11500
+ // src/cliTools/oneLiner.ts
11501
+ var GROUPABLE_MANAGERS = /* @__PURE__ */ new Set([
11502
+ "brew",
11503
+ "apt",
11504
+ "apt-get",
11505
+ "dnf",
11506
+ "yum",
11507
+ "pacman",
11508
+ "scoop",
11509
+ "cargo"
11510
+ ]);
11511
+ var JOIN = " \\\n && ";
11512
+ function parseGroupable(command, manager) {
11513
+ if (!manager || !GROUPABLE_MANAGERS.has(manager)) return null;
11514
+ if (command.includes("&&") || command.includes("|") || command.includes(";")) return null;
11515
+ const re = new RegExp(`^(sudo\\s+)?${manager}\\s+install\\s+(\\S+)$`);
11516
+ const match = command.match(re);
11517
+ if (!match) return null;
11518
+ const sudo = match[1] ?? "";
11519
+ const pkg = match[2];
11520
+ return { prefix: `${sudo}${manager} install`, pkg };
11521
+ }
11522
+ function buildOneLiner(plans) {
11523
+ if (plans.length === 0) return "";
11524
+ const groupOrder = [];
11525
+ const groups = /* @__PURE__ */ new Map();
11526
+ const standalones = [];
11527
+ for (const plan of plans) {
11528
+ if (!plan.command) {
11529
+ standalones.push(`# install ${plan.id} manually: see ${plan.homepage}`);
11530
+ continue;
11531
+ }
11532
+ const parsed = parseGroupable(plan.command, plan.manager);
11533
+ if (!parsed) {
11534
+ standalones.push(plan.command);
11535
+ continue;
11536
+ }
11537
+ const existing = groups.get(parsed.prefix);
11538
+ if (existing) {
11539
+ existing.push(parsed.pkg);
11540
+ } else {
11541
+ groupOrder.push(parsed.prefix);
11542
+ groups.set(parsed.prefix, [parsed.pkg]);
11543
+ }
11544
+ }
11545
+ const chunks = [];
11546
+ for (const prefix of groupOrder) {
11547
+ const pkgs = groups.get(prefix);
11548
+ if (!pkgs) continue;
11549
+ chunks.push(`${prefix} ${pkgs.join(" ")}`);
11550
+ }
11551
+ for (const cmd of standalones) {
11552
+ chunks.push(cmd);
11553
+ }
11554
+ if (chunks.length === 0) return "";
11555
+ return chunks.join(JOIN);
11556
+ }
11557
+
11558
+ // src/cliTools/install.ts
11559
+ function currentOsKey() {
11560
+ if (process.platform === "win32") return "win";
11561
+ if (process.platform === "linux" || process.platform === "freebsd") return "linux";
11562
+ return "mac";
11563
+ }
11564
+ function buildInstallPlan(ids, os = currentOsKey()) {
11565
+ const plans = [];
11566
+ for (const id of ids) {
11567
+ const meta = AVAILABLE_CLI_TOOLS[id];
11568
+ if (!meta) continue;
11569
+ const cmd = meta.install[os]?.[0];
11570
+ plans.push({
11571
+ id: meta.id,
11572
+ probe: meta.probe,
11573
+ manager: cmd?.manager,
11574
+ command: cmd?.command,
11575
+ homepage: meta.homepage
11576
+ });
11577
+ }
11578
+ return plans;
11579
+ }
11580
+ async function offerInstaller(missing, opts = {}) {
11581
+ if (missing.length === 0) return true;
11582
+ const os = opts.os ?? currentOsKey();
11583
+ const interactive = opts.interactive ?? true;
11584
+ const plan = buildInstallPlan(missing, os);
11585
+ const osLabel = os === "mac" ? "macOS" : os === "linux" ? "Linux" : "Windows";
11586
+ console.log("");
11587
+ console.log(chalk6.yellow(`${missing.length} CLI tool${missing.length === 1 ? "" : "s"} not detected on PATH (${osLabel}):`));
11588
+ console.log("");
11589
+ for (const entry of plan) {
11590
+ const header = entry.manager ? `${chalk6.cyan(entry.id)} (${entry.manager})` : chalk6.cyan(entry.id);
11591
+ console.log(` ${header}`);
11592
+ if (entry.command) {
11593
+ console.log(` ${chalk6.gray("$")} ${entry.command}`);
11594
+ } else {
11595
+ console.log(` ${chalk6.gray("see")} ${entry.homepage}`);
11596
+ }
11597
+ }
11598
+ console.log("");
11599
+ console.log(chalk6.gray("hatch3r will not run these commands for you \u2014 copy-paste in your shell."));
11600
+ console.log("");
11601
+ const oneLiner = buildOneLiner(plan);
11602
+ if (oneLiner) {
11603
+ console.log(chalk6.yellow("Or copy-paste this one-liner to install everything at once:"));
11604
+ console.log("");
11605
+ for (const line of oneLiner.split("\n")) {
11606
+ console.log(` ${chalk6.cyan(line)}`);
11607
+ }
11608
+ console.log("");
11609
+ }
11610
+ if (!interactive) return true;
11611
+ const { proceed } = await inquirer5.prompt([
11612
+ {
11613
+ type: "confirm",
11614
+ name: "proceed",
11615
+ message: "Mark these tools as 'install pending' and continue?",
11616
+ default: true
11617
+ }
11618
+ ]);
11619
+ return proceed;
11620
+ }
11621
+ function printMissingCliToolsDisclaimer(missing, totalSelected, os = currentOsKey()) {
11622
+ if (missing.length === 0) return;
11623
+ const plan = buildInstallPlan(missing, os);
11624
+ const oneLiner = buildOneLiner(plan);
11625
+ const osLabel = os === "mac" ? "macOS" : os === "linux" ? "Linux" : "Windows";
11626
+ const lines = [
11627
+ `${missing.length} of ${totalSelected} selected CLI tools are missing.`,
11628
+ `hatch3r does NOT install them for you.`,
11629
+ "",
11630
+ `Copy-paste to install everything (${osLabel}):`,
11631
+ "",
11632
+ ...oneLiner.split("\n").map((l) => ` ${l}`)
11633
+ ];
11634
+ printBox("CLI tools not installed", lines, "warning");
11635
+ }
11636
+
11637
+ // src/cliTools/triggers.ts
11638
+ var DATA_LANGUAGES = /* @__PURE__ */ new Set(["python", "r", "sql"]);
11639
+ var WEB_FRAMEWORKS = /* @__PURE__ */ new Set([
11640
+ "next",
11641
+ "nuxt",
11642
+ "astro",
11643
+ "sveltekit",
11644
+ "remix",
11645
+ "angular",
11646
+ "vue",
11647
+ "react",
11648
+ "svelte"
11649
+ ]);
11650
+ function evaluateTriggers(repoInfo) {
11651
+ const active = /* @__PURE__ */ new Set();
11652
+ if (repoInfo.frameworks.some((f) => WEB_FRAMEWORKS.has(f))) {
11653
+ active.add("web-project");
11654
+ }
11655
+ if (repoInfo.languages.some((l) => DATA_LANGUAGES.has(l))) {
11656
+ active.add("data-project");
11657
+ }
11658
+ if (repoInfo.languages.includes("rust")) {
11659
+ active.add("rust-project");
11660
+ }
11661
+ if (repoInfo.languages.includes("python")) {
11662
+ active.add("python-project");
11663
+ }
11664
+ if (process.stdout && process.stdout.isTTY) {
11665
+ active.add("interactive-tty");
11666
+ }
11667
+ return active;
11668
+ }
11669
+ function evaluateTier2Triggers(repoInfo) {
11670
+ const triggers = evaluateTriggers(repoInfo);
11671
+ const ids = /* @__PURE__ */ new Set();
11672
+ for (const trigger of triggers) {
11673
+ for (const id of TIER2_CLI_TOOLS_BY_TRIGGER[trigger]) {
11674
+ ids.add(id);
11675
+ }
11676
+ }
11677
+ return [...ids];
11678
+ }
11679
+ function applyPlatformTriggers(platform, base) {
11680
+ const out = new Set(base);
11681
+ if (platform === "gitlab") {
11682
+ for (const id of TIER2_CLI_TOOLS_BY_TRIGGER["gitlab-remote"]) out.add(id);
11683
+ } else if (platform === "azure-devops") {
11684
+ for (const id of TIER2_CLI_TOOLS_BY_TRIGGER["azure-remote"]) out.add(id);
11685
+ }
11686
+ return [...out];
11687
+ }
11688
+
10741
11689
  // src/cli/commands/init.ts
10742
11690
  init_integrity();
10743
11691
  init_version();
@@ -10819,6 +11767,15 @@ function validateWorkspaceManifest(data) {
10819
11767
  if (typeof content.teamSize !== "string") return false;
10820
11768
  if (!content.items || typeof content.items !== "object") return false;
10821
11769
  }
11770
+ if (defaults.cliTools !== void 0) {
11771
+ if (typeof defaults.cliTools !== "object" || defaults.cliTools === null) return false;
11772
+ const cli = defaults.cliTools;
11773
+ if (typeof cli.enabled !== "boolean") return false;
11774
+ if (!Array.isArray(cli.selected)) return false;
11775
+ for (const id of cli.selected) {
11776
+ if (typeof id !== "string") return false;
11777
+ }
11778
+ }
10822
11779
  for (const repo of obj.repos) {
10823
11780
  if (!repo || typeof repo !== "object") return false;
10824
11781
  const r = repo;
@@ -10933,7 +11890,30 @@ function resolveRepoConfig(defaults, overrides, protectedIds) {
10933
11890
  }
10934
11891
  }
10935
11892
  }
10936
- return { platform, tools, features, mcp, models, contentIds, excludedContent, addedContent };
11893
+ return {
11894
+ platform,
11895
+ tools,
11896
+ features,
11897
+ mcp,
11898
+ models,
11899
+ contentIds,
11900
+ excludedContent,
11901
+ addedContent,
11902
+ cliTools: defaults.cliTools
11903
+ };
11904
+ }
11905
+ function applyMemberCliToolsOverrides(workspaceDefault, memberLocal, memberExcluded) {
11906
+ if (!workspaceDefault && (!memberLocal || memberLocal.length === 0)) {
11907
+ return void 0;
11908
+ }
11909
+ const base = new Set(workspaceDefault?.selected ?? []);
11910
+ for (const id of memberLocal ?? []) base.add(id);
11911
+ for (const id of memberExcluded ?? []) base.delete(id);
11912
+ const selected = [...base];
11913
+ return {
11914
+ enabled: selected.length > 0,
11915
+ selected
11916
+ };
10937
11917
  }
10938
11918
  function buildSelectionFromIds(ids, baseSelection, allItems) {
10939
11919
  const items = {
@@ -11163,6 +12143,11 @@ async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry,
11163
12143
  gitRepo = existingManifest.repo;
11164
12144
  }
11165
12145
  if (!gitBranch) gitBranch = "main";
12146
+ const effectiveCliTools = applyMemberCliToolsOverrides(
12147
+ resolved.cliTools,
12148
+ existingManifest?.workspace?.localCliTools,
12149
+ existingManifest?.workspace?.excludedCliTools
12150
+ );
11166
12151
  const manifest = createManifest({
11167
12152
  platform: gitPlatform ?? resolved.platform,
11168
12153
  owner: gitOwner,
@@ -11174,7 +12159,8 @@ async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry,
11174
12159
  features: resolved.features,
11175
12160
  mcpServers: resolved.mcp.servers,
11176
12161
  content: effectiveSelection,
11177
- languages: repoInfo.languages
12162
+ languages: repoInfo.languages,
12163
+ cliTools: effectiveCliTools
11178
12164
  });
11179
12165
  manifest.workspace = {
11180
12166
  rootPath: relative4(repoDir, workspaceRoot),
@@ -11182,7 +12168,9 @@ async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry,
11182
12168
  syncVersion: HATCH3R_VERSION,
11183
12169
  workspaceChecksum: wsChecksum,
11184
12170
  excludedContent: resolved.excludedContent.length > 0 ? resolved.excludedContent : void 0,
11185
- localContent: existingManifest?.workspace?.localContent
12171
+ localContent: existingManifest?.workspace?.localContent,
12172
+ localCliTools: existingManifest?.workspace?.localCliTools,
12173
+ excludedCliTools: existingManifest?.workspace?.excludedCliTools
11186
12174
  };
11187
12175
  if (resolved.models) {
11188
12176
  manifest.models = resolved.models;
@@ -11251,6 +12239,82 @@ var CONTENT_ROOT2 = findPackageRoot(__dirname2);
11251
12239
  var DEFAULT_TOOLS = ["claude"];
11252
12240
  var DEFAULT_FEATURE_KEYS = Object.keys(DEFAULT_FEATURES);
11253
12241
  var DEFAULT_MCP = ["playwright", "github", "context7"];
12242
+ var HANDOFFS_README_SEED = `# Project Handoffs
12243
+
12244
+ This directory holds active and archived handoff documents surfaced by the
12245
+ \`hatch3r-handoff-loader\` agent at session start and consumed by
12246
+ \`/hatch3r-handoff resume\`.
12247
+
12248
+ ## Layout
12249
+
12250
+ - \`active/<id>.md\` \u2014 handoffs in any non-terminal status (open, in-progress, blocked, handed-off, resumed)
12251
+ - \`archived/<id>.md\` \u2014 handoffs in terminal status (completed, expired, superseded)
12252
+
12253
+ ## ID scheme
12254
+
12255
+ \`<YYYY-MM-DD>_T<HHmm>_<5hex>_<kebab-slug>\` \u2014 chronologically sortable, collision-safe.
12256
+
12257
+ Example: \`2026-05-17_T1430_a3f2c_issue-42-cache-refactor.md\`.
12258
+
12259
+ ## Lifecycle
12260
+
12261
+ - Created by \`/hatch3r-handoff prepare\` or the \`on-context-switch\` hook.
12262
+ - Loaded at session start by \`hatch3r-handoff-loader\`.
12263
+ - Resumed via \`/hatch3r-handoff resume [<id>]\` (lists actives if no id given).
12264
+ - \`expires_after\`: ISO-8601 timestamp; preparer default stamps \`created + 30 days\`.
12265
+ - Archived (never deleted by hatch3r) on completion or expiry.
12266
+
12267
+ ## Required frontmatter
12268
+
12269
+ | Field | Type | Notes |
12270
+ | --- | --- | --- |
12271
+ | \`id\` | string | Filename without \`.md\` |
12272
+ | \`type\` | literal \`handoff\` | |
12273
+ | \`created\` | ISO-8601 | Immutable |
12274
+ | \`updated\` | ISO-8601 | Re-stamped on status change |
12275
+ | \`status\` | enum | open \\| in-progress \\| blocked \\| handed-off \\| resumed \\| completed \\| archived |
12276
+ | \`source_agent\` | string | Tool/role that prepared the handoff |
12277
+ | \`target_agent\` | string | \`any\` allowed but warned (avoids handoff loops) |
12278
+ | \`git_ref\` | string | \`branch@sha7\` \u2014 staleness signal |
12279
+ | \`branch\` | string | |
12280
+ | \`confidence\` | 0..1 | |
12281
+ | \`completeness\` | 0..1 | |
12282
+ | \`integrity\` | string | \`sha256:<hex>\` \u2014 SHA-256 of body |
12283
+
12284
+ Optional: \`work_item\` (platform-prefixed: \`gh:owner/repo#42\`, \`ado:org/project:work-item/123\`, \`gl:owner/repo!42\`), \`expires_after\`, \`summary\` (\u2264200 chars), \`requirements\`, \`compaction_count\`, \`hatch3r_version\`, \`tags\`, \`superseded_by\`, \`parent_handoff\`.
12285
+
12286
+ ## Body sections (required, in order)
12287
+
12288
+ Wrap the body in user-tier instruction-hierarchy markers:
12289
+
12290
+ \`\`\`markdown
12291
+ --- BEGIN USER-TIER CONTENT: handoff ---
12292
+
12293
+ ## Problem (1-3 paragraphs)
12294
+ ## Decisions (bullet list)
12295
+ ## Work Done (from end-of-session Iteration Summary)
12296
+ ## Work Remaining
12297
+ ## Blockers
12298
+ ## Next Steps (ordered list)
12299
+ ## Build & Test Status (table: Check | Status | Notes)
12300
+ ## File Manifest (table: Path | Status | Last action)
12301
+
12302
+ --- END USER-TIER CONTENT: handoff ---
12303
+ \`\`\`
12304
+
12305
+ ## Caps and validation
12306
+
12307
+ - Body \u2264 50 KB, total file \u2264 60 KB.
12308
+ - Soft cap 25 active handoffs per repo (warn at 20, refuse briefing at 50).
12309
+ - Injection-pattern scan (P-LEARN-01..05) at write and read; reuses learnings catalog.
12310
+ - Integrity hash mismatch downgrades confidence to low; included with warning.
12311
+
12312
+ ## Cross-tool portability
12313
+
12314
+ Handoffs are plain Markdown \u2014 readable by humans and any AI tool. Tool-specific adapters (Cursor, Claude, Copilot, etc.) surface active handoffs in their native context file on session-start so a handoff written from one tool resumes cleanly in another.
12315
+
12316
+ See \`agents/hatch3r-handoff-loader.md\`, \`skills/hatch3r-handoff-resume/SKILL.md\`, and \`rules/hatch3r-handoff-readiness.md\` for the full protocols.
12317
+ `;
11254
12318
  var LEARNINGS_README_SEED = `# Project Learnings
11255
12319
 
11256
12320
  This directory holds project-specific learnings surfaced by the
@@ -11357,7 +12421,7 @@ function selectionHasBoardContent(selection) {
11357
12421
  function warnBoardPrerequisites(selection) {
11358
12422
  if (!selectionHasBoardContent(selection)) return;
11359
12423
  info(
11360
- `Board commands selected. Prerequisites: ${chalk5.bold("GitHub Projects V2")} must be enabled and your PAT needs the ${chalk5.bold("project")} scope. See ${chalk5.dim("https://docs.github.com/en/issues/planning-and-tracking-with-projects")}`
12424
+ `Board commands selected. Prerequisites: ${chalk7.bold("GitHub Projects V2")} must be enabled and your PAT needs the ${chalk7.bold("project")} scope. See ${chalk7.dim("https://docs.github.com/en/issues/planning-and-tracking-with-projects")}`
11361
12425
  );
11362
12426
  }
11363
12427
  function languagesForSelection(repoInfo) {
@@ -11396,7 +12460,7 @@ async function runInit(options) {
11396
12460
  }
11397
12461
  }
11398
12462
  async function runInitInner(options) {
11399
- const { rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection, worktreeEnabled, customization } = options;
12463
+ const { rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection, worktreeEnabled, customization, cliTools } = options;
11400
12464
  const skipInitPrompts = options.yes === true;
11401
12465
  const agentsDir = join27(rootDir, AGENTS_DIR);
11402
12466
  const totalSteps = 4;
@@ -11427,6 +12491,18 @@ async function runInitInner(options) {
11427
12491
  throw err;
11428
12492
  }
11429
12493
  }
12494
+ await mkdir8(join27(agentsDir, "handoffs", "active"), { recursive: true });
12495
+ await mkdir8(join27(agentsDir, "handoffs", "archived"), { recursive: true });
12496
+ const handoffsReadmePath = join27(agentsDir, "handoffs", "README.md");
12497
+ try {
12498
+ await access9(handoffsReadmePath);
12499
+ } catch (err) {
12500
+ if (err.code === "ENOENT") {
12501
+ await safeWriteFile(handoffsReadmePath, HANDOFFS_README_SEED);
12502
+ } else {
12503
+ throw err;
12504
+ }
12505
+ }
11430
12506
  const mcpPath = join27(agentsDir, "mcp", "mcp.json");
11431
12507
  await filterMcpJsonOnDisk(mcpPath, new Set(mcpServers));
11432
12508
  const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
@@ -11435,7 +12511,7 @@ async function runInitInner(options) {
11435
12511
  const s2 = createSpinner(step(2, totalSteps, "Preparing manifest..."));
11436
12512
  s2.start();
11437
12513
  const effectiveCustomization = customization ?? existingManifest?.customization;
11438
- const manifest = createManifest({ platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, content: contentSelection, languages: repoInfo.languages, worktreeEnabled, customization: effectiveCustomization });
12514
+ const manifest = createManifest({ platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, content: contentSelection, languages: repoInfo.languages, worktreeEnabled, customization: effectiveCustomization, cliTools });
11439
12515
  const preservedFields = options.preservedManifestFields ?? (existingManifest ? extractPreservedManifestFields(existingManifest) : void 0);
11440
12516
  if (preservedFields) {
11441
12517
  applyPreservedManifestFields(manifest, preservedFields);
@@ -11544,18 +12620,22 @@ async function runInitInner(options) {
11544
12620
  const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
11545
12621
  summaryLines.push("");
11546
12622
  if (isGreenfield) {
11547
- summaryLines.push(`${chalk5.cyan("\u2192")} Run ${chalk5.bold(formatCommandHint(tools, "project-spec"))} to define your new project`);
12623
+ summaryLines.push(`${chalk7.cyan("\u2192")} Run ${chalk7.bold(formatCommandHint(tools, "project-spec"))} to define your new project`);
11548
12624
  } else {
11549
- summaryLines.push(`${chalk5.cyan("\u2192")} Run ${chalk5.bold(formatCommandHint(tools, "codebase-map"))} to map your existing codebase`);
12625
+ summaryLines.push(`${chalk7.cyan("\u2192")} Run ${chalk7.bold(formatCommandHint(tools, "codebase-map"))} to map your existing codebase`);
11550
12626
  }
11551
12627
  if (envResult && envResult.newVars.length > 0) {
11552
12628
  summaryLines.push("");
11553
- summaryLines.push(`${chalk5.yellow("!")} Add your secrets to ${chalk5.bold(".env.mcp")}: ${envResult.newVars.join(", ")}`);
11554
- summaryLines.push(` Then run: ${chalk5.dim(getSourceEnvMcpCommand())}`);
12629
+ summaryLines.push(`${chalk7.yellow("!")} Add your secrets to ${chalk7.bold(".env.mcp")}: ${envResult.newVars.join(", ")}`);
12630
+ summaryLines.push(` Then run: ${chalk7.dim(getSourceEnvMcpCommand())}`);
11555
12631
  }
11556
12632
  printBox("Hatch complete", summaryLines, "success");
12633
+ if (cliTools && cliTools.selected.length > 0) {
12634
+ const finalMissing = await findMissingCliTools(cliTools.selected);
12635
+ printMissingCliToolsDisclaimer(finalMissing, cliTools.selected.length);
12636
+ }
11557
12637
  if (!skipInitPrompts) {
11558
- const { create } = await inquirer4.prompt([{
12638
+ const { create } = await inquirer6.prompt([{
11559
12639
  type: "confirm",
11560
12640
  name: "create",
11561
12641
  message: "Would you like to create your first custom artifact now?",
@@ -11590,7 +12670,7 @@ async function checkExisting(rootDir, skipPrompt, newSelection) {
11590
12670
  }
11591
12671
  }
11592
12672
  }
11593
- const { proceed } = await inquirer4.prompt([
12673
+ const { proceed } = await inquirer6.prompt([
11594
12674
  {
11595
12675
  type: "confirm",
11596
12676
  name: "proceed",
@@ -11599,7 +12679,7 @@ async function checkExisting(rootDir, skipPrompt, newSelection) {
11599
12679
  }
11600
12680
  ]);
11601
12681
  if (!proceed) {
11602
- console.log(chalk5.dim("\n Init cancelled.\n"));
12682
+ console.log(chalk7.dim("\n Init cancelled.\n"));
11603
12683
  throw new HatchError("Init cancelled.", 0);
11604
12684
  }
11605
12685
  }
@@ -11636,10 +12716,10 @@ async function initCommand(opts = {}) {
11636
12716
  const detectedRepos = await detectSubRepos(rootDir);
11637
12717
  if (opts.yes) {
11638
12718
  opts.workspace = true;
11639
- info(chalk5.dim(`No git repo found. ${detectedRepos.length} git repo(s) detected in subdirectories \u2014 initializing as workspace.`));
12719
+ info(chalk7.dim(`No git repo found. ${detectedRepos.length} git repo(s) detected in subdirectories \u2014 initializing as workspace.`));
11640
12720
  } else {
11641
12721
  info(`No git repo found, but ${detectedRepos.length} git repo(s) detected in subdirectories.`);
11642
- const { useWorkspace } = await inquirer4.prompt([
12722
+ const { useWorkspace } = await inquirer6.prompt([
11643
12723
  {
11644
12724
  type: "confirm",
11645
12725
  name: "useWorkspace",
@@ -11671,7 +12751,7 @@ async function initCommand(opts = {}) {
11671
12751
  }
11672
12752
  if (repoInfo.isMonorepo) detected.push("monorepo");
11673
12753
  if (detected.length > 0) {
11674
- info(chalk5.dim(`Detected: ${detected.join(", ")}`));
12754
+ info(chalk7.dim(`Detected: ${detected.join(", ")}`));
11675
12755
  }
11676
12756
  if (opts.yes) {
11677
12757
  const remoteUrl2 = getGitRemoteUrl();
@@ -11686,7 +12766,7 @@ async function initCommand(opts = {}) {
11686
12766
  const invalid = rawTools.filter((t) => !VALID_TOOLS.has(t));
11687
12767
  if (invalid.length > 0) {
11688
12768
  error(`Invalid tool(s): ${invalid.join(", ")}`);
11689
- console.log(chalk5.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
12769
+ console.log(chalk7.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
11690
12770
  throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1, "VALIDATION_ERROR");
11691
12771
  }
11692
12772
  tools2 = rawTools;
@@ -11698,7 +12778,18 @@ async function initCommand(opts = {}) {
11698
12778
  const worktreeEnabled2 = opts.worktree ?? tools2.some((t) => WORKTREE_CAPABLE_TOOLS.has(t));
11699
12779
  const features2 = { ...DEFAULT_FEATURES };
11700
12780
  const platformMcp = PLATFORM_MCP_SERVER[platform2];
11701
- const mcpServers2 = features2.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
12781
+ const mcpServers2 = features2.mcp && opts.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
12782
+ let cliToolsConfig2;
12783
+ if (opts.noCliTools) {
12784
+ cliToolsConfig2 = { enabled: false, selected: [] };
12785
+ } else {
12786
+ const explicit = resolveCliToolsFlag(opts.cliTools, repoInfo, platform2);
12787
+ const selected = explicit ?? Array.from(/* @__PURE__ */ new Set([
12788
+ ...DEFAULT_CLI_TOOLS,
12789
+ ...applyPlatformTriggers(platform2, evaluateTier2Triggers(repoInfo))
12790
+ ]));
12791
+ cliToolsConfig2 = { enabled: selected.length > 0, selected };
12792
+ }
11702
12793
  const defaultBranch2 = parseGitDefaultBranch();
11703
12794
  const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
11704
12795
  const presetId = validateFlag(opts.preset, ["minimal", "standard", "full"], "full", "preset");
@@ -11714,13 +12805,13 @@ async function initCommand(opts = {}) {
11714
12805
  }
11715
12806
  warnBoardPrerequisites(contentSelection2);
11716
12807
  await checkExisting(rootDir, true, contentSelection2);
11717
- await runInit({ rootDir, platform: platform2, owner: owner2, repo: repo2, namespace: namespace2, project: project2, defaultBranch: defaultBranch2, tools: tools2, features: features2, mcpServers: mcpServers2, repoInfo, contentSelection: contentSelection2, worktreeEnabled: worktreeEnabled2, yes: true });
12808
+ await runInit({ rootDir, platform: platform2, owner: owner2, repo: repo2, namespace: namespace2, project: project2, defaultBranch: defaultBranch2, tools: tools2, features: features2, mcpServers: mcpServers2, repoInfo, contentSelection: contentSelection2, worktreeEnabled: worktreeEnabled2, cliTools: cliToolsConfig2, yes: true });
11718
12809
  return;
11719
12810
  }
11720
12811
  console.log();
11721
12812
  const remoteUrl = getGitRemoteUrl();
11722
12813
  const detectedPlatform = detectPlatformFromRemote(remoteUrl);
11723
- const platformAnswer = await inquirer4.prompt([
12814
+ const platformAnswer = await inquirer6.prompt([
11724
12815
  {
11725
12816
  type: "select",
11726
12817
  name: "platform",
@@ -11739,7 +12830,7 @@ async function initCommand(opts = {}) {
11739
12830
  let namespace;
11740
12831
  let project;
11741
12832
  if (platform === "azure-devops") {
11742
- const adoAnswers = await inquirer4.prompt([
12833
+ const adoAnswers = await inquirer6.prompt([
11743
12834
  { type: "input", name: "org", message: "Azure DevOps organization:", default: remote.owner || void 0 },
11744
12835
  { type: "input", name: "project", message: "Azure DevOps project:" },
11745
12836
  { type: "input", name: "repo", message: "Repository name:", default: remote.repo || void 0 }
@@ -11749,7 +12840,7 @@ async function initCommand(opts = {}) {
11749
12840
  namespace = owner;
11750
12841
  project = sanitizeInput(adoAnswers.project);
11751
12842
  } else if (platform === "gitlab") {
11752
- const glAnswers = await inquirer4.prompt([
12843
+ const glAnswers = await inquirer6.prompt([
11753
12844
  { type: "input", name: "namespace", message: "GitLab namespace (group or username):", default: remote.owner || void 0 },
11754
12845
  { type: "input", name: "project", message: "Project name:", default: remote.repo || void 0 }
11755
12846
  ]);
@@ -11758,7 +12849,7 @@ async function initCommand(opts = {}) {
11758
12849
  namespace = owner;
11759
12850
  project = repo;
11760
12851
  } else {
11761
- const repoAnswers = await inquirer4.prompt([
12852
+ const repoAnswers = await inquirer6.prompt([
11762
12853
  { type: "input", name: "owner", message: "GitHub owner (org or username):", default: remote.owner || void 0 },
11763
12854
  { type: "input", name: "repo", message: "Repository name:", default: remote.repo || void 0 }
11764
12855
  ]);
@@ -11768,7 +12859,7 @@ async function initCommand(opts = {}) {
11768
12859
  project = repo;
11769
12860
  }
11770
12861
  const defaultBranchDefault = parseGitDefaultBranch();
11771
- const defaultBranchAnswers = await inquirer4.prompt([
12862
+ const defaultBranchAnswers = await inquirer6.prompt([
11772
12863
  {
11773
12864
  type: "input",
11774
12865
  name: "defaultBranch",
@@ -11789,7 +12880,7 @@ async function initCommand(opts = {}) {
11789
12880
  const isAutoGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
11790
12881
  const greenfieldExcl = countProjectTypeExclusions("greenfield", filterIndex.items);
11791
12882
  const brownfieldExcl = countProjectTypeExclusions("brownfield", filterIndex.items);
11792
- const projectTypeAnswer = await inquirer4.prompt([
12883
+ const projectTypeAnswer = await inquirer6.prompt([
11793
12884
  {
11794
12885
  type: "select",
11795
12886
  name: "projectType",
@@ -11803,7 +12894,7 @@ async function initCommand(opts = {}) {
11803
12894
  ]);
11804
12895
  const projectType = projectTypeAnswer.projectType;
11805
12896
  const soloExcl = countTeamSizeExclusions("solo", filterIndex.items);
11806
- const teamSizeAnswer = await inquirer4.prompt([
12897
+ const teamSizeAnswer = await inquirer6.prompt([
11807
12898
  {
11808
12899
  type: "select",
11809
12900
  name: "teamSize",
@@ -11817,7 +12908,7 @@ async function initCommand(opts = {}) {
11817
12908
  ]);
11818
12909
  const teamSize = teamSizeAnswer.teamSize;
11819
12910
  const totalItems = filterIndex.items.length;
11820
- const presetAnswer = await inquirer4.prompt([
12911
+ const presetAnswer = await inquirer6.prompt([
11821
12912
  {
11822
12913
  type: "select",
11823
12914
  name: "preset",
@@ -11836,7 +12927,7 @@ async function initCommand(opts = {}) {
11836
12927
  }
11837
12928
  ]);
11838
12929
  const selectedPreset = getPreset(presetAnswer.preset);
11839
- const wslTheme = isWSL() ? { icon: { checked: chalk5.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
12930
+ const wslTheme = isWSL() ? { icon: { checked: chalk7.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
11840
12931
  let customSelections;
11841
12932
  if (selectedPreset.id === "custom") {
11842
12933
  const contentIndex = filterIndex;
@@ -11844,7 +12935,7 @@ async function initCommand(opts = {}) {
11844
12935
  contentIndex.items,
11845
12936
  (item) => item.protected || item.tags.includes("core")
11846
12937
  );
11847
- const customAnswer = await inquirer4.prompt([
12938
+ const customAnswer = await inquirer6.prompt([
11848
12939
  {
11849
12940
  type: "checkbox",
11850
12941
  name: "items",
@@ -11856,7 +12947,7 @@ async function initCommand(opts = {}) {
11856
12947
  customSelections = customAnswer.items;
11857
12948
  }
11858
12949
  const toolDefaults = repoInfo.existingTools.length > 0 ? repoInfo.existingTools : DEFAULT_TOOLS;
11859
- const toolAnswers = await inquirer4.prompt([
12950
+ const toolAnswers = await inquirer6.prompt([
11860
12951
  {
11861
12952
  type: "checkbox",
11862
12953
  name: "tools",
@@ -11872,7 +12963,7 @@ async function initCommand(opts = {}) {
11872
12963
  if (opts.worktree !== void 0) {
11873
12964
  worktreeEnabled = opts.worktree;
11874
12965
  } else if (hasWorktreeTool) {
11875
- const wtAnswer = await inquirer4.prompt([{
12966
+ const wtAnswer = await inquirer6.prompt([{
11876
12967
  type: "confirm",
11877
12968
  name: "enabled",
11878
12969
  message: "Enable worktree file isolation (for parallel agent sessions)?",
@@ -11882,14 +12973,50 @@ async function initCommand(opts = {}) {
11882
12973
  } else {
11883
12974
  worktreeEnabled = false;
11884
12975
  }
12976
+ const tier2Suggested = Array.from(/* @__PURE__ */ new Set([
12977
+ ...evaluateTier2Triggers(repoInfo),
12978
+ ...applyPlatformTriggers(platform, [])
12979
+ ]));
12980
+ const selectedCliTools = await pickCliTools({
12981
+ tier2Suggested,
12982
+ wslTheme
12983
+ });
12984
+ if (selectedCliTools.length > 0) {
12985
+ const detectSpinner2 = createSpinner(`Detecting ${selectedCliTools.length} CLI tool(s)...`);
12986
+ detectSpinner2.start();
12987
+ const missing = await findMissingCliTools(selectedCliTools);
12988
+ if (missing.length === 0) {
12989
+ detectSpinner2.succeed(`All ${selectedCliTools.length} CLI tool(s) detected on PATH`);
12990
+ } else {
12991
+ detectSpinner2.warn(`${selectedCliTools.length - missing.length}/${selectedCliTools.length} CLI tool(s) detected; ${missing.length} missing`);
12992
+ await offerInstaller(missing, { interactive: true });
12993
+ }
12994
+ const cliEnvVars = [];
12995
+ for (const id of selectedCliTools) {
12996
+ const notes = CLI_TOOL_SECRET_NOTES[id];
12997
+ if (notes && notes.length > 0) {
12998
+ cliEnvVars.push(`${id}: ${notes.join(", ")}`);
12999
+ }
13000
+ }
13001
+ if (cliEnvVars.length > 0) {
13002
+ info(chalk7.dim("CLI tool environment variables required:"));
13003
+ for (const note of cliEnvVars) {
13004
+ info(chalk7.dim(` ${note}`));
13005
+ }
13006
+ }
13007
+ }
13008
+ const cliToolsConfig = {
13009
+ enabled: selectedCliTools.length > 0,
13010
+ selected: selectedCliTools
13011
+ };
11885
13012
  const secretNotes = tools.map((t) => TOOL_SECRET_NOTES[t]).filter(Boolean);
11886
13013
  if (secretNotes.length > 0) {
11887
- info(chalk5.dim("MCP secret loading by tool:"));
13014
+ info(chalk7.dim("MCP secret loading by tool:"));
11888
13015
  for (const note of secretNotes) {
11889
- info(chalk5.dim(` ${note}`));
13016
+ info(chalk7.dim(` ${note}`));
11890
13017
  }
11891
13018
  }
11892
- const featureAnswers = await inquirer4.prompt([
13019
+ const featureAnswers = await inquirer6.prompt([
11893
13020
  {
11894
13021
  type: "checkbox",
11895
13022
  name: "features",
@@ -11906,23 +13033,9 @@ async function initCommand(opts = {}) {
11906
13033
  }
11907
13034
  let mcpServers = [];
11908
13035
  if (features.mcp) {
11909
- const platformMcp = PLATFORM_MCP_SERVER[platform];
11910
- const defaultMcpForPlatform = Array.from(
11911
- /* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])
11912
- );
11913
- const mcpAnswers = await inquirer4.prompt([
11914
- {
11915
- type: "checkbox",
11916
- name: "mcp",
11917
- message: "Select MCP servers:",
11918
- choices: MCP_CHOICES,
11919
- default: defaultMcpForPlatform,
11920
- ...wslTheme && { theme: wslTheme }
11921
- }
11922
- ]);
11923
- mcpServers = mcpAnswers.mcp ?? [];
11924
- if (!mcpServers.includes(platformMcp)) {
11925
- mcpServers.unshift(platformMcp);
13036
+ const proceedMcp = await confirmMcpGate({ hasExisting: false, defaultYes: false });
13037
+ if (proceedMcp) {
13038
+ mcpServers = await pickMcpServers({ platform, wslTheme });
11926
13039
  }
11927
13040
  }
11928
13041
  const contentSelection = resolveSelection(selectedPreset, projectType, teamSize, filterIndex, customSelections, projectLanguages);
@@ -11932,7 +13045,7 @@ async function initCommand(opts = {}) {
11932
13045
  }
11933
13046
  warnBoardPrerequisites(contentSelection);
11934
13047
  await checkExisting(rootDir, false, contentSelection);
11935
- await runInit({ rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection, worktreeEnabled, yes: false });
13048
+ await runInit({ rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection, worktreeEnabled, cliTools: cliToolsConfig, yes: false });
11936
13049
  }
11937
13050
  async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
11938
13051
  const headless = !!opts.yes;
@@ -11945,13 +13058,21 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
11945
13058
  const tools2 = resolveToolsFromOpts(opts.tools, repoInfo);
11946
13059
  const features2 = { ...DEFAULT_FEATURES };
11947
13060
  const platformMcp = PLATFORM_MCP_SERVER[platform2];
11948
- const mcpServers2 = features2.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
13061
+ const mcpServers2 = features2.mcp && opts.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
13062
+ const cliToolsBase = opts.noCliTools ? { enabled: false, selected: [] } : (() => {
13063
+ const explicit = resolveCliToolsFlag(opts.cliTools, repoInfo, platform2);
13064
+ const selected = explicit ?? Array.from(/* @__PURE__ */ new Set([
13065
+ ...DEFAULT_CLI_TOOLS,
13066
+ ...applyPlatformTriggers(platform2, evaluateTier2Triggers(repoInfo))
13067
+ ]));
13068
+ return { enabled: selected.length > 0, selected };
13069
+ })();
11949
13070
  const index = await buildContentIndex(CONTENT_ROOT2);
11950
13071
  const projectLanguages = languagesForSelection(repoInfo);
11951
13072
  const contentSelection2 = resolveSelection(getPreset("full"), "brownfield", "solo", index, void 0, projectLanguages);
11952
13073
  const wsManifest2 = createWorkspaceManifest(
11953
13074
  basename2(rootDir) || "workspace",
11954
- { platform: platform2, tools: tools2, features: features2, mcp: { servers: mcpServers2 }, content: contentSelection2 },
13075
+ { platform: platform2, tools: tools2, features: features2, mcp: { servers: mcpServers2 }, cliTools: cliToolsBase, content: contentSelection2 },
11955
13076
  [],
11956
13077
  "manual"
11957
13078
  );
@@ -11964,20 +13085,20 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
11964
13085
  }));
11965
13086
  wsSpinner.succeed(`Workspace: ${detectedRepos.length} repo(s) detected`);
11966
13087
  console.log();
11967
- console.log(chalk5.dim(" Repo Platform Owner/Repo Branch"));
13088
+ console.log(chalk7.dim(" Repo Platform Owner/Repo Branch"));
11968
13089
  for (const r of enriched) {
11969
13090
  const name = (r.name ?? r.path).padEnd(16);
11970
13091
  if (r.owner && r.repo) {
11971
13092
  const platLabel = PLATFORM_DISPLAY_NAMES[r.platform].padEnd(14);
11972
13093
  const identity = `${r.owner}/${r.repo}`.padEnd(32);
11973
- console.log(` ${name}${chalk5.dim(platLabel)}${chalk5.dim(identity)}${chalk5.dim(r.defaultBranch)}`);
13094
+ console.log(` ${name}${chalk7.dim(platLabel)}${chalk7.dim(identity)}${chalk7.dim(r.defaultBranch)}`);
11974
13095
  } else {
11975
- console.log(` ${name}${chalk5.dim("(no remote detected)")}`);
13096
+ console.log(` ${name}${chalk7.dim("(no remote detected)")}`);
11976
13097
  }
11977
13098
  }
11978
13099
  console.log();
11979
13100
  if (!headless) {
11980
- const { acceptIdentity } = await inquirer4.prompt([
13101
+ const { acceptIdentity } = await inquirer6.prompt([
11981
13102
  {
11982
13103
  type: "confirm",
11983
13104
  name: "acceptIdentity",
@@ -11987,9 +13108,9 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
11987
13108
  ]);
11988
13109
  if (!acceptIdentity) {
11989
13110
  for (const r of enriched) {
11990
- console.log(chalk5.bold(`
13111
+ console.log(chalk7.bold(`
11991
13112
  ${r.name ?? r.path}:`));
11992
- const identity = await inquirer4.prompt([
13113
+ const identity = await inquirer6.prompt([
11993
13114
  { type: "input", name: "owner", message: " Owner:", default: r.owner || void 0 },
11994
13115
  { type: "input", name: "repo", message: " Repo:", default: r.repo || void 0 },
11995
13116
  {
@@ -12018,12 +13139,23 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12018
13139
  let mcpServers;
12019
13140
  let contentSelection;
12020
13141
  let worktreeEnabled;
13142
+ let wsCliTools;
12021
13143
  if (headless) {
12022
13144
  tools = resolveToolsFromOpts(opts.tools, repoInfo);
12023
13145
  worktreeEnabled = opts.worktree ?? tools.some((t) => WORKTREE_CAPABLE_TOOLS.has(t));
12024
13146
  features = { ...DEFAULT_FEATURES };
12025
13147
  const platformMcp = PLATFORM_MCP_SERVER[platform];
12026
- mcpServers = features.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
13148
+ mcpServers = features.mcp && opts.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
13149
+ if (opts.noCliTools) {
13150
+ wsCliTools = { enabled: false, selected: [] };
13151
+ } else {
13152
+ const explicit = resolveCliToolsFlag(opts.cliTools, repoInfo, platform);
13153
+ const selected = explicit ?? Array.from(/* @__PURE__ */ new Set([
13154
+ ...DEFAULT_CLI_TOOLS,
13155
+ ...applyPlatformTriggers(platform, evaluateTier2Triggers(repoInfo))
13156
+ ]));
13157
+ wsCliTools = { enabled: selected.length > 0, selected };
13158
+ }
12027
13159
  const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
12028
13160
  const presetId = validateFlag(opts.preset, ["minimal", "standard", "full"], "full", "preset");
12029
13161
  const projectType = validateFlag(opts.projectType, ["greenfield", "brownfield"], isGreenfield ? "greenfield" : "brownfield", "project-type");
@@ -12033,13 +13165,13 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12033
13165
  const projectLanguages = languagesForSelection(repoInfo);
12034
13166
  contentSelection = resolveSelection(preset, projectType, teamSize, index, void 0, projectLanguages);
12035
13167
  } else {
12036
- const wslTheme = isWSL() ? { icon: { checked: chalk5.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
13168
+ const wslTheme = isWSL() ? { icon: { checked: chalk7.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
12037
13169
  const wsFilterIndex = await buildContentIndex(CONTENT_ROOT2);
12038
13170
  const projectLanguages = languagesForSelection(repoInfo);
12039
13171
  const isAutoGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
12040
13172
  const wsGreenfieldExcl = countProjectTypeExclusions("greenfield", wsFilterIndex.items);
12041
13173
  const wsBrownfieldExcl = countProjectTypeExclusions("brownfield", wsFilterIndex.items);
12042
- const projectTypeAnswer = await inquirer4.prompt([
13174
+ const projectTypeAnswer = await inquirer6.prompt([
12043
13175
  {
12044
13176
  type: "select",
12045
13177
  name: "projectType",
@@ -12053,7 +13185,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12053
13185
  ]);
12054
13186
  const projectType = projectTypeAnswer.projectType;
12055
13187
  const wsSoloExcl = countTeamSizeExclusions("solo", wsFilterIndex.items);
12056
- const teamSizeAnswer = await inquirer4.prompt([
13188
+ const teamSizeAnswer = await inquirer6.prompt([
12057
13189
  {
12058
13190
  type: "select",
12059
13191
  name: "teamSize",
@@ -12067,7 +13199,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12067
13199
  ]);
12068
13200
  const teamSize = teamSizeAnswer.teamSize;
12069
13201
  const wsTotalItems = wsFilterIndex.items.length;
12070
- const presetAnswer = await inquirer4.prompt([
13202
+ const presetAnswer = await inquirer6.prompt([
12071
13203
  {
12072
13204
  type: "select",
12073
13205
  name: "preset",
@@ -12093,7 +13225,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12093
13225
  contentIndex.items,
12094
13226
  (item) => item.protected || item.tags.includes("core")
12095
13227
  );
12096
- const customAnswer = await inquirer4.prompt([
13228
+ const customAnswer = await inquirer6.prompt([
12097
13229
  {
12098
13230
  type: "checkbox",
12099
13231
  name: "items",
@@ -12105,7 +13237,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12105
13237
  customSelections = customAnswer.items;
12106
13238
  }
12107
13239
  const toolDefaults = repoInfo.existingTools.length > 0 ? repoInfo.existingTools : DEFAULT_TOOLS;
12108
- const toolAnswers = await inquirer4.prompt([
13240
+ const toolAnswers = await inquirer6.prompt([
12109
13241
  {
12110
13242
  type: "checkbox",
12111
13243
  name: "tools",
@@ -12120,7 +13252,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12120
13252
  if (opts.worktree !== void 0) {
12121
13253
  worktreeEnabled = opts.worktree;
12122
13254
  } else if (wsHasWorktreeTool) {
12123
- const wsWtAnswer = await inquirer4.prompt([{
13255
+ const wsWtAnswer = await inquirer6.prompt([{
12124
13256
  type: "confirm",
12125
13257
  name: "enabled",
12126
13258
  message: "Enable worktree file isolation (for parallel agent sessions)?",
@@ -12130,14 +13262,37 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12130
13262
  } else {
12131
13263
  worktreeEnabled = false;
12132
13264
  }
13265
+ const wsTier2Suggested = Array.from(/* @__PURE__ */ new Set([
13266
+ ...evaluateTier2Triggers(repoInfo),
13267
+ ...applyPlatformTriggers(platform, [])
13268
+ ]));
13269
+ const wsSelectedCliTools = await pickCliTools({
13270
+ tier2Suggested: wsTier2Suggested,
13271
+ wslTheme
13272
+ });
13273
+ if (wsSelectedCliTools.length > 0) {
13274
+ const wsDetectSpinner = createSpinner(`Detecting ${wsSelectedCliTools.length} CLI tool(s)...`);
13275
+ wsDetectSpinner.start();
13276
+ const wsMissing = await findMissingCliTools(wsSelectedCliTools);
13277
+ if (wsMissing.length === 0) {
13278
+ wsDetectSpinner.succeed(`All ${wsSelectedCliTools.length} CLI tool(s) detected on PATH`);
13279
+ } else {
13280
+ wsDetectSpinner.warn(`${wsSelectedCliTools.length - wsMissing.length}/${wsSelectedCliTools.length} CLI tool(s) detected; ${wsMissing.length} missing`);
13281
+ await offerInstaller(wsMissing, { interactive: true });
13282
+ }
13283
+ }
13284
+ wsCliTools = {
13285
+ enabled: wsSelectedCliTools.length > 0,
13286
+ selected: wsSelectedCliTools
13287
+ };
12133
13288
  const wsSecretNotes = tools.map((t) => TOOL_SECRET_NOTES[t]).filter(Boolean);
12134
13289
  if (wsSecretNotes.length > 0) {
12135
- info(chalk5.dim("MCP secret loading by tool:"));
13290
+ info(chalk7.dim("MCP secret loading by tool:"));
12136
13291
  for (const note of wsSecretNotes) {
12137
- info(chalk5.dim(` ${note}`));
13292
+ info(chalk7.dim(` ${note}`));
12138
13293
  }
12139
13294
  }
12140
- const featureAnswers = await inquirer4.prompt([
13295
+ const featureAnswers = await inquirer6.prompt([
12141
13296
  {
12142
13297
  type: "checkbox",
12143
13298
  name: "features",
@@ -12154,23 +13309,9 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12154
13309
  }
12155
13310
  mcpServers = [];
12156
13311
  if (features.mcp) {
12157
- const platformMcp = PLATFORM_MCP_SERVER[platform];
12158
- const defaultMcpForPlatform = Array.from(
12159
- /* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])
12160
- );
12161
- const mcpAnswers = await inquirer4.prompt([
12162
- {
12163
- type: "checkbox",
12164
- name: "mcp",
12165
- message: "Select MCP servers:",
12166
- choices: MCP_CHOICES,
12167
- default: defaultMcpForPlatform,
12168
- ...wslTheme && { theme: wslTheme }
12169
- }
12170
- ]);
12171
- mcpServers = mcpAnswers.mcp ?? [];
12172
- if (!mcpServers.includes(platformMcp)) {
12173
- mcpServers.unshift(platformMcp);
13312
+ const wsProceedMcp = await confirmMcpGate({ hasExisting: false, defaultYes: false });
13313
+ if (wsProceedMcp) {
13314
+ mcpServers = await pickMcpServers({ platform, wslTheme });
12174
13315
  }
12175
13316
  }
12176
13317
  contentSelection = resolveSelection(selectedPreset, projectType, teamSize, wsFilterIndex, customSelections, projectLanguages);
@@ -12195,6 +13336,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12195
13336
  repoInfo,
12196
13337
  contentSelection,
12197
13338
  worktreeEnabled,
13339
+ cliTools: wsCliTools,
12198
13340
  yes: headless
12199
13341
  });
12200
13342
  let repoEntries;
@@ -12209,14 +13351,14 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12209
13351
  platform: r.platform || void 0
12210
13352
  }));
12211
13353
  } else {
12212
- const wslTheme = isWSL() ? { icon: { checked: chalk5.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
12213
- const { syncRepos } = await inquirer4.prompt([
13354
+ const wslTheme = isWSL() ? { icon: { checked: chalk7.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
13355
+ const { syncRepos } = await inquirer6.prompt([
12214
13356
  {
12215
13357
  type: "checkbox",
12216
13358
  name: "syncRepos",
12217
13359
  message: "Select repos to sync workspace content to:",
12218
13360
  choices: enriched.map((r) => ({
12219
- name: `${r.name}${r.hasHatch3r ? chalk5.dim(" (has existing hatch3r)") : ""}`,
13361
+ name: `${r.name}${r.hasHatch3r ? chalk7.dim(" (has existing hatch3r)") : ""}`,
12220
13362
  value: r.path,
12221
13363
  checked: false
12222
13364
  })),
@@ -12230,9 +13372,9 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12230
13372
  `${conflictingRepos.length} selected repo(s) already have hatch3r installed; their managed files will be overwritten by workspace content.`
12231
13373
  );
12232
13374
  for (const r of conflictingRepos) {
12233
- console.log(chalk5.dim(` - ${r.name ?? r.path}`));
13375
+ console.log(chalk7.dim(` - ${r.name ?? r.path}`));
12234
13376
  }
12235
- const { confirmConflict } = await inquirer4.prompt([
13377
+ const { confirmConflict } = await inquirer6.prompt([
12236
13378
  {
12237
13379
  type: "confirm",
12238
13380
  name: "confirmConflict",
@@ -12244,7 +13386,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12244
13386
  for (const r of conflictingRepos) {
12245
13387
  syncSet.delete(r.path);
12246
13388
  }
12247
- info(chalk5.dim(" Skipped syncing conflicting repos. They remain registered in the workspace \u2014 run `hatch3r sync --repos <path>` after reviewing their managed files."));
13389
+ info(chalk7.dim(" Skipped syncing conflicting repos. They remain registered in the workspace \u2014 run `hatch3r sync --repos <path>` after reviewing their managed files."));
12248
13390
  }
12249
13391
  }
12250
13392
  repoEntries = enriched.map((r) => ({
@@ -12260,7 +13402,7 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12260
13402
  const dirName = basename2(rootDir) || "workspace";
12261
13403
  const wsManifest = createWorkspaceManifest(
12262
13404
  dirName,
12263
- { platform, tools, features, mcp: { servers: mcpServers }, content: contentSelection },
13405
+ { platform, tools, features, mcp: { servers: mcpServers }, cliTools: wsCliTools, content: contentSelection },
12264
13406
  repoEntries,
12265
13407
  "manual"
12266
13408
  );
@@ -12291,6 +13433,10 @@ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
12291
13433
  label("Manifest", `${AGENTS_DIR}/workspace.json`)
12292
13434
  ];
12293
13435
  printBox("Workspace ready", wsLines, "success");
13436
+ if (wsCliTools.selected.length > 0) {
13437
+ const finalMissing = await findMissingCliTools(wsCliTools.selected);
13438
+ printMissingCliToolsDisclaimer(finalMissing, wsCliTools.selected.length);
13439
+ }
12294
13440
  }
12295
13441
  function resolveToolsFromOpts(toolsFlag, repoInfo) {
12296
13442
  if (toolsFlag) {
@@ -12298,7 +13444,7 @@ function resolveToolsFromOpts(toolsFlag, repoInfo) {
12298
13444
  const invalid = rawTools.filter((t) => !VALID_TOOLS.has(t));
12299
13445
  if (invalid.length > 0) {
12300
13446
  error(`Invalid tool(s): ${invalid.join(", ")}`);
12301
- console.log(chalk5.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
13447
+ console.log(chalk7.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
12302
13448
  throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
12303
13449
  }
12304
13450
  return rawTools;
@@ -12306,6 +13452,22 @@ function resolveToolsFromOpts(toolsFlag, repoInfo) {
12306
13452
  if (repoInfo.existingTools.length > 0) return repoInfo.existingTools;
12307
13453
  return DEFAULT_TOOLS;
12308
13454
  }
13455
+ function resolveCliToolsFlag(flag, _repoInfo, _platform) {
13456
+ if (!flag) return void 0;
13457
+ const trimmed = flag.trim();
13458
+ if (trimmed === "") return void 0;
13459
+ if (trimmed === "tier1") return [...TIER1_CLI_TOOLS];
13460
+ if (trimmed === "all") return Object.keys(AVAILABLE_CLI_TOOLS);
13461
+ const rawIds = trimmed.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
13462
+ const valid = new Set(Object.keys(AVAILABLE_CLI_TOOLS));
13463
+ const invalid = rawIds.filter((id) => !valid.has(id));
13464
+ if (invalid.length > 0) {
13465
+ error(`Invalid CLI tool(s): ${invalid.join(", ")}`);
13466
+ console.log(chalk7.dim(` Valid ids: ${[...valid].join(", ")}`));
13467
+ throw new HatchError(`Invalid CLI tool(s): ${invalid.join(", ")}`, 1, "VALIDATION_ERROR");
13468
+ }
13469
+ return rawIds;
13470
+ }
12309
13471
 
12310
13472
  // src/cli/commands/clean.ts
12311
13473
  init_hatchJson();
@@ -12328,44 +13490,45 @@ function captureConfig(manifest) {
12328
13490
  },
12329
13491
  worktreeEnabled: manifest.worktree?.enabled ?? false,
12330
13492
  customization: manifest.customization,
13493
+ cliTools: manifest.cliTools,
12331
13494
  preservedFields: extractPreservedManifestFields(manifest)
12332
13495
  };
12333
13496
  }
12334
13497
  function printInventory(inventory) {
12335
13498
  const sections = [];
12336
13499
  if (inventory.adapterFiles.length > 0) {
12337
- sections.push(` ${chalk6.red("\xD7")} ${inventory.adapterFiles.length} adapter output file(s)`);
13500
+ sections.push(` ${chalk8.red("\xD7")} ${inventory.adapterFiles.length} adapter output file(s)`);
12338
13501
  }
12339
13502
  if (inventory.canonicalDir) {
12340
13503
  if ((inventory.userContentCount ?? 0) > 0) {
12341
- sections.push(` ${chalk6.red("\xD7")} .agents/ canonical directory ${chalk6.dim("(.agents/user/ preserved)")}`);
13504
+ sections.push(` ${chalk8.red("\xD7")} .agents/ canonical directory ${chalk8.dim("(.agents/user/ preserved)")}`);
12342
13505
  } else {
12343
- sections.push(` ${chalk6.red("\xD7")} .agents/ canonical directory`);
13506
+ sections.push(` ${chalk8.red("\xD7")} .agents/ canonical directory`);
12344
13507
  }
12345
13508
  }
12346
13509
  if (inventory.worktreeInclude) {
12347
- sections.push(` ${chalk6.red("\xD7")} .worktreeinclude`);
13510
+ sections.push(` ${chalk8.red("\xD7")} .worktreeinclude`);
12348
13511
  }
12349
13512
  if (inventory.archiveDir) {
12350
- sections.push(` ${chalk6.red("\xD7")} .hatch3r-archive/`);
13513
+ sections.push(` ${chalk8.red("\xD7")} .hatch3r-archive/`);
12351
13514
  }
12352
13515
  if (inventory.envMcp) {
12353
- sections.push(` ${chalk6.green("\u2713")} .env.mcp ${chalk6.dim("(kept \u2014 contains secrets)")}`);
13516
+ sections.push(` ${chalk8.green("\u2713")} .env.mcp ${chalk8.dim("(kept \u2014 contains secrets)")}`);
12354
13517
  }
12355
13518
  if (inventory.customizeDir) {
12356
- sections.push(` ${chalk6.green("\u2713")} .hatch3r/ ${chalk6.dim("(kept \u2014 customizations)")}`);
13519
+ sections.push(` ${chalk8.green("\u2713")} .hatch3r/ ${chalk8.dim("(kept \u2014 customizations)")}`);
12357
13520
  }
12358
13521
  if ((inventory.userContentCount ?? 0) > 0) {
12359
13522
  sections.push(
12360
- ` ${chalk6.green("\u2713")} .agents/user/ ${chalk6.dim(`(${inventory.userContentCount} user artifact(s) \u2014 kept, user-authored)`)}`
13523
+ ` ${chalk8.green("\u2713")} .agents/user/ ${chalk8.dim(`(${inventory.userContentCount} user artifact(s) \u2014 kept, user-authored)`)}`
12361
13524
  );
12362
13525
  }
12363
13526
  if (inventory.learnings.length > 0) {
12364
- sections.push(` ${chalk6.green("\u2713")} ${inventory.learnings.length} learning(s) ${chalk6.dim("(backed up for reinit)")}`);
13527
+ sections.push(` ${chalk8.green("\u2713")} ${inventory.learnings.length} learning(s) ${chalk8.dim("(backed up for reinit)")}`);
12365
13528
  }
12366
13529
  if (sections.length > 0) {
12367
13530
  console.log("");
12368
- console.log(chalk6.bold(" Cleanup inventory:"));
13531
+ console.log(chalk8.bold(" Cleanup inventory:"));
12369
13532
  for (const s of sections) {
12370
13533
  console.log(s);
12371
13534
  }
@@ -12389,22 +13552,22 @@ async function cleanCommand(opts = {}) {
12389
13552
  printInventory(inventory);
12390
13553
  if (inventory.isWorkspaceRoot) {
12391
13554
  warn("This is a workspace root. Member repos still reference this workspace.");
12392
- console.log(chalk6.dim(" Clean member repos individually or reinitialize them.\n"));
13555
+ console.log(chalk8.dim(" Clean member repos individually or reinitialize them.\n"));
12393
13556
  }
12394
13557
  if (inventory.isWorkspaceMember) {
12395
- warn(`This repo is managed by a workspace at ${chalk6.bold(inventory.workspaceRootPath ?? "..")}.`);
13558
+ warn(`This repo is managed by a workspace at ${chalk8.bold(inventory.workspaceRootPath ?? "..")}.`);
12396
13559
  console.log("");
12397
13560
  }
12398
13561
  if (opts.dryRun) {
12399
13562
  const result2 = await executeClean(rootDir, inventory, true);
12400
- console.log(chalk6.bold(" Would remove:"));
13563
+ console.log(chalk8.bold(" Would remove:"));
12401
13564
  for (const f of result2.removed) {
12402
- console.log(` ${chalk6.red("\xD7")} ${f}`);
13565
+ console.log(` ${chalk8.red("\xD7")} ${f}`);
12403
13566
  }
12404
13567
  if (result2.kept.length > 0) {
12405
- console.log(chalk6.bold("\n Would keep:"));
13568
+ console.log(chalk8.bold("\n Would keep:"));
12406
13569
  for (const f of result2.kept) {
12407
- console.log(` ${chalk6.green("\u2713")} ${f}`);
13570
+ console.log(` ${chalk8.green("\u2713")} ${f}`);
12408
13571
  }
12409
13572
  }
12410
13573
  console.log("");
@@ -12414,7 +13577,7 @@ async function cleanCommand(opts = {}) {
12414
13577
  return;
12415
13578
  }
12416
13579
  if (!opts.yes) {
12417
- const { proceed } = await inquirer5.prompt([
13580
+ const { proceed } = await inquirer7.prompt([
12418
13581
  {
12419
13582
  type: "confirm",
12420
13583
  name: "proceed",
@@ -12423,7 +13586,7 @@ async function cleanCommand(opts = {}) {
12423
13586
  }
12424
13587
  ]);
12425
13588
  if (!proceed) {
12426
- console.log(chalk6.dim("\n Clean cancelled.\n"));
13589
+ console.log(chalk8.dim("\n Clean cancelled.\n"));
12427
13590
  if (learningsBackup) {
12428
13591
  const { rm: rm5 } = await import("fs/promises");
12429
13592
  await rm5(learningsBackup, { recursive: true, force: true });
@@ -12445,7 +13608,7 @@ async function cleanCommand(opts = {}) {
12445
13608
  }
12446
13609
  if (!opts.yes && config) {
12447
13610
  console.log("");
12448
- const { reinit } = await inquirer5.prompt([
13611
+ const { reinit } = await inquirer7.prompt([
12449
13612
  {
12450
13613
  type: "confirm",
12451
13614
  name: "reinit",
@@ -12478,6 +13641,10 @@ async function cleanCommand(opts = {}) {
12478
13641
  // manifest preserves integration config and per-artifact overrides
12479
13642
  // across a clean -> reinit cycle.
12480
13643
  customization: config.customization,
13644
+ // 1.7.5 (CLI-tooling pivot): carry the previous CLI-tools
13645
+ // selection forward so clean -> reinit does not silently
13646
+ // re-pick from the default.
13647
+ cliTools: config.cliTools,
12481
13648
  // 1.7.1: carry full platform/user manifest state (board IDs,
12482
13649
  // costTracking, specs, extension config, worktree extras) forward
12483
13650
  // so a clean -> reinit cycle no longer wipes them.
@@ -12496,13 +13663,13 @@ async function cleanCommand(opts = {}) {
12496
13663
  ""
12497
13664
  ];
12498
13665
  if (learningsBackup) {
12499
- summaryLines2.push(`${chalk6.green("\u2713")} Learnings restored`);
13666
+ summaryLines2.push(`${chalk8.green("\u2713")} Learnings restored`);
12500
13667
  }
12501
13668
  if (inventory.customizeDir) {
12502
- summaryLines2.push(`${chalk6.green("\u2713")} Customizations preserved`);
13669
+ summaryLines2.push(`${chalk8.green("\u2713")} Customizations preserved`);
12503
13670
  }
12504
13671
  if (inventory.envMcp) {
12505
- summaryLines2.push(`${chalk6.green("\u2713")} .env.mcp preserved`);
13672
+ summaryLines2.push(`${chalk8.green("\u2713")} .env.mcp preserved`);
12506
13673
  }
12507
13674
  printBox("Reinit complete", summaryLines2, "success");
12508
13675
  } catch (err) {
@@ -12522,16 +13689,16 @@ async function cleanCommand(opts = {}) {
12522
13689
  await rm4(learningsBackup, { recursive: true, force: true });
12523
13690
  }
12524
13691
  const summaryLines = [
12525
- `${chalk6.red("\xD7")} ${result.removed.length} artifact(s) removed`
13692
+ `${chalk8.red("\xD7")} ${result.removed.length} artifact(s) removed`
12526
13693
  ];
12527
13694
  if (inventory.envMcp) {
12528
- summaryLines.push(`${chalk6.green("\u2713")} .env.mcp preserved`);
13695
+ summaryLines.push(`${chalk8.green("\u2713")} .env.mcp preserved`);
12529
13696
  }
12530
13697
  if (inventory.customizeDir) {
12531
- summaryLines.push(`${chalk6.green("\u2713")} .hatch3r/ customizations preserved`);
13698
+ summaryLines.push(`${chalk8.green("\u2713")} .hatch3r/ customizations preserved`);
12532
13699
  }
12533
13700
  summaryLines.push("");
12534
- summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold("hatch3r init")} when ready to set up again.`);
13701
+ summaryLines.push(`${chalk8.cyan("\u2192")} Run ${chalk8.bold("hatch3r init")} when ready to set up again.`);
12535
13702
  printBox("Clean complete", summaryLines, "success");
12536
13703
  }
12537
13704
 
@@ -12546,18 +13713,20 @@ init_paths();
12546
13713
  import { fileURLToPath as fileURLToPath6 } from "url";
12547
13714
  import { readFile as readFile21 } from "fs/promises";
12548
13715
  import { dirname as dirname16, join as join30 } from "path";
12549
- import chalk8 from "chalk";
12550
- import inquirer7 from "inquirer";
13716
+ import chalk10 from "chalk";
13717
+ import inquirer9 from "inquirer";
12551
13718
  init_content();
12552
13719
  init_agentsContent();
12553
13720
  init_safeWrite();
12554
13721
  init_worktree();
12555
13722
  var __dirname4 = dirname16(fileURLToPath6(import.meta.url));
12556
- function computeDiff(oldManifest, newTools, newFeatures, newMcp, newPlatform, newOwner, newRepo, newNamespace, newProject) {
13723
+ function computeDiff(oldManifest, newTools, newFeatures, newMcp, newPlatform, newOwner, newRepo, newNamespace, newProject, newCliToolIds) {
12557
13724
  const oldToolSet = new Set(oldManifest.tools);
12558
13725
  const newToolSet = new Set(newTools);
12559
13726
  const oldMcpSet = new Set(oldManifest.mcp.servers);
12560
13727
  const newMcpSet = new Set(newMcp);
13728
+ const oldCliSet = new Set(oldManifest.cliTools?.selected ?? []);
13729
+ const newCliSet = new Set(newCliToolIds);
12561
13730
  const enabledFeatures = [];
12562
13731
  const disabledFeatures = [];
12563
13732
  for (const key of Object.keys(DEFAULT_FEATURES)) {
@@ -12574,11 +13743,13 @@ function computeDiff(oldManifest, newTools, newFeatures, newMcp, newPlatform, ne
12574
13743
  platformChanged: newPlatform !== oldManifest.platform,
12575
13744
  repoChanged: newOwner !== oldManifest.owner || newRepo !== oldManifest.repo || newNamespace !== oldManifest.namespace || newProject !== oldManifest.project,
12576
13745
  addedContent: [],
12577
- removedContent: []
13746
+ removedContent: [],
13747
+ addedCliTools: newCliToolIds.filter((id) => !oldCliSet.has(id)),
13748
+ removedCliTools: [...oldCliSet].filter((id) => !newCliSet.has(id))
12578
13749
  };
12579
13750
  }
12580
13751
  function isDiffEmpty(diff) {
12581
- return diff.addedTools.length === 0 && diff.removedTools.length === 0 && diff.addedMcp.length === 0 && diff.removedMcp.length === 0 && diff.enabledFeatures.length === 0 && diff.disabledFeatures.length === 0 && !diff.platformChanged && !diff.repoChanged && diff.addedContent.length === 0 && diff.removedContent.length === 0;
13752
+ return diff.addedTools.length === 0 && diff.removedTools.length === 0 && diff.addedMcp.length === 0 && diff.removedMcp.length === 0 && diff.enabledFeatures.length === 0 && diff.disabledFeatures.length === 0 && !diff.platformChanged && !diff.repoChanged && diff.addedContent.length === 0 && diff.removedContent.length === 0 && diff.addedCliTools.length === 0 && diff.removedCliTools.length === 0;
12582
13753
  }
12583
13754
  function printCurrentConfig(manifest) {
12584
13755
  const platformLabel = manifest.platform ? `${PLATFORM_DISPLAY_NAMES[manifest.platform]} (${manifest.namespace || manifest.owner}/${manifest.project || manifest.repo})` : "Not set";
@@ -12589,9 +13760,13 @@ function printCurrentConfig(manifest) {
12589
13760
  label("Platform", platformLabel),
12590
13761
  label("Branch", branch),
12591
13762
  label("Tools", toolNames),
12592
- label("Features", enabledFeatures.join(", ")),
12593
- label("MCP", manifest.mcp.servers.length > 0 ? manifest.mcp.servers.join(", ") : "none")
13763
+ label("Features", enabledFeatures.join(", "))
12594
13764
  ];
13765
+ const cliSelected = manifest.cliTools?.selected ?? [];
13766
+ lines.push(label("CLI tools", cliSelected.length > 0 ? cliSelected.join(", ") : "none"));
13767
+ if (manifest.mcp.servers.length > 0) {
13768
+ lines.push(label("MCP", manifest.mcp.servers.join(", ")));
13769
+ }
12595
13770
  if (manifest.content) {
12596
13771
  const total = countSelectionItems(manifest.content);
12597
13772
  lines.push(label("Content", `${total} items (${selectionSummary(manifest.content)})`));
@@ -12604,7 +13779,7 @@ async function configCommand() {
12604
13779
  const manifest = await readManifest(rootDir);
12605
13780
  if (!manifest) {
12606
13781
  error("No .agents/hatch.json found.");
12607
- console.log(chalk8.dim(" Run `npx hatch3r init` to set up your project first.\n"));
13782
+ console.log(chalk10.dim(" Run `npx hatch3r init` to set up your project first.\n"));
12608
13783
  throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
12609
13784
  }
12610
13785
  const wsContext = await detectWorkspaceContext(rootDir);
@@ -12613,7 +13788,7 @@ async function configCommand() {
12613
13788
  `This repo is managed by workspace at ${wsContext.workspaceRoot}. Changes here may be overwritten on next workspace sync.`
12614
13789
  );
12615
13790
  console.log();
12616
- const { action } = await inquirer7.prompt([
13791
+ const { action } = await inquirer9.prompt([
12617
13792
  {
12618
13793
  type: "select",
12619
13794
  name: "action",
@@ -12644,8 +13819,8 @@ async function configCommand() {
12644
13819
  );
12645
13820
  }
12646
13821
  printCurrentConfig(manifest);
12647
- const wslTheme = isWSL() ? { icon: { checked: chalk8.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
12648
- const platformAnswer = await inquirer7.prompt([
13822
+ const wslTheme = isWSL() ? { icon: { checked: chalk10.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
13823
+ const platformAnswer = await inquirer9.prompt([
12649
13824
  {
12650
13825
  type: "select",
12651
13826
  name: "platform",
@@ -12664,7 +13839,7 @@ async function configCommand() {
12664
13839
  let namespace;
12665
13840
  let project;
12666
13841
  if (platform === "azure-devops") {
12667
- const adoAnswers = await inquirer7.prompt([
13842
+ const adoAnswers = await inquirer9.prompt([
12668
13843
  { type: "input", name: "org", message: "Azure DevOps organization:", default: manifest.owner || void 0 },
12669
13844
  { type: "input", name: "project", message: "Azure DevOps project:", default: manifest.project || void 0 },
12670
13845
  { type: "input", name: "repo", message: "Repository name:", default: manifest.repo || void 0 }
@@ -12674,7 +13849,7 @@ async function configCommand() {
12674
13849
  namespace = owner;
12675
13850
  project = sanitizeInput(adoAnswers.project);
12676
13851
  } else if (platform === "gitlab") {
12677
- const glAnswers = await inquirer7.prompt([
13852
+ const glAnswers = await inquirer9.prompt([
12678
13853
  { type: "input", name: "namespace", message: "GitLab namespace (group or username):", default: manifest.namespace || manifest.owner || void 0 },
12679
13854
  { type: "input", name: "project", message: "Project name:", default: manifest.project || manifest.repo || void 0 }
12680
13855
  ]);
@@ -12683,7 +13858,7 @@ async function configCommand() {
12683
13858
  namespace = owner;
12684
13859
  project = repo;
12685
13860
  } else {
12686
- const repoAnswers = await inquirer7.prompt([
13861
+ const repoAnswers = await inquirer9.prompt([
12687
13862
  { type: "input", name: "owner", message: "GitHub owner (org or username):", default: manifest.owner || void 0 },
12688
13863
  { type: "input", name: "repo", message: "Repository name:", default: manifest.repo || void 0 }
12689
13864
  ]);
@@ -12693,7 +13868,7 @@ async function configCommand() {
12693
13868
  project = repo;
12694
13869
  }
12695
13870
  const currentBranch = manifest.board?.defaultBranch ?? "main";
12696
- const branchAnswer = await inquirer7.prompt([
13871
+ const branchAnswer = await inquirer9.prompt([
12697
13872
  {
12698
13873
  type: "input",
12699
13874
  name: "defaultBranch",
@@ -12710,7 +13885,7 @@ async function configCommand() {
12710
13885
  }
12711
13886
  ]);
12712
13887
  const defaultBranch = branchAnswer.defaultBranch.trim() || currentBranch;
12713
- const toolAnswers = await inquirer7.prompt([
13888
+ const toolAnswers = await inquirer9.prompt([
12714
13889
  {
12715
13890
  type: "checkbox",
12716
13891
  name: "tools",
@@ -12725,8 +13900,28 @@ async function configCommand() {
12725
13900
  error("At least one tool must be selected.");
12726
13901
  throw new HatchError("At least one tool must be selected.", 1, "VALIDATION_ERROR");
12727
13902
  }
13903
+ const existingCliTools = manifest.cliTools?.selected ?? [];
13904
+ const selectedCliTools = await pickCliTools({
13905
+ existing: existingCliTools,
13906
+ wslTheme
13907
+ });
13908
+ if (selectedCliTools.length > 0) {
13909
+ const cliSpinner = createSpinner(`Detecting ${selectedCliTools.length} CLI tool(s)...`);
13910
+ cliSpinner.start();
13911
+ const missing = await findMissingCliTools(selectedCliTools);
13912
+ if (missing.length === 0) {
13913
+ cliSpinner.succeed(`All ${selectedCliTools.length} CLI tool(s) detected on PATH`);
13914
+ } else {
13915
+ cliSpinner.warn(`${selectedCliTools.length - missing.length}/${selectedCliTools.length} CLI tool(s) detected; ${missing.length} missing`);
13916
+ await offerInstaller(missing, { interactive: true });
13917
+ }
13918
+ }
13919
+ const cliToolsConfig = {
13920
+ enabled: selectedCliTools.length > 0,
13921
+ selected: selectedCliTools
13922
+ };
12728
13923
  const currentFeatureKeys = Object.keys(DEFAULT_FEATURES).filter((k) => manifest.features[k]);
12729
- const featureAnswers = await inquirer7.prompt([
13924
+ const featureAnswers = await inquirer9.prompt([
12730
13925
  {
12731
13926
  type: "checkbox",
12732
13927
  name: "features",
@@ -12741,27 +13936,21 @@ async function configCommand() {
12741
13936
  for (const k of Object.keys(features)) {
12742
13937
  features[k] = selectedFeatures.includes(k);
12743
13938
  }
12744
- let mcpServers = [];
13939
+ const hasExistingMcp = manifest.mcp.servers.length > 0;
13940
+ let mcpServers = hasExistingMcp ? [...manifest.mcp.servers] : [];
12745
13941
  if (features.mcp) {
12746
- const platformMcp = PLATFORM_MCP_SERVER[platform];
12747
- const mcpAnswers = await inquirer7.prompt([
12748
- {
12749
- type: "checkbox",
12750
- name: "mcp",
12751
- message: "Select MCP servers:",
12752
- choices: MCP_CHOICES,
12753
- default: manifest.mcp.servers,
12754
- ...wslTheme && { theme: wslTheme }
12755
- }
12756
- ]);
12757
- mcpServers = mcpAnswers.mcp ?? [];
12758
- if (!mcpServers.includes(platformMcp)) {
12759
- mcpServers.unshift(platformMcp);
13942
+ const proceedMcp = await confirmMcpGate({ hasExisting: hasExistingMcp });
13943
+ if (proceedMcp) {
13944
+ mcpServers = await pickMcpServers({
13945
+ platform,
13946
+ existing: manifest.mcp.servers,
13947
+ wslTheme
13948
+ });
12760
13949
  }
12761
13950
  }
12762
13951
  const hasWorktreeTool = tools.some((t) => WORKTREE_CAPABLE_TOOLS.has(t));
12763
13952
  if (hasWorktreeTool) {
12764
- const wtAnswer = await inquirer7.prompt([{
13953
+ const wtAnswer = await inquirer9.prompt([{
12765
13954
  type: "confirm",
12766
13955
  name: "enabled",
12767
13956
  message: "Enable worktree file isolation (for parallel agent sessions)?",
@@ -12776,7 +13965,7 @@ async function configCommand() {
12776
13965
  let contentMetadataChanged = false;
12777
13966
  if (manifest.content) {
12778
13967
  info(
12779
- chalk8.dim("Config adds/removes content items. To customize an item's behavior without ") + chalk8.dim("removing it, use .hatch3r/<type>/<id>.customize.yaml instead.")
13968
+ chalk10.dim("Config adds/removes content items. To customize an item's behavior without ") + chalk10.dim("removing it, use .hatch3r/<type>/<id>.customize.yaml instead.")
12780
13969
  );
12781
13970
  console.log();
12782
13971
  const contentRoot = findPackageRoot(__dirname4);
@@ -12785,7 +13974,7 @@ async function configCommand() {
12785
13974
  const previousContent = manifest.content;
12786
13975
  const { projectType, teamSize } = manifest.content;
12787
13976
  const totalItems = index.items.length;
12788
- const presetAnswer = await inquirer7.prompt([
13977
+ const presetAnswer = await inquirer9.prompt([
12789
13978
  {
12790
13979
  type: "select",
12791
13980
  name: "preset",
@@ -12808,7 +13997,7 @@ async function configCommand() {
12808
13997
  if (selectedPreset.id === "custom") {
12809
13998
  const currentIds = getAllContentIds(manifest.content);
12810
13999
  const groupedChoices = buildTagGroupedCustomContentChoices(index.items, (item) => currentIds.has(item.id));
12811
- const customAnswer = await inquirer7.prompt([
14000
+ const customAnswer = await inquirer9.prompt([
12812
14001
  {
12813
14002
  type: "checkbox",
12814
14003
  name: "items",
@@ -12855,7 +14044,7 @@ async function configCommand() {
12855
14044
  console.log();
12856
14045
  warn("Dependency warnings for removed content:");
12857
14046
  for (const w of dependencyWarnings) {
12858
- console.log(chalk8.dim(` ${w}`));
14047
+ console.log(chalk10.dim(` ${w}`));
12859
14048
  }
12860
14049
  console.log();
12861
14050
  }
@@ -12889,7 +14078,7 @@ async function configCommand() {
12889
14078
  });
12890
14079
  }
12891
14080
  }
12892
- const diff = computeDiff(manifest, tools, features, mcpServers, platform, owner, repo, namespace, project);
14081
+ const diff = computeDiff(manifest, tools, features, mcpServers, platform, owner, repo, namespace, project, selectedCliTools);
12893
14082
  diff.addedContent = contentChanges.added;
12894
14083
  diff.removedContent = contentChanges.removed;
12895
14084
  if (isDiffEmpty(diff) && defaultBranch === currentBranch && !contentMetadataChanged) {
@@ -12926,6 +14115,7 @@ async function configCommand() {
12926
14115
  manifest.tools = tools;
12927
14116
  manifest.features = features;
12928
14117
  manifest.mcp = { servers: mcpServers };
14118
+ manifest.cliTools = cliToolsConfig;
12929
14119
  if (manifest.board) {
12930
14120
  manifest.board.owner = owner;
12931
14121
  manifest.board.repo = repo;
@@ -12973,37 +14163,43 @@ async function configCommand() {
12973
14163
  console.log();
12974
14164
  const summaryLines = [];
12975
14165
  if (diff.addedTools.length > 0) {
12976
- summaryLines.push(`${chalk8.green("+")} Tools added: ${diff.addedTools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")}`);
14166
+ summaryLines.push(`${chalk10.green("+")} Tools added: ${diff.addedTools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")}`);
12977
14167
  }
12978
14168
  if (diff.removedTools.length > 0) {
12979
- summaryLines.push(`${chalk8.red("-")} Tools removed: ${diff.removedTools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")}`);
14169
+ summaryLines.push(`${chalk10.red("-")} Tools removed: ${diff.removedTools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")}`);
12980
14170
  }
12981
14171
  if (diff.addedMcp.length > 0) {
12982
- summaryLines.push(`${chalk8.green("+")} MCP added: ${diff.addedMcp.join(", ")}`);
14172
+ summaryLines.push(`${chalk10.green("+")} MCP added: ${diff.addedMcp.join(", ")}`);
12983
14173
  }
12984
14174
  if (diff.removedMcp.length > 0) {
12985
- summaryLines.push(`${chalk8.red("-")} MCP removed: ${diff.removedMcp.join(", ")}`);
14175
+ summaryLines.push(`${chalk10.red("-")} MCP removed: ${diff.removedMcp.join(", ")}`);
12986
14176
  }
12987
14177
  if (diff.enabledFeatures.length > 0) {
12988
- summaryLines.push(`${chalk8.green("+")} Features enabled: ${diff.enabledFeatures.join(", ")}`);
14178
+ summaryLines.push(`${chalk10.green("+")} Features enabled: ${diff.enabledFeatures.join(", ")}`);
12989
14179
  }
12990
14180
  if (diff.disabledFeatures.length > 0) {
12991
- summaryLines.push(`${chalk8.red("-")} Features disabled: ${diff.disabledFeatures.join(", ")}`);
14181
+ summaryLines.push(`${chalk10.red("-")} Features disabled: ${diff.disabledFeatures.join(", ")}`);
12992
14182
  }
12993
14183
  if (diff.platformChanged) {
12994
- summaryLines.push(`${chalk8.yellow("~")} Platform: ${PLATFORM_DISPLAY_NAMES[platform]}`);
14184
+ summaryLines.push(`${chalk10.yellow("~")} Platform: ${PLATFORM_DISPLAY_NAMES[platform]}`);
12995
14185
  }
12996
14186
  if (diff.repoChanged) {
12997
- summaryLines.push(`${chalk8.yellow("~")} Repo: ${namespace}/${project}`);
14187
+ summaryLines.push(`${chalk10.yellow("~")} Repo: ${namespace}/${project}`);
12998
14188
  }
12999
14189
  if (diff.addedContent.length > 0) {
13000
- summaryLines.push(`${chalk8.green("+")} Content added: ${diff.addedContent.length} item(s)`);
14190
+ summaryLines.push(`${chalk10.green("+")} Content added: ${diff.addedContent.length} item(s)`);
13001
14191
  }
13002
14192
  if (diff.removedContent.length > 0) {
13003
- summaryLines.push(`${chalk8.red("-")} Content removed: ${diff.removedContent.length} item(s)`);
14193
+ summaryLines.push(`${chalk10.red("-")} Content removed: ${diff.removedContent.length} item(s)`);
14194
+ }
14195
+ if (diff.addedCliTools.length > 0) {
14196
+ summaryLines.push(`${chalk10.green("+")} CLI tools added: ${diff.addedCliTools.join(", ")}`);
14197
+ }
14198
+ if (diff.removedCliTools.length > 0) {
14199
+ summaryLines.push(`${chalk10.red("-")} CLI tools removed: ${diff.removedCliTools.join(", ")}`);
13004
14200
  }
13005
14201
  if (defaultBranch !== currentBranch) {
13006
- summaryLines.push(`${chalk8.yellow("~")} Default branch: ${defaultBranch}`);
14202
+ summaryLines.push(`${chalk10.yellow("~")} Default branch: ${defaultBranch}`);
13007
14203
  }
13008
14204
  summaryLines.push("");
13009
14205
  summaryLines.push(label("Files", `${updateResult.copiedFiles} canonical files updated`));
@@ -13018,7 +14214,7 @@ async function configCommand() {
13018
14214
  console.log();
13019
14215
  info("Customizations migrated to .hatch3r/ (tool-agnostic):");
13020
14216
  for (const m of allMigrations) {
13021
- console.log(` ${chalk8.dim(m.from)} ${chalk8.cyan("\u2192")} ${m.to}`);
14217
+ console.log(` ${chalk10.dim(m.from)} ${chalk10.cyan("\u2192")} ${m.to}`);
13022
14218
  }
13023
14219
  console.log();
13024
14220
  }
@@ -13026,12 +14222,12 @@ async function configCommand() {
13026
14222
  console.log();
13027
14223
  info("Tool migration notes:");
13028
14224
  if (diff.removedTools.length > 0) {
13029
- info(chalk8.dim(` Removed tool output archived to .hatch3r-archive/ (recoverable).`));
13030
- info(chalk8.dim(` Customizations in .hatch3r/ are tool-agnostic and carry forward.`));
14225
+ info(chalk10.dim(` Removed tool output archived to .hatch3r-archive/ (recoverable).`));
14226
+ info(chalk10.dim(` Customizations in .hatch3r/ are tool-agnostic and carry forward.`));
13031
14227
  }
13032
14228
  if (diff.addedTools.length > 0) {
13033
- info(chalk8.dim(` New tool output generated. Restart your editor to pick up changes.`));
13034
- info(chalk8.dim(` MCP secrets (.env.mcp) are shared across tools \u2014 no re-entry needed.`));
14229
+ info(chalk10.dim(` New tool output generated. Restart your editor to pick up changes.`));
14230
+ info(chalk10.dim(` MCP secrets (.env.mcp) are shared across tools \u2014 no re-entry needed.`));
13035
14231
  }
13036
14232
  console.log();
13037
14233
  }
@@ -13041,6 +14237,7 @@ async function configCommand() {
13041
14237
  wsManifestFinal.defaults.tools = tools;
13042
14238
  wsManifestFinal.defaults.features = features;
13043
14239
  wsManifestFinal.defaults.mcp = { servers: mcpServers };
14240
+ wsManifestFinal.defaults.cliTools = cliToolsConfig;
13044
14241
  if (manifest.content) {
13045
14242
  wsManifestFinal.defaults.content = manifest.content;
13046
14243
  }
@@ -13049,11 +14246,11 @@ async function configCommand() {
13049
14246
  }
13050
14247
  }
13051
14248
  console.log();
13052
- info(chalk8.bold("Workspace configuration"));
14249
+ info(chalk10.bold("Workspace configuration"));
13053
14250
  const currentRepos = wsManifestFinal.repos.map((r) => r.path);
13054
- console.log(chalk8.dim(` Repos: ${currentRepos.join(", ") || "(none)"}`));
13055
- console.log(chalk8.dim(` Sync strategy: ${wsManifestFinal.syncStrategy}`));
13056
- const { manageWorkspace } = await inquirer7.prompt([
14251
+ console.log(chalk10.dim(` Repos: ${currentRepos.join(", ") || "(none)"}`));
14252
+ console.log(chalk10.dim(` Sync strategy: ${wsManifestFinal.syncStrategy}`));
14253
+ const { manageWorkspace } = await inquirer9.prompt([
13057
14254
  {
13058
14255
  type: "confirm",
13059
14256
  name: "manageWorkspace",
@@ -13066,7 +14263,7 @@ async function configCommand() {
13066
14263
  const existingPaths = new Set(wsManifestFinal.repos.map((r) => r.path));
13067
14264
  const newRepos = detectedRepos.filter((r) => !existingPaths.has(r.path));
13068
14265
  if (newRepos.length > 0) {
13069
- const { addRepos } = await inquirer7.prompt([
14266
+ const { addRepos } = await inquirer9.prompt([
13070
14267
  {
13071
14268
  type: "checkbox",
13072
14269
  name: "addRepos",
@@ -13084,7 +14281,7 @@ async function configCommand() {
13084
14281
  }
13085
14282
  }
13086
14283
  if (wsManifestFinal.repos.length > 0) {
13087
- const { syncRepos } = await inquirer7.prompt([
14284
+ const { syncRepos } = await inquirer9.prompt([
13088
14285
  {
13089
14286
  type: "checkbox",
13090
14287
  name: "syncRepos",
@@ -13103,7 +14300,7 @@ async function configCommand() {
13103
14300
  }
13104
14301
  }
13105
14302
  if (wsManifestFinal.repos.length > 0) {
13106
- const { editIdentity } = await inquirer7.prompt([
14303
+ const { editIdentity } = await inquirer9.prompt([
13107
14304
  {
13108
14305
  type: "select",
13109
14306
  name: "editIdentity",
@@ -13127,9 +14324,9 @@ async function configCommand() {
13127
14324
  info("Re-detected git identities for all repos.");
13128
14325
  } else if (editIdentity === "edit") {
13129
14326
  for (const repo2 of wsManifestFinal.repos) {
13130
- console.log(chalk8.bold(`
14327
+ console.log(chalk10.bold(`
13131
14328
  ${repo2.name ?? repo2.path}:`));
13132
- const identity = await inquirer7.prompt([
14329
+ const identity = await inquirer9.prompt([
13133
14330
  { type: "input", name: "owner", message: " Owner:", default: repo2.owner || void 0 },
13134
14331
  { type: "input", name: "repo", message: " Repo:", default: repo2.repo || void 0 },
13135
14332
  {
@@ -13152,7 +14349,7 @@ async function configCommand() {
13152
14349
  }
13153
14350
  }
13154
14351
  }
13155
- const { strategy } = await inquirer7.prompt([
14352
+ const { strategy } = await inquirer9.prompt([
13156
14353
  {
13157
14354
  type: "select",
13158
14355
  name: "strategy",
@@ -13169,7 +14366,7 @@ async function configCommand() {
13169
14366
  let syncAttempted = false;
13170
14367
  let syncFailed = false;
13171
14368
  if (syncCount > 0) {
13172
- const { syncNow } = await inquirer7.prompt([
14369
+ const { syncNow } = await inquirer9.prompt([
13173
14370
  {
13174
14371
  type: "confirm",
13175
14372
  name: "syncNow",
@@ -13211,6 +14408,10 @@ async function configCommand() {
13211
14408
  await writeWorkspaceManifest(rootDir, wsManifestFinal);
13212
14409
  }
13213
14410
  }
14411
+ if (selectedCliTools.length > 0) {
14412
+ const finalMissing = await findMissingCliTools(selectedCliTools);
14413
+ printMissingCliToolsDisclaimer(finalMissing, selectedCliTools.length);
14414
+ }
13214
14415
  }
13215
14416
 
13216
14417
  // src/cli/commands/sync.ts
@@ -13219,7 +14420,7 @@ init_adapters();
13219
14420
  import { appendFile as appendFile3, readFile as readFile23, stat as stat8, readdir as readdir13 } from "fs/promises";
13220
14421
  import { join as join32 } from "path";
13221
14422
  import { execFileSync as execFileSync8 } from "child_process";
13222
- import chalk9 from "chalk";
14423
+ import chalk11 from "chalk";
13223
14424
 
13224
14425
  // src/adapters/contextBudget.ts
13225
14426
  var CONTEXT_BUDGET_TOKENS = {
@@ -13453,14 +14654,14 @@ async function syncCommand(opts = {}) {
13453
14654
  const wsContext = await detectWorkspaceContext(rootDir);
13454
14655
  if (wsContext.type === "workspace-member") {
13455
14656
  warn(
13456
- `This repository appears to be managed by a workspace at ${wsContext.workspaceRoot ?? ".."}. Run ${chalk9.cyan("hatch3r sync")} from the workspace root to sync all repos.`
14657
+ `This repository appears to be managed by a workspace at ${wsContext.workspaceRoot ?? ".."}. Run ${chalk11.cyan("hatch3r sync")} from the workspace root to sync all repos.`
13457
14658
  );
13458
14659
  }
13459
14660
  const agentsDir = join32(rootDir, AGENTS_DIR);
13460
14661
  const manifest = await readManifest(rootDir);
13461
14662
  if (!manifest) {
13462
14663
  error("No .agents/hatch.json found.");
13463
- console.log(chalk9.dim(" Run `npx hatch3r init` to set up your project first.\n"));
14664
+ console.log(chalk11.dim(" Run `npx hatch3r init` to set up your project first.\n"));
13464
14665
  throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
13465
14666
  }
13466
14667
  const m = manifest;
@@ -13785,23 +14986,23 @@ async function syncCommand(opts = {}) {
13785
14986
  }
13786
14987
  console.log();
13787
14988
  const icons = {
13788
- created: chalk9.green("+"),
13789
- updated: chalk9.yellow("~"),
14989
+ created: chalk11.green("+"),
14990
+ updated: chalk11.yellow("~"),
13790
14991
  // G5: "unchanged" is a no-op action returned by safeWriteFile when the
13791
14992
  // computed bytes match the file on disk. We render it the same as
13792
14993
  // "skipped" (dim "=") so the human summary remains readable while still
13793
14994
  // signalling that nothing changed.
13794
- unchanged: chalk9.dim("="),
13795
- skipped: chalk9.dim("="),
13796
- "dry-run": chalk9.cyan("?")
14995
+ unchanged: chalk11.dim("="),
14996
+ skipped: chalk11.dim("="),
14997
+ "dry-run": chalk11.cyan("?")
13797
14998
  };
13798
14999
  const compactedResults = compactPhaseOutput(results);
13799
15000
  const summaryLines = compactedResults.map((r) => {
13800
15001
  if (typeof r === "string") {
13801
- return chalk9.dim(r);
15002
+ return chalk11.dim(r);
13802
15003
  }
13803
- const icon = icons[r.action] ?? chalk9.dim(" ");
13804
- return `${icon} ${r.path} ${chalk9.dim(`(${r.action})`)}`;
15004
+ const icon = icons[r.action] ?? chalk11.dim(" ");
15005
+ return `${icon} ${r.path} ${chalk11.dim(`(${r.action})`)}`;
13805
15006
  });
13806
15007
  if (isPipelineTimedOut(pipelineState)) {
13807
15008
  const { report } = terminatePipeline(pipelineState);
@@ -13813,11 +15014,11 @@ async function syncCommand(opts = {}) {
13813
15014
  const before = diffBefore.get(filePath) ?? null;
13814
15015
  const after = diffAfter.get(filePath) ?? null;
13815
15016
  if (before === null && after !== null) {
13816
- diffLines.push(`${chalk9.green("+ added")} ${filePath}`);
15017
+ diffLines.push(`${chalk11.green("+ added")} ${filePath}`);
13817
15018
  } else if (before !== null && after !== null && before !== after) {
13818
- diffLines.push(`${chalk9.yellow("~ modified")} ${filePath}`);
15019
+ diffLines.push(`${chalk11.yellow("~ modified")} ${filePath}`);
13819
15020
  } else if (before !== null && after !== null && before === after) {
13820
- diffLines.push(`${chalk9.dim("= unchanged")} ${filePath}`);
15021
+ diffLines.push(`${chalk11.dim("= unchanged")} ${filePath}`);
13821
15022
  }
13822
15023
  }
13823
15024
  if (diffLines.length > 0) {
@@ -13848,7 +15049,7 @@ async function syncCommand(opts = {}) {
13848
15049
  const syncableCount = wsManifest.repos.filter((r) => r.sync).length;
13849
15050
  if (!syncReposRequested && !syncOnSync) {
13850
15051
  if (syncableCount > 0) {
13851
- info(`Workspace: ${syncableCount} repo(s) available for sync. Run ${chalk9.bold("hatch3r sync --repos")} to propagate.`);
15052
+ info(`Workspace: ${syncableCount} repo(s) available for sync. Run ${chalk11.bold("hatch3r sync --repos")} to propagate.`);
13852
15053
  }
13853
15054
  return;
13854
15055
  }
@@ -13887,12 +15088,12 @@ init_version();
13887
15088
  init_customization();
13888
15089
  init_content();
13889
15090
  init_paths();
13890
- import { readdir as readdir16, readFile as readFile26, access as access11, stat as stat9 } from "fs/promises";
15091
+ import { readdir as readdir18, readFile as readFile28, access as access11, stat as stat11 } from "fs/promises";
13891
15092
  import { existsSync as existsSync4 } from "fs";
13892
- import { dirname as dirname18, join as join35, posix as posix4 } from "path";
15093
+ import { dirname as dirname18, join as join37, posix as posix4 } from "path";
13893
15094
  import { fileURLToPath as fileURLToPath8 } from "url";
13894
- import chalk10 from "chalk";
13895
- import { parse as parseYaml4 } from "yaml";
15095
+ import chalk12 from "chalk";
15096
+ import { parse as parseYaml6 } from "yaml";
13896
15097
 
13897
15098
  // src/content/learningsValidation.ts
13898
15099
  init_customization();
@@ -14035,6 +15236,306 @@ async function validateLearningsDirectory(learningsDir) {
14035
15236
  };
14036
15237
  }
14037
15238
 
15239
+ // src/content/handoffs/index.ts
15240
+ init_safeWrite();
15241
+ import { mkdir as mkdir10, readdir as readdir16, readFile as readFile26, stat as stat10, unlink as unlink4 } from "fs/promises";
15242
+ import { join as join35 } from "path";
15243
+ import { parse as parseYaml5, stringify as stringifyYaml } from "yaml";
15244
+
15245
+ // src/content/handoffs/validation.ts
15246
+ import { createHash as createHash5, randomBytes as randomBytes3 } from "crypto";
15247
+ import { readdir as readdir15, readFile as readFile25, stat as stat9 } from "fs/promises";
15248
+ import { join as join34 } from "path";
15249
+ import { parse as parseYaml4 } from "yaml";
15250
+
15251
+ // src/content/handoffs/schema.ts
15252
+ var HANDOFF_STATUSES = [
15253
+ "open",
15254
+ "in-progress",
15255
+ "blocked",
15256
+ "handed-off",
15257
+ "resumed",
15258
+ "completed",
15259
+ "archived"
15260
+ ];
15261
+ function isHandoffStatus(value) {
15262
+ return typeof value === "string" && HANDOFF_STATUSES.includes(value);
15263
+ }
15264
+
15265
+ // src/content/handoffs/validation.ts
15266
+ var MAX_HANDOFF_BODY_BYTES = 51200;
15267
+ var MAX_HANDOFF_FILE_BYTES = 61440;
15268
+ var MAX_ACTIVE_HANDOFFS_PER_REPO = 25;
15269
+ var MAX_SUMMARY_LENGTH = 200;
15270
+ var REQUIRED_BODY_SECTIONS = [
15271
+ "Problem",
15272
+ "Decisions",
15273
+ "Work Done",
15274
+ "Work Remaining",
15275
+ "Blockers",
15276
+ "Next Steps",
15277
+ "Build & Test Status",
15278
+ "File Manifest"
15279
+ ];
15280
+ var HANDOFF_ID_PATTERN = /^[12][0-9]{3}-[01][0-9]-[0-3][0-9]_T[0-2][0-9][0-5][0-9]_[0-9a-f]{5}_[a-z0-9][a-z0-9-]{0,59}$/;
15281
+ var BINARY_CONTENT_PATTERN2 = /\0/;
15282
+ var SECTION_HEADING_PATTERN = /^##\s+(.+?)\s*$/gm;
15283
+ var INTEGRITY_PATTERN = /^sha256:[0-9a-f]{64}$/;
15284
+ function computeHandoffIntegrity(body) {
15285
+ const trimmed = body.trim();
15286
+ const hash = createHash5("sha256").update(trimmed, "utf-8").digest("hex");
15287
+ return `sha256:${hash}`;
15288
+ }
15289
+ function isHandoffExpired(handoff, now = /* @__PURE__ */ new Date()) {
15290
+ const expires = handoff.frontmatter.expires_after;
15291
+ if (!expires) return false;
15292
+ const expiresMs = Date.parse(expires);
15293
+ if (Number.isNaN(expiresMs)) return false;
15294
+ return expiresMs <= now.getTime();
15295
+ }
15296
+ function extractSectionHeadings(body) {
15297
+ const headings = [];
15298
+ SECTION_HEADING_PATTERN.lastIndex = 0;
15299
+ let match;
15300
+ while ((match = SECTION_HEADING_PATTERN.exec(body)) !== null) {
15301
+ headings.push(match[1].trim());
15302
+ }
15303
+ return headings;
15304
+ }
15305
+ function scanForInjectionPatterns(body) {
15306
+ const hits = [];
15307
+ for (const { patternId, pattern } of LEARNINGS_INJECTION_PATTERNS) {
15308
+ if (pattern.test(body)) hits.push(patternId);
15309
+ }
15310
+ return hits;
15311
+ }
15312
+ function validateHandoffContent(handoff, options = {}) {
15313
+ const errors = [];
15314
+ const warnings = [];
15315
+ const driftWarnings = [];
15316
+ const fm = handoff.frontmatter;
15317
+ if (typeof handoff.body !== "string" || handoff.body.trim().length === 0) {
15318
+ errors.push("Handoff body is empty.");
15319
+ } else if (BINARY_CONTENT_PATTERN2.test(handoff.body)) {
15320
+ errors.push(
15321
+ "Handoff body contains binary content (null bytes detected). Only UTF-8 text is allowed."
15322
+ );
15323
+ } else {
15324
+ const bodyBytes = Buffer.byteLength(handoff.body, "utf-8");
15325
+ if (bodyBytes > MAX_HANDOFF_BODY_BYTES) {
15326
+ errors.push(
15327
+ `Handoff body exceeds ${MAX_HANDOFF_BODY_BYTES} byte limit (${bodyBytes} bytes). Split or compact the handoff.`
15328
+ );
15329
+ }
15330
+ }
15331
+ if (typeof fm.id !== "string" || !HANDOFF_ID_PATTERN.test(fm.id)) {
15332
+ errors.push(
15333
+ `Handoff id is missing or malformed (expected pattern <YYYY-MM-DD>_T<HHmm>_<5hex>_<slug>, got "${String(fm.id)}").`
15334
+ );
15335
+ }
15336
+ if (fm.type !== "handoff") {
15337
+ errors.push(`Handoff frontmatter.type must be "handoff" (got "${String(fm.type)}").`);
15338
+ }
15339
+ if (typeof fm.created !== "string" || Number.isNaN(Date.parse(fm.created))) {
15340
+ errors.push(`Handoff frontmatter.created must be ISO-8601 (got "${String(fm.created)}").`);
15341
+ }
15342
+ if (typeof fm.updated !== "string" || Number.isNaN(Date.parse(fm.updated))) {
15343
+ errors.push(`Handoff frontmatter.updated must be ISO-8601 (got "${String(fm.updated)}").`);
15344
+ }
15345
+ if (!isHandoffStatus(fm.status)) {
15346
+ errors.push(`Handoff status is invalid: "${String(fm.status)}".`);
15347
+ }
15348
+ if (typeof fm.source_agent !== "string" || fm.source_agent.length === 0) {
15349
+ errors.push("Handoff source_agent is missing.");
15350
+ }
15351
+ if (typeof fm.target_agent !== "string" || fm.target_agent.length === 0) {
15352
+ errors.push("Handoff target_agent is missing.");
15353
+ } else if (fm.target_agent === "any") {
15354
+ warnings.push(
15355
+ 'target_agent is "any" \u2014 explicit target_agent recommended to avoid handoff loops.'
15356
+ );
15357
+ }
15358
+ if (typeof fm.git_ref !== "string" || fm.git_ref.length === 0) {
15359
+ errors.push("Handoff git_ref is missing.");
15360
+ }
15361
+ if (typeof fm.branch !== "string" || fm.branch.length === 0) {
15362
+ errors.push("Handoff branch is missing.");
15363
+ }
15364
+ if (typeof fm.confidence !== "number" || Number.isNaN(fm.confidence) || fm.confidence < 0 || fm.confidence > 1) {
15365
+ errors.push(`Handoff confidence must be 0..1 inclusive (got ${String(fm.confidence)}).`);
15366
+ }
15367
+ if (typeof fm.completeness !== "number" || Number.isNaN(fm.completeness) || fm.completeness < 0 || fm.completeness > 1) {
15368
+ errors.push(`Handoff completeness must be 0..1 inclusive (got ${String(fm.completeness)}).`);
15369
+ }
15370
+ if (typeof fm.integrity !== "string" || !INTEGRITY_PATTERN.test(fm.integrity)) {
15371
+ errors.push(
15372
+ `Handoff integrity must be "sha256:<64-hex>" (got "${String(fm.integrity)}").`
15373
+ );
15374
+ } else if (typeof handoff.body === "string" && handoff.body.trim().length > 0) {
15375
+ const computed = computeHandoffIntegrity(handoff.body);
15376
+ if (computed !== fm.integrity) {
15377
+ errors.push(
15378
+ `Handoff integrity hash does not match body content (declared ${fm.integrity}, computed ${computed}).`
15379
+ );
15380
+ }
15381
+ }
15382
+ if (typeof fm.summary === "string" && fm.summary.length > MAX_SUMMARY_LENGTH) {
15383
+ warnings.push(
15384
+ `Handoff summary exceeds ${MAX_SUMMARY_LENGTH} chars (${fm.summary.length} chars). Compact the summary; full context belongs in the body.`
15385
+ );
15386
+ }
15387
+ if (typeof handoff.body === "string" && handoff.body.length > 0) {
15388
+ const present = new Set(extractSectionHeadings(handoff.body));
15389
+ for (const required of REQUIRED_BODY_SECTIONS) {
15390
+ if (!present.has(required)) {
15391
+ errors.push(`Handoff body is missing required section "## ${required}".`);
15392
+ }
15393
+ }
15394
+ if (!options.skipInjectionScan) {
15395
+ const hits = scanForInjectionPatterns(handoff.body);
15396
+ for (const patternId of hits) {
15397
+ errors.push(
15398
+ `Handoff body matches injection pattern ${patternId}. Review and sanitize before consuming.`
15399
+ );
15400
+ }
15401
+ }
15402
+ }
15403
+ if (isHandoffExpired(handoff, options.now)) {
15404
+ driftWarnings.push(
15405
+ `Handoff expired at ${String(fm.expires_after)}. Archive or refresh before resuming.`
15406
+ );
15407
+ }
15408
+ if (typeof options.currentGitRef === "string" && options.currentGitRef.length > 0) {
15409
+ if (fm.git_ref !== options.currentGitRef) {
15410
+ driftWarnings.push(
15411
+ `Handoff git_ref "${fm.git_ref}" differs from current "${options.currentGitRef}". Code has moved since the handoff was authored.`
15412
+ );
15413
+ }
15414
+ }
15415
+ const result = {
15416
+ valid: errors.length === 0,
15417
+ errors,
15418
+ warnings
15419
+ };
15420
+ if (driftWarnings.length > 0) result.driftWarnings = driftWarnings;
15421
+ return result;
15422
+ }
15423
+ async function loadHandoffFile(filePath) {
15424
+ let raw;
15425
+ let readError = null;
15426
+ try {
15427
+ const fileStat = await stat9(filePath);
15428
+ if (fileStat.size > MAX_HANDOFF_FILE_BYTES) {
15429
+ return {
15430
+ error: `Handoff file "${filePath}" exceeds ${MAX_HANDOFF_FILE_BYTES} byte limit (${fileStat.size} bytes).`
15431
+ };
15432
+ }
15433
+ raw = await readFile25(filePath, "utf-8");
15434
+ } catch (err) {
15435
+ readError = `Failed to read handoff "${filePath}": ${err.message}`;
15436
+ raw = "";
15437
+ }
15438
+ if (readError !== null) {
15439
+ return { error: readError };
15440
+ }
15441
+ if (!raw.startsWith("---")) {
15442
+ return { error: `Handoff "${filePath}" is missing YAML frontmatter delimiter.` };
15443
+ }
15444
+ const afterFirst = raw.slice(3);
15445
+ const newlineAfterFirst = afterFirst.indexOf("\n");
15446
+ if (newlineAfterFirst < 0) {
15447
+ return { error: `Handoff "${filePath}" has malformed frontmatter delimiter.` };
15448
+ }
15449
+ const fmStart = newlineAfterFirst + 1;
15450
+ const closeMatch = afterFirst.slice(fmStart).match(/\n---\s*(?:\r?\n|$)/);
15451
+ if (!closeMatch || typeof closeMatch.index !== "number") {
15452
+ return { error: `Handoff "${filePath}" frontmatter is not closed by "---".` };
15453
+ }
15454
+ const yamlBlock = afterFirst.slice(fmStart, fmStart + closeMatch.index);
15455
+ const bodyStart = fmStart + closeMatch.index + closeMatch[0].length;
15456
+ const body = afterFirst.slice(bodyStart);
15457
+ let parsed;
15458
+ let parseError = null;
15459
+ try {
15460
+ parsed = parseYaml4(yamlBlock);
15461
+ } catch (err) {
15462
+ parseError = `Handoff "${filePath}" has invalid YAML frontmatter: ${err.message}`;
15463
+ parsed = null;
15464
+ }
15465
+ if (parseError !== null) {
15466
+ return { error: parseError };
15467
+ }
15468
+ if (parsed === null || typeof parsed !== "object") {
15469
+ return { error: `Handoff "${filePath}" frontmatter is not a YAML mapping.` };
15470
+ }
15471
+ return {
15472
+ handoff: {
15473
+ frontmatter: parsed,
15474
+ body,
15475
+ filePath
15476
+ }
15477
+ };
15478
+ }
15479
+ async function validateHandoffsDirectory(activeDir, options = {}) {
15480
+ const errors = [];
15481
+ const warnings = [];
15482
+ async function listMdFiles(dir) {
15483
+ try {
15484
+ const entries = await readdir15(dir);
15485
+ return entries.filter((f) => f.endsWith(".md"));
15486
+ } catch (err) {
15487
+ const code = err.code;
15488
+ if (code === "ENOENT") return null;
15489
+ warnings.push(`Failed to list handoff dir "${dir}": ${err.message}`);
15490
+ return null;
15491
+ }
15492
+ }
15493
+ const activeFiles = await listMdFiles(activeDir) ?? [];
15494
+ if (activeFiles.length > MAX_ACTIVE_HANDOFFS_PER_REPO) {
15495
+ warnings.push(
15496
+ `Active handoff count (${activeFiles.length}) exceeds soft cap (${MAX_ACTIVE_HANDOFFS_PER_REPO}). Archive completed handoffs to reduce drift surface.`
15497
+ );
15498
+ }
15499
+ for (const file of activeFiles) {
15500
+ const { handoff, error: error2 } = await loadHandoffFile(join34(activeDir, file));
15501
+ if (error2 || !handoff) {
15502
+ errors.push(error2 ?? `Failed to load handoff "${file}".`);
15503
+ continue;
15504
+ }
15505
+ const r = validateHandoffContent(handoff, { currentGitRef: options.currentGitRef });
15506
+ for (const e of r.errors) errors.push(`[${file}] ${e}`);
15507
+ for (const w of r.warnings) warnings.push(`[${file}] ${w}`);
15508
+ if (r.driftWarnings) {
15509
+ for (const dw of r.driftWarnings) warnings.push(`[${file}] ${dw}`);
15510
+ }
15511
+ }
15512
+ let archivedCount = 0;
15513
+ if (options.archivedDir) {
15514
+ const archivedFiles = await listMdFiles(options.archivedDir) ?? [];
15515
+ archivedCount = archivedFiles.length;
15516
+ for (const file of archivedFiles) {
15517
+ const { handoff, error: error2 } = await loadHandoffFile(join34(options.archivedDir, file));
15518
+ if (error2 || !handoff) {
15519
+ errors.push(error2 ?? `Failed to load archived handoff "${file}".`);
15520
+ continue;
15521
+ }
15522
+ const r = validateHandoffContent(handoff);
15523
+ for (const e of r.errors) errors.push(`[archived/${file}] ${e}`);
15524
+ for (const w of r.warnings) warnings.push(`[archived/${file}] ${w}`);
15525
+ }
15526
+ }
15527
+ return {
15528
+ valid: errors.length === 0,
15529
+ errors,
15530
+ warnings,
15531
+ activeCount: activeFiles.length,
15532
+ archivedCount
15533
+ };
15534
+ }
15535
+
15536
+ // src/content/handoffs/index.ts
15537
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
15538
+
14038
15539
  // src/cli/commands/validate.ts
14039
15540
  init_customize();
14040
15541
  init_mcpEnv();
@@ -14171,8 +15672,8 @@ function detectSecrets(envVars) {
14171
15672
 
14172
15673
  // src/pipeline/complianceVerification.ts
14173
15674
  init_agentToolAllowlist();
14174
- import { readFile as readFile25, readdir as readdir15 } from "fs/promises";
14175
- import { dirname as dirname17, join as join34 } from "path";
15675
+ import { readFile as readFile27, readdir as readdir17 } from "fs/promises";
15676
+ import { dirname as dirname17, join as join36 } from "path";
14176
15677
  import { fileURLToPath as fileURLToPath7 } from "url";
14177
15678
 
14178
15679
  // src/pipeline/reviewLoop.ts
@@ -14211,13 +15712,13 @@ var RESILIENCE_MODULES = [
14211
15712
  var __dirname5 = dirname17(fileURLToPath7(import.meta.url));
14212
15713
  async function resolveCommandsDir() {
14213
15714
  const candidates = [
14214
- join34(__dirname5, "..", "cli", "commands"),
14215
- join34(__dirname5, "..", "..", "src", "cli", "commands"),
14216
- join34(__dirname5, "..", "cli")
15715
+ join36(__dirname5, "..", "cli", "commands"),
15716
+ join36(__dirname5, "..", "..", "src", "cli", "commands"),
15717
+ join36(__dirname5, "..", "cli")
14217
15718
  ];
14218
15719
  for (const candidate of candidates) {
14219
15720
  try {
14220
- const entries = await readdir15(candidate);
15721
+ const entries = await readdir17(candidate);
14221
15722
  if (entries.length > 0) return candidate;
14222
15723
  } catch {
14223
15724
  }
@@ -14230,11 +15731,11 @@ async function detectResilienceInvocations() {
14230
15731
  if (!commandsDir) return invoked;
14231
15732
  let entries;
14232
15733
  try {
14233
- entries = await readdir15(commandsDir, { recursive: true });
15734
+ entries = await readdir17(commandsDir, { recursive: true });
14234
15735
  } catch {
14235
15736
  return invoked;
14236
15737
  }
14237
- const files = entries.filter((e) => typeof e === "string" && (e.endsWith(".ts") || e.endsWith(".js"))).map((e) => join34(commandsDir, e));
15738
+ const files = entries.filter((e) => typeof e === "string" && (e.endsWith(".ts") || e.endsWith(".js"))).map((e) => join36(commandsDir, e));
14238
15739
  const patterns = {};
14239
15740
  for (const mod of RESILIENCE_MODULES) {
14240
15741
  patterns[mod] = new RegExp(`pipeline/${mod}(?:\\.js)?["']`);
@@ -14242,7 +15743,7 @@ async function detectResilienceInvocations() {
14242
15743
  for (const file of files) {
14243
15744
  let contents;
14244
15745
  try {
14245
- contents = await readFile25(file, "utf-8");
15746
+ contents = await readFile27(file, "utf-8");
14246
15747
  } catch {
14247
15748
  continue;
14248
15749
  }
@@ -14403,6 +15904,8 @@ var DEFAULT_KNOWN_AGENTS = /* @__PURE__ */ new Set([
14403
15904
  "hatch3r-devops",
14404
15905
  "hatch3r-docs-writer",
14405
15906
  "hatch3r-fixer",
15907
+ "hatch3r-handoff-loader",
15908
+ "hatch3r-handoff-preparer",
14406
15909
  "hatch3r-implementer",
14407
15910
  "hatch3r-learnings-loader",
14408
15911
  "hatch3r-lint-fixer",
@@ -14434,7 +15937,7 @@ async function validateManifest2(rootDir, manifest, result) {
14434
15937
  if (!manifest.tools || manifest.tools.length === 0) result.warnings.push("hatch.json: no tools configured");
14435
15938
  for (const managedFile of manifest.managedFiles ?? []) {
14436
15939
  try {
14437
- await access11(join35(rootDir, managedFile));
15940
+ await access11(join37(rootDir, managedFile));
14438
15941
  } catch (err) {
14439
15942
  if (err.code !== "ENOENT") throw err;
14440
15943
  result.warnings.push(`Managed file missing from disk: ${managedFile}`);
@@ -14446,7 +15949,7 @@ async function validateDirectories(agentsDir, result) {
14446
15949
  const optionalDirs = ["commands", "prompts", "mcp", "policy", "github-agents"];
14447
15950
  for (const dir of requiredDirs) {
14448
15951
  try {
14449
- await access11(join35(agentsDir, dir));
15952
+ await access11(join37(agentsDir, dir));
14450
15953
  } catch (err) {
14451
15954
  if (err.code !== "ENOENT") throw err;
14452
15955
  result.errors.push(`Required directory missing: .agents/${dir}/`);
@@ -14454,7 +15957,7 @@ async function validateDirectories(agentsDir, result) {
14454
15957
  }
14455
15958
  for (const dir of optionalDirs) {
14456
15959
  try {
14457
- await access11(join35(agentsDir, dir));
15960
+ await access11(join37(agentsDir, dir));
14458
15961
  } catch (err) {
14459
15962
  if (err.code !== "ENOENT") throw err;
14460
15963
  verboseWarn(result, `Optional directory missing: .agents/${dir}/`);
@@ -14465,13 +15968,13 @@ async function validateFrontmatter(agentsDir, result) {
14465
15968
  const requiredDirs = ["agents", "skills", "rules"];
14466
15969
  const optionalDirs = ["commands", "prompts", "mcp", "policy", "github-agents"];
14467
15970
  for (const dir of [...requiredDirs, ...optionalDirs]) {
14468
- const dirPath = join35(agentsDir, dir);
15971
+ const dirPath = join37(agentsDir, dir);
14469
15972
  try {
14470
- const entries = await readdir16(dirPath, { withFileTypes: true });
15973
+ const entries = await readdir18(dirPath, { withFileTypes: true });
14471
15974
  for (const entry of entries) {
14472
15975
  if (entry.isFile() && entry.name.endsWith(".md")) {
14473
- const filePath = join35(dirPath, entry.name);
14474
- const content = await readFile26(filePath, "utf-8");
15976
+ const filePath = join37(dirPath, entry.name);
15977
+ const content = await readFile28(filePath, "utf-8");
14475
15978
  if (!content.startsWith("---")) {
14476
15979
  result.warnings.push(`Missing frontmatter: .agents/${dir}/${entry.name}`);
14477
15980
  } else {
@@ -14480,7 +15983,7 @@ async function validateFrontmatter(agentsDir, result) {
14480
15983
  result.errors.push(`Invalid frontmatter (no closing ---): .agents/${dir}/${entry.name}`);
14481
15984
  } else {
14482
15985
  const frontmatter = content.slice(3, endIdx).trim();
14483
- const parsedFm = parseYaml4(frontmatter);
15986
+ const parsedFm = parseYaml6(frontmatter);
14484
15987
  const idField = dir === "github-agents" ? "name" : "id";
14485
15988
  if (!parsedFm || typeof parsedFm !== "object" || !parsedFm[idField]) {
14486
15989
  result.warnings.push(`Missing '${idField}' in frontmatter: .agents/${dir}/${entry.name}`);
@@ -14498,7 +16001,7 @@ async function validateFrontmatter(agentsDir, result) {
14498
16001
  }
14499
16002
  } else if (entry.isDirectory()) {
14500
16003
  if (dir !== "skills") continue;
14501
- const skillPath = join35(dirPath, entry.name, "SKILL.md");
16004
+ const skillPath = join37(dirPath, entry.name, "SKILL.md");
14502
16005
  try {
14503
16006
  await access11(skillPath);
14504
16007
  } catch (err) {
@@ -14512,7 +16015,7 @@ async function validateFrontmatter(agentsDir, result) {
14512
16015
  }
14513
16016
  }
14514
16017
  try {
14515
- await access11(join35(agentsDir, "AGENTS.md"));
16018
+ await access11(join37(agentsDir, "AGENTS.md"));
14516
16019
  } catch (err) {
14517
16020
  if (err.code !== "ENOENT") throw err;
14518
16021
  result.warnings.push("Missing .agents/AGENTS.md");
@@ -14634,29 +16137,29 @@ async function validateManagedFilePrefixes(manifest, result) {
14634
16137
  }
14635
16138
  async function validateHooks(agentsDir, manifest, result) {
14636
16139
  if (!manifest.features.hooks) return;
14637
- const hooksDir = join35(agentsDir, "hooks");
16140
+ const hooksDir = join37(agentsDir, "hooks");
14638
16141
  try {
14639
- const hookFiles = await readdir16(hooksDir);
16142
+ const hookFiles = await readdir18(hooksDir);
14640
16143
  const mdHooks = hookFiles.filter((f) => f.endsWith(".md"));
14641
16144
  if (mdHooks.length === 0) {
14642
16145
  result.warnings.push("Hooks feature enabled but no hook definitions found in .agents/hooks/");
14643
16146
  }
14644
16147
  let agentFiles;
14645
16148
  try {
14646
- const agentEntries = await readdir16(join35(agentsDir, "agents"));
16149
+ const agentEntries = await readdir18(join37(agentsDir, "agents"));
14647
16150
  agentFiles = new Set(agentEntries.filter((f) => f.endsWith(".md")));
14648
16151
  } catch (err) {
14649
16152
  if (err.code !== "ENOENT") throw err;
14650
16153
  }
14651
16154
  for (const hookFile of mdHooks) {
14652
- const hookContent = await readFile26(join35(hooksDir, hookFile), "utf-8");
16155
+ const hookContent = await readFile28(join37(hooksDir, hookFile), "utf-8");
14653
16156
  if (!hookContent.startsWith("---")) {
14654
16157
  result.warnings.push(`Hook missing frontmatter: .agents/hooks/${hookFile}`);
14655
16158
  continue;
14656
16159
  }
14657
16160
  const endIdx = hookContent.indexOf("---", 3);
14658
16161
  if (endIdx === -1) continue;
14659
- const fm = parseYaml4(hookContent.slice(3, endIdx).trim());
16162
+ const fm = parseYaml6(hookContent.slice(3, endIdx).trim());
14660
16163
  if (fm?.event && typeof fm.event === "string") {
14661
16164
  if (!isValidHookEvent(fm.event)) {
14662
16165
  result.errors.push(`Hook "${hookFile}" has invalid event "${fm.event}". Valid events: pre-commit, post-merge, ci-failure, file-save, session-start, pre-push`);
@@ -14681,9 +16184,9 @@ async function validateHooks(agentsDir, manifest, result) {
14681
16184
  }
14682
16185
  async function validateMcp(agentsDir, manifest, result) {
14683
16186
  if (!manifest.features.mcp || manifest.mcp.servers.length === 0) return;
14684
- const mcpPath = join35(agentsDir, "mcp", "mcp.json");
16187
+ const mcpPath = join37(agentsDir, "mcp", "mcp.json");
14685
16188
  try {
14686
- const mcpContent = await readFile26(mcpPath, "utf-8");
16189
+ const mcpContent = await readFile28(mcpPath, "utf-8");
14687
16190
  const mcpParsed = JSON.parse(mcpContent);
14688
16191
  if (!mcpParsed.mcpServers || typeof mcpParsed.mcpServers !== "object") {
14689
16192
  result.errors.push("MCP config missing 'mcpServers' key");
@@ -14696,6 +16199,18 @@ async function validateMcp(agentsDir, manifest, result) {
14696
16199
  }
14697
16200
  }
14698
16201
  }
16202
+ async function validateCliTools(manifest, result) {
16203
+ const cli = manifest.cliTools;
16204
+ if (!cli?.enabled || cli.selected.length === 0) return;
16205
+ const detection = await detectCliTools(cli.selected);
16206
+ for (const r of detection) {
16207
+ if (!r.installed) {
16208
+ result.warnings.push(
16209
+ `CLI tool '${r.id}' not found on PATH \u2014 run \`npx hatch3r cli-tools install\``
16210
+ );
16211
+ }
16212
+ }
16213
+ }
14699
16214
  async function validateModels(manifest, result) {
14700
16215
  if (!manifest.models) return;
14701
16216
  if (manifest.models.default && typeof manifest.models.default !== "string") {
@@ -14738,21 +16253,21 @@ async function validateCustomizeYaml(rootDir, result) {
14738
16253
  enabled: "boolean"
14739
16254
  };
14740
16255
  for (const { dir } of CUSTOMIZATION_TYPES) {
14741
- const customDir = join35(rootDir, ".hatch3r", dir);
16256
+ const customDir = join37(rootDir, ".hatch3r", dir);
14742
16257
  let files;
14743
16258
  try {
14744
- files = await readdir16(customDir);
16259
+ files = await readdir18(customDir);
14745
16260
  } catch (err) {
14746
16261
  if (err.code === "ENOENT") continue;
14747
16262
  throw err;
14748
16263
  }
14749
16264
  const yamlFiles = files.filter((f) => f.endsWith(".customize.yaml"));
14750
16265
  for (const file of yamlFiles) {
14751
- const filePath = join35(customDir, file);
16266
+ const filePath = join37(customDir, file);
14752
16267
  const itemId = file.replace(".customize.yaml", "");
14753
16268
  let raw;
14754
16269
  try {
14755
- raw = await readFile26(filePath, "utf-8");
16270
+ raw = await readFile28(filePath, "utf-8");
14756
16271
  } catch {
14757
16272
  continue;
14758
16273
  }
@@ -14764,7 +16279,7 @@ async function validateCustomizeYaml(rootDir, result) {
14764
16279
  }
14765
16280
  let parsed;
14766
16281
  try {
14767
- parsed = parseYaml4(raw);
16282
+ parsed = parseYaml6(raw);
14768
16283
  } catch {
14769
16284
  result.errors.push(
14770
16285
  `Invalid YAML syntax in .hatch3r/${dir}/${file}`
@@ -14815,13 +16330,13 @@ async function validateCustomizeYaml(rootDir, result) {
14815
16330
  }
14816
16331
  async function validateCustomizations(rootDir, agentsDir, manifest, result) {
14817
16332
  for (const { dir, canonical } of CUSTOMIZATION_TYPES) {
14818
- const customDir = join35(rootDir, ".hatch3r", dir);
16333
+ const customDir = join37(rootDir, ".hatch3r", dir);
14819
16334
  try {
14820
- const customFiles = await readdir16(customDir);
16335
+ const customFiles = await readdir18(customDir);
14821
16336
  for (const file of customFiles) {
14822
16337
  if (file.endsWith(".customize.yaml")) {
14823
16338
  const itemId = file.replace(".customize.yaml", "");
14824
- const canonicalPath = canonical === "skills" ? join35(agentsDir, canonical, itemId) : join35(agentsDir, canonical, `${itemId}.md`);
16339
+ const canonicalPath = canonical === "skills" ? join37(agentsDir, canonical, itemId) : join37(agentsDir, canonical, `${itemId}.md`);
14825
16340
  try {
14826
16341
  await access11(canonicalPath);
14827
16342
  } catch (err) {
@@ -14838,7 +16353,7 @@ async function validateCustomizations(rootDir, agentsDir, manifest, result) {
14838
16353
  async function findContentFile(agentsDir, cfg, id) {
14839
16354
  const baseId = id.startsWith("cmd-") ? id.slice(4) : id;
14840
16355
  if (cfg.strategy === "subdir") {
14841
- const path = join35(agentsDir, cfg.dir, baseId, "SKILL.md");
16356
+ const path = join37(agentsDir, cfg.dir, baseId, "SKILL.md");
14842
16357
  try {
14843
16358
  await access11(path);
14844
16359
  return path;
@@ -14846,19 +16361,19 @@ async function findContentFile(agentsDir, cfg, id) {
14846
16361
  return null;
14847
16362
  }
14848
16363
  }
14849
- const root = join35(agentsDir, cfg.dir);
16364
+ const root = join37(agentsDir, cfg.dir);
14850
16365
  const stack = [root];
14851
16366
  const mdFiles = [];
14852
16367
  while (stack.length > 0) {
14853
16368
  const dir = stack.pop();
14854
16369
  let entries;
14855
16370
  try {
14856
- entries = await readdir16(dir, { withFileTypes: true });
16371
+ entries = await readdir18(dir, { withFileTypes: true });
14857
16372
  } catch {
14858
16373
  continue;
14859
16374
  }
14860
16375
  for (const entry of entries) {
14861
- const full = join35(dir, entry.name);
16376
+ const full = join37(dir, entry.name);
14862
16377
  if (entry.isDirectory()) {
14863
16378
  stack.push(full);
14864
16379
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -14871,11 +16386,11 @@ async function findContentFile(agentsDir, cfg, id) {
14871
16386
  }
14872
16387
  for (const file of mdFiles) {
14873
16388
  try {
14874
- const raw = await readFile26(file, "utf-8");
16389
+ const raw = await readFile28(file, "utf-8");
14875
16390
  if (!raw.startsWith("---")) continue;
14876
16391
  const endIdx = raw.indexOf("---", 3);
14877
16392
  if (endIdx === -1) continue;
14878
- const fm = parseYaml4(raw.slice(3, endIdx).trim());
16393
+ const fm = parseYaml6(raw.slice(3, endIdx).trim());
14879
16394
  const fmId = fm && typeof fm === "object" && typeof fm.id === "string" ? fm.id : null;
14880
16395
  if (fmId && (fmId === id || fmId === baseId)) {
14881
16396
  return file;
@@ -14910,9 +16425,9 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
14910
16425
  for (const id of ids) allContentIds.add(id);
14911
16426
  }
14912
16427
  for (const { dir } of CUSTOMIZATION_TYPES) {
14913
- const customDir = join35(rootDir, ".hatch3r", dir);
16428
+ const customDir = join37(rootDir, ".hatch3r", dir);
14914
16429
  try {
14915
- const files = await readdir16(customDir);
16430
+ const files = await readdir18(customDir);
14916
16431
  for (const f of files.filter((f2) => f2.endsWith(".customize.yaml") || f2.endsWith(".customize.md"))) {
14917
16432
  const itemId = f.replace(/\.customize\.(yaml|md)$/, "");
14918
16433
  if (!allContentIds.has(itemId) && !allContentIds.has(`${HATCH3R_PREFIX}${itemId}`) && !allContentIds.has(`cmd-${itemId}`) && !allContentIds.has(`cmd-${HATCH3R_PREFIX}${itemId}`)) {
@@ -14924,7 +16439,7 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
14924
16439
  }
14925
16440
  }
14926
16441
  }
14927
- const learningsDir = join35(agentsDir, "learnings");
16442
+ const learningsDir = join37(agentsDir, "learnings");
14928
16443
  const learningsResult = await validateLearningsDirectory(learningsDir);
14929
16444
  for (const e of learningsResult.errors) {
14930
16445
  result.errors.push(e);
@@ -14932,6 +16447,17 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
14932
16447
  for (const w of learningsResult.warnings) {
14933
16448
  result.warnings.push(w);
14934
16449
  }
16450
+ const handoffsActiveDir = join37(agentsDir, "handoffs", "active");
16451
+ const handoffsArchivedDir = join37(agentsDir, "handoffs", "archived");
16452
+ const handoffsResult = await validateHandoffsDirectory(handoffsActiveDir, {
16453
+ archivedDir: handoffsArchivedDir
16454
+ });
16455
+ for (const e of handoffsResult.errors) {
16456
+ result.errors.push(e);
16457
+ }
16458
+ for (const w of handoffsResult.warnings) {
16459
+ result.warnings.push(w);
16460
+ }
14935
16461
  }
14936
16462
  var USER_CONTENT_ANTI_SLOP = [
14937
16463
  "best possible",
@@ -14961,7 +16487,7 @@ var USER_CONTENT_TYPE_DIRS = {
14961
16487
  async function validateUserContent(rootDir, agentsDir, result, index) {
14962
16488
  const userRoot = resolveUserContentRoot(rootDir);
14963
16489
  try {
14964
- await stat9(userRoot);
16490
+ await stat11(userRoot);
14965
16491
  } catch (err) {
14966
16492
  if (err.code === "ENOENT") return;
14967
16493
  throw err;
@@ -14970,10 +16496,10 @@ async function validateUserContent(rootDir, agentsDir, result, index) {
14970
16496
  if (userItems.length === 0) return;
14971
16497
  for (const item of userItems) {
14972
16498
  const fileLabel = `.agents/user/${item.relativePath}`;
14973
- const absPath = item.type === "skill" ? join35(userRoot, item.relativePath, "SKILL.md") : join35(userRoot, item.relativePath);
16499
+ const absPath = item.type === "skill" ? join37(userRoot, item.relativePath, "SKILL.md") : join37(userRoot, item.relativePath);
14974
16500
  let raw;
14975
16501
  try {
14976
- raw = await readFile26(absPath, "utf-8");
16502
+ raw = await readFile28(absPath, "utf-8");
14977
16503
  } catch (err) {
14978
16504
  result.errors.push(
14979
16505
  `User content unreadable: ${fileLabel} (${err instanceof Error ? err.message : String(err)})`
@@ -14997,7 +16523,7 @@ async function validateUserContent(rootDir, agentsDir, result, index) {
14997
16523
  const fmRaw = raw.slice(3, fmEnd).trim();
14998
16524
  let fm;
14999
16525
  try {
15000
- fm = parseYaml4(fmRaw);
16526
+ fm = parseYaml6(fmRaw);
15001
16527
  } catch (err) {
15002
16528
  result.errors.push(
15003
16529
  `${fileLabel}: YAML parse error in frontmatter \u2014 ${err instanceof Error ? err.message : String(err)}`
@@ -15057,7 +16583,7 @@ async function validateUserContent(rootDir, agentsDir, result, index) {
15057
16583
  if (item.type === "rule") {
15058
16584
  const mdcPath = absPath.replace(/\.md$/, ".mdc");
15059
16585
  try {
15060
- await stat9(mdcPath);
16586
+ await stat11(mdcPath);
15061
16587
  } catch (err) {
15062
16588
  if (err.code === "ENOENT") {
15063
16589
  result.errors.push(
@@ -15225,16 +16751,16 @@ async function validateDocsCounts(rootDir) {
15225
16751
  let checked = 0;
15226
16752
  const actual = {};
15227
16753
  const dirs = [
15228
- ["adapters", join35(rootDir, "src/adapters"), (e) => e.endsWith(".ts") && !["base.ts", "index.ts", "canonical.ts", "customization.ts", "types.ts", "mcp-utils.ts", "toml-utils.ts", "contextBudget.ts"].includes(e)],
15229
- ["commands", join35(rootDir, "src/cli/commands"), (e) => e.endsWith(".ts")],
15230
- ["agents", join35(rootDir, "agents"), (e) => e.endsWith(".md")],
15231
- ["skills", join35(rootDir, "skills"), (e) => true],
15232
- ["rules", join35(rootDir, "rules"), (e) => e.endsWith(".md")],
15233
- ["hooks", join35(rootDir, "hooks"), (e) => e.endsWith(".md")]
16754
+ ["adapters", join37(rootDir, "src/adapters"), (e) => e.endsWith(".ts") && !["base.ts", "index.ts", "canonical.ts", "customization.ts", "types.ts", "mcp-utils.ts", "toml-utils.ts", "contextBudget.ts"].includes(e)],
16755
+ ["commands", join37(rootDir, "src/cli/commands"), (e) => e.endsWith(".ts")],
16756
+ ["agents", join37(rootDir, "agents"), (e) => e.endsWith(".md")],
16757
+ ["skills", join37(rootDir, "skills"), (e) => true],
16758
+ ["rules", join37(rootDir, "rules"), (e) => e.endsWith(".md")],
16759
+ ["hooks", join37(rootDir, "hooks"), (e) => e.endsWith(".md")]
15234
16760
  ];
15235
16761
  for (const [name, dir, filter] of dirs) {
15236
16762
  try {
15237
- const entries = await readdir16(dir, { withFileTypes: true });
16763
+ const entries = await readdir18(dir, { withFileTypes: true });
15238
16764
  if (name === "skills") {
15239
16765
  actual[name] = entries.filter((e) => e.isDirectory()).length;
15240
16766
  } else {
@@ -15244,9 +16770,9 @@ async function validateDocsCounts(rootDir) {
15244
16770
  actual[name] = 0;
15245
16771
  }
15246
16772
  }
15247
- const readmePath = join35(rootDir, "README.md");
16773
+ const readmePath = join37(rootDir, "README.md");
15248
16774
  try {
15249
- const readme = await readFile26(readmePath, "utf-8");
16775
+ const readme = await readFile28(readmePath, "utf-8");
15250
16776
  const countPatterns = [
15251
16777
  ["adapters", /(\d+)\s+Adapters/i],
15252
16778
  ["skills", /(\d+)\s+skills/i],
@@ -15319,11 +16845,11 @@ async function validateCommand(opts) {
15319
16845
  }
15320
16846
  return;
15321
16847
  }
15322
- const agentsDir = join35(rootDir, AGENTS_DIR);
16848
+ const agentsDir = join37(rootDir, AGENTS_DIR);
15323
16849
  const result = { errors: [], warnings: [] };
15324
16850
  const spinner = jsonMode ? null : createSpinner("Validating .agents/ structure...");
15325
16851
  spinner?.start();
15326
- const cwdIsFrameworkSource = existsSync4(join35(rootDir, "agents")) && existsSync4(join35(rootDir, "skills")) && existsSync4(join35(rootDir, "rules")) && existsSync4(join35(rootDir, "commands"));
16852
+ const cwdIsFrameworkSource = existsSync4(join37(rootDir, "agents")) && existsSync4(join37(rootDir, "skills")) && existsSync4(join37(rootDir, "rules")) && existsSync4(join37(rootDir, "commands"));
15327
16853
  try {
15328
16854
  await access11(agentsDir);
15329
16855
  } catch (err) {
@@ -15361,9 +16887,9 @@ async function validateCommand(opts) {
15361
16887
  printBox(
15362
16888
  "Canonical content lint failed",
15363
16889
  [
15364
- `${chalk10.red("\u2716")} ${result.errors.length} error(s)`,
15365
- `${chalk10.yellow("\u26A0")} ${result.warnings.length} warning(s)`,
15366
- chalk10.dim("(framework-source mode \u2014 .agents/ not required)")
16890
+ `${chalk12.red("\u2716")} ${result.errors.length} error(s)`,
16891
+ `${chalk12.yellow("\u26A0")} ${result.warnings.length} warning(s)`,
16892
+ chalk12.dim("(framework-source mode \u2014 .agents/ not required)")
15367
16893
  ],
15368
16894
  "error"
15369
16895
  );
@@ -15376,16 +16902,16 @@ async function validateCommand(opts) {
15376
16902
  printBox(
15377
16903
  "Canonical content lint",
15378
16904
  [
15379
- `${chalk10.green("\u2714")} 0 errors`,
15380
- `${chalk10.yellow("\u26A0")} ${result.warnings.length} warning(s)`,
15381
- chalk10.dim("(framework-source mode \u2014 .agents/ not required)")
16905
+ `${chalk12.green("\u2714")} 0 errors`,
16906
+ `${chalk12.yellow("\u26A0")} ${result.warnings.length} warning(s)`,
16907
+ chalk12.dim("(framework-source mode \u2014 .agents/ not required)")
15382
16908
  ],
15383
16909
  "success"
15384
16910
  );
15385
16911
  } else {
15386
16912
  printBox(
15387
16913
  "Canonical content lint",
15388
- [chalk10.green("All checks passed")],
16914
+ [chalk12.green("All checks passed")],
15389
16915
  "success"
15390
16916
  );
15391
16917
  }
@@ -15426,6 +16952,8 @@ async function validateCommand(opts) {
15426
16952
  await validateHooks(agentsDir, manifest, result);
15427
16953
  verbose("Checking MCP configuration...");
15428
16954
  await validateMcp(agentsDir, manifest, result);
16955
+ verbose("Checking CLI tools...");
16956
+ await validateCliTools(manifest, result);
15429
16957
  verbose("Checking model configuration...");
15430
16958
  await validateModels(manifest, result);
15431
16959
  verbose("Checking cost tracking...");
@@ -15482,7 +17010,7 @@ async function validateCommand(opts) {
15482
17010
  let hasCustomizations = false;
15483
17011
  for (const { dir } of CUSTOMIZATION_TYPES) {
15484
17012
  try {
15485
- const files = await readdir16(join35(rootDir, ".hatch3r", dir));
17013
+ const files = await readdir18(join37(rootDir, ".hatch3r", dir));
15486
17014
  if (files.some((f) => f.endsWith(".customize.yaml") || f.endsWith(".customize.md"))) {
15487
17015
  hasCustomizations = true;
15488
17016
  break;
@@ -15510,7 +17038,7 @@ async function validateCommand(opts) {
15510
17038
  return;
15511
17039
  }
15512
17040
  if (result.errors.length === 0 && result.warnings.length === 0) {
15513
- printBox("Validation", [chalk10.green("All checks passed")], "success");
17041
+ printBox("Validation", [chalk12.green("All checks passed")], "success");
15514
17042
  if (hasCustomizations) {
15515
17043
  printCustomizationHint();
15516
17044
  }
@@ -15531,15 +17059,15 @@ async function validateCommand(opts) {
15531
17059
  }
15532
17060
  if (result.errors.length > 0) {
15533
17061
  const summaryLines = [
15534
- `${chalk10.red("\u2716")} ${result.errors.length} error(s)`,
15535
- `${chalk10.yellow("\u26A0")} ${result.warnings.length} warning(s)`
17062
+ `${chalk12.red("\u2716")} ${result.errors.length} error(s)`,
17063
+ `${chalk12.yellow("\u26A0")} ${result.warnings.length} warning(s)`
15536
17064
  ];
15537
17065
  printBox("Validation failed", summaryLines, "error");
15538
17066
  throw new HatchError("Validation failed", 1, "VALIDATION_ERROR");
15539
17067
  } else {
15540
17068
  const summaryLines = [
15541
- `${chalk10.green("\u2714")} 0 errors`,
15542
- `${chalk10.yellow("\u26A0")} ${result.warnings.length} warning(s)`
17069
+ `${chalk12.green("\u2714")} 0 errors`,
17070
+ `${chalk12.yellow("\u26A0")} ${result.warnings.length} warning(s)`
15543
17071
  ];
15544
17072
  printBox("Validation passed", summaryLines, "success");
15545
17073
  }
@@ -15548,10 +17076,10 @@ async function validateCommand(opts) {
15548
17076
  }
15549
17077
  }
15550
17078
  async function validateEnvMcpSecrets(rootDir, result) {
15551
- const envMcpPath = join35(rootDir, ".env.mcp");
17079
+ const envMcpPath = join37(rootDir, ".env.mcp");
15552
17080
  if (!existsSync4(envMcpPath)) return;
15553
17081
  try {
15554
- const raw = await readFile26(envMcpPath, "utf-8");
17082
+ const raw = await readFile28(envMcpPath, "utf-8");
15555
17083
  const vars = parseEnvFile(raw);
15556
17084
  const detection = detectSecrets(vars);
15557
17085
  for (const finding of detection.findings) {
@@ -15568,7 +17096,7 @@ async function validateEnvMcpSecrets(rootDir, result) {
15568
17096
  async function validateCanonicalDescriptionQuality(rootDir, result) {
15569
17097
  const __filename = fileURLToPath8(import.meta.url);
15570
17098
  const packageRoot = findPackageRoot(dirname18(__filename));
15571
- const canonicalRoot = existsSync4(join35(rootDir, "agents")) && existsSync4(join35(rootDir, "skills")) && existsSync4(join35(rootDir, "rules")) && existsSync4(join35(rootDir, "commands")) ? rootDir : packageRoot;
17099
+ const canonicalRoot = existsSync4(join37(rootDir, "agents")) && existsSync4(join37(rootDir, "skills")) && existsSync4(join37(rootDir, "rules")) && existsSync4(join37(rootDir, "commands")) ? rootDir : packageRoot;
15572
17100
  try {
15573
17101
  const index = await buildContentIndex(canonicalRoot);
15574
17102
  if (index.items.length === 0) return;
@@ -15607,14 +17135,14 @@ async function validateSecurityCompliance(result) {
15607
17135
  }
15608
17136
  function printCustomizationHint() {
15609
17137
  console.log();
15610
- info(chalk10.bold("Customization mechanisms detected. Quick reference:"));
15611
- console.log(chalk10.dim(" 1. hatch3r- prefix: Files prefixed with hatch3r- are managed by hatch3r and"));
15612
- console.log(chalk10.dim(" overwritten on update. Do not edit these directly."));
15613
- console.log(chalk10.dim(" 2. Managed blocks: Sections between <!-- HATCH3R:BEGIN --> and"));
15614
- console.log(chalk10.dim(" <!-- HATCH3R:END --> are auto-updated. Add content outside these markers."));
15615
- console.log(chalk10.dim(" 3. .customize.yaml/.md: Place in .hatch3r/{type}/ to override model, scope,"));
15616
- console.log(chalk10.dim(" description, or disable items. Use .customize.md for content additions."));
15617
- console.log(chalk10.dim(" See: https://docs.hatch3r.com/docs/guides/customization"));
17138
+ info(chalk12.bold("Customization mechanisms detected. Quick reference:"));
17139
+ console.log(chalk12.dim(" 1. hatch3r- prefix: Files prefixed with hatch3r- are managed by hatch3r and"));
17140
+ console.log(chalk12.dim(" overwritten on update. Do not edit these directly."));
17141
+ console.log(chalk12.dim(" 2. Managed blocks: Sections between <!-- HATCH3R:BEGIN --> and"));
17142
+ console.log(chalk12.dim(" <!-- HATCH3R:END --> are auto-updated. Add content outside these markers."));
17143
+ console.log(chalk12.dim(" 3. .customize.yaml/.md: Place in .hatch3r/{type}/ to override model, scope,"));
17144
+ console.log(chalk12.dim(" description, or disable items. Use .customize.md for content additions."));
17145
+ console.log(chalk12.dim(" See: https://docs.hatch3r.com/docs/guides/customization"));
15618
17146
  }
15619
17147
 
15620
17148
  // src/cli/commands/verify.ts
@@ -15626,26 +17154,26 @@ init_pipelineTimeout();
15626
17154
  init_phaseOutputSchema();
15627
17155
  init_retryWithBackoff();
15628
17156
  init_ui();
15629
- import { join as join36 } from "path";
15630
- import chalk11 from "chalk";
17157
+ import { join as join38 } from "path";
17158
+ import chalk13 from "chalk";
15631
17159
  async function runVerifyPass(agentsDir) {
15632
17160
  const results = await verifyIntegrity(agentsDir);
15633
17161
  if (results.length === 0) {
15634
17162
  return { passed: true, counts: { pass: 0 }, hasModifiedOrMissing: false, hasTampered: false };
15635
17163
  }
15636
17164
  const icons = {
15637
- pass: chalk11.green("\u2714"),
15638
- modified: chalk11.yellow("\u2716"),
15639
- missing: chalk11.red("\u2716"),
15640
- new: chalk11.cyan("+"),
15641
- tampered: chalk11.red("\u26A0")
17165
+ pass: chalk13.green("\u2714"),
17166
+ modified: chalk13.yellow("\u2716"),
17167
+ missing: chalk13.red("\u2716"),
17168
+ new: chalk13.cyan("+"),
17169
+ tampered: chalk13.red("\u26A0")
15642
17170
  };
15643
17171
  const labels = {
15644
- pass: chalk11.green("PASS"),
15645
- modified: chalk11.yellow("MODIFIED"),
15646
- missing: chalk11.red("MISSING"),
15647
- new: chalk11.cyan("NEW"),
15648
- tampered: chalk11.red("TAMPERED")
17172
+ pass: chalk13.green("PASS"),
17173
+ modified: chalk13.yellow("MODIFIED"),
17174
+ missing: chalk13.red("MISSING"),
17175
+ new: chalk13.cyan("NEW"),
17176
+ tampered: chalk13.red("TAMPERED")
15649
17177
  };
15650
17178
  console.log();
15651
17179
  for (const r of results) {
@@ -15665,11 +17193,11 @@ async function runVerifyPass(agentsDir) {
15665
17193
  }
15666
17194
  function printSummary(counts, title, style) {
15667
17195
  const summaryLines = [];
15668
- if ((counts.pass ?? 0) > 0) summaryLines.push(`${chalk11.green("\u2714")} Passed: ${counts.pass}`);
15669
- if ((counts.modified ?? 0) > 0) summaryLines.push(`${chalk11.yellow("\u2716")} Modified: ${counts.modified}`);
15670
- if ((counts.missing ?? 0) > 0) summaryLines.push(`${chalk11.red("\u2716")} Missing: ${counts.missing}`);
15671
- if ((counts.new ?? 0) > 0) summaryLines.push(`${chalk11.cyan("+")} New: ${counts.new}`);
15672
- if ((counts.tampered ?? 0) > 0) summaryLines.push(`${chalk11.red("\u26A0")} Tampered: ${counts.tampered}`);
17196
+ if ((counts.pass ?? 0) > 0) summaryLines.push(`${chalk13.green("\u2714")} Passed: ${counts.pass}`);
17197
+ if ((counts.modified ?? 0) > 0) summaryLines.push(`${chalk13.yellow("\u2716")} Modified: ${counts.modified}`);
17198
+ if ((counts.missing ?? 0) > 0) summaryLines.push(`${chalk13.red("\u2716")} Missing: ${counts.missing}`);
17199
+ if ((counts.new ?? 0) > 0) summaryLines.push(`${chalk13.cyan("+")} New: ${counts.new}`);
17200
+ if ((counts.tampered ?? 0) > 0) summaryLines.push(`${chalk13.red("\u26A0")} Tampered: ${counts.tampered}`);
15673
17201
  printBox(title, summaryLines, style);
15674
17202
  }
15675
17203
  var MAX_FIX_ATTEMPTS = 5;
@@ -15680,7 +17208,7 @@ async function verifyCommand(options = {}) {
15680
17208
  DEFAULT_PIPELINE_TIMEOUT_MS
15681
17209
  );
15682
17210
  const rootDir = process.cwd();
15683
- const agentsDir = join36(rootDir, AGENTS_DIR);
17211
+ const agentsDir = join38(rootDir, AGENTS_DIR);
15684
17212
  const spinner = createSpinner("Verifying file integrity...");
15685
17213
  spinner.start();
15686
17214
  const manifest = await readIntegrityManifest(agentsDir);
@@ -15706,7 +17234,7 @@ async function verifyCommand(options = {}) {
15706
17234
  const compactedCounts = compactPhaseOutput(result.counts);
15707
17235
  if (result.passed) {
15708
17236
  if (compactedCounts.pass === 0) {
15709
- printBox("Integrity", [chalk11.dim("No files to verify")], "info");
17237
+ printBox("Integrity", [chalk13.dim("No files to verify")], "info");
15710
17238
  } else {
15711
17239
  printSummary(compactedCounts, "Integrity check passed", "success");
15712
17240
  }
@@ -15722,7 +17250,7 @@ async function verifyCommand(options = {}) {
15722
17250
  error("Integrity manifest has been tampered with. Re-run `hatch3r update` to regenerate it.");
15723
17251
  }
15724
17252
  if ((result.counts.modified ?? 0) > 0) {
15725
- info(`Modified files may have been tampered with. Run ${chalk11.bold("hatch3r update")} to restore originals.`);
17253
+ info(`Modified files may have been tampered with. Run ${chalk13.bold("hatch3r update")} to restore originals.`);
15726
17254
  }
15727
17255
  console.log();
15728
17256
  throw new HatchError("Integrity check failed", 1, "INTEGRITY_ERROR");
@@ -15815,24 +17343,24 @@ init_adapters();
15815
17343
  init_types();
15816
17344
  init_managedBlocks();
15817
17345
  init_integrity();
15818
- import { access as access12, readFile as readFile27, readdir as readdir17, stat as stat10 } from "fs/promises";
15819
- import { join as join37 } from "path";
15820
- import chalk12 from "chalk";
17346
+ import { access as access12, readFile as readFile29, readdir as readdir19, stat as stat12 } from "fs/promises";
17347
+ import { join as join39 } from "path";
17348
+ import chalk14 from "chalk";
15821
17349
  init_ui();
15822
17350
  async function dirCharCount(dir) {
15823
17351
  let total = 0;
15824
17352
  let entries;
15825
17353
  try {
15826
- entries = await readdir17(dir, { withFileTypes: true });
17354
+ entries = await readdir19(dir, { withFileTypes: true });
15827
17355
  } catch {
15828
17356
  return 0;
15829
17357
  }
15830
17358
  for (const entry of entries) {
15831
- const fullPath = join37(dir, entry.name);
17359
+ const fullPath = join39(dir, entry.name);
15832
17360
  if (entry.isDirectory()) {
15833
17361
  total += await dirCharCount(fullPath);
15834
17362
  } else if (entry.isFile()) {
15835
- const info2 = await stat10(fullPath);
17363
+ const info2 = await stat12(fullPath);
15836
17364
  total += info2.size;
15837
17365
  }
15838
17366
  }
@@ -15854,7 +17382,7 @@ async function runFastStatusCheck(rootDir, agentsDir, manifest) {
15854
17382
  const successful = new Set(integrityManifest.successfulAdapters);
15855
17383
  for (const tool of expected) {
15856
17384
  if (!successful.has(tool)) {
15857
- fileLines.push(chalk12.yellow(`${tool}: last sync did not complete (check hatch3r sync output)`));
17385
+ fileLines.push(chalk14.yellow(`${tool}: last sync did not complete (check hatch3r sync output)`));
15858
17386
  }
15859
17387
  }
15860
17388
  }
@@ -15862,21 +17390,21 @@ async function runFastStatusCheck(rootDir, agentsDir, manifest) {
15862
17390
  const adapter = getAdapter(tool);
15863
17391
  const paths = await adapter.getOutputPaths(agentsDir, manifest);
15864
17392
  verbose(`${tool}: ${paths.length} output path(s) to check (fast path)`);
15865
- fileLines.push(chalk12.bold(`${tool}:`));
17393
+ fileLines.push(chalk14.bold(`${tool}:`));
15866
17394
  for (const p of paths) {
15867
- const destPath = join37(rootDir, p);
17395
+ const destPath = join39(rootDir, p);
15868
17396
  try {
15869
- const fileStat = await stat10(destPath);
17397
+ const fileStat = await stat12(destPath);
15870
17398
  if (fileStat.mtimeMs > sealMs) {
15871
- fileLines.push(` ${chalk12.yellow("~")} ${p} ${chalk12.dim("(drifted)")}`);
17399
+ fileLines.push(` ${chalk14.yellow("~")} ${p} ${chalk14.dim("(drifted)")}`);
15872
17400
  stats.drifted++;
15873
17401
  } else {
15874
- fileLines.push(` ${chalk12.green("=")} ${p}`);
17402
+ fileLines.push(` ${chalk14.green("=")} ${p}`);
15875
17403
  stats.synced++;
15876
17404
  }
15877
17405
  } catch (err) {
15878
17406
  if (err.code !== "ENOENT") throw err;
15879
- fileLines.push(` ${chalk12.red("+")} ${p} ${chalk12.dim("(missing)")}`);
17407
+ fileLines.push(` ${chalk14.red("+")} ${p} ${chalk14.dim("(missing)")}`);
15880
17408
  stats.missing++;
15881
17409
  }
15882
17410
  }
@@ -15890,23 +17418,23 @@ async function runDeepStatusCheck(rootDir, agentsDir, manifest) {
15890
17418
  const adapter = getAdapter(tool);
15891
17419
  const outputs = await adapter.generate(agentsDir, manifest);
15892
17420
  verbose(`${tool}: ${outputs.length} output file(s) to check`);
15893
- fileLines.push(chalk12.bold(`${tool}:`));
17421
+ fileLines.push(chalk14.bold(`${tool}:`));
15894
17422
  for (const out of outputs) {
15895
- const destPath = join37(rootDir, out.path);
17423
+ const destPath = join39(rootDir, out.path);
15896
17424
  try {
15897
- const existing = await readFile27(destPath, "utf-8");
17425
+ const existing = await readFile29(destPath, "utf-8");
15898
17426
  const existingBlock = extractManagedBlock(existing);
15899
17427
  const expectedBlock = out.managedContent ?? extractManagedBlock(out.content);
15900
17428
  if (existingBlock !== null && expectedBlock !== null ? existingBlock === expectedBlock : existing === out.content) {
15901
- fileLines.push(` ${chalk12.green("=")} ${out.path}`);
17429
+ fileLines.push(` ${chalk14.green("=")} ${out.path}`);
15902
17430
  stats.synced++;
15903
17431
  } else {
15904
- fileLines.push(` ${chalk12.yellow("~")} ${out.path} ${chalk12.dim("(drifted)")}`);
17432
+ fileLines.push(` ${chalk14.yellow("~")} ${out.path} ${chalk14.dim("(drifted)")}`);
15905
17433
  stats.drifted++;
15906
17434
  }
15907
17435
  } catch (err) {
15908
17436
  if (err.code !== "ENOENT") throw err;
15909
- fileLines.push(` ${chalk12.red("+")} ${out.path} ${chalk12.dim("(missing)")}`);
17437
+ fileLines.push(` ${chalk14.red("+")} ${out.path} ${chalk14.dim("(missing)")}`);
15910
17438
  stats.missing++;
15911
17439
  }
15912
17440
  }
@@ -15917,11 +17445,11 @@ async function statusCommand(opts) {
15917
17445
  setVerbose(!!opts?.verbose);
15918
17446
  printBanner(true);
15919
17447
  const rootDir = process.cwd();
15920
- const agentsDir = join37(rootDir, AGENTS_DIR);
17448
+ const agentsDir = join39(rootDir, AGENTS_DIR);
15921
17449
  const manifest = await readManifest(rootDir);
15922
17450
  if (!manifest) {
15923
17451
  error("No .agents/hatch.json found.");
15924
- console.log(chalk12.dim(" Run `npx hatch3r init` to set up your project first.\n"));
17452
+ console.log(chalk14.dim(" Run `npx hatch3r init` to set up your project first.\n"));
15925
17453
  throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
15926
17454
  }
15927
17455
  const spinner = createSpinner("Checking sync status...");
@@ -15948,27 +17476,44 @@ async function statusCommand(opts) {
15948
17476
  }
15949
17477
  console.log();
15950
17478
  const summaryLines = [
15951
- `${chalk12.green("=")} In sync: ${stats.synced}`
17479
+ `${chalk14.green("=")} In sync: ${stats.synced}`
15952
17480
  ];
15953
17481
  if (stats.drifted > 0) {
15954
- summaryLines.push(`${chalk12.yellow("~")} Drifted: ${stats.drifted}`);
17482
+ summaryLines.push(`${chalk14.yellow("~")} Drifted: ${stats.drifted}`);
15955
17483
  }
15956
17484
  if (stats.missing > 0) {
15957
- summaryLines.push(`${chalk12.red("+")} Missing: ${stats.missing}`);
17485
+ summaryLines.push(`${chalk14.red("+")} Missing: ${stats.missing}`);
15958
17486
  }
15959
17487
  const totalChars = await dirCharCount(agentsDir);
15960
17488
  const estimatedTokens = Math.round(totalChars / 4);
15961
17489
  const formattedTokens = estimatedTokens.toLocaleString("en-US");
15962
- summaryLines.push(`${chalk12.dim("~")} Estimated canonical tokens: ~${formattedTokens}`);
17490
+ summaryLines.push(`${chalk14.dim("~")} Estimated canonical tokens: ~${formattedTokens}`);
15963
17491
  if (usedFastPath) {
15964
- summaryLines.push(chalk12.dim("mode: fast (integrity-manifest). Pass --deep for byte-for-byte regeneration check."));
17492
+ summaryLines.push(chalk14.dim("mode: fast (integrity-manifest). Pass --deep for byte-for-byte regeneration check."));
15965
17493
  }
15966
17494
  const style = stats.drifted > 0 || stats.missing > 0 ? "info" : "success";
15967
17495
  printBox("Status", summaryLines, style);
15968
17496
  if (stats.drifted > 0 || stats.missing > 0) {
15969
- info(`Run ${chalk12.bold("hatch3r sync")} to regenerate drifted/missing files.`);
17497
+ info(`Run ${chalk14.bold("hatch3r sync")} to regenerate drifted/missing files.`);
15970
17498
  console.log();
15971
17499
  }
17500
+ const cliSelected = manifest.cliTools?.selected ?? [];
17501
+ if (manifest.cliTools?.enabled && cliSelected.length > 0) {
17502
+ const cliResults = await detectCliTools(cliSelected);
17503
+ const installed = cliResults.filter((r) => r.installed).length;
17504
+ const cliLines = [];
17505
+ cliLines.push(label("Installed", `${installed}/${cliResults.length}`));
17506
+ const missing = cliResults.filter((r) => !r.installed);
17507
+ if (missing.length > 0) {
17508
+ cliLines.push("");
17509
+ for (const r of missing) {
17510
+ cliLines.push(` ${chalk14.yellow("\u2717")} ${r.id} not on PATH`);
17511
+ }
17512
+ cliLines.push("");
17513
+ cliLines.push(chalk14.dim(`Run \`npx hatch3r cli-tools install\` to see install commands.`));
17514
+ }
17515
+ printBox("CLI tools", cliLines, missing.length === 0 ? "success" : "info");
17516
+ }
15972
17517
  let userTypes = null;
15973
17518
  let userTotal = 0;
15974
17519
  let userLastModified = null;
@@ -16006,7 +17551,7 @@ async function statusCommand(opts) {
16006
17551
  printBox("User content", userLines, "info");
16007
17552
  }
16008
17553
  if (manifest.tools.includes("codex")) {
16009
- const overridePath = join37(rootDir, "AGENTS.override.md");
17554
+ const overridePath = join39(rootDir, "AGENTS.override.md");
16010
17555
  try {
16011
17556
  await access12(overridePath);
16012
17557
  warn(
@@ -16022,34 +17567,341 @@ async function statusCommand(opts) {
16022
17567
  if (wsManifest && wsManifest.repos.length > 0) {
16023
17568
  const wsLines = [];
16024
17569
  for (const repo of wsManifest.repos) {
16025
- const icon = repo.sync ? chalk12.green("\u2713") : chalk12.dim("\u25CB");
17570
+ const icon = repo.sync ? chalk14.green("\u2713") : chalk14.dim("\u25CB");
16026
17571
  let detail;
16027
17572
  if (!repo.sync) {
16028
- detail = chalk12.dim("sync disabled");
17573
+ detail = chalk14.dim("sync disabled");
16029
17574
  } else if (repo.lastSync) {
16030
17575
  const elapsed = Math.max(0, Date.now() - new Date(repo.lastSync).getTime());
16031
17576
  const hours = Math.floor(elapsed / (1e3 * 60 * 60));
16032
17577
  const timeAgo = hours < 1 ? "just now" : hours < 24 ? `${hours}h ago` : `${Math.floor(hours / 24)}d ago`;
16033
17578
  detail = `synced ${timeAgo}`;
16034
17579
  } else {
16035
- detail = chalk12.yellow("never synced");
17580
+ detail = chalk14.yellow("never synced");
16036
17581
  }
16037
- const identity = repo.owner && repo.repo ? chalk12.dim(`${repo.owner}/${repo.repo}`) : "";
16038
- const branch = repo.defaultBranch ? chalk12.dim(`[${repo.defaultBranch}]`) : "";
17582
+ const identity = repo.owner && repo.repo ? chalk14.dim(`${repo.owner}/${repo.repo}`) : "";
17583
+ const branch = repo.defaultBranch ? chalk14.dim(`[${repo.defaultBranch}]`) : "";
16039
17584
  const identityPart = identity || branch ? ` ${identity} ${branch}` : "";
16040
- wsLines.push(`${icon} ${repo.name ?? repo.path}${identityPart} ${chalk12.dim(`(${detail})`)}`);
17585
+ wsLines.push(`${icon} ${repo.name ?? repo.path}${identityPart} ${chalk14.dim(`(${detail})`)}`);
16041
17586
  }
16042
17587
  printBox(`Workspace: ${wsManifest.name} (${wsManifest.repos.length} repos)`, wsLines, "info");
16043
17588
  }
16044
17589
  if (manifest.workspace) {
16045
17590
  const wsInfo = [
16046
- `Managed by workspace at ${chalk12.bold(manifest.workspace.rootPath)}`,
17591
+ `Managed by workspace at ${chalk14.bold(manifest.workspace.rootPath)}`,
16047
17592
  `Last synced: ${manifest.workspace.lastSync ? new Date(manifest.workspace.lastSync).toLocaleString() : "never"}`
16048
17593
  ];
16049
17594
  printBox("Workspace member", wsInfo, "info");
16050
17595
  }
16051
17596
  }
16052
17597
 
17598
+ // src/cli/commands/mcp.ts
17599
+ init_hatchJson();
17600
+ init_types();
17601
+ init_mcpEnv();
17602
+ init_ui();
17603
+ import { existsSync as existsSync5 } from "fs";
17604
+ import { readFile as readFile30 } from "fs/promises";
17605
+ import { join as join40 } from "path";
17606
+ import chalk15 from "chalk";
17607
+ function requireManifest(rootDir, manifest) {
17608
+ if (!manifest) {
17609
+ error("No .agents/hatch.json found.");
17610
+ console.log(chalk15.dim(` Run \`npx hatch3r init\` to set up your project first.
17611
+ `));
17612
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
17613
+ }
17614
+ }
17615
+ function wslThemeOrUndefined() {
17616
+ return isWSL() ? { icon: { checked: chalk15.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
17617
+ }
17618
+ async function mcpSetupCommand() {
17619
+ printBanner(true);
17620
+ const rootDir = process.cwd();
17621
+ const manifest = await readManifest(rootDir);
17622
+ requireManifest(rootDir, manifest);
17623
+ const platform = manifest.platform ?? "github";
17624
+ const selected = await pickMcpServers({
17625
+ platform,
17626
+ existing: manifest.mcp.servers,
17627
+ wslTheme: wslThemeOrUndefined()
17628
+ });
17629
+ manifest.mcp = { servers: selected };
17630
+ await writeManifest(rootDir, manifest);
17631
+ if (selected.length > 0) {
17632
+ const envResult = await ensureEnvMcp(rootDir, selected);
17633
+ await ensureGitignoreEntry(rootDir);
17634
+ if (envResult.newVars.length > 0) {
17635
+ warn(`Add new secrets to .env.mcp: ${envResult.newVars.join(", ")}`);
17636
+ info(`Run this then start/restart your editor: ${getSourceEnvMcpCommand()}`);
17637
+ }
17638
+ }
17639
+ printBox(
17640
+ "MCP configured",
17641
+ [
17642
+ label("Servers", selected.length > 0 ? selected.join(", ") : "none"),
17643
+ label("Manifest", ".agents/hatch.json"),
17644
+ label("Next", "Run `npx hatch3r sync` to regenerate adapter MCP configs")
17645
+ ],
17646
+ "success"
17647
+ );
17648
+ }
17649
+ async function mcpListCommand() {
17650
+ printBanner(true);
17651
+ const rootDir = process.cwd();
17652
+ const manifest = await readManifest(rootDir);
17653
+ requireManifest(rootDir, manifest);
17654
+ const servers = manifest.mcp.servers;
17655
+ const envPath = join40(rootDir, ".env.mcp");
17656
+ const hasEnvFile = existsSync5(envPath);
17657
+ const envExisting = hasEnvFile ? parseEnvFile(await readFile30(envPath, "utf-8")) : {};
17658
+ const requiredVars = collectRequiredEnvVars(servers);
17659
+ const missingVars = requiredVars.filter((v) => !(v.name in envExisting) || envExisting[v.name] === "");
17660
+ const lines = [];
17661
+ if (servers.length === 0) {
17662
+ lines.push("(no MCP servers configured)");
17663
+ lines.push("");
17664
+ lines.push("Run `npx hatch3r mcp setup` to open the server picker.");
17665
+ } else {
17666
+ for (const id of servers) {
17667
+ const meta = AVAILABLE_MCP_SERVERS[id];
17668
+ const desc = meta?.description ?? "(unknown server)";
17669
+ lines.push(` ${chalk15.cyan(id)} \u2014 ${desc}`);
17670
+ }
17671
+ lines.push("");
17672
+ lines.push(label(".env.mcp", hasEnvFile ? "present" : chalk15.yellow("missing")));
17673
+ if (requiredVars.length > 0) {
17674
+ lines.push(label("Required vars", requiredVars.map((v) => v.name).join(", ")));
17675
+ if (missingVars.length > 0) {
17676
+ lines.push(label("Missing", chalk15.yellow(missingVars.map((v) => v.name).join(", "))));
17677
+ } else {
17678
+ lines.push(label("Status", chalk15.green("all required vars set")));
17679
+ }
17680
+ }
17681
+ }
17682
+ printBox("MCP servers", lines, "info");
17683
+ }
17684
+ async function mcpRemoveCommand(id) {
17685
+ printBanner(true);
17686
+ const rootDir = process.cwd();
17687
+ const manifest = await readManifest(rootDir);
17688
+ requireManifest(rootDir, manifest);
17689
+ const before = manifest.mcp.servers;
17690
+ if (!before.includes(id)) {
17691
+ error(`MCP server "${id}" is not configured.`);
17692
+ console.log(chalk15.dim(` Current servers: ${before.length > 0 ? before.join(", ") : "(none)"}
17693
+ `));
17694
+ throw new HatchError(`MCP server "${id}" not configured`, 1, "VALIDATION_ERROR");
17695
+ }
17696
+ manifest.mcp = { servers: before.filter((s) => s !== id) };
17697
+ await writeManifest(rootDir, manifest);
17698
+ printBox(
17699
+ "MCP server removed",
17700
+ [
17701
+ label("Removed", id),
17702
+ label("Remaining", manifest.mcp.servers.length > 0 ? manifest.mcp.servers.join(", ") : "none"),
17703
+ label("Next", "Run `npx hatch3r sync` to regenerate adapter MCP configs")
17704
+ ],
17705
+ "success"
17706
+ );
17707
+ }
17708
+ async function mcpEnvCheckCommand() {
17709
+ printBanner(true);
17710
+ const rootDir = process.cwd();
17711
+ const manifest = await readManifest(rootDir);
17712
+ requireManifest(rootDir, manifest);
17713
+ const servers = manifest.mcp.servers;
17714
+ const envPath = join40(rootDir, ".env.mcp");
17715
+ const hasEnvFile = existsSync5(envPath);
17716
+ const envExisting = hasEnvFile ? parseEnvFile(await readFile30(envPath, "utf-8")) : {};
17717
+ const lines = [];
17718
+ if (servers.length === 0) {
17719
+ lines.push("(no MCP servers configured \u2014 nothing to check)");
17720
+ printBox("MCP env check", lines, "info");
17721
+ return;
17722
+ }
17723
+ let missingTotal = 0;
17724
+ for (const id of servers) {
17725
+ const meta = AVAILABLE_MCP_SERVERS[id];
17726
+ const required = meta?.requiresEnv ?? [];
17727
+ if (required.length === 0) {
17728
+ lines.push(`${chalk15.green("\u2713")} ${id} \u2014 no env vars required`);
17729
+ continue;
17730
+ }
17731
+ const missing = required.filter((name) => !(name in envExisting) || envExisting[name] === "");
17732
+ if (missing.length === 0) {
17733
+ lines.push(`${chalk15.green("\u2713")} ${id} \u2014 ${required.join(", ")}`);
17734
+ } else {
17735
+ lines.push(`${chalk15.yellow("!")} ${id} \u2014 missing: ${missing.join(", ")}`);
17736
+ missingTotal += missing.length;
17737
+ }
17738
+ }
17739
+ lines.push("");
17740
+ lines.push(label(".env.mcp", hasEnvFile ? "present" : chalk15.yellow("missing")));
17741
+ if (missingTotal > 0) {
17742
+ lines.push(label("Action", `Fill ${missingTotal} env var(s) in .env.mcp, then \`${getSourceEnvMcpCommand()}\``));
17743
+ }
17744
+ printBox("MCP env check", lines, missingTotal > 0 ? "info" : "success");
17745
+ }
17746
+
17747
+ // src/cli/commands/cliTools.ts
17748
+ init_hatchJson();
17749
+ init_types();
17750
+ import chalk16 from "chalk";
17751
+ init_ui();
17752
+ function requireManifest2(_rootDir, manifest) {
17753
+ if (!manifest) {
17754
+ error("No .agents/hatch.json found.");
17755
+ console.log(chalk16.dim(` Run \`npx hatch3r init\` to set up your project first.
17756
+ `));
17757
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
17758
+ }
17759
+ }
17760
+ function wslThemeOrUndefined2() {
17761
+ return isWSL() ? { icon: { checked: chalk16.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
17762
+ }
17763
+ async function cliToolsCommand() {
17764
+ printBanner(true);
17765
+ const rootDir = process.cwd();
17766
+ const manifest = await readManifest(rootDir);
17767
+ requireManifest2(rootDir, manifest);
17768
+ const existing = manifest.cliTools?.selected ?? [];
17769
+ const selected = await pickCliTools({
17770
+ existing,
17771
+ wslTheme: wslThemeOrUndefined2()
17772
+ });
17773
+ if (selected.length > 0) {
17774
+ const spinner = createSpinner(`Detecting ${selected.length} CLI tool(s)...`);
17775
+ spinner.start();
17776
+ const missing = await findMissingCliTools(selected);
17777
+ if (missing.length === 0) {
17778
+ spinner.succeed(`All ${selected.length} CLI tool(s) detected on PATH`);
17779
+ } else {
17780
+ spinner.warn(`${selected.length - missing.length}/${selected.length} CLI tool(s) detected; ${missing.length} missing`);
17781
+ await offerInstaller(missing, { interactive: true });
17782
+ }
17783
+ const secretNotes = [];
17784
+ for (const id of selected) {
17785
+ const notes = CLI_TOOL_SECRET_NOTES[id];
17786
+ if (notes && notes.length > 0) {
17787
+ secretNotes.push(`${id}: ${notes.join(", ")}`);
17788
+ }
17789
+ }
17790
+ if (secretNotes.length > 0) {
17791
+ info(chalk16.dim("CLI tool environment variables required:"));
17792
+ for (const note of secretNotes) {
17793
+ info(chalk16.dim(` ${note}`));
17794
+ }
17795
+ }
17796
+ }
17797
+ const cliToolsConfig = {
17798
+ enabled: selected.length > 0,
17799
+ selected
17800
+ };
17801
+ manifest.cliTools = cliToolsConfig;
17802
+ await writeManifest(rootDir, manifest);
17803
+ printBox(
17804
+ "CLI tools configured",
17805
+ [
17806
+ label("Selected", selected.length > 0 ? selected.join(", ") : "none"),
17807
+ label("Manifest", ".agents/hatch.json"),
17808
+ label("Next", "Run `npx hatch3r sync` so adapters re-emit the filtered skill set")
17809
+ ],
17810
+ "success"
17811
+ );
17812
+ if (selected.length > 0) {
17813
+ const finalMissing = await findMissingCliTools(selected);
17814
+ printMissingCliToolsDisclaimer(finalMissing, selected.length);
17815
+ }
17816
+ }
17817
+ async function cliToolsListCommand() {
17818
+ printBanner(true);
17819
+ const rootDir = process.cwd();
17820
+ const manifest = await readManifest(rootDir);
17821
+ requireManifest2(rootDir, manifest);
17822
+ const selected = manifest.cliTools?.selected ?? [];
17823
+ if (selected.length === 0) {
17824
+ printBox(
17825
+ "CLI tools",
17826
+ [
17827
+ "(no CLI tools selected)",
17828
+ "",
17829
+ "Run `npx hatch3r cli-tools` to open the picker."
17830
+ ],
17831
+ "info"
17832
+ );
17833
+ return;
17834
+ }
17835
+ const results = await detectCliTools(selected);
17836
+ const lines = [];
17837
+ for (const r of results) {
17838
+ const meta = AVAILABLE_CLI_TOOLS[r.id];
17839
+ const tierLabel = meta ? `tier ${meta.tier}` : "(unknown)";
17840
+ const status = r.installed ? `${chalk16.green("\u2713")} ${r.path}` : `${chalk16.yellow("\u2717")} not on PATH`;
17841
+ lines.push(` ${chalk16.cyan(r.id)} (${tierLabel}) \u2014 ${status}`);
17842
+ }
17843
+ const installed = results.filter((r) => r.installed).length;
17844
+ lines.push("");
17845
+ lines.push(label("Detected", `${installed}/${results.length} installed`));
17846
+ printBox("CLI tools", lines, installed === results.length ? "success" : "info");
17847
+ }
17848
+ async function cliToolsInstallCommand() {
17849
+ printBanner(true);
17850
+ const rootDir = process.cwd();
17851
+ const manifest = await readManifest(rootDir);
17852
+ requireManifest2(rootDir, manifest);
17853
+ const selected = manifest.cliTools?.selected ?? [];
17854
+ if (selected.length === 0) {
17855
+ info("No CLI tools selected \u2014 run `npx hatch3r cli-tools` first to opt in.");
17856
+ return;
17857
+ }
17858
+ const spinner = createSpinner(`Detecting ${selected.length} CLI tool(s)...`);
17859
+ spinner.start();
17860
+ const missing = await findMissingCliTools(selected);
17861
+ if (missing.length === 0) {
17862
+ spinner.succeed(`All ${selected.length} CLI tool(s) already installed`);
17863
+ return;
17864
+ }
17865
+ spinner.warn(`${missing.length}/${selected.length} CLI tool(s) missing`);
17866
+ await offerInstaller(missing, { interactive: true });
17867
+ if (selected.length > 0) {
17868
+ const finalMissing = await findMissingCliTools(selected);
17869
+ printMissingCliToolsDisclaimer(finalMissing, selected.length);
17870
+ }
17871
+ }
17872
+ async function cliToolsDetectCommand() {
17873
+ printBanner(true);
17874
+ const rootDir = process.cwd();
17875
+ const manifest = await readManifest(rootDir);
17876
+ requireManifest2(rootDir, manifest);
17877
+ const selected = manifest.cliTools?.selected ?? [];
17878
+ if (selected.length === 0) {
17879
+ info("No CLI tools selected \u2014 run `npx hatch3r cli-tools` first to opt in.");
17880
+ return;
17881
+ }
17882
+ const results = await detectCliTools(selected);
17883
+ const lines = [];
17884
+ let installed = 0;
17885
+ for (const r of results) {
17886
+ if (r.installed) {
17887
+ lines.push(` ${chalk16.green("\u2713")} ${r.id} \u2014 ${r.path}`);
17888
+ installed++;
17889
+ } else {
17890
+ lines.push(` ${chalk16.yellow("\u2717")} ${r.id} \u2014 not on PATH`);
17891
+ }
17892
+ }
17893
+ lines.push("");
17894
+ lines.push(label("Installed", `${installed}/${results.length}`));
17895
+ if (installed < results.length) {
17896
+ lines.push("");
17897
+ lines.push(`Run ${chalk16.bold("npx hatch3r cli-tools install")} to see install commands.`);
17898
+ }
17899
+ printBox("CLI tool detection", lines, installed === results.length ? "success" : "info");
17900
+ if (installed < results.length) {
17901
+ warn(`${results.length - installed} CLI tool(s) not detected`);
17902
+ }
17903
+ }
17904
+
16053
17905
  // src/cli/program.ts
16054
17906
  init_version();
16055
17907
  init_types();
@@ -16097,7 +17949,7 @@ function createProgram() {
16097
17949
  program2.command("init").description("Install a complete agent setup into the current repo (first-run: creates .agents/ directory)").option(
16098
17950
  "--tools <tools>",
16099
17951
  `Comma-separated tools (${TOOL_CHOICES})`
16100
- ).option("--yes", "Skip interactive prompts, use defaults").option("--quick", "Skip all prompts and use smart defaults (alias for --yes)").option("--default", "Skip all prompts and use smart defaults (alias for --yes)").option("--preset <preset>", "Content preset: minimal, standard, full (default: full)").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").option("--worktree", "Enable git worktree file isolation (overrides tool auto-detect)").option("--no-worktree", "Disable git worktree file isolation").option("--workspace", "Initialize as a multi-repo workspace").action(initCommand);
17952
+ ).option("--yes", "Skip interactive prompts, use defaults").option("--quick", "Skip all prompts and use smart defaults (alias for --yes)").option("--default", "Skip all prompts and use smart defaults (alias for --yes)").option("--preset <preset>", "Content preset: minimal, standard, full (default: full)").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").option("--worktree", "Enable git worktree file isolation (overrides tool auto-detect)").option("--no-worktree", "Disable git worktree file isolation").option("--workspace", "Initialize as a multi-repo workspace").option("--cli-tools <ids>", "CLI tools to opt in on --yes: 'tier1', 'all', or comma-separated ids (default: tier-1 + triggered tier-2)").option("--no-cli-tools", "Skip the CLI-tools opt-in on --yes").option("--mcp", "Re-opt-in to MCP servers on --yes (MCP is now opt-in by default)").action(initCommand);
16101
17953
  program2.command("sync").description("Re-generate tool outputs from canonical .agents/ state (run after editing .agents/)").option("--repos [paths...]", "Sync workspace content to sub-repos (all opted-in if no paths given)").option("--dry-run", "Show what would change without modifying files").option("--diff", "Show a before/after diff summary for each generated file").option("--force", "Overwrite locally modified files in sub-repos").option("--minimal", "Generate stripped-down output (no comments, minimal formatting) to reduce token usage").option("--strict-budget", "Fail sync if any adapter's generated output exceeds its context budget (default: warn)").option("--verbose", "Show detailed output for each file processed").action(syncCommand);
16102
17954
  program2.command("status").description("Check sync status between canonical .agents/ and generated files").option("--verbose", "Show detailed per-file status information").option("--deep", "Regenerate every adapter's output in-memory to compare byte-for-byte (slower; default uses integrity-manifest fast path)").action(statusCommand);
16103
17955
  program2.command("update").description("Pull latest hatch3r templates with safe merge (preserves customizations)").option("--yes", "Skip interactive prompts, use defaults").option("--diff", "Show a before/after diff summary for each generated file").option("--force", "Override the preflight integrity check and proceed despite drift").option("--offline, --skip-fetch", "Skip the package fetch step; regenerate only from already-installed canonical content").option("--dry-run", "Preview what would change (added/modified/unchanged per adapter) without writing files").action(updateCommand);
@@ -16128,6 +17980,15 @@ function createProgram() {
16128
17980
  ).action(addCommand);
16129
17981
  program2.command("worktree-setup [name]").description("Create a git worktree by name and populate hatch3r files (auto-resolved to .worktrees/<name>)").option("--from <path>", "Main repo path (auto-detected by default)").option("--from-path <path>", "Legacy mode: populate an existing worktree at <path> (skips git worktree add). Used by editor hooks.").option("--dry-run", "Show what would be done without changes").option("--force", "Overwrite existing files in the worktree").option("--yes", "Skip the secret-propagation confirmation prompt").action(worktreeSetupCommand);
16130
17982
  program2.command("worktree-cleanup").description("Discover hatch3r-managed worktrees from the main repo, then clean files and remove the selected worktree(s)").option("--dry-run", "Show what would be done without changes").option("--all", "Skip the all/specific prompt and clean every hatch3r-managed worktree").option("--yes", "Skip selection and confirmation prompts (implies --all unless paths are filtered upstream)").option("--files-only", "Remove hatch3r-managed files only; keep the git worktree and its directory").action(worktreeCleanupCommand);
17983
+ const mcpCmd = program2.command("mcp").description("Manage MCP servers (now opt-in; CLI tools are the default)");
17984
+ mcpCmd.command("setup").description("Open the MCP server picker and update the manifest + .env.mcp").action(mcpSetupCommand);
17985
+ mcpCmd.command("list").description("Show current MCP server configuration plus .env.mcp status").action(mcpListCommand);
17986
+ mcpCmd.command("remove <id>").description("Remove an MCP server by id").action(mcpRemoveCommand);
17987
+ mcpCmd.command("env-check").description("Audit .env.mcp for missing required environment variables").action(mcpEnvCheckCommand);
17988
+ const cliCmd = program2.command("cli-tools").description("Manage CLI tool integrations (ripgrep, jq, gh, \u2026)").action(cliToolsCommand);
17989
+ cliCmd.command("list").description("Show current CLI tool selection plus detection status").action(cliToolsListCommand);
17990
+ cliCmd.command("install").description("Print install commands for any selected CLI tools missing on PATH").action(cliToolsInstallCommand);
17991
+ cliCmd.command("detect").description("Read-only detection report for the current CLI tool selection").action(cliToolsDetectCommand);
16131
17992
  program2.on("command:*", (operands) => {
16132
17993
  const cmd = operands[0];
16133
17994
  if (cmd && AGENT_COMMAND_NAMES.has(cmd)) {
@@ -16177,15 +18038,15 @@ function classifyCliError(err, flags) {
16177
18038
  // src/cli/shared/updateNotifier.ts
16178
18039
  init_version();
16179
18040
  import updateNotifier from "update-notifier";
16180
- import chalk13 from "chalk";
18041
+ import chalk17 from "chalk";
16181
18042
  var ONE_DAY_MS = 1e3 * 60 * 60 * 24;
16182
18043
  function buildMessage(update) {
16183
- const fromTo = `${chalk13.dim(update.current)} \u2192 ${chalk13.green(update.latest)}`;
16184
- const cyan = chalk13.hex("#06b6d4");
18044
+ const fromTo = `${chalk17.dim(update.current)} \u2192 ${chalk17.green(update.latest)}`;
18045
+ const cyan = chalk17.hex("#06b6d4");
16185
18046
  return [
16186
18047
  `Update available ${fromTo}`,
16187
- chalk13.dim("Run ") + cyan("npm i -g hatch3r@latest") + chalk13.dim(" to update the CLI"),
16188
- chalk13.dim("Then run ") + cyan("hatch3r update") + chalk13.dim(" to refresh project content")
18048
+ chalk17.dim("Run ") + cyan("npm i -g hatch3r@latest") + chalk17.dim(" to update the CLI"),
18049
+ chalk17.dim("Then run ") + cyan("hatch3r update") + chalk17.dim(" to refresh project content")
16189
18050
  ].join("\n");
16190
18051
  }
16191
18052
  function checkForUpdates() {