hatch3r 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +38 -1
  2. package/agents/hatch3r-a11y-auditor.md +7 -14
  3. package/agents/hatch3r-architect.md +7 -14
  4. package/agents/hatch3r-ci-watcher.md +7 -13
  5. package/agents/hatch3r-context-rules.md +5 -10
  6. package/agents/hatch3r-dependency-auditor.md +10 -19
  7. package/agents/hatch3r-devops.md +7 -16
  8. package/agents/hatch3r-docs-writer.md +7 -14
  9. package/agents/hatch3r-fixer.md +2 -8
  10. package/agents/hatch3r-implementer.md +2 -8
  11. package/agents/hatch3r-learnings-loader.md +150 -21
  12. package/agents/hatch3r-lint-fixer.md +7 -12
  13. package/agents/hatch3r-perf-profiler.md +7 -14
  14. package/agents/hatch3r-researcher.md +7 -14
  15. package/agents/hatch3r-reviewer.md +7 -13
  16. package/agents/hatch3r-security-auditor.md +7 -15
  17. package/agents/hatch3r-test-writer.md +7 -14
  18. package/agents/modes/architecture.md +44 -0
  19. package/agents/modes/boundary-analysis.md +45 -0
  20. package/agents/modes/codebase-impact.md +81 -0
  21. package/agents/modes/complexity-risk.md +40 -0
  22. package/agents/modes/coverage-analysis.md +44 -0
  23. package/agents/modes/current-state.md +52 -0
  24. package/agents/modes/feature-design.md +39 -0
  25. package/agents/modes/impact-analysis.md +45 -0
  26. package/agents/modes/library-docs.md +31 -0
  27. package/agents/modes/migration-path.md +55 -0
  28. package/agents/modes/prior-art.md +31 -0
  29. package/agents/modes/refactoring-strategy.md +55 -0
  30. package/agents/modes/regression.md +45 -0
  31. package/agents/modes/requirements-elicitation.md +68 -0
  32. package/agents/modes/risk-assessment.md +41 -0
  33. package/agents/modes/risk-prioritization.md +43 -0
  34. package/agents/modes/root-cause.md +39 -0
  35. package/agents/modes/similar-implementation.md +70 -0
  36. package/agents/modes/symptom-trace.md +39 -0
  37. package/agents/modes/test-pattern.md +61 -0
  38. package/agents/shared/external-knowledge.md +32 -0
  39. package/agents/shared/quality-charter.md +78 -0
  40. package/commands/board/pickup-azure-devops.md +4 -0
  41. package/commands/board/pickup-delegation-multi.md +3 -0
  42. package/commands/board/pickup-delegation.md +3 -0
  43. package/commands/board/pickup-github.md +4 -0
  44. package/commands/board/pickup-gitlab.md +4 -0
  45. package/commands/board/pickup-post-impl.md +8 -1
  46. package/commands/board/shared-azure-devops.md +13 -3
  47. package/commands/board/shared-github.md +1 -0
  48. package/commands/board/shared-gitlab.md +9 -2
  49. package/commands/hatch3r-agent-customize.md +5 -1
  50. package/commands/hatch3r-board-groom.md +55 -2
  51. package/commands/hatch3r-board-init.md +5 -2
  52. package/commands/hatch3r-board-shared.md +62 -2
  53. package/commands/hatch3r-command-customize.md +4 -0
  54. package/commands/hatch3r-context-health.md +22 -2
  55. package/commands/hatch3r-cost-tracking.md +14 -0
  56. package/commands/hatch3r-hooks.md +1 -1
  57. package/commands/hatch3r-learn.md +68 -2
  58. package/commands/hatch3r-quick-change.md +29 -3
  59. package/commands/hatch3r-revision.md +136 -16
  60. package/commands/hatch3r-rule-customize.md +4 -0
  61. package/commands/hatch3r-skill-customize.md +4 -0
  62. package/commands/hatch3r-workflow.md +10 -1
  63. package/dist/cli/index.js +2528 -640
  64. package/dist/cli/index.js.map +1 -1
  65. package/package.json +12 -9
  66. package/rules/hatch3r-agent-orchestration-detail.md +159 -0
  67. package/rules/hatch3r-agent-orchestration-detail.mdc +156 -0
  68. package/rules/hatch3r-agent-orchestration.md +91 -318
  69. package/rules/hatch3r-agent-orchestration.mdc +127 -149
  70. package/rules/hatch3r-code-standards.mdc +10 -2
  71. package/rules/hatch3r-component-conventions.mdc +0 -1
  72. package/rules/hatch3r-deep-context.mdc +30 -8
  73. package/rules/hatch3r-dependency-management.mdc +17 -5
  74. package/rules/hatch3r-i18n.mdc +0 -1
  75. package/rules/hatch3r-migrations.mdc +12 -1
  76. package/rules/hatch3r-observability.mdc +289 -0
  77. package/rules/hatch3r-security-patterns.mdc +11 -0
  78. package/rules/hatch3r-testing.mdc +1 -1
  79. package/rules/hatch3r-theming.mdc +0 -1
  80. package/rules/hatch3r-tooling-hierarchy.mdc +18 -4
  81. package/skills/hatch3r-agent-customize/SKILL.md +4 -72
  82. package/skills/hatch3r-command-customize/SKILL.md +4 -62
  83. package/skills/hatch3r-customize/SKILL.md +117 -0
  84. package/skills/hatch3r-dep-audit/SKILL.md +1 -1
  85. package/skills/hatch3r-rule-customize/SKILL.md +4 -65
  86. package/skills/hatch3r-skill-customize/SKILL.md +4 -62
package/dist/cli/index.js CHANGED
@@ -12,7 +12,7 @@ import ora from "ora";
12
12
  import boxen from "boxen";
13
13
 
14
14
  // src/version.ts
15
- var HATCH3R_VERSION = "1.2.0";
15
+ var HATCH3R_VERSION = "1.4.0";
16
16
 
17
17
  // src/cli/shared/ui.ts
18
18
  var CYAN = chalk.hex("#06b6d4");
@@ -132,7 +132,7 @@ import { execFileSync as execFileSync2 } from "child_process";
132
132
  import chalk3 from "chalk";
133
133
 
134
134
  // src/types.ts
135
- var TOOLS = ["cursor", "copilot", "claude", "opencode", "windsurf", "amp", "codex", "gemini", "cline", "aider", "kiro", "goose", "zed", "amazon-q"];
135
+ var TOOLS = ["cursor", "copilot", "claude", "opencode", "windsurf", "amp", "codex", "gemini", "cline", "aider", "kiro", "goose", "zed", "amazon-q", "antigravity"];
136
136
  var VALID_TOOLS = new Set(TOOLS);
137
137
  var TOOL_CHOICES = TOOLS.join(", ");
138
138
  var MANAGED_BLOCK_START = "<!-- HATCH3R:BEGIN -->";
@@ -140,9 +140,10 @@ var MANAGED_BLOCK_END = "<!-- HATCH3R:END -->";
140
140
  var HATCH3R_PREFIX = "hatch3r-";
141
141
  var AGENTS_DIR = ".agents";
142
142
  var HatchError = class extends Error {
143
- constructor(message, exitCode = 1) {
143
+ constructor(message, exitCode = 1, errorCode = "UNKNOWN_ERROR") {
144
144
  super(message);
145
145
  this.exitCode = exitCode;
146
+ this.errorCode = errorCode;
146
147
  this.name = "HatchError";
147
148
  }
148
149
  };
@@ -275,8 +276,8 @@ async function resolvePatterns(rootDir, patterns) {
275
276
  }
276
277
  function isInsideWorktree(dir) {
277
278
  try {
278
- const stat5 = statSync(join(dir, ".git"));
279
- return stat5.isFile();
279
+ const stat6 = statSync(join(dir, ".git"));
280
+ return stat6.isFile();
280
281
  } catch {
281
282
  return false;
282
283
  }
@@ -356,6 +357,9 @@ var ADAPTER_WORKTREE_PATTERNS = {
356
357
  ],
357
358
  "amazon-q": [
358
359
  { pattern: ".amazonq/", strategy: "copy", reason: "Amazon Q adapter output (rules, settings)" }
360
+ ],
361
+ antigravity: [
362
+ { pattern: ".antigravity/", strategy: "copy", reason: "Antigravity adapter output (rules, skills, settings)" }
359
363
  ]
360
364
  };
361
365
  async function generateWorktreeInclude(manifest, rootDir) {
@@ -535,7 +539,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
535
539
  error("Worktree path is required when running from the main repo.");
536
540
  console.log(chalk3.dim(" Usage: hatch3r worktree-setup <worktree-path>"));
537
541
  console.log(chalk3.dim(" Or run this command from inside a worktree.\n"));
538
- throw new HatchError("Missing worktree path", 1);
542
+ throw new HatchError("Missing worktree path", 1, "VALIDATION_ERROR");
539
543
  }
540
544
  targetRoot = join3(cwd, worktreePath);
541
545
  }
@@ -547,7 +551,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
547
551
  if (err.code === "ENOENT") {
548
552
  error(`No ${WORKTREE_INCLUDE_FILE} found in ${mainRoot}`);
549
553
  console.log(chalk3.dim(" Run `hatch3r init` or `hatch3r sync` to generate it.\n"));
550
- throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1);
554
+ throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1, "FS_ERROR");
551
555
  }
552
556
  throw err;
553
557
  }
@@ -610,8 +614,9 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
610
614
  }
611
615
 
612
616
  // src/cli/commands/config.ts
613
- import { fileURLToPath as fileURLToPath2 } from "url";
614
- import { dirname as dirname9, join as join17 } from "path";
617
+ import { fileURLToPath as fileURLToPath3 } from "url";
618
+ import { readFile as readFile17 } from "fs/promises";
619
+ import { dirname as dirname11, join as join21 } from "path";
615
620
  import chalk5 from "chalk";
616
621
  import inquirer2 from "inquirer";
617
622
 
@@ -627,7 +632,8 @@ import {
627
632
  access,
628
633
  rename,
629
634
  unlink as unlink2,
630
- open
635
+ open,
636
+ copyFile as copyFile2
631
637
  } from "fs/promises";
632
638
  import { dirname as dirname3, basename } from "path";
633
639
  import { randomBytes as randomBytes2 } from "crypto";
@@ -774,7 +780,16 @@ var DENY_PATTERNS = [
774
780
  /override\s+(all\s+)?security/i,
775
781
  /(?:atob|Buffer\.from)\s*\([^)]*(?:eval|exec|require)/i,
776
782
  /(?:chmod|chown)\s+[0-7]{3,4}/i,
777
- /(?:api[_-]?key|password|token|secret)\s*[:=]\s*.{8,}/i
783
+ /(?:api[_-]?key|password|token|secret)\s*[:=]\s*.{8,}/i,
784
+ // Prompt injection indicators
785
+ /ignore\s+(all\s+)?previous\s+instructions/i,
786
+ /disregard\s+(all\s+)?(previous|prior|above)/i,
787
+ /you\s+are\s+now\s+(?:a|an|the)\s/i,
788
+ /new\s+instructions\s*:/i,
789
+ /system\s+prompt\s*:/i,
790
+ /forget\s+(all\s+)?(previous|prior|above)\s+(instructions|rules|context)/i,
791
+ /act\s+as\s+(?:a|an)\s+(?:unrestricted|unfiltered|jailbroken)/i,
792
+ /do\s+not\s+follow\s+(?:any|the|your)\s+(?:previous|prior|above|original)\s/i
778
793
  ];
779
794
  var ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\uFEFF\u00AD]/g;
780
795
  var MAX_CUSTOMIZE_MD_BYTES = 10240;
@@ -966,6 +981,14 @@ async function atomicWriteFile(filePath, content) {
966
981
  throw err;
967
982
  }
968
983
  }
984
+ } catch (err) {
985
+ const code = err.code;
986
+ if (code === "ENOSPC") {
987
+ throw new Error(
988
+ `Not enough disk space to write ${filePath}. Free up space and re-run the command.`
989
+ );
990
+ }
991
+ throw err;
969
992
  } finally {
970
993
  try {
971
994
  await unlink2(tmpPath);
@@ -991,7 +1014,7 @@ async function safeWriteFile(filePath, content, options = {}) {
991
1014
  return {
992
1015
  path: filePath,
993
1016
  action: "skipped",
994
- warning: `Skipped ${filePath}: existing file without managed block. Run hatch3r init --force to overwrite.`
1017
+ warning: `Skipped ${filePath}: managed block markers (HATCH3R:BEGIN/END) missing. To fix: restore the markers around hatch3r content, or move your custom content and re-run hatch3r update.`
995
1018
  };
996
1019
  }
997
1020
  const customContent = extractCustomContent(existingContent);
@@ -1000,11 +1023,13 @@ async function safeWriteFile(filePath, content, options = {}) {
1000
1023
  try {
1001
1024
  merged = insertManagedBlock(existingContent, options.managedContent);
1002
1025
  } catch {
1026
+ const bakPath = filePath + ".bak";
1027
+ await copyFile2(filePath, bakPath);
1003
1028
  await atomicWriteFile(filePath, content);
1004
1029
  return {
1005
1030
  path: filePath,
1006
1031
  action: "updated",
1007
- warning: `Auto-repaired corrupted managed block in ${filePath}`
1032
+ warning: `Auto-repaired corrupted managed block in ${filePath} (backup saved to ${bakPath})`
1008
1033
  };
1009
1034
  }
1010
1035
  await atomicWriteFile(filePath, merged);
@@ -1023,7 +1048,7 @@ async function safeWriteFile(filePath, content, options = {}) {
1023
1048
  return {
1024
1049
  path: filePath,
1025
1050
  action: "skipped",
1026
- warning: `Skipped ${filePath}: existing file without managed block. Run hatch3r init --force to overwrite.`
1051
+ warning: `Skipped ${filePath}: managed block markers (HATCH3R:BEGIN/END) missing. To fix: restore the markers around hatch3r content, or move your custom content and re-run hatch3r update.`
1027
1052
  };
1028
1053
  }
1029
1054
 
@@ -1137,6 +1162,22 @@ function validateManifest(data) {
1137
1162
  if (!specs.paths.every((v) => typeof v === "string")) return false;
1138
1163
  if (specs.lastGenerated !== void 0 && typeof specs.lastGenerated !== "string") return false;
1139
1164
  }
1165
+ if (obj.workspace !== void 0) {
1166
+ if (typeof obj.workspace !== "object" || obj.workspace === null) return false;
1167
+ const ws = obj.workspace;
1168
+ if (typeof ws.rootPath !== "string") return false;
1169
+ if (typeof ws.lastSync !== "string") return false;
1170
+ if (typeof ws.syncVersion !== "string") return false;
1171
+ if (typeof ws.workspaceChecksum !== "string") return false;
1172
+ if (ws.excludedContent !== void 0) {
1173
+ if (!Array.isArray(ws.excludedContent)) return false;
1174
+ if (!ws.excludedContent.every((v) => typeof v === "string")) return false;
1175
+ }
1176
+ if (ws.localContent !== void 0) {
1177
+ if (!Array.isArray(ws.localContent)) return false;
1178
+ if (!ws.localContent.every((v) => typeof v === "string")) return false;
1179
+ }
1180
+ }
1140
1181
  return true;
1141
1182
  }
1142
1183
  async function readManifest(rootDir) {
@@ -1294,10 +1335,10 @@ async function ensureEnvMcp(rootDir, servers) {
1294
1335
  }
1295
1336
 
1296
1337
  // src/cli/commands/update.ts
1297
- import { cp as cp2, mkdir as mkdir4, readdir as readdir6, stat } from "fs/promises";
1338
+ import { cp as cp3, mkdir as mkdir5, readdir as readdir7, stat as stat2 } from "fs/promises";
1298
1339
  import { execFileSync as execFileSync3 } from "child_process";
1299
1340
  import { fileURLToPath } from "url";
1300
- import { dirname as dirname7, join as join15 } from "path";
1341
+ import { dirname as dirname8, join as join16 } from "path";
1301
1342
  import chalk4 from "chalk";
1302
1343
  import inquirer from "inquirer";
1303
1344
 
@@ -1397,7 +1438,7 @@ async function readGlobMd(baseDir, fileType) {
1397
1438
  let entries;
1398
1439
  try {
1399
1440
  const all = await readdir(baseDir, { recursive: true });
1400
- entries = all.filter((f) => f.endsWith(".md"));
1441
+ entries = all.filter((f) => f.endsWith(".md")).sort();
1401
1442
  } catch (err) {
1402
1443
  if (err.code !== "ENOENT") throw err;
1403
1444
  return [];
@@ -1433,7 +1474,7 @@ async function readGlobMd(baseDir, fileType) {
1433
1474
  async function readSkillSubdirs(baseDir) {
1434
1475
  let dirents;
1435
1476
  try {
1436
- dirents = await readdir(baseDir, { withFileTypes: true });
1477
+ dirents = (await readdir(baseDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1437
1478
  } catch (err) {
1438
1479
  if (err.code !== "ENOENT") throw err;
1439
1480
  return [];
@@ -1517,7 +1558,16 @@ Full protocol: \`hatch3r-agent-orchestration\` rule in \`/.agents/rules/\`.
1517
1558
  - Rules: \`/.agents/rules/\` \u2014 Agents: \`/.agents/agents/\` \u2014 Skills: \`/.agents/skills/\`
1518
1559
  - Commands: \`/.agents/commands/\` \u2014 MCP: \`/.agents/mcp/mcp.json\` \u2014 Policy: \`/.agents/policy/\`
1519
1560
 
1520
- Do not edit \`hatch3r-\` prefixed files \u2014 managed by hatch3r, overwritten on update.`;
1561
+ Do not edit \`hatch3r-\` prefixed files \u2014 managed by hatch3r, overwritten on update.
1562
+
1563
+ ## Getting Started (staged introduction)
1564
+
1565
+ New to hatch3r? Start here and expand as you go:
1566
+
1567
+ **Day 1 \u2014 Core workflow:** Use the 4-phase pipeline above for any task. Start by invoking \`hatch3r-researcher\` for context, then \`hatch3r-implementer\` for changes.
1568
+ **Week 1 \u2014 Skills & commands:** Load skills from \`/.agents/skills/\` matching your task type. Try \`/hatch3r-feature\` or \`/hatch3r-bug-fix\` commands.
1569
+ **Week 2 \u2014 Board & team:** If using project management, run \`/hatch3r-board-init\` to set up your board. Use \`/hatch3r-board-pickup\` for structured delivery.
1570
+ **Ongoing \u2014 Customization:** Override agent behavior via \`.hatch3r/{type}/{id}.customize.yaml\`. Add project learnings to \`/.agents/learnings/\`.`;
1521
1571
  async function generateBridgeOrchestration(agentsDir) {
1522
1572
  const skills = await readSkillDirs(join8(agentsDir, "skills"));
1523
1573
  if (skills.length === 0) return BRIDGE_ORCHESTRATION;
@@ -1897,13 +1947,14 @@ var BaseAdapter = class {
1897
1947
  * Adapters that violate these invariants will produce broken output files or
1898
1948
  * corrupt user content during the merge phase.
1899
1949
  */
1900
- async generate(agentsDir, manifest) {
1950
+ async generate(agentsDir, manifest, generationMode = "standard") {
1901
1951
  this.warnings = [];
1902
1952
  return this.doGenerate({
1903
1953
  agentsDir,
1904
1954
  manifest,
1905
1955
  features: manifest.features,
1906
- projectRoot: dirname4(agentsDir)
1956
+ projectRoot: dirname4(agentsDir),
1957
+ generationMode
1907
1958
  });
1908
1959
  }
1909
1960
  /**
@@ -1915,8 +1966,19 @@ var BaseAdapter = class {
1915
1966
  const outputs = await this.generate(agentsDir, manifest);
1916
1967
  return outputs.map((o) => o.path);
1917
1968
  }
1918
- async bridgeHeader(agentsDir, agentsPath = "/.agents/AGENTS.md") {
1919
- const orchestration = await generateBridgeOrchestration(agentsDir);
1969
+ async bridgeHeader(ctx, agentsPath = "/.agents/AGENTS.md") {
1970
+ const orchestration = await generateBridgeOrchestration(ctx.agentsDir);
1971
+ if (this.isMinimal(ctx)) {
1972
+ return [
1973
+ "",
1974
+ "# Hatch3r Agent Instructions",
1975
+ "",
1976
+ `Instructions: \`${agentsPath}\``,
1977
+ "",
1978
+ this.stripMinimal(orchestration),
1979
+ ""
1980
+ ];
1981
+ }
1920
1982
  return [
1921
1983
  "",
1922
1984
  "# Hatch3r Agent Instructions",
@@ -1931,12 +1993,17 @@ var BaseAdapter = class {
1931
1993
  if (!ctx.features.rules) return [];
1932
1994
  const lines = [];
1933
1995
  const rules = await readCanonicalFiles(ctx.agentsDir, "rules");
1996
+ const minimal = this.isMinimal(ctx);
1934
1997
  for (const rule of rules) {
1935
1998
  const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, rule);
1936
1999
  this.warnings.push(...warnings);
1937
2000
  if (skip) continue;
1938
2001
  const desc = overrides.description ?? rule.description;
1939
- lines.push(`## ${rule.id}`, "", desc, "", content, "");
2002
+ if (minimal) {
2003
+ lines.push(`## ${rule.id}`, "", this.stripMinimal(content), "");
2004
+ } else {
2005
+ lines.push(`## ${rule.id}`, "", desc, "", content, "");
2006
+ }
1940
2007
  }
1941
2008
  return lines;
1942
2009
  }
@@ -1944,6 +2011,7 @@ var BaseAdapter = class {
1944
2011
  if (!ctx.features.agents) return [];
1945
2012
  const lines = [];
1946
2013
  const agents = await readCanonicalFiles(ctx.agentsDir, "agents");
2014
+ const minimal = this.isMinimal(ctx);
1947
2015
  for (const agent of agents) {
1948
2016
  const { content, skip, overrides, warnings } = await applyCustomization(ctx.projectRoot, agent);
1949
2017
  this.warnings.push(...warnings);
@@ -1953,7 +2021,11 @@ var BaseAdapter = class {
1953
2021
  const fmt = model ? (formatModel ?? defaultModelFormat)(model) : void 0;
1954
2022
  lines.push(`## Agent: ${agent.id}`);
1955
2023
  if (fmt && !fmt.after) lines.push(fmt.text);
1956
- lines.push("", desc, "", content);
2024
+ if (minimal) {
2025
+ lines.push("", this.stripMinimal(content));
2026
+ } else {
2027
+ lines.push("", desc, "", content);
2028
+ }
1957
2029
  if (fmt?.after) lines.push("", fmt.text);
1958
2030
  lines.push("");
1959
2031
  }
@@ -2036,6 +2108,23 @@ ${wrapInManagedBlock(content)}`, content));
2036
2108
  if (!ctx.features.hooks) return [];
2037
2109
  return readHookDefinitions(ctx.agentsDir);
2038
2110
  }
2111
+ /** Returns true when the adapter is running in minimal generation mode. */
2112
+ isMinimal(ctx) {
2113
+ return ctx.generationMode === "minimal";
2114
+ }
2115
+ /**
2116
+ * Strip verbose content for minimal generation mode.
2117
+ * Removes markdown comments, collapses excessive blank lines,
2118
+ * strips decorative formatting, and trims descriptions.
2119
+ */
2120
+ stripMinimal(content) {
2121
+ let result = content;
2122
+ result = result.replace(/<!--[\s\S]*?-->/g, "");
2123
+ result = result.replace(/^[-*_]{3,}\s*$/gm, "");
2124
+ result = result.replace(/\n{3,}/g, "\n\n");
2125
+ result = result.trim();
2126
+ return result;
2127
+ }
2039
2128
  };
2040
2129
 
2041
2130
  // src/adapters/aider.ts
@@ -2043,7 +2132,7 @@ var AiderAdapter = class extends BaseAdapter {
2043
2132
  name = "aider";
2044
2133
  async doGenerate(ctx) {
2045
2134
  const inner = [
2046
- ...await this.bridgeHeader(ctx.agentsDir),
2135
+ ...await this.bridgeHeader(ctx),
2047
2136
  ...await this.inlineRules(ctx),
2048
2137
  ...await this.inlineAgents(ctx)
2049
2138
  ].join("\n");
@@ -2071,7 +2160,7 @@ var AmazonQAdapter = class extends BaseAdapter {
2071
2160
  async doGenerate(ctx) {
2072
2161
  const results = [];
2073
2162
  const inner = [
2074
- ...await this.bridgeHeader(ctx.agentsDir),
2163
+ ...await this.bridgeHeader(ctx),
2075
2164
  ...await this.inlineRules(ctx),
2076
2165
  ...await this.inlineAgents(ctx)
2077
2166
  ].join("\n");
@@ -2083,7 +2172,7 @@ var AmazonQAdapter = class extends BaseAdapter {
2083
2172
  if (mcp && Object.keys(mcp).length > 0) {
2084
2173
  const entries = this.buildStdMcpEntries(mcp);
2085
2174
  if (Object.keys(entries).length > 0) {
2086
- results.push(output(".amazonq/settings.json", JSON.stringify({ mcpServers: entries }, null, 2)));
2175
+ results.push(output(".amazonq/mcp.json", JSON.stringify({ mcpServers: entries }, null, 2)));
2087
2176
  }
2088
2177
  }
2089
2178
  return results;
@@ -2096,15 +2185,15 @@ var AmpAdapter = class extends BaseAdapter {
2096
2185
  async doGenerate(ctx) {
2097
2186
  const results = [];
2098
2187
  const inner = [
2099
- ...await this.bridgeHeader(ctx.agentsDir),
2188
+ ...await this.bridgeHeader(ctx),
2100
2189
  ...await this.inlineRules(ctx),
2101
2190
  ...await this.inlineAgents(ctx, (m) => ({
2102
2191
  text: `**Recommended model:** \`${m}\`. Use Smart mode for Opus, Rush for Haiku, Deep for Codex.`
2103
2192
  }))
2104
2193
  ].join("\n");
2105
- results.push(output(".amp/AGENTS.md", wrapInManagedBlock(inner), inner));
2194
+ results.push(output("AGENTS.md", wrapInManagedBlock(inner), inner));
2106
2195
  results.push(
2107
- ...await this.processSkillsRaw(ctx, (id) => `.amp/skills/${toPrefixedId(id)}/SKILL.md`)
2196
+ ...await this.processSkillsRaw(ctx, (id) => `.agents/skills/${toPrefixedId(id)}/SKILL.md`)
2108
2197
  );
2109
2198
  const mcp = await this.readFilteredMcp(ctx);
2110
2199
  if (mcp && Object.keys(mcp).length > 0) {
@@ -2117,22 +2206,55 @@ var AmpAdapter = class extends BaseAdapter {
2117
2206
  }
2118
2207
  };
2119
2208
 
2209
+ // src/adapters/antigravity.ts
2210
+ var AntigravityAdapter = class extends BaseAdapter {
2211
+ name = "antigravity";
2212
+ async doGenerate(ctx) {
2213
+ const results = [];
2214
+ const inner = [
2215
+ ...await this.bridgeHeader(ctx, ".agents/AGENTS.md"),
2216
+ ...await this.inlineRules(ctx),
2217
+ ...await this.inlineAgents(ctx)
2218
+ ].join("\n");
2219
+ results.push(output(".antigravity/rules.md", wrapInManagedBlock(inner), inner));
2220
+ results.push(
2221
+ ...await this.processSkillsRaw(ctx, (id) => `.antigravity/skills/${toPrefixedId(id)}/SKILL.md`)
2222
+ );
2223
+ const mcp = await this.readFilteredMcp(ctx);
2224
+ if (mcp && Object.keys(mcp).length > 0) {
2225
+ const entries = this.buildStdMcpEntries(mcp);
2226
+ if (Object.keys(entries).length > 0) {
2227
+ results.push(output(".antigravity/settings.json", JSON.stringify({ mcpServers: entries }, null, 2)));
2228
+ }
2229
+ }
2230
+ return results;
2231
+ }
2232
+ };
2233
+
2120
2234
  // src/adapters/claude.ts
2121
2235
  var AGENT_TEAMS_SECTION = [
2122
- "## Agent Teams (Experimental)",
2236
+ "## Agent Teams",
2123
2237
  "",
2124
- "This project uses hatch3r's 4-phase sub-agent pipeline (Research \u2192 Implement \u2192 Review \u2192 Quality)",
2238
+ "This project uses hatch3r's 4-phase sub-agent pipeline (Research -> Implement -> Review -> Quality)",
2125
2239
  "which maps directly to Claude Code Agent Teams. Each phase becomes a teammate role.",
2126
2240
  "",
2127
2241
  "### Enabling Agent Teams",
2128
2242
  "",
2129
- "Agent Teams is experimental. Enable by setting `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in your",
2130
- "environment or in `.claude/settings.json` under `env`. Once enabled, request a team in the prompt:",
2243
+ "Agent Teams is enabled via `.claude/settings.json`. The env var `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`",
2244
+ "is set automatically by hatch3r. Once enabled, request a team in the prompt:",
2131
2245
  "",
2132
2246
  "```",
2133
2247
  "Create an agent team for this task. Use the hatch3r 4-phase pipeline.",
2134
2248
  "```",
2135
2249
  "",
2250
+ "### Teammate Display Modes",
2251
+ "",
2252
+ "Agent Teams supports two display modes configured via `teammateMode` in `.claude/settings.json`:",
2253
+ "",
2254
+ '- `"auto"` (default): uses split panes if inside tmux, in-process otherwise.',
2255
+ '- `"in-process"`: all teammates run inside your main terminal. Use Shift+Down to cycle.',
2256
+ '- `"tmux"`: each teammate gets its own pane. Requires tmux or iTerm2.',
2257
+ "",
2136
2258
  "### Pipeline-to-Team Mapping",
2137
2259
  "",
2138
2260
  "| Phase | Teammate Role | hatch3r Agents | Delegation Notes |",
@@ -2183,6 +2305,20 @@ var AGENT_TEAMS_SECTION = [
2183
2305
  "- Assign explicit file boundaries to avoid edit conflicts between teammates.",
2184
2306
  "- Use the `hatch3r-agent-team` command (`/hatch3r-agent-team`) for guided team creation."
2185
2307
  ];
2308
+ var AGENT_TEAMS_SECTION_MINIMAL = [
2309
+ "## Agent Teams",
2310
+ "",
2311
+ "Pipeline maps to Claude Code Agent Teams. Enable via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`.",
2312
+ "",
2313
+ "| Phase | Role | Agents |",
2314
+ "|-------|------|--------|",
2315
+ "| Research | `researcher` | `hatch3r-researcher` |",
2316
+ "| Implement | `implementer` | `hatch3r-implementer` |",
2317
+ "| Review | `reviewer` | `hatch3r-reviewer`, `hatch3r-fixer` |",
2318
+ "| Quality | `quality-*` | `hatch3r-test-writer`, `hatch3r-security-auditor`, + conditional |",
2319
+ "",
2320
+ "Use `/hatch3r-agent-team` for guided team creation."
2321
+ ];
2186
2322
  var AGENT_TEAM_COMMAND = `# hatch3r Agent Team
2187
2323
 
2188
2324
  Create a Claude Code Agent Team that follows the hatch3r 4-phase pipeline.
@@ -2237,7 +2373,8 @@ Each quality teammate owns distinct files to avoid conflicts.
2237
2373
 
2238
2374
  ## Important
2239
2375
 
2240
- - Use \`--teammate-mode tmux\` or \`--teammate-mode in-process\` based on your terminal
2376
+ - Teammate display mode is configured in \`.claude/settings.json\` via \`teammateMode\` (\`auto\`, \`in-process\`, or \`tmux\`)
2377
+ - Override for a single session: \`claude --teammate-mode in-process\`
2241
2378
  - Each teammate reads CLAUDE.md and inherits project rules automatically
2242
2379
  - Assign explicit file/directory boundaries to each teammate
2243
2380
  - The lead coordinates; it should NOT implement code itself (use delegate mode)
@@ -2288,8 +2425,20 @@ var ClaudeAdapter = class extends BaseAdapter {
2288
2425
  name = "claude";
2289
2426
  async doGenerate(ctx) {
2290
2427
  const results = [];
2428
+ const minimal = this.isMinimal(ctx);
2291
2429
  const bridgeOrchestration = await generateBridgeOrchestration(ctx.agentsDir);
2292
- const innerContent = [
2430
+ const teamsSection = minimal ? AGENT_TEAMS_SECTION_MINIMAL : AGENT_TEAMS_SECTION;
2431
+ const innerParts = minimal ? [
2432
+ "",
2433
+ "# Hatch3r Project Instructions",
2434
+ "",
2435
+ "Instructions: `.agents/AGENTS.md`. Rules: `.claude/rules/`. Agents: `.claude/agents/`.",
2436
+ "",
2437
+ this.stripMinimal(bridgeOrchestration),
2438
+ "",
2439
+ ...teamsSection,
2440
+ ""
2441
+ ] : [
2293
2442
  "",
2294
2443
  "# Hatch3r Project Instructions",
2295
2444
  "",
@@ -2298,14 +2447,24 @@ var ClaudeAdapter = class extends BaseAdapter {
2298
2447
  "",
2299
2448
  bridgeOrchestration,
2300
2449
  "",
2301
- ...AGENT_TEAMS_SECTION,
2450
+ ...teamsSection,
2302
2451
  "",
2303
2452
  "## Personal Settings",
2304
2453
  "",
2305
2454
  "Create `CLAUDE.local.md` for personal settings (not committed to git).",
2306
2455
  "Claude Code reads this file for user-specific preferences.",
2456
+ "",
2457
+ "## Getting Started with Claude Code",
2458
+ "",
2459
+ "New to this project's agent setup? Progress through these stages:",
2460
+ "",
2461
+ "**Start here:** Rules in `.claude/rules/` are loaded automatically. The orchestration bridge above guides your workflow.",
2462
+ "**Next:** Use `/hatch3r-feature` or `/hatch3r-bug-fix` commands for guided workflows.",
2463
+ "**Then:** Delegate to agents in `.claude/agents/` \u2014 use Agent Teams for parallel execution.",
2464
+ "**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files.",
2307
2465
  ""
2308
- ].join("\n");
2466
+ ];
2467
+ const innerContent = innerParts.join("\n");
2309
2468
  results.push(output("CLAUDE.md", wrapInManagedBlock(innerContent), innerContent));
2310
2469
  if (ctx.features.rules) {
2311
2470
  const rules = await readCanonicalFiles(ctx.agentsDir, "rules");
@@ -2314,7 +2473,9 @@ var ClaudeAdapter = class extends BaseAdapter {
2314
2473
  this.warnings.push(...warnings);
2315
2474
  if (skip) continue;
2316
2475
  const desc = overrides.description ?? rule.description;
2317
- const body = `# ${rule.id}
2476
+ const body = minimal ? `# ${rule.id}
2477
+
2478
+ ${this.stripMinimal(content)}` : `# ${rule.id}
2318
2479
 
2319
2480
  ${desc}
2320
2481
 
@@ -2330,23 +2491,33 @@ ${content}`;
2330
2491
  if (skip) continue;
2331
2492
  const agentId = toPrefixedId(agent.id);
2332
2493
  const model = resolveAgentModel(agent.id, agent, ctx.manifest, overrides);
2333
- const modelGuidance = model ? `
2334
-
2335
- ## Recommended Model
2336
-
2337
- Preferred: \`${model}\`. Set via \`/model ${model}\` or env \`CLAUDE_CODE_SUBAGENT_MODEL=${model}\`.` : "";
2338
2494
  const desc = overrides.description ?? agent.description;
2339
2495
  const fm = `---
2340
2496
  description: ${desc}
2341
2497
  ---`;
2342
- const body = `${content}${modelGuidance}`;
2343
- results.push(output(`.claude/agents/${agentId}.md`, `${fm}
2498
+ if (minimal) {
2499
+ const modelNote = model ? `
2500
+ Model: \`${model}\`` : "";
2501
+ const body = `${this.stripMinimal(content)}${modelNote}`;
2502
+ results.push(output(`.claude/agents/${agentId}.md`, `${fm}
2503
+
2504
+ ${wrapInManagedBlock(body)}`, body));
2505
+ } else {
2506
+ const modelGuidance = model ? `
2507
+
2508
+ ## Recommended Model
2509
+
2510
+ Preferred: \`${model}\`. Set via \`/model ${model}\` or env \`CLAUDE_CODE_SUBAGENT_MODEL=${model}\`.` : "";
2511
+ const body = `${content}${modelGuidance}`;
2512
+ results.push(output(`.claude/agents/${agentId}.md`, `${fm}
2344
2513
 
2345
2514
  ${wrapInManagedBlock(body)}`, body));
2515
+ }
2346
2516
  }
2347
2517
  }
2348
2518
  const defaultAllow = ["Read", "Edit", "MultiEdit", "Write", "Grep", "Glob", "LS", "TodoRead", "TodoWrite"];
2349
2519
  const claudeConfig = ctx.manifest.claude;
2520
+ const teammateMode = claudeConfig?.teammateMode ?? "auto";
2350
2521
  const settingsObj = {
2351
2522
  _hatch3r: {
2352
2523
  version: HATCH3R_VERSION,
@@ -2356,7 +2527,7 @@ ${wrapInManagedBlock(body)}`, body));
2356
2527
  allow: claudeConfig?.permissions?.allow ?? defaultAllow,
2357
2528
  deny: claudeConfig?.permissions?.deny ?? []
2358
2529
  },
2359
- teammateMode: claudeConfig?.teammateMode ?? "tool-using"
2530
+ teammateMode
2360
2531
  };
2361
2532
  const hooksConfig = {};
2362
2533
  const hooks = await this.readHooks(ctx);
@@ -2387,7 +2558,9 @@ ${wrapInManagedBlock(body)}`, body));
2387
2558
  });
2388
2559
  }
2389
2560
  settingsObj.hooks = hooksConfig;
2390
- if (ctx.manifest.claude?.agentTeams !== false) {
2561
+ const agentTeamsSetting = ctx.manifest.claude?.agentTeams;
2562
+ if (agentTeamsSetting === "ga") {
2563
+ } else if (agentTeamsSetting !== false) {
2391
2564
  settingsObj.env = { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" };
2392
2565
  }
2393
2566
  results.push(output(".claude/settings.json", JSON.stringify(settingsObj, null, 2)));
@@ -2509,7 +2682,16 @@ ${content}`;
2509
2682
  "Canonical agent instructions live at `/.agents/AGENTS.md`.",
2510
2683
  "Rules and skills are managed in `.roo/rules/` and `.cline/skills/`.",
2511
2684
  "",
2512
- bridgeOrchestration
2685
+ bridgeOrchestration,
2686
+ "",
2687
+ "## Getting Started with Roo Code",
2688
+ "",
2689
+ "New to this project's agent setup? Progress through these stages:",
2690
+ "",
2691
+ "**Start here:** Rules in `.roo/rules/` are loaded automatically. The orchestration bridge above guides your workflow.",
2692
+ "**Next:** Use workflow commands in `.clinerules/workflows/` for guided task execution.",
2693
+ "**Then:** Switch to custom modes (defined in `.roomodes`) for specialized agent behaviors.",
2694
+ "**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files."
2513
2695
  ].join("\n");
2514
2696
  results.push(output(".roo/rules/hatch3r-bridge.md", wrapInManagedBlock(bridgeBody), bridgeBody));
2515
2697
  return results;
@@ -2660,6 +2842,15 @@ ${r.rule.description}
2660
2842
 
2661
2843
  ${r.content}`
2662
2844
  ),
2845
+ "",
2846
+ "## Getting Started with Copilot",
2847
+ "",
2848
+ "New to this project's agent setup? Progress through these stages:",
2849
+ "",
2850
+ "**Start here:** Instructions in `.github/instructions/` scope rules to specific file patterns. The orchestration bridge above guides your workflow.",
2851
+ "**Next:** Use prompts in `.github/prompts/` and commands in `.github/copilot/commands/` for guided workflows.",
2852
+ "**Then:** Delegate to agents in `.github/agents/` for specialized tasks.",
2853
+ "**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files.",
2663
2854
  ""
2664
2855
  ].join("\n");
2665
2856
  results.push(output(".github/copilot-instructions.md", wrapInManagedBlock(innerContent), innerContent));
@@ -2806,7 +2997,7 @@ var CursorAdapter = class extends BaseAdapter {
2806
2997
  const lines = [`name: ${agent.id}`, `description: ${desc}`];
2807
2998
  if (model) lines.push(`model: ${model}`);
2808
2999
  if (agent.readonly) lines.push("readonly: true");
2809
- if (agent.background) lines.push("background: true");
3000
+ if (agent.background) lines.push("is_background: true");
2810
3001
  const fm = `---
2811
3002
  ${lines.join("\n")}
2812
3003
  ---`;
@@ -2867,7 +3058,16 @@ Background subagents write output to \`~/.cursor/subagents/\` for later inspecti
2867
3058
 
2868
3059
  Cursor v2.6 added MCP Apps (interactive UIs in agent chats) and Team Marketplaces for plugins.
2869
3060
  If this project includes MCP servers that expose UI components, they will render inline as MCP Apps.
2870
- Plugin configurations in \`.cursor/mcp.json\` are compatible with Team Marketplace distribution.`;
3061
+ Plugin configurations in \`.cursor/mcp.json\` are compatible with Team Marketplace distribution.
3062
+
3063
+ ## Getting Started with Cursor
3064
+
3065
+ New to this project's agent setup? Progress through these stages:
3066
+
3067
+ **Start here:** Rules in \`.cursor/rules/\` are loaded automatically. The orchestration bridge above guides your workflow.
3068
+ **Next:** Use \`/hatch3r-feature\` or \`/hatch3r-bug-fix\` commands in Cursor chat for guided workflows.
3069
+ **Then:** Delegate to agents in \`.cursor/agents/\` \u2014 Cursor supports up to 4 subagents in parallel.
3070
+ **Later:** Customize agent behavior via \`.hatch3r/{type}/{id}.customize.yaml\` without editing managed files.`;
2871
3071
  results.push(mdcOutput(".cursor/rules/hatch3r-bridge.mdc", bridgeFm, bridgeBody));
2872
3072
  if (ctx.manifest.tools.includes("cursor")) {
2873
3073
  const envConfig = {
@@ -2899,7 +3099,7 @@ var GeminiAdapter = class extends BaseAdapter {
2899
3099
  async doGenerate(ctx) {
2900
3100
  const results = [];
2901
3101
  const inner = [
2902
- ...await this.bridgeHeader(ctx.agentsDir, ".agents/AGENTS.md"),
3102
+ ...await this.bridgeHeader(ctx, ".agents/AGENTS.md"),
2903
3103
  ...await this.inlineRules(ctx),
2904
3104
  ...await this.inlineAgents(ctx, (m) => ({
2905
3105
  text: `**Recommended model:** \`${m}\`. Set via \`gemini --model ${m}\` or select in Google AI Studio.`,
@@ -2958,11 +3158,12 @@ var GeminiAdapter = class extends BaseAdapter {
2958
3158
  };
2959
3159
 
2960
3160
  // src/adapters/goose.ts
3161
+ import { stringify as yamlStringify } from "yaml";
2961
3162
  var GooseAdapter = class extends BaseAdapter {
2962
3163
  name = "goose";
2963
3164
  async doGenerate(ctx) {
2964
3165
  const lines = [
2965
- ...await this.bridgeHeader(ctx.agentsDir),
3166
+ ...await this.bridgeHeader(ctx),
2966
3167
  ...await this.inlineRules(ctx),
2967
3168
  ...await this.inlineAgents(ctx)
2968
3169
  ];
@@ -2988,8 +3189,108 @@ var GooseAdapter = class extends BaseAdapter {
2988
3189
  results.push(output(".goose/mcp.json", JSON.stringify(gooseMcp, null, 2)));
2989
3190
  }
2990
3191
  }
3192
+ const agents = ctx.features.agents ? await readCanonicalFiles(ctx.agentsDir, "agents") : [];
3193
+ const profile = await this.buildProfile(ctx, agents, mcp);
3194
+ const profileYaml = yamlStringify(profile);
3195
+ results.push(output(".goose/profiles/hatch3r.yaml", profileYaml));
2991
3196
  return results;
2992
3197
  }
3198
+ /** Build a Goose profile that maps hatch3r content to Goose's recipe system. */
3199
+ async buildProfile(ctx, agents, mcp) {
3200
+ const extensions = this.buildExtensions(mcp);
3201
+ const recipe = await this.buildRecipe(ctx, agents);
3202
+ const capabilities = this.deriveAcpCapabilities(ctx, agents);
3203
+ return {
3204
+ name: "hatch3r",
3205
+ description: `hatch3r-managed Goose profile for ${ctx.manifest.project || ctx.manifest.repo}. Provides agent pipeline, recipe interoperability, and ACP compatibility.`,
3206
+ instructions: `Follow the canonical agent instructions at .agents/AGENTS.md. Use the hatch3r 4-phase pipeline: Research, Implement, Review, Quality.`,
3207
+ ...extensions.length > 0 ? { extensions } : {},
3208
+ recipes: [recipe],
3209
+ acp: {
3210
+ enabled: true,
3211
+ version: "0.2",
3212
+ capabilities
3213
+ }
3214
+ };
3215
+ }
3216
+ /** Map MCP servers to Goose extensions. */
3217
+ buildExtensions(mcp) {
3218
+ if (!mcp) return [];
3219
+ const extensions = [];
3220
+ for (const [name, entry] of Object.entries(mcp)) {
3221
+ if (entry.command) {
3222
+ extensions.push({
3223
+ name,
3224
+ type: "mcp",
3225
+ config: {
3226
+ command: entry.command,
3227
+ args: entry.args || [],
3228
+ ...entry.env && Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
3229
+ }
3230
+ });
3231
+ } else if (entry.url) {
3232
+ extensions.push({
3233
+ name,
3234
+ type: "mcp",
3235
+ config: { url: entry.url }
3236
+ });
3237
+ }
3238
+ }
3239
+ return extensions;
3240
+ }
3241
+ /** Build a Goose recipe from hatch3r's agent pipeline. */
3242
+ async buildRecipe(ctx, agents) {
3243
+ const steps = [];
3244
+ const phaseMap = [
3245
+ { phase: "Research", agentPattern: "researcher", fallback: "Gather context from the codebase. Identify affected files, patterns, and conventions. Do not modify any files." },
3246
+ { phase: "Implement", agentPattern: "implementer", fallback: "Implement the requested changes following project conventions. Require plan approval before making changes." },
3247
+ { phase: "Review", agentPattern: "reviewer", fallback: "Review all changes for correctness, style, security, and adherence to project rules. Report findings as Critical/Warning/Info." },
3248
+ { phase: "Quality", agentPattern: "test-writer", fallback: "Write or update tests for the implemented changes. Run the test suite and verify all tests pass." }
3249
+ ];
3250
+ for (const { phase, agentPattern, fallback } of phaseMap) {
3251
+ const matchingAgent = agents.find(
3252
+ (a) => a.id.includes(agentPattern)
3253
+ );
3254
+ if (matchingAgent) {
3255
+ const { skip, warnings } = await applyCustomization(ctx.projectRoot, matchingAgent);
3256
+ this.warnings.push(...warnings);
3257
+ if (!skip) {
3258
+ const instruction = matchingAgent.description || fallback;
3259
+ steps.push({
3260
+ instruction: `[${phase}] ${instruction}`,
3261
+ agent: toPrefixedId(matchingAgent.id)
3262
+ });
3263
+ continue;
3264
+ }
3265
+ }
3266
+ steps.push({ instruction: `[${phase}] ${fallback}` });
3267
+ }
3268
+ return {
3269
+ name: "hatch3r-pipeline",
3270
+ description: "hatch3r 4-phase development pipeline: Research, Implement, Review, Quality.",
3271
+ steps
3272
+ };
3273
+ }
3274
+ /** Derive ACP capability advertisements from project configuration. */
3275
+ deriveAcpCapabilities(ctx, agents) {
3276
+ const capabilities = [
3277
+ "code-generation",
3278
+ "code-review"
3279
+ ];
3280
+ if (agents.some((a) => a.id.includes("test"))) {
3281
+ capabilities.push("test-generation");
3282
+ }
3283
+ if (agents.some((a) => a.id.includes("security"))) {
3284
+ capabilities.push("security-audit");
3285
+ }
3286
+ if (agents.some((a) => a.id.includes("docs"))) {
3287
+ capabilities.push("documentation");
3288
+ }
3289
+ if (ctx.features.mcp) {
3290
+ capabilities.push("tool-use");
3291
+ }
3292
+ return capabilities;
3293
+ }
2993
3294
  };
2994
3295
 
2995
3296
  // src/adapters/kiro.ts
@@ -3006,7 +3307,7 @@ var KiroAdapter = class extends BaseAdapter {
3006
3307
  name = "kiro";
3007
3308
  async doGenerate(ctx) {
3008
3309
  const results = [];
3009
- const lines = [...await this.bridgeHeader(ctx.agentsDir)];
3310
+ const lines = [...await this.bridgeHeader(ctx)];
3010
3311
  if (ctx.features.rules) {
3011
3312
  const rules = await readCanonicalFiles(ctx.agentsDir, "rules");
3012
3313
  for (const rule of rules) {
@@ -3139,7 +3440,7 @@ function isGlobPattern(scope) {
3139
3440
  function ruleTrigger(scope) {
3140
3441
  if (!scope) return "model_decision";
3141
3442
  if (scope === "always") return "always_on";
3142
- return "glob_pattern";
3443
+ return "glob";
3143
3444
  }
3144
3445
  var WindsurfAdapter = class extends BaseAdapter {
3145
3446
  name = "windsurf";
@@ -3155,7 +3456,17 @@ var WindsurfAdapter = class extends BaseAdapter {
3155
3456
  "",
3156
3457
  bridgeOrchestration,
3157
3458
  "",
3158
- ...await this.inlineAgents(ctx)
3459
+ ...await this.inlineAgents(ctx),
3460
+ "",
3461
+ "## Getting Started with Windsurf",
3462
+ "",
3463
+ "New to this project's agent setup? Progress through these stages:",
3464
+ "",
3465
+ "**Start here:** Rules in `.windsurf/rules/` are loaded automatically. The orchestration bridge above guides your workflow.",
3466
+ "**Next:** Use commands in `.windsurf/workflows/` for guided workflows (e.g., feature development, bug fixes).",
3467
+ "**Then:** Use parallel Cascade sessions for independent tasks to maximize throughput.",
3468
+ "**Later:** Customize agent behavior via `.hatch3r/{type}/{id}.customize.yaml` without editing managed files.",
3469
+ ""
3159
3470
  ].join("\n");
3160
3471
  results.push(output(".windsurfrules", wrapInManagedBlock(windsurfInner), windsurfInner));
3161
3472
  if (ctx.features.rules) {
@@ -3166,7 +3477,7 @@ var WindsurfAdapter = class extends BaseAdapter {
3166
3477
  if (skip) continue;
3167
3478
  const scope = overrides.scope ?? rule.scope;
3168
3479
  const trigger = ruleTrigger(scope);
3169
- const globScope = trigger === "glob_pattern" && scope ? isGlobPattern(scope) ? scope : `${scope}/**` : void 0;
3480
+ const globScope = trigger === "glob" && scope ? isGlobPattern(scope) ? scope : `${scope}/**` : void 0;
3170
3481
  const fm = `---
3171
3482
  trigger: ${trigger}${globScope ? `
3172
3483
  globs: "${globScope}"` : ""}
@@ -3204,7 +3515,7 @@ var ZedAdapter = class extends BaseAdapter {
3204
3515
  name = "zed";
3205
3516
  async doGenerate(ctx) {
3206
3517
  const inner = [
3207
- ...await this.bridgeHeader(ctx.agentsDir),
3518
+ ...await this.bridgeHeader(ctx),
3208
3519
  ...await this.inlineRules(ctx),
3209
3520
  ...await this.inlineAgents(ctx)
3210
3521
  ].join("\n");
@@ -3227,7 +3538,8 @@ var adapters = {
3227
3538
  kiro: new KiroAdapter(),
3228
3539
  goose: new GooseAdapter(),
3229
3540
  zed: new ZedAdapter(),
3230
- "amazon-q": new AmazonQAdapter()
3541
+ "amazon-q": new AmazonQAdapter(),
3542
+ antigravity: new AntigravityAdapter()
3231
3543
  };
3232
3544
  function getAdapter(tool) {
3233
3545
  const adapter = adapters[tool];
@@ -3250,7 +3562,8 @@ var ADAPTER_CAPABILITIES = {
3250
3562
  kiro: { agents: true, skills: true, rules: true, hooks: true, mcp: true, commands: false, prompts: false, githubAgents: false },
3251
3563
  aider: { agents: true, skills: true, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false },
3252
3564
  goose: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false },
3253
- zed: { agents: true, skills: false, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false }
3565
+ zed: { agents: true, skills: false, rules: true, hooks: false, mcp: false, commands: false, prompts: false, githubAgents: false },
3566
+ antigravity: { agents: true, skills: true, rules: true, hooks: false, mcp: true, commands: false, prompts: false, githubAgents: false }
3254
3567
  };
3255
3568
  function getUnsupportedFeatureWarnings(tool, manifest) {
3256
3569
  const caps = ADAPTER_CAPABILITIES[tool];
@@ -3349,7 +3662,7 @@ function validateIntegrityManifest(data) {
3349
3662
  for (const val of Object.values(obj.files)) {
3350
3663
  if (typeof val !== "string") return false;
3351
3664
  }
3352
- if ("checksum" in obj && typeof obj.checksum !== "string") return false;
3665
+ if (typeof obj.checksum !== "string") return false;
3353
3666
  return true;
3354
3667
  }
3355
3668
  async function readIntegrityManifest(agentsDir) {
@@ -3370,12 +3683,10 @@ async function verifyIntegrity(agentsDir) {
3370
3683
  return [];
3371
3684
  }
3372
3685
  const results = [];
3373
- if (manifest.checksum !== void 0) {
3374
- const expected = createHash("sha256").update(JSON.stringify(manifest.files)).digest("hex");
3375
- if (manifest.checksum !== expected) {
3376
- results.push({ file: INTEGRITY_FILE, status: "tampered" });
3377
- return results;
3378
- }
3686
+ const expected = createHash("sha256").update(JSON.stringify(manifest.files)).digest("hex");
3687
+ if (manifest.checksum !== expected) {
3688
+ results.push({ file: INTEGRITY_FILE, status: "tampered" });
3689
+ return results;
3379
3690
  }
3380
3691
  const manifestFiles = new Set(Object.keys(manifest.files));
3381
3692
  for (const [filePath, expectedHash] of Object.entries(manifest.files)) {
@@ -3423,52 +3734,248 @@ async function verifyIntegrity(agentsDir) {
3423
3734
  return results;
3424
3735
  }
3425
3736
 
3426
- // src/content/index.ts
3427
- import { readFile as readFile12, readdir as readdir5, cp, mkdir as mkdir3, rm } from "fs/promises";
3428
- import { join as join14, dirname as dirname6, normalize, isAbsolute } from "path";
3429
- function assertSafePath(relativePath, label2) {
3430
- const normalized = normalize(relativePath);
3431
- if (normalized.startsWith("..") || isAbsolute(normalized)) {
3432
- throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1);
3737
+ // src/archive/index.ts
3738
+ import { access as access3, cp, mkdir as mkdir3, readFile as readFile12, readdir as readdir5, rm, stat, writeFile as writeFile3 } from "fs/promises";
3739
+ import { dirname as dirname6, join as join14, sep } from "path";
3740
+ function toPosixPath(p) {
3741
+ return sep === "\\" ? p.replaceAll("\\", "/") : p;
3742
+ }
3743
+ var ARCHIVE_DIR = ".hatch3r-archive";
3744
+ var TOOL_PATH_PREFIXES = {
3745
+ cursor: [".cursor/"],
3746
+ claude: [".claude/", "CLAUDE.md", ".mcp.json"],
3747
+ copilot: [".github/copilot-instructions.md", ".github/workflows/copilot-setup-steps.yml", ".vscode/mcp.json"],
3748
+ windsurf: [".windsurf/", ".windsurfrules"],
3749
+ amp: [".amp/"],
3750
+ codex: [".codex/"],
3751
+ gemini: [".gemini/", "GEMINI.md"],
3752
+ cline: [".roo/", ".roomodes"],
3753
+ aider: ["CONVENTIONS.md", ".aider.conf.yml"],
3754
+ kiro: [".kiro/"],
3755
+ opencode: ["opencode.json"],
3756
+ goose: [".goosehints"],
3757
+ zed: [".rules"],
3758
+ "amazon-q": [".amazonq/"],
3759
+ antigravity: [".antigravity/"]
3760
+ };
3761
+ var PATH_PATTERNS = [
3762
+ { pattern: /\/rules\/([^/]+)\.(mdc|md)$/, type: "rules" },
3763
+ { pattern: /\/agents\/([^/]+)\.md$/, type: "agents" },
3764
+ { pattern: /\/skills\/([^/]+)\/SKILL\.md$/, type: "skills" },
3765
+ { pattern: /\/commands\/([^/]+)\.md$/, type: "commands" }
3766
+ ];
3767
+ function parseOutputPath(filePath) {
3768
+ for (const { pattern, type } of PATH_PATTERNS) {
3769
+ const match = filePath.match(pattern);
3770
+ if (match) {
3771
+ let id = match[1];
3772
+ if (id.startsWith(HATCH3R_PREFIX)) {
3773
+ id = id.slice(HATCH3R_PREFIX.length);
3774
+ }
3775
+ id = sanitizeId(id);
3776
+ if (id.length > 0) return { type, id };
3777
+ }
3433
3778
  }
3779
+ return null;
3434
3780
  }
3435
- function extractContentReferences(content) {
3436
- const refs = /* @__PURE__ */ new Set();
3437
- const pattern = /`(hatch3r-[a-z0-9-]+)`/g;
3438
- let match;
3439
- while ((match = pattern.exec(content)) !== null) {
3440
- refs.add(match[1]);
3781
+ function stripFrontmatter(content) {
3782
+ const trimmed = content.trimStart();
3783
+ if (trimmed.startsWith("---")) {
3784
+ const endIdx = trimmed.indexOf("\n---", 3);
3785
+ if (endIdx !== -1) {
3786
+ return trimmed.slice(endIdx + 4).trim();
3787
+ }
3441
3788
  }
3442
- return [...refs];
3789
+ return content.trim();
3443
3790
  }
3444
- async function validateCrossReferences(contentRoot, index) {
3445
- const warnings = [];
3446
- const allIds = new Set(index.items.map((item) => item.id));
3447
- for (const item of index.items) {
3791
+ async function fileExists2(path) {
3792
+ try {
3793
+ await access3(path);
3794
+ return true;
3795
+ } catch {
3796
+ return false;
3797
+ }
3798
+ }
3799
+ async function collectToolFiles(rootDir, tool) {
3800
+ const prefixes = TOOL_PATH_PREFIXES[tool];
3801
+ if (!prefixes) return [];
3802
+ const files = [];
3803
+ for (const prefix of prefixes) {
3804
+ const absPath = join14(rootDir, prefix);
3805
+ if (prefix.endsWith("/")) {
3806
+ try {
3807
+ const entries = await readdir5(absPath, { recursive: true, withFileTypes: true });
3808
+ for (const entry of entries) {
3809
+ if (entry.isFile()) {
3810
+ const parent = entry.parentPath ?? entry.path ?? absPath;
3811
+ const relPath = toPosixPath(join14(prefix, parent.slice(absPath.length), entry.name));
3812
+ files.push(relPath);
3813
+ }
3814
+ }
3815
+ } catch {
3816
+ }
3817
+ } else if (await fileExists2(absPath)) {
3818
+ files.push(prefix);
3819
+ }
3820
+ }
3821
+ return files;
3822
+ }
3823
+ async function archiveToolOutputs(rootDir, tool) {
3824
+ const filesToArchive = await collectToolFiles(rootDir, tool);
3825
+ if (filesToArchive.length === 0) {
3826
+ return { archivedFiles: [], migrations: [] };
3827
+ }
3828
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3829
+ const archiveBase = join14(rootDir, ARCHIVE_DIR, tool, timestamp);
3830
+ const archivedFiles = [];
3831
+ const migrations = [];
3832
+ for (const relPath of filesToArchive) {
3833
+ const absPath = join14(rootDir, relPath);
3834
+ if (!await fileExists2(absPath)) continue;
3448
3835
  let content;
3449
3836
  try {
3450
- const filePath = item.type === "skill" ? join14(contentRoot, item.relativePath, "SKILL.md") : join14(contentRoot, `${item.relativePath}`);
3451
- content = await readFile12(filePath, "utf-8");
3837
+ content = await readFile12(absPath, "utf-8");
3452
3838
  } catch {
3453
3839
  continue;
3454
3840
  }
3455
- const refs = extractContentReferences(content);
3456
- for (const ref of refs) {
3457
- if (ref === item.id) continue;
3458
- if (!allIds.has(ref)) {
3459
- warnings.push(
3460
- `${item.type} "${item.id}" references "${ref}" which does not exist in the content index`
3461
- );
3841
+ if (hasManagedBlock(content)) {
3842
+ const customContent = stripFrontmatter(extractCustomContent(content));
3843
+ if (customContent.length > 0) {
3844
+ const parsed = parseOutputPath(relPath);
3845
+ if (parsed) {
3846
+ const customizePath = join14(rootDir, ".hatch3r", parsed.type, `${parsed.id}.customize.md`);
3847
+ if (!await fileExists2(customizePath)) {
3848
+ await mkdir3(dirname6(customizePath), { recursive: true });
3849
+ await writeFile3(customizePath, customContent + "\n", "utf-8");
3850
+ migrations.push({
3851
+ from: relPath,
3852
+ to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
3853
+ type: parsed.type,
3854
+ id: parsed.id
3855
+ });
3856
+ }
3857
+ }
3462
3858
  }
3463
3859
  }
3860
+ const archiveDest = join14(archiveBase, relPath);
3861
+ await mkdir3(dirname6(archiveDest), { recursive: true });
3862
+ await cp(absPath, archiveDest);
3863
+ const srcStat = await stat(absPath);
3864
+ const destStat = await stat(archiveDest);
3865
+ if (destStat.size !== srcStat.size) {
3866
+ throw new Error(`Archive copy size mismatch for ${relPath}: source=${srcStat.size}, dest=${destStat.size}`);
3867
+ }
3868
+ await rm(absPath);
3869
+ archivedFiles.push(relPath);
3464
3870
  }
3465
- return { warnings };
3871
+ await cleanEmptyDirs(rootDir, filesToArchive);
3872
+ return { archivedFiles, migrations };
3466
3873
  }
3467
- var ORCHESTRATION_REQUIRED_AGENTS = [
3468
- "hatch3r-researcher",
3469
- "hatch3r-implementer",
3470
- "hatch3r-reviewer",
3471
- "hatch3r-test-writer",
3874
+ async function cleanEmptyDirs(rootDir, paths) {
3875
+ const dirs = /* @__PURE__ */ new Set();
3876
+ for (const p of paths) {
3877
+ let dir = dirname6(join14(rootDir, p));
3878
+ while (dir !== rootDir && dir.length > rootDir.length) {
3879
+ dirs.add(dir);
3880
+ dir = dirname6(dir);
3881
+ }
3882
+ }
3883
+ const sorted = [...dirs].sort((a, b) => b.length - a.length);
3884
+ for (const dir of sorted) {
3885
+ try {
3886
+ const entries = await readdir5(dir);
3887
+ if (entries.length === 0) {
3888
+ await rm(dir, { recursive: true });
3889
+ }
3890
+ } catch {
3891
+ }
3892
+ }
3893
+ }
3894
+ function removeManagedFilesForPaths(manifest, paths) {
3895
+ const pathSet = new Set(paths);
3896
+ manifest.managedFiles = manifest.managedFiles.filter((f) => !pathSet.has(f));
3897
+ }
3898
+ var MAX_ARCHIVE_ENTRIES = 5;
3899
+ async function pruneArchives(rootDir) {
3900
+ const archiveRoot = join14(rootDir, ARCHIVE_DIR);
3901
+ const pruned = [];
3902
+ let toolDirs;
3903
+ try {
3904
+ toolDirs = await readdir5(archiveRoot);
3905
+ } catch (err) {
3906
+ if (err.code === "ENOENT") return [];
3907
+ throw err;
3908
+ }
3909
+ for (const toolDir of toolDirs) {
3910
+ const toolPath = join14(archiveRoot, toolDir);
3911
+ let entries;
3912
+ try {
3913
+ const s = await stat(toolPath);
3914
+ if (!s.isDirectory()) continue;
3915
+ entries = await readdir5(toolPath);
3916
+ } catch {
3917
+ continue;
3918
+ }
3919
+ entries.sort((a, b) => b.localeCompare(a));
3920
+ for (const entry of entries.slice(MAX_ARCHIVE_ENTRIES)) {
3921
+ const entryPath = join14(toolPath, entry);
3922
+ await rm(entryPath, { recursive: true, force: true });
3923
+ pruned.push(`${toolDir}/${entry}`);
3924
+ }
3925
+ }
3926
+ return pruned;
3927
+ }
3928
+
3929
+ // src/content/index.ts
3930
+ import { readFile as readFile13, readdir as readdir6, writeFile as writeFile4, cp as cp2, mkdir as mkdir4, rm as rm2 } from "fs/promises";
3931
+ import { join as join15, dirname as dirname7, normalize, isAbsolute } from "path";
3932
+ function assertSafePath(relativePath, label2) {
3933
+ const sanitized = relativePath.replace(/\0/g, "");
3934
+ const normalized = normalize(sanitized);
3935
+ if (normalized.startsWith("..") || isAbsolute(normalized)) {
3936
+ throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3937
+ }
3938
+ if (sanitized !== relativePath) {
3939
+ throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3940
+ }
3941
+ }
3942
+ function extractContentReferences(content) {
3943
+ const refs = /* @__PURE__ */ new Set();
3944
+ const pattern = /`(hatch3r-[a-z0-9-]+)`/g;
3945
+ let match;
3946
+ while ((match = pattern.exec(content)) !== null) {
3947
+ refs.add(match[1]);
3948
+ }
3949
+ return [...refs];
3950
+ }
3951
+ async function validateCrossReferences(contentRoot, index) {
3952
+ const warnings = [];
3953
+ const allIds = new Set(index.items.map((item) => item.id));
3954
+ for (const item of index.items) {
3955
+ let content;
3956
+ try {
3957
+ const filePath = item.type === "skill" ? join15(contentRoot, item.relativePath, "SKILL.md") : join15(contentRoot, `${item.relativePath}`);
3958
+ content = await readFile13(filePath, "utf-8");
3959
+ } catch {
3960
+ continue;
3961
+ }
3962
+ const refs = extractContentReferences(content);
3963
+ for (const ref of refs) {
3964
+ if (ref === item.id) continue;
3965
+ if (!allIds.has(ref)) {
3966
+ warnings.push(
3967
+ `${item.type} "${item.id}" references "${ref}" which does not exist in the content index`
3968
+ );
3969
+ }
3970
+ }
3971
+ }
3972
+ return { warnings };
3973
+ }
3974
+ var ORCHESTRATION_REQUIRED_AGENTS = [
3975
+ "hatch3r-researcher",
3976
+ "hatch3r-implementer",
3977
+ "hatch3r-reviewer",
3978
+ "hatch3r-test-writer",
3472
3979
  "hatch3r-security-auditor"
3473
3980
  ];
3474
3981
  function validateOrchestrationDependencies(selection) {
@@ -3485,6 +3992,12 @@ function validateOrchestrationDependencies(selection) {
3485
3992
  }
3486
3993
  return warnings;
3487
3994
  }
3995
+ function typeIdKey(type, id) {
3996
+ return `${type}:${id}`;
3997
+ }
3998
+ function getAllItemsById(index, id) {
3999
+ return index.items.filter((item) => item.id === id);
4000
+ }
3488
4001
  var CONTENT_TYPE_CONFIGS = [
3489
4002
  { dir: "agents", type: "agent", strategy: "glob" },
3490
4003
  { dir: "commands", type: "command", strategy: "glob" },
@@ -3497,20 +4010,20 @@ var CONTENT_TYPE_CONFIGS = [
3497
4010
  async function buildContentIndex(contentRoot) {
3498
4011
  const items = [];
3499
4012
  for (const config of CONTENT_TYPE_CONFIGS) {
3500
- const dirPath = join14(contentRoot, config.dir);
4013
+ const dirPath = join15(contentRoot, config.dir);
3501
4014
  if (config.strategy === "subdirectory") {
3502
4015
  let dirents;
3503
4016
  try {
3504
- dirents = await readdir5(dirPath, { withFileTypes: true });
4017
+ dirents = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3505
4018
  } catch (err) {
3506
4019
  if (err.code === "ENOENT") continue;
3507
4020
  throw err;
3508
4021
  }
3509
4022
  for (const dirent of dirents) {
3510
4023
  if (!dirent.isDirectory()) continue;
3511
- const skillPath = join14(dirPath, dirent.name, "SKILL.md");
4024
+ const skillPath = join15(dirPath, dirent.name, "SKILL.md");
3512
4025
  try {
3513
- const raw = await readFile12(skillPath, "utf-8");
4026
+ const raw = await readFile13(skillPath, "utf-8");
3514
4027
  const { metadata } = parseFrontmatter(raw);
3515
4028
  const id = metadata.id || metadata.name || dirent.name;
3516
4029
  items.push({
@@ -3519,7 +4032,7 @@ async function buildContentIndex(contentRoot) {
3519
4032
  description: metadata.description ?? "",
3520
4033
  tags: metadata.tags ?? [],
3521
4034
  protected: metadata.protected,
3522
- relativePath: join14(config.dir, dirent.name)
4035
+ relativePath: join15(config.dir, dirent.name)
3523
4036
  });
3524
4037
  } catch (err) {
3525
4038
  if (err.code !== "ENOENT") throw err;
@@ -3528,15 +4041,15 @@ async function buildContentIndex(contentRoot) {
3528
4041
  } else {
3529
4042
  let entries;
3530
4043
  try {
3531
- const all = await readdir5(dirPath);
3532
- entries = all.filter((f) => f.endsWith(".md"));
4044
+ const all = await readdir6(dirPath);
4045
+ entries = all.filter((f) => f.endsWith(".md")).sort();
3533
4046
  } catch (err) {
3534
4047
  if (err.code === "ENOENT") continue;
3535
4048
  throw err;
3536
4049
  }
3537
4050
  for (const file of entries) {
3538
- const filePath = join14(dirPath, file);
3539
- const raw = await readFile12(filePath, "utf-8");
4051
+ const filePath = join15(dirPath, file);
4052
+ const raw = await readFile13(filePath, "utf-8");
3540
4053
  const { metadata } = parseFrontmatter(raw);
3541
4054
  const id = metadata.id || metadata.name || file.replace(/\.md$/, "");
3542
4055
  const item = {
@@ -3545,13 +4058,13 @@ async function buildContentIndex(contentRoot) {
3545
4058
  description: metadata.description ?? "",
3546
4059
  tags: metadata.tags ?? [],
3547
4060
  protected: metadata.protected,
3548
- relativePath: join14(config.dir, file)
4061
+ relativePath: join15(config.dir, file)
3549
4062
  };
3550
4063
  if (config.type === "rule") {
3551
4064
  const mdcFile = file.replace(/\.md$/, ".mdc");
3552
4065
  try {
3553
- await readFile12(join14(dirPath, mdcFile), "utf-8");
3554
- item.companionPath = join14(config.dir, mdcFile);
4066
+ await readFile13(join15(dirPath, mdcFile), "utf-8");
4067
+ item.companionPath = join15(config.dir, mdcFile);
3555
4068
  } catch {
3556
4069
  }
3557
4070
  }
@@ -3561,18 +4074,36 @@ async function buildContentIndex(contentRoot) {
3561
4074
  }
3562
4075
  const byType = {};
3563
4076
  const byId = /* @__PURE__ */ new Map();
4077
+ const byTypeAndId = /* @__PURE__ */ new Map();
4078
+ const collisions = [];
3564
4079
  for (const item of items) {
3565
4080
  if (!byType[item.type]) byType[item.type] = [];
3566
4081
  byType[item.type].push(item);
4082
+ byTypeAndId.set(typeIdKey(item.type, item.id), item);
3567
4083
  const existing = byId.get(item.id);
3568
- if (existing && existing.type !== item.type) {
3569
- console.warn(
3570
- `[hatch3r] Content ID collision: "${item.id}" exists as both ${existing.type} and ${item.type}. The ${item.type} entry will shadow the ${existing.type} entry in ID lookups.`
3571
- );
4084
+ if (existing) {
4085
+ const kind = existing.type !== item.type ? "cross-type" : "same-type";
4086
+ collisions.push({
4087
+ id: item.id,
4088
+ kind,
4089
+ existingType: existing.type,
4090
+ existingPath: existing.relativePath,
4091
+ duplicateType: item.type,
4092
+ duplicatePath: item.relativePath
4093
+ });
4094
+ if (kind === "cross-type") {
4095
+ console.warn(
4096
+ `[hatch3r] Content ID collision: "${item.id}" exists as both ${existing.type} (${existing.relativePath}) and ${item.type} (${item.relativePath}). Use index.byTypeAndId for collision-safe lookup.`
4097
+ );
4098
+ } else {
4099
+ console.warn(
4100
+ `[hatch3r] Duplicate content ID: "${item.id}" found in ${existing.relativePath} and ${item.relativePath}. The later entry will shadow the earlier one in ID lookups.`
4101
+ );
4102
+ }
3572
4103
  }
3573
4104
  byId.set(item.id, item);
3574
4105
  }
3575
- return { items, byType, byId };
4106
+ return { items, byType, byId, byTypeAndId, collisions };
3576
4107
  }
3577
4108
  var TYPE_TO_SELECTION_KEY = {
3578
4109
  agent: "agents",
@@ -3644,6 +4175,52 @@ function resolveSelection(preset, projectType, teamSize, index, customSelections
3644
4175
  items
3645
4176
  };
3646
4177
  }
4178
+ function countPresetExclusions(preset, index) {
4179
+ if (preset.id === "custom") return 0;
4180
+ if (preset.id === "full") return 0;
4181
+ let count = 0;
4182
+ for (const item of index.items) {
4183
+ if (item.protected) continue;
4184
+ if (preset.includeTags.length > 0) {
4185
+ const includeSet = new Set(preset.includeTags);
4186
+ if (item.tags.length > 0 && !item.tags.some((t) => includeSet.has(t))) {
4187
+ count++;
4188
+ continue;
4189
+ }
4190
+ }
4191
+ if (preset.excludeTags.length > 0) {
4192
+ const excludeSet = new Set(preset.excludeTags);
4193
+ if (item.tags.every((t) => excludeSet.has(t))) {
4194
+ count++;
4195
+ }
4196
+ }
4197
+ }
4198
+ return count;
4199
+ }
4200
+ function countProjectTypeExclusions(projectType, items) {
4201
+ const opposite = projectType === "greenfield" ? "brownfield" : "greenfield";
4202
+ let count = 0;
4203
+ for (const item of items) {
4204
+ if (item.protected) continue;
4205
+ if (item.tags.includes(opposite) && !item.tags.some((t) => t !== opposite && t !== "team" && t !== "solo")) {
4206
+ count++;
4207
+ }
4208
+ }
4209
+ return count;
4210
+ }
4211
+ function countTeamSizeExclusions(teamSize, items) {
4212
+ if (teamSize !== "solo") return 0;
4213
+ let count = 0;
4214
+ for (const item of items) {
4215
+ if (item.protected) continue;
4216
+ if (!item.tags.includes("team") && !item.tags.includes("board")) continue;
4217
+ const hasOther = item.tags.some(
4218
+ (t) => t !== "team" && t !== "board" && t !== "solo" && t !== "greenfield" && t !== "brownfield"
4219
+ );
4220
+ if (!hasOther) count++;
4221
+ }
4222
+ return count;
4223
+ }
3647
4224
  async function copySelectedContent(contentRoot, agentsDir, selection, index) {
3648
4225
  const copied = [];
3649
4226
  const selectedIds = /* @__PURE__ */ new Set();
@@ -3656,21 +4233,21 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
3656
4233
  if (item.companionPath) {
3657
4234
  assertSafePath(item.companionPath, "copySelectedContent companion");
3658
4235
  }
3659
- const srcPath = join14(contentRoot, item.relativePath);
3660
- const destPath = join14(agentsDir, item.relativePath);
4236
+ const srcPath = join15(contentRoot, item.relativePath);
4237
+ const destPath = join15(agentsDir, item.relativePath);
3661
4238
  if (item.type === "skill") {
3662
- await mkdir3(destPath, { recursive: true });
3663
- await cp(srcPath, destPath, { recursive: true, force: true });
4239
+ await mkdir4(destPath, { recursive: true });
4240
+ await cp2(srcPath, destPath, { recursive: true, force: true });
3664
4241
  copied.push(item.relativePath);
3665
4242
  } else {
3666
- await mkdir3(dirname6(destPath), { recursive: true });
3667
- await cp(srcPath, destPath, { force: true });
4243
+ await mkdir4(dirname7(destPath), { recursive: true });
4244
+ await cp2(srcPath, destPath, { force: true });
3668
4245
  copied.push(item.relativePath);
3669
4246
  if (item.companionPath) {
3670
- const mdcSrc = join14(contentRoot, item.companionPath);
3671
- const mdcDest = join14(agentsDir, item.companionPath);
4247
+ const mdcSrc = join15(contentRoot, item.companionPath);
4248
+ const mdcDest = join15(agentsDir, item.companionPath);
3672
4249
  try {
3673
- await cp(mdcSrc, mdcDest, { force: true });
4250
+ await cp2(mdcSrc, mdcDest, { force: true });
3674
4251
  copied.push(item.companionPath);
3675
4252
  } catch (err) {
3676
4253
  if (err.code !== "ENOENT") throw err;
@@ -3678,19 +4255,34 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
3678
4255
  }
3679
4256
  }
3680
4257
  }
4258
+ for (const config of CONTENT_TYPE_CONFIGS) {
4259
+ if (config.strategy !== "glob") continue;
4260
+ try {
4261
+ const dirEntries = await readdir6(join15(contentRoot, config.dir), { withFileTypes: true });
4262
+ for (const entry of dirEntries) {
4263
+ if (!entry.isDirectory() || entry.name.startsWith("hatch3r-")) continue;
4264
+ const subSrc = join15(contentRoot, config.dir, entry.name);
4265
+ const subDest = join15(agentsDir, config.dir, entry.name);
4266
+ await mkdir4(subDest, { recursive: true });
4267
+ await cp2(subSrc, subDest, { recursive: true, force: true });
4268
+ }
4269
+ } catch (err) {
4270
+ if (err.code !== "ENOENT") throw err;
4271
+ }
4272
+ }
3681
4273
  try {
3682
- const checksSrc = join14(contentRoot, "checks");
3683
- const checksDest = join14(agentsDir, "checks");
3684
- await mkdir3(checksDest, { recursive: true });
3685
- await cp(checksSrc, checksDest, { recursive: true, force: true });
4274
+ const checksSrc = join15(contentRoot, "checks");
4275
+ const checksDest = join15(agentsDir, "checks");
4276
+ await mkdir4(checksDest, { recursive: true });
4277
+ await cp2(checksSrc, checksDest, { recursive: true, force: true });
3686
4278
  } catch (err) {
3687
4279
  if (err.code !== "ENOENT") throw err;
3688
4280
  }
3689
4281
  try {
3690
- const mcpSrc = join14(contentRoot, "mcp");
3691
- const mcpDest = join14(agentsDir, "mcp");
3692
- await mkdir3(mcpDest, { recursive: true });
3693
- await cp(mcpSrc, mcpDest, { recursive: true, force: true });
4282
+ const mcpSrc = join15(contentRoot, "mcp");
4283
+ const mcpDest = join15(agentsDir, "mcp");
4284
+ await mkdir4(mcpDest, { recursive: true });
4285
+ await cp2(mcpSrc, mcpDest, { recursive: true, force: true });
3694
4286
  } catch (err) {
3695
4287
  if (err.code !== "ENOENT") throw err;
3696
4288
  }
@@ -3707,16 +4299,16 @@ async function buildSelectionsFromDisk(agentsDir) {
3707
4299
  githubAgents: []
3708
4300
  };
3709
4301
  for (const config of CONTENT_TYPE_CONFIGS) {
3710
- const dirPath = join14(agentsDir, config.dir);
4302
+ const dirPath = join15(agentsDir, config.dir);
3711
4303
  const key = TYPE_TO_SELECTION_KEY[config.type];
3712
4304
  if (!key) continue;
3713
4305
  if (config.strategy === "subdirectory") {
3714
4306
  try {
3715
- const dirents = await readdir5(dirPath, { withFileTypes: true });
4307
+ const dirents = await readdir6(dirPath, { withFileTypes: true });
3716
4308
  for (const d of dirents) {
3717
4309
  if (!d.isDirectory()) continue;
3718
4310
  try {
3719
- const raw = await readFile12(join14(dirPath, d.name, "SKILL.md"), "utf-8");
4311
+ const raw = await readFile13(join15(dirPath, d.name, "SKILL.md"), "utf-8");
3720
4312
  const { metadata } = parseFrontmatter(raw);
3721
4313
  items[key].push(metadata.id || metadata.name || d.name);
3722
4314
  } catch {
@@ -3726,9 +4318,9 @@ async function buildSelectionsFromDisk(agentsDir) {
3726
4318
  }
3727
4319
  } else {
3728
4320
  try {
3729
- const files = await readdir5(dirPath);
4321
+ const files = await readdir6(dirPath);
3730
4322
  for (const f of files.filter((f2) => f2.endsWith(".md"))) {
3731
- const raw = await readFile12(join14(dirPath, f), "utf-8");
4323
+ const raw = await readFile13(join15(dirPath, f), "utf-8");
3732
4324
  const { metadata } = parseFrontmatter(raw);
3733
4325
  items[key].push(metadata.id || metadata.name || f.replace(/\.md$/, ""));
3734
4326
  }
@@ -3748,20 +4340,20 @@ async function addContentItem(contentRoot, agentsDir, item) {
3748
4340
  if (item.companionPath) {
3749
4341
  assertSafePath(item.companionPath, "addContentItem companion");
3750
4342
  }
3751
- const srcPath = join14(contentRoot, item.relativePath);
3752
- const destPath = join14(agentsDir, item.relativePath);
4343
+ const srcPath = join15(contentRoot, item.relativePath);
4344
+ const destPath = join15(agentsDir, item.relativePath);
3753
4345
  try {
3754
4346
  if (item.type === "skill") {
3755
- await mkdir3(destPath, { recursive: true });
3756
- await cp(srcPath, destPath, { recursive: true, force: true });
4347
+ await mkdir4(destPath, { recursive: true });
4348
+ await cp2(srcPath, destPath, { recursive: true, force: true });
3757
4349
  } else {
3758
- await mkdir3(dirname6(destPath), { recursive: true });
3759
- await cp(srcPath, destPath, { force: true });
4350
+ await mkdir4(dirname7(destPath), { recursive: true });
4351
+ await cp2(srcPath, destPath, { force: true });
3760
4352
  if (item.companionPath) {
3761
4353
  try {
3762
- await cp(
3763
- join14(contentRoot, item.companionPath),
3764
- join14(agentsDir, item.companionPath),
4354
+ await cp2(
4355
+ join15(contentRoot, item.companionPath),
4356
+ join15(agentsDir, item.companionPath),
3765
4357
  { force: true }
3766
4358
  );
3767
4359
  } catch (err) {
@@ -3773,7 +4365,8 @@ async function addContentItem(contentRoot, agentsDir, item) {
3773
4365
  if (err.code === "ENOENT") {
3774
4366
  throw new HatchError(
3775
4367
  `Content "${item.id}" (${item.type}) not found in package at ${item.relativePath}. It may have been renamed or removed in this hatch3r version.`,
3776
- 1
4368
+ 1,
4369
+ "FS_ERROR"
3777
4370
  );
3778
4371
  }
3779
4372
  throw err;
@@ -3784,13 +4377,13 @@ async function removeContentItem(agentsDir, item, options) {
3784
4377
  if (item.companionPath) {
3785
4378
  assertSafePath(item.companionPath, "removeContentItem companion");
3786
4379
  }
3787
- const destPath = join14(agentsDir, item.relativePath);
4380
+ const destPath = join15(agentsDir, item.relativePath);
3788
4381
  if (item.type === "skill") {
3789
- await rm(destPath, { recursive: true, force: true });
4382
+ await rm2(destPath, { recursive: true, force: true });
3790
4383
  } else {
3791
- await rm(destPath, { force: true });
4384
+ await rm2(destPath, { force: true });
3792
4385
  if (item.companionPath) {
3793
- await rm(join14(agentsDir, item.companionPath), { force: true });
4386
+ await rm2(join15(agentsDir, item.companionPath), { force: true });
3794
4387
  }
3795
4388
  }
3796
4389
  if (options?.rootDir) {
@@ -3802,10 +4395,10 @@ async function removeContentItem(agentsDir, item, options) {
3802
4395
  };
3803
4396
  const customDir = typeToDir[item.type];
3804
4397
  if (customDir) {
3805
- const yamlPath = join14(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.yaml`);
3806
- const mdPath = join14(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.md`);
3807
- await rm(yamlPath, { force: true });
3808
- await rm(mdPath, { force: true });
4398
+ const yamlPath = join15(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.yaml`);
4399
+ const mdPath = join15(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.md`);
4400
+ await rm2(yamlPath, { force: true });
4401
+ await rm2(mdPath, { force: true });
3809
4402
  }
3810
4403
  }
3811
4404
  }
@@ -3816,6 +4409,10 @@ function getAllContentIds(selection) {
3816
4409
  }
3817
4410
  return ids;
3818
4411
  }
4412
+ function estimatePresetItemCount(preset, projectType, teamSize, index) {
4413
+ const selection = resolveSelection(preset, projectType, teamSize, index);
4414
+ return Object.values(selection.items).reduce((sum, arr) => sum + arr.length, 0);
4415
+ }
3819
4416
  function countSelectionItems(selection) {
3820
4417
  return Object.values(selection.items).reduce((sum, arr) => sum + arr.length, 0);
3821
4418
  }
@@ -3833,40 +4430,40 @@ function selectionSummary(selection) {
3833
4430
  }
3834
4431
 
3835
4432
  // src/cli/commands/update.ts
3836
- var __dirname = dirname7(fileURLToPath(import.meta.url));
4433
+ var __dirname = dirname8(fileURLToPath(import.meta.url));
3837
4434
  var CONTENT_DIRS = ["agents", "commands", "rules", "skills", "prompts", "github-agents", "mcp", "hooks"];
3838
4435
  var ALWAYS_COPY_FILES = /* @__PURE__ */ new Set(["mcp.json"]);
3839
4436
  async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selectedIds) {
3840
4437
  const copied = [];
3841
4438
  let entries;
3842
4439
  try {
3843
- entries = await readdir6(srcDir, { withFileTypes: true });
4440
+ entries = await readdir7(srcDir, { withFileTypes: true });
3844
4441
  } catch (err) {
3845
4442
  if (err.code === "ENOENT") return [];
3846
4443
  throw err;
3847
4444
  }
3848
4445
  for (const entry of entries) {
3849
- const srcPath = join15(srcDir, entry.name);
3850
- const destPath = join15(destDir, entry.name);
4446
+ const srcPath = join16(srcDir, entry.name);
4447
+ const destPath = join16(destDir, entry.name);
3851
4448
  if (entry.isDirectory()) {
3852
4449
  if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
3853
4450
  if (!selectedIds.has(entry.name)) continue;
3854
4451
  }
3855
- await mkdir4(destPath, { recursive: true });
4452
+ await mkdir5(destPath, { recursive: true });
3856
4453
  const subCopied = await copyHatch3rFiles(
3857
4454
  srcPath,
3858
4455
  destPath,
3859
- entry.name.startsWith(HATCH3R_PREFIX),
4456
+ insideHatch3rDir || !entry.name.startsWith(HATCH3R_PREFIX),
3860
4457
  selectedIds
3861
4458
  );
3862
- copied.push(...subCopied.map((p) => join15(entry.name, p)));
4459
+ copied.push(...subCopied.map((p) => join16(entry.name, p)));
3863
4460
  } else if (entry.name.startsWith(HATCH3R_PREFIX) || insideHatch3rDir || ALWAYS_COPY_FILES.has(entry.name)) {
3864
4461
  if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
3865
4462
  const baseId = entry.name.replace(/\.(md|mdc)$/, "");
3866
4463
  if (!selectedIds.has(baseId)) continue;
3867
4464
  }
3868
- await mkdir4(dirname7(destPath), { recursive: true });
3869
- await cp2(srcPath, destPath, { force: true });
4465
+ await mkdir5(dirname8(destPath), { recursive: true });
4466
+ await cp3(srcPath, destPath, { force: true });
3870
4467
  copied.push(entry.name);
3871
4468
  }
3872
4469
  }
@@ -3875,7 +4472,7 @@ async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selec
3875
4472
  async function runUpdate(rootDir, manifest, options = {}) {
3876
4473
  const offset = options.stepOffset ?? 0;
3877
4474
  const total = options.totalSteps ?? 4;
3878
- const agentsDir = join15(rootDir, AGENTS_DIR);
4475
+ const agentsDir = join16(rootDir, AGENTS_DIR);
3879
4476
  let contentRoot = findPackageRoot(__dirname);
3880
4477
  const pm = await detectPackageManager(rootDir);
3881
4478
  const s0 = createSpinner(step(offset + 1, total, "Updating package..."));
@@ -3889,7 +4486,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
3889
4486
  const msg = isTimeout ? "Package update timed out after 30s. Check network connectivity and retry." : err instanceof Error ? err.message : String(err);
3890
4487
  s0.fail(step(offset + 1, total, "Failed to update package"));
3891
4488
  error(msg);
3892
- throw new HatchError(msg, 1);
4489
+ throw new HatchError(msg, 1, isTimeout ? "NETWORK_ERROR" : "UNKNOWN_ERROR");
3893
4490
  }
3894
4491
  s0.succeed(step(offset + 1, total, "Package updated"));
3895
4492
  const s1 = createSpinner(step(offset + 2, total, "Updating canonical files..."));
@@ -3903,16 +4500,16 @@ async function runUpdate(rootDir, manifest, options = {}) {
3903
4500
  }
3904
4501
  const copied = [];
3905
4502
  for (const dir of CONTENT_DIRS) {
3906
- const srcDir = join15(contentRoot, dir);
4503
+ const srcDir = join16(contentRoot, dir);
3907
4504
  try {
3908
- const dirCopied = await copyHatch3rFiles(srcDir, join15(agentsDir, dir), false, selectedIds);
3909
- copied.push(...dirCopied.map((p) => join15(dir, p)));
4505
+ const dirCopied = await copyHatch3rFiles(srcDir, join16(agentsDir, dir), false, selectedIds);
4506
+ copied.push(...dirCopied.map((p) => join16(dir, p)));
3910
4507
  } catch (err) {
3911
4508
  if (err.code !== "ENOENT") throw err;
3912
4509
  }
3913
4510
  }
3914
4511
  const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
3915
- await safeWriteFile(join15(agentsDir, "AGENTS.md"), canonicalAgentsMd);
4512
+ await safeWriteFile(join16(agentsDir, "AGENTS.md"), canonicalAgentsMd);
3916
4513
  s1.succeed(step(offset + 2, total, `Updated ${copied.length} canonical files`));
3917
4514
  const s2 = createSpinner(step(offset + 3, total, "Re-syncing adapter output..."));
3918
4515
  s2.start();
@@ -3925,7 +4522,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
3925
4522
  warn(w);
3926
4523
  }
3927
4524
  for (const out of outputs) {
3928
- const fullPath = join15(rootDir, out.path);
4525
+ const fullPath = join16(rootDir, out.path);
3929
4526
  if (out.managedContent) {
3930
4527
  await safeWriteFile(fullPath, out.content, {
3931
4528
  managedContent: out.managedContent
@@ -3947,7 +4544,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
3947
4544
  }
3948
4545
  if (adapterFailures.length === manifest.tools.length) {
3949
4546
  s2.fail(step(offset + 3, total, "All adapters failed"));
3950
- throw new HatchError("All adapters failed", 1);
4547
+ throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
3951
4548
  }
3952
4549
  }
3953
4550
  s2.succeed(step(offset + 3, total, adapterFailures.length > 0 ? `Re-synced ${manifest.tools.length - adapterFailures.length}/${manifest.tools.length} tool(s)` : `Re-synced ${manifest.tools.length} tool(s)`));
@@ -3957,6 +4554,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
3957
4554
  await writeManifest(rootDir, manifest);
3958
4555
  const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
3959
4556
  await writeIntegrityManifest(agentsDir, integrityManifest);
4557
+ await pruneArchives(rootDir);
3960
4558
  s3.succeed(step(offset + 4, total, "Manifest updated"));
3961
4559
  return {
3962
4560
  copiedFiles: copied.length,
@@ -3969,35 +4567,40 @@ var MIGRATION_CHECKPOINTS = [
3969
4567
  {
3970
4568
  id: "content-selections-init",
3971
4569
  condition: async (manifest) => manifest.content === void 0,
3972
- execute: async (manifest, rootDir) => {
3973
- const agentsDir = join15(rootDir, AGENTS_DIR);
4570
+ execute: async (manifest, rootDir, headless) => {
4571
+ const agentsDir = join16(rootDir, AGENTS_DIR);
3974
4572
  const content = await buildSelectionsFromDisk(agentsDir);
3975
- const { projectType } = await inquirer.prompt([
3976
- {
3977
- type: "list",
3978
- name: "projectType",
3979
- message: "For content tracking \u2014 is this a greenfield or brownfield project?",
3980
- choices: [
3981
- { name: "Greenfield \u2014 new project", value: "greenfield" },
3982
- { name: "Brownfield \u2014 existing codebase", value: "brownfield" }
3983
- ],
3984
- default: "brownfield"
3985
- }
3986
- ]);
3987
- const { teamSize } = await inquirer.prompt([
3988
- {
3989
- type: "list",
3990
- name: "teamSize",
3991
- message: "Solo developer or team?",
3992
- choices: [
3993
- { name: "Solo", value: "solo" },
3994
- { name: "Team", value: "team" }
3995
- ],
3996
- default: "team"
3997
- }
3998
- ]);
3999
- content.projectType = projectType;
4000
- content.teamSize = teamSize;
4573
+ if (headless) {
4574
+ content.projectType = "brownfield";
4575
+ content.teamSize = "team";
4576
+ } else {
4577
+ const { projectType } = await inquirer.prompt([
4578
+ {
4579
+ type: "list",
4580
+ name: "projectType",
4581
+ message: "For content tracking \u2014 is this a greenfield or brownfield project?",
4582
+ choices: [
4583
+ { name: "Greenfield \u2014 new project", value: "greenfield" },
4584
+ { name: "Brownfield \u2014 existing codebase", value: "brownfield" }
4585
+ ],
4586
+ default: "brownfield"
4587
+ }
4588
+ ]);
4589
+ const { teamSize } = await inquirer.prompt([
4590
+ {
4591
+ type: "list",
4592
+ name: "teamSize",
4593
+ message: "Solo developer or team?",
4594
+ choices: [
4595
+ { name: "Solo", value: "solo" },
4596
+ { name: "Team", value: "team" }
4597
+ ],
4598
+ default: "team"
4599
+ }
4600
+ ]);
4601
+ content.projectType = projectType;
4602
+ content.teamSize = teamSize;
4603
+ }
4001
4604
  return {
4002
4605
  manifest: { ...manifest, content },
4003
4606
  notices: ["Migrated to explicit content tracking (all existing items preserved)"]
@@ -4007,20 +4610,26 @@ var MIGRATION_CHECKPOINTS = [
4007
4610
  {
4008
4611
  id: "platform-selection",
4009
4612
  condition: async (manifest) => !manifest.platform,
4010
- execute: async (manifest) => {
4011
- const { platform } = await inquirer.prompt([
4012
- {
4013
- type: "list",
4014
- name: "platform",
4015
- message: "hatch3r now supports multiple platforms. Select your platform:",
4016
- choices: [
4017
- { name: "GitHub", value: "github" },
4018
- { name: "Azure DevOps", value: "azure-devops" },
4019
- { name: "GitLab", value: "gitlab" }
4020
- ],
4021
- default: "github"
4022
- }
4023
- ]);
4613
+ execute: async (manifest, _rootDir, headless) => {
4614
+ let platform;
4615
+ if (headless) {
4616
+ platform = "github";
4617
+ } else {
4618
+ const answer = await inquirer.prompt([
4619
+ {
4620
+ type: "list",
4621
+ name: "platform",
4622
+ message: "hatch3r now supports multiple platforms. Select your platform:",
4623
+ choices: [
4624
+ { name: "GitHub", value: "github" },
4625
+ { name: "Azure DevOps", value: "azure-devops" },
4626
+ { name: "GitLab", value: "gitlab" }
4627
+ ],
4628
+ default: "github"
4629
+ }
4630
+ ]);
4631
+ platform = answer.platform;
4632
+ }
4024
4633
  const updated = { ...manifest, platform };
4025
4634
  const notices = [];
4026
4635
  if (platform === "github") {
@@ -4048,12 +4657,12 @@ var MIGRATION_CHECKPOINTS = [
4048
4657
  {
4049
4658
  id: "customize-yaml-size",
4050
4659
  condition: async (_manifest, rootDir) => {
4051
- const agentsDir = join15(rootDir, AGENTS_DIR);
4660
+ const agentsDir = join16(rootDir, AGENTS_DIR);
4052
4661
  try {
4053
- const entries = await readdir6(agentsDir, { recursive: true });
4662
+ const entries = await readdir7(agentsDir, { recursive: true });
4054
4663
  for (const entry of entries) {
4055
4664
  if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
4056
- const s = await stat(join15(agentsDir, entry));
4665
+ const s = await stat2(join16(agentsDir, entry));
4057
4666
  if (s.size > 10240) return true;
4058
4667
  }
4059
4668
  }
@@ -4062,14 +4671,14 @@ var MIGRATION_CHECKPOINTS = [
4062
4671
  }
4063
4672
  return false;
4064
4673
  },
4065
- execute: async (manifest, rootDir) => {
4674
+ execute: async (manifest, rootDir, _headless) => {
4066
4675
  const notices = [];
4067
- const agentsDir = join15(rootDir, AGENTS_DIR);
4676
+ const agentsDir = join16(rootDir, AGENTS_DIR);
4068
4677
  try {
4069
- const entries = await readdir6(agentsDir, { recursive: true });
4678
+ const entries = await readdir7(agentsDir, { recursive: true });
4070
4679
  for (const entry of entries) {
4071
4680
  if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
4072
- const s = await stat(join15(agentsDir, entry));
4681
+ const s = await stat2(join16(agentsDir, entry));
4073
4682
  if (s.size > 10240) {
4074
4683
  notices.push(`Large customize file detected: ${entry} (${Math.round(s.size / 1024)}KB) \u2014 consider splitting`);
4075
4684
  }
@@ -4088,18 +4697,24 @@ var MIGRATION_CHECKPOINTS = [
4088
4697
  const worktreeCapableTools = /* @__PURE__ */ new Set(["claude"]);
4089
4698
  return manifest.tools.some((t) => worktreeCapableTools.has(t));
4090
4699
  },
4091
- execute: async (manifest, rootDir) => {
4092
- const { enabled } = await inquirer.prompt([{
4093
- type: "confirm",
4094
- name: "enabled",
4095
- message: "hatch3r now supports worktree file isolation for parallel agent sessions. Enable it?",
4096
- default: true
4097
- }]);
4700
+ execute: async (manifest, rootDir, headless) => {
4701
+ let enabled;
4702
+ if (headless) {
4703
+ enabled = true;
4704
+ } else {
4705
+ const answer = await inquirer.prompt([{
4706
+ type: "confirm",
4707
+ name: "enabled",
4708
+ message: "hatch3r now supports worktree file isolation for parallel agent sessions. Enable it?",
4709
+ default: true
4710
+ }]);
4711
+ enabled = answer.enabled;
4712
+ }
4098
4713
  const updated = { ...manifest, worktree: { enabled } };
4099
4714
  const notices = [];
4100
4715
  if (enabled) {
4101
4716
  const wtContent = await generateWorktreeInclude(updated, rootDir);
4102
- await safeWriteFile(join15(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
4717
+ await safeWriteFile(join16(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
4103
4718
  appendIfNoBlock: true
4104
4719
  });
4105
4720
  notices.push("Worktree isolation enabled \u2014 .worktreeinclude generated");
@@ -4110,12 +4725,12 @@ var MIGRATION_CHECKPOINTS = [
4110
4725
  }
4111
4726
  }
4112
4727
  ];
4113
- async function runMigrationCheckpoints(manifest, rootDir) {
4728
+ async function runMigrationCheckpoints(manifest, rootDir, headless = false) {
4114
4729
  let current = manifest;
4115
4730
  const allNotices = [];
4116
4731
  for (const checkpoint of MIGRATION_CHECKPOINTS) {
4117
4732
  if (await checkpoint.condition(current, rootDir)) {
4118
- const { manifest: updated, notices } = await checkpoint.execute(current, rootDir);
4733
+ const { manifest: updated, notices } = await checkpoint.execute(current, rootDir, headless);
4119
4734
  current = updated;
4120
4735
  allNotices.push(...notices);
4121
4736
  }
@@ -4129,9 +4744,10 @@ async function updateCommand(_opts) {
4129
4744
  if (!manifest) {
4130
4745
  error("No .agents/hatch.json found.");
4131
4746
  console.log(chalk4.dim(" Run `npx hatch3r init` to set up your project first.\n"));
4132
- throw new HatchError("No .agents/hatch.json found.", 1);
4747
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
4133
4748
  }
4134
- const { manifest: migrated, allNotices } = await runMigrationCheckpoints(manifest, rootDir);
4749
+ const headless = !!_opts?.yes;
4750
+ const { manifest: migrated, allNotices } = await runMigrationCheckpoints(manifest, rootDir, headless);
4135
4751
  const m = migrated;
4136
4752
  for (const notice of allNotices) {
4137
4753
  warn(notice);
@@ -4152,165 +4768,659 @@ async function updateCommand(_opts) {
4152
4768
  ], "success");
4153
4769
  }
4154
4770
 
4155
- // src/archive/index.ts
4156
- import { access as access3, cp as cp3, mkdir as mkdir5, readFile as readFile13, readdir as readdir7, rm as rm2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
4157
- import { dirname as dirname8, join as join16, sep } from "path";
4158
- function toPosixPath(p) {
4159
- return sep === "\\" ? p.replaceAll("\\", "/") : p;
4771
+ // src/workspace/manifest.ts
4772
+ import { readFile as readFile14 } from "fs/promises";
4773
+ import { join as join17, normalize as normalize2, isAbsolute as isAbsolute2 } from "path";
4774
+
4775
+ // src/workspace/types.ts
4776
+ var WORKSPACE_MANIFEST_FILE = "workspace.json";
4777
+ var WORKSPACE_MANIFEST_VERSION = "1.0.0";
4778
+
4779
+ // src/workspace/manifest.ts
4780
+ function isUnsafeRepoPath(repoPath) {
4781
+ if (repoPath.includes("\0")) return true;
4782
+ if (isAbsolute2(repoPath)) return true;
4783
+ const normalized = normalize2(repoPath);
4784
+ if (normalized.startsWith("..")) return true;
4785
+ return false;
4160
4786
  }
4161
- var ARCHIVE_DIR = ".hatch3r-archive";
4162
- var TOOL_PATH_PREFIXES = {
4163
- cursor: [".cursor/"],
4164
- claude: [".claude/", "CLAUDE.md", ".mcp.json"],
4165
- copilot: [".github/copilot-instructions.md", ".github/workflows/copilot-setup-steps.yml", ".vscode/mcp.json"],
4166
- windsurf: [".windsurf/", ".windsurfrules"],
4167
- amp: [".amp/"],
4168
- codex: [".codex/"],
4169
- gemini: [".gemini/", "GEMINI.md"],
4170
- cline: [".roo/", ".roomodes"],
4171
- aider: ["CONVENTIONS.md", ".aider.conf.yml"],
4172
- kiro: [".kiro/"],
4173
- opencode: ["opencode.json"],
4174
- goose: [".goosehints"],
4175
- zed: [".rules"],
4176
- "amazon-q": [".amazonq/"]
4177
- };
4178
- var PATH_PATTERNS = [
4179
- { pattern: /\/rules\/([^/]+)\.(mdc|md)$/, type: "rules" },
4180
- { pattern: /\/agents\/([^/]+)\.md$/, type: "agents" },
4181
- { pattern: /\/skills\/([^/]+)\/SKILL\.md$/, type: "skills" },
4182
- { pattern: /\/commands\/([^/]+)\.md$/, type: "commands" }
4183
- ];
4184
- function parseOutputPath(filePath) {
4185
- for (const { pattern, type } of PATH_PATTERNS) {
4186
- const match = filePath.match(pattern);
4187
- if (match) {
4188
- let id = match[1];
4189
- if (id.startsWith(HATCH3R_PREFIX)) {
4190
- id = id.slice(HATCH3R_PREFIX.length);
4191
- }
4192
- id = sanitizeId(id);
4193
- if (id.length > 0) return { type, id };
4787
+ function validateWorkspaceManifest(data) {
4788
+ if (!data || typeof data !== "object") return false;
4789
+ const obj = data;
4790
+ if (typeof obj.version !== "string") return false;
4791
+ if (typeof obj.hatch3rVersion !== "string") return false;
4792
+ if (typeof obj.name !== "string") return false;
4793
+ if (!Array.isArray(obj.repos)) return false;
4794
+ if (typeof obj.syncStrategy !== "string") return false;
4795
+ if (!["manual", "on-sync"].includes(obj.syncStrategy)) return false;
4796
+ if (!obj.defaults || typeof obj.defaults !== "object") return false;
4797
+ const defaults = obj.defaults;
4798
+ if (!Array.isArray(defaults.tools)) return false;
4799
+ if (!defaults.features || typeof defaults.features !== "object") return false;
4800
+ if (!defaults.mcp || typeof defaults.mcp !== "object") return false;
4801
+ const mcp = defaults.mcp;
4802
+ if (!Array.isArray(mcp.servers)) return false;
4803
+ if (defaults.content !== void 0) {
4804
+ if (typeof defaults.content !== "object" || defaults.content === null) return false;
4805
+ const content = defaults.content;
4806
+ if (typeof content.preset !== "string") return false;
4807
+ if (typeof content.projectType !== "string") return false;
4808
+ if (typeof content.teamSize !== "string") return false;
4809
+ if (!content.items || typeof content.items !== "object") return false;
4810
+ }
4811
+ for (const repo of obj.repos) {
4812
+ if (!repo || typeof repo !== "object") return false;
4813
+ const r = repo;
4814
+ if (typeof r.path !== "string") return false;
4815
+ if (isUnsafeRepoPath(r.path)) return false;
4816
+ if (typeof r.sync !== "boolean") return false;
4817
+ if (r.owner !== void 0 && typeof r.owner !== "string") return false;
4818
+ if (r.repo !== void 0 && typeof r.repo !== "string") return false;
4819
+ if (r.defaultBranch !== void 0 && typeof r.defaultBranch !== "string") return false;
4820
+ if (r.platform !== void 0 && typeof r.platform !== "string") return false;
4821
+ }
4822
+ return true;
4823
+ }
4824
+ async function readWorkspaceManifest(rootDir) {
4825
+ const manifestPath = join17(rootDir, AGENTS_DIR, WORKSPACE_MANIFEST_FILE);
4826
+ let raw;
4827
+ try {
4828
+ raw = await readFile14(manifestPath, "utf-8");
4829
+ } catch (err) {
4830
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
4831
+ return null;
4194
4832
  }
4833
+ throw err;
4195
4834
  }
4196
- return null;
4835
+ let parsed;
4836
+ try {
4837
+ parsed = JSON.parse(raw);
4838
+ } catch (err) {
4839
+ throw new HatchError(
4840
+ `Malformed JSON in ${manifestPath}: ${err instanceof Error ? err.message : String(err)}`,
4841
+ 1,
4842
+ "CONFIG_ERROR"
4843
+ );
4844
+ }
4845
+ if (!validateWorkspaceManifest(parsed)) {
4846
+ throw new HatchError(
4847
+ `Invalid workspace manifest in ${manifestPath}: required fields missing or malformed.`,
4848
+ 1,
4849
+ "VALIDATION_ERROR"
4850
+ );
4851
+ }
4852
+ return parsed;
4197
4853
  }
4198
- function stripFrontmatter(content) {
4199
- const trimmed = content.trimStart();
4200
- if (trimmed.startsWith("---")) {
4201
- const endIdx = trimmed.indexOf("\n---", 3);
4202
- if (endIdx !== -1) {
4203
- return trimmed.slice(endIdx + 4).trim();
4854
+ async function writeWorkspaceManifest(rootDir, manifest) {
4855
+ const manifestPath = join17(rootDir, AGENTS_DIR, WORKSPACE_MANIFEST_FILE);
4856
+ await atomicWriteFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
4857
+ }
4858
+ function createWorkspaceManifest(name, defaults, repos, syncStrategy = "manual") {
4859
+ return {
4860
+ version: WORKSPACE_MANIFEST_VERSION,
4861
+ hatch3rVersion: HATCH3R_VERSION,
4862
+ name,
4863
+ repos,
4864
+ defaults,
4865
+ syncStrategy
4866
+ };
4867
+ }
4868
+
4869
+ // src/workspace/detect.ts
4870
+ import { readdir as readdir8, stat as stat3, access as access4 } from "fs/promises";
4871
+ import { join as join18, dirname as dirname9, relative as relative2 } from "path";
4872
+ async function detectSubRepos(rootDir) {
4873
+ const repos = [];
4874
+ let entries;
4875
+ try {
4876
+ entries = await readdir8(rootDir, { withFileTypes: true });
4877
+ } catch {
4878
+ return repos;
4879
+ }
4880
+ for (const entry of entries) {
4881
+ if (!entry.isDirectory()) continue;
4882
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
4883
+ const subDir = join18(rootDir, entry.name);
4884
+ const gitPath = join18(subDir, ".git");
4885
+ let isGitRepo = false;
4886
+ try {
4887
+ const gitStat = await stat3(gitPath);
4888
+ isGitRepo = gitStat.isDirectory() || gitStat.isFile();
4889
+ } catch {
4204
4890
  }
4891
+ if (!isGitRepo) continue;
4892
+ let hasHatch3r = false;
4893
+ try {
4894
+ await access4(join18(subDir, AGENTS_DIR, "hatch.json"));
4895
+ hasHatch3r = true;
4896
+ } catch {
4897
+ }
4898
+ repos.push({
4899
+ path: entry.name,
4900
+ name: entry.name,
4901
+ hasHatch3r
4902
+ });
4205
4903
  }
4206
- return content.trim();
4904
+ return repos.sort((a, b) => a.name.localeCompare(b.name));
4207
4905
  }
4208
- async function fileExists2(path) {
4906
+ async function hasGitDir(dir) {
4209
4907
  try {
4210
- await access3(path);
4211
- return true;
4908
+ const gitStat = await stat3(join18(dir, ".git"));
4909
+ return gitStat.isDirectory() || gitStat.isFile();
4212
4910
  } catch {
4213
4911
  return false;
4214
4912
  }
4215
4913
  }
4216
- async function collectToolFiles(rootDir, tool) {
4217
- const prefixes = TOOL_PATH_PREFIXES[tool];
4218
- if (!prefixes) return [];
4219
- const files = [];
4220
- for (const prefix of prefixes) {
4221
- const absPath = join16(rootDir, prefix);
4222
- if (prefix.endsWith("/")) {
4223
- try {
4224
- const entries = await readdir7(absPath, { recursive: true, withFileTypes: true });
4225
- for (const entry of entries) {
4226
- if (entry.isFile()) {
4227
- const parent = entry.parentPath ?? entry.path ?? absPath;
4228
- const relPath = toPosixPath(join16(prefix, parent.slice(absPath.length), entry.name));
4229
- files.push(relPath);
4230
- }
4231
- }
4232
- } catch {
4914
+ async function shouldSuggestWorkspace(dir) {
4915
+ if (await hasGitDir(dir)) return false;
4916
+ const repos = await detectSubRepos(dir);
4917
+ return repos.length > 0;
4918
+ }
4919
+
4920
+ // src/workspace/sync.ts
4921
+ import { createHash as createHash2 } from "crypto";
4922
+ import { mkdir as mkdir6, access as access6, readFile as readFile16 } from "fs/promises";
4923
+ import { join as join20, relative as relative3 } from "path";
4924
+ import { fileURLToPath as fileURLToPath2 } from "url";
4925
+ import { dirname as dirname10 } from "path";
4926
+
4927
+ // src/detect/repoAnalyzer.ts
4928
+ import { access as access5, readFile as readFile15, readdir as readdir9 } from "fs/promises";
4929
+ import { join as join19 } from "path";
4930
+ async function analyzeRepo(rootDir) {
4931
+ const [languages, pm, isMonorepo, hasExistingAgents, existingTools, frameworks] = await Promise.all([
4932
+ detectLanguages(rootDir),
4933
+ detectPackageManager(rootDir),
4934
+ detectMonorepo(rootDir),
4935
+ detectExistingAgents(rootDir),
4936
+ detectExistingTools(rootDir),
4937
+ detectFrameworks(rootDir)
4938
+ ]);
4939
+ const packageManager = pm.name;
4940
+ return {
4941
+ languages,
4942
+ packageManager,
4943
+ frameworks,
4944
+ isMonorepo,
4945
+ hasExistingAgents,
4946
+ existingTools,
4947
+ rootDir
4948
+ };
4949
+ }
4950
+ async function detectLanguages(rootDir) {
4951
+ const languages = [];
4952
+ const indicators = {
4953
+ typescript: ["tsconfig.json", "tsconfig.base.json"],
4954
+ javascript: ["jsconfig.json"],
4955
+ python: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
4956
+ rust: ["Cargo.toml", "Cargo.lock"],
4957
+ go: ["go.mod", "go.sum"],
4958
+ java: ["pom.xml", "build.gradle"],
4959
+ kotlin: ["build.gradle.kts"],
4960
+ ruby: ["Gemfile"],
4961
+ php: ["composer.json"],
4962
+ swift: ["Package.swift"],
4963
+ dart: ["pubspec.yaml"],
4964
+ elixir: ["mix.exs"]
4965
+ };
4966
+ for (const [lang, files] of Object.entries(indicators)) {
4967
+ for (const file of files) {
4968
+ if (await pathExists(join19(rootDir, file))) {
4969
+ languages.push(lang);
4970
+ break;
4233
4971
  }
4234
- } else if (await fileExists2(absPath)) {
4235
- files.push(prefix);
4236
4972
  }
4237
4973
  }
4238
- return files;
4974
+ try {
4975
+ const rootEntries = await readdir9(rootDir);
4976
+ if (rootEntries.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
4977
+ languages.push("csharp");
4978
+ }
4979
+ } catch (err) {
4980
+ if (err.code !== "ENOENT") throw err;
4981
+ }
4982
+ if (languages.length === 0) {
4983
+ languages.push("unknown");
4984
+ }
4985
+ return languages;
4239
4986
  }
4240
- async function archiveToolOutputs(rootDir, tool) {
4241
- const filesToArchive = await collectToolFiles(rootDir, tool);
4242
- if (filesToArchive.length === 0) {
4243
- return { archivedFiles: [], migrations: [] };
4987
+ async function detectMonorepo(rootDir) {
4988
+ if (await pathExists(join19(rootDir, "pnpm-workspace.yaml"))) return true;
4989
+ if (await pathExists(join19(rootDir, "lerna.json"))) return true;
4990
+ if (await pathExists(join19(rootDir, "nx.json"))) return true;
4991
+ if (await pathExists(join19(rootDir, "turbo.json"))) return true;
4992
+ if (await pathExists(join19(rootDir, "pants.toml"))) return true;
4993
+ try {
4994
+ const pkgJson = await readFile15(join19(rootDir, "package.json"), "utf-8");
4995
+ const pkg = JSON.parse(pkgJson);
4996
+ if (pkg.workspaces) return true;
4997
+ } catch (err) {
4998
+ const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
4999
+ if (!isExpected) throw err;
4244
5000
  }
4245
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4246
- const archiveBase = join16(rootDir, ARCHIVE_DIR, tool, timestamp);
4247
- const archivedFiles = [];
4248
- const migrations = [];
4249
- for (const relPath of filesToArchive) {
4250
- const absPath = join16(rootDir, relPath);
4251
- if (!await fileExists2(absPath)) continue;
4252
- let content;
4253
- try {
4254
- content = await readFile13(absPath, "utf-8");
4255
- } catch {
4256
- continue;
5001
+ return false;
5002
+ }
5003
+ async function detectExistingAgents(rootDir) {
5004
+ return pathExists(join19(rootDir, ".agents"));
5005
+ }
5006
+ var TOOL_INDICATORS = [
5007
+ { tool: "cursor", paths: [".cursor"] },
5008
+ { tool: "copilot", paths: [join19(".github", "copilot-instructions.md")] },
5009
+ { tool: "claude", paths: ["CLAUDE.md", ".claude"] },
5010
+ { tool: "opencode", paths: ["opencode.json", "opencode.jsonc"] },
5011
+ { tool: "windsurf", paths: [".windsurfrules"] },
5012
+ { tool: "amp", paths: [".amp"] },
5013
+ { tool: "codex", paths: [".codex"] },
5014
+ { tool: "gemini", paths: [".gemini", "GEMINI.md"] },
5015
+ { tool: "cline", paths: [".clinerules", ".roo", ".roomodes"] },
5016
+ { tool: "aider", paths: [".aider", ".aider.conf.yml"] },
5017
+ { tool: "kiro", paths: [".kiro"] },
5018
+ { tool: "goose", paths: [".goosehints", ".goose"] },
5019
+ { tool: "zed", paths: [".rules"] },
5020
+ { tool: "amazon-q", paths: [".amazonq"] }
5021
+ ];
5022
+ async function detectExistingTools(rootDir) {
5023
+ const results = await Promise.allSettled(
5024
+ TOOL_INDICATORS.map(async ({ tool, paths }) => {
5025
+ for (const p of paths) {
5026
+ if (await pathExists(join19(rootDir, p))) return tool;
5027
+ }
5028
+ return null;
5029
+ })
5030
+ );
5031
+ return results.filter(
5032
+ (r) => r.status === "fulfilled" && r.value !== null
5033
+ ).map((r) => r.value);
5034
+ }
5035
+ var FRAMEWORK_CONFIG_INDICATORS = [
5036
+ { framework: "next", configs: ["next.config.js", "next.config.mjs", "next.config.ts"] },
5037
+ { framework: "angular", configs: ["angular.json"] },
5038
+ { framework: "svelte", configs: ["svelte.config.js", "svelte.config.ts"] },
5039
+ { framework: "nuxt", configs: ["nuxt.config.js", "nuxt.config.ts"] },
5040
+ { framework: "astro", configs: ["astro.config.mjs", "astro.config.ts"] }
5041
+ ];
5042
+ var FRAMEWORK_DEP_INDICATORS = [
5043
+ { framework: "next", deps: ["next"] },
5044
+ { framework: "angular", deps: ["@angular/core"] },
5045
+ { framework: "sveltekit", deps: ["@sveltejs/kit"] },
5046
+ { framework: "svelte", deps: ["svelte"] },
5047
+ { framework: "nuxt", deps: ["nuxt"] },
5048
+ { framework: "remix", deps: ["@remix-run/react"] },
5049
+ { framework: "astro", deps: ["astro"] },
5050
+ { framework: "vue", deps: ["vue"] },
5051
+ { framework: "react", deps: ["react"] },
5052
+ { framework: "express", deps: ["express"] },
5053
+ { framework: "fastify", deps: ["fastify"] },
5054
+ { framework: "hono", deps: ["hono"] }
5055
+ ];
5056
+ var FRAMEWORK_SUPPRESSION = {
5057
+ next: "react",
5058
+ remix: "react",
5059
+ nuxt: "vue",
5060
+ sveltekit: "svelte"
5061
+ };
5062
+ async function detectFrameworks(rootDir) {
5063
+ const detected = /* @__PURE__ */ new Set();
5064
+ const configResults = await Promise.allSettled(
5065
+ FRAMEWORK_CONFIG_INDICATORS.map(async ({ framework, configs }) => {
5066
+ for (const cfg of configs) {
5067
+ if (await pathExists(join19(rootDir, cfg))) return framework;
5068
+ }
5069
+ return null;
5070
+ })
5071
+ );
5072
+ for (const r of configResults) {
5073
+ if (r.status === "fulfilled" && r.value !== null) {
5074
+ detected.add(r.value);
4257
5075
  }
4258
- if (hasManagedBlock(content)) {
4259
- const customContent = stripFrontmatter(extractCustomContent(content));
4260
- if (customContent.length > 0) {
4261
- const parsed = parseOutputPath(relPath);
4262
- if (parsed) {
4263
- const customizePath = join16(rootDir, ".hatch3r", parsed.type, `${parsed.id}.customize.md`);
4264
- if (!await fileExists2(customizePath)) {
4265
- await mkdir5(dirname8(customizePath), { recursive: true });
4266
- await writeFile3(customizePath, customContent + "\n", "utf-8");
4267
- migrations.push({
4268
- from: relPath,
4269
- to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
4270
- type: parsed.type,
4271
- id: parsed.id
4272
- });
4273
- }
4274
- }
5076
+ }
5077
+ try {
5078
+ const raw = await readFile15(join19(rootDir, "package.json"), "utf-8");
5079
+ const pkg = JSON.parse(raw);
5080
+ const allDeps = {
5081
+ ...pkg.dependencies,
5082
+ ...pkg.devDependencies
5083
+ };
5084
+ for (const { framework, deps } of FRAMEWORK_DEP_INDICATORS) {
5085
+ if (deps.some((d) => d in allDeps)) {
5086
+ detected.add(framework);
4275
5087
  }
4276
5088
  }
4277
- const archiveDest = join16(archiveBase, relPath);
4278
- await mkdir5(dirname8(archiveDest), { recursive: true });
4279
- await cp3(absPath, archiveDest);
4280
- const srcStat = await stat2(absPath);
4281
- const destStat = await stat2(archiveDest);
4282
- if (destStat.size !== srcStat.size) {
4283
- throw new Error(`Archive copy size mismatch for ${relPath}: source=${srcStat.size}, dest=${destStat.size}`);
5089
+ } catch (err) {
5090
+ const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
5091
+ if (!isExpected) throw err;
5092
+ }
5093
+ for (const [meta, base] of Object.entries(FRAMEWORK_SUPPRESSION)) {
5094
+ if (detected.has(meta)) {
5095
+ detected.delete(base);
4284
5096
  }
4285
- await rm2(absPath);
4286
- archivedFiles.push(relPath);
4287
5097
  }
4288
- await cleanEmptyDirs(rootDir, filesToArchive);
4289
- return { archivedFiles, migrations };
5098
+ return [...detected];
4290
5099
  }
4291
- async function cleanEmptyDirs(rootDir, paths) {
4292
- const dirs = /* @__PURE__ */ new Set();
4293
- for (const p of paths) {
4294
- let dir = dirname8(join16(rootDir, p));
4295
- while (dir !== rootDir && dir.length > rootDir.length) {
4296
- dirs.add(dir);
4297
- dir = dirname8(dir);
5100
+ async function pathExists(path) {
5101
+ try {
5102
+ await access5(path);
5103
+ return true;
5104
+ } catch (err) {
5105
+ if (err.code !== "ENOENT") throw err;
5106
+ return false;
5107
+ }
5108
+ }
5109
+
5110
+ // src/workspace/resolve.ts
5111
+ function resolveRepoConfig(defaults, overrides, protectedIds) {
5112
+ const tools = overrides?.tools ?? defaults.tools;
5113
+ const features = { ...defaults.features, ...overrides?.features ?? {} };
5114
+ const mcp = overrides?.mcp ?? defaults.mcp;
5115
+ const platform = overrides?.platform ?? defaults.platform;
5116
+ let models;
5117
+ if (defaults.models || overrides?.models) {
5118
+ models = {
5119
+ ...defaults.models,
5120
+ ...overrides?.models,
5121
+ agents: {
5122
+ ...defaults.models?.agents,
5123
+ ...overrides?.models?.agents
5124
+ }
5125
+ };
5126
+ if (!models.default && !models.agents) models = void 0;
5127
+ }
5128
+ const contentIds = getAllContentIds(defaults.content);
5129
+ const excludedContent = [];
5130
+ const addedContent = [];
5131
+ if (overrides?.contentOverrides?.exclude) {
5132
+ for (const id of overrides.contentOverrides.exclude) {
5133
+ if (protectedIds?.has(id)) continue;
5134
+ if (contentIds.has(id)) {
5135
+ contentIds.delete(id);
5136
+ excludedContent.push(id);
5137
+ }
4298
5138
  }
4299
5139
  }
4300
- const sorted = [...dirs].sort((a, b) => b.length - a.length);
4301
- for (const dir of sorted) {
5140
+ if (overrides?.contentOverrides?.include) {
5141
+ for (const id of overrides.contentOverrides.include) {
5142
+ if (!contentIds.has(id)) {
5143
+ contentIds.add(id);
5144
+ addedContent.push(id);
5145
+ }
5146
+ }
5147
+ }
5148
+ return { platform, tools, features, mcp, models, contentIds, excludedContent, addedContent };
5149
+ }
5150
+ function buildSelectionFromIds(ids, baseSelection, allItems) {
5151
+ const items = {
5152
+ agents: [],
5153
+ skills: [],
5154
+ rules: [],
5155
+ commands: [],
5156
+ prompts: [],
5157
+ hooks: [],
5158
+ githubAgents: []
5159
+ };
5160
+ for (const item of allItems) {
5161
+ if (!ids.has(item.id)) continue;
5162
+ const key = TYPE_TO_SELECTION_KEY[item.type];
5163
+ if (key) items[key].push(item.id);
5164
+ }
5165
+ return {
5166
+ preset: "custom",
5167
+ projectType: baseSelection.projectType,
5168
+ teamSize: baseSelection.teamSize,
5169
+ items
5170
+ };
5171
+ }
5172
+
5173
+ // src/workspace/git.ts
5174
+ import { execFileSync as execFileSync4 } from "child_process";
5175
+ function parseGitRemote(cwd) {
5176
+ try {
5177
+ const url = execFileSync4("git", ["remote", "get-url", "origin"], {
5178
+ cwd,
5179
+ stdio: "pipe"
5180
+ }).toString().trim();
5181
+ const sshMatch = url.match(/[:\/]([^/]+)\/([^/]+?)(?:\.git)?$/);
5182
+ if (sshMatch) {
5183
+ return { owner: sshMatch[1], repo: sshMatch[2] };
5184
+ }
5185
+ return { owner: "", repo: "" };
5186
+ } catch {
5187
+ return { owner: "", repo: "" };
5188
+ }
5189
+ }
5190
+ function parseGitDefaultBranch(cwd) {
5191
+ try {
5192
+ const ref = execFileSync4("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], {
5193
+ cwd,
5194
+ stdio: "pipe"
5195
+ }).toString().trim();
5196
+ if (ref && ref.startsWith("origin/")) {
5197
+ return ref.replace(/^origin\//, "");
5198
+ }
5199
+ return "main";
5200
+ } catch {
5201
+ return "main";
5202
+ }
5203
+ }
5204
+ function detectPlatformFromRemote(remoteUrl) {
5205
+ if (remoteUrl.includes("dev.azure.com") || remoteUrl.includes("visualstudio.com")) return "azure-devops";
5206
+ if (remoteUrl.includes("gitlab.com") || remoteUrl.includes("gitlab.")) return "gitlab";
5207
+ return "github";
5208
+ }
5209
+ function getGitRemoteUrl(cwd) {
5210
+ try {
5211
+ return execFileSync4("git", ["remote", "get-url", "origin"], { cwd, stdio: "pipe" }).toString().trim();
5212
+ } catch {
5213
+ return "";
5214
+ }
5215
+ }
5216
+ function detectRepoGitIdentity(repoDir) {
5217
+ const remoteUrl = getGitRemoteUrl(repoDir);
5218
+ const { owner, repo } = parseGitRemote(repoDir);
5219
+ const defaultBranch = parseGitDefaultBranch(repoDir);
5220
+ const platform = remoteUrl ? detectPlatformFromRemote(remoteUrl) : "github";
5221
+ return { owner, repo, defaultBranch, platform };
5222
+ }
5223
+
5224
+ // src/workspace/sync.ts
5225
+ var __dirname2 = dirname10(fileURLToPath2(import.meta.url));
5226
+ var CONTENT_ROOT = findPackageRoot(__dirname2);
5227
+ var CHARS_PER_TOKEN = 4;
5228
+ async function estimateTokensForContent(contentIds, index) {
5229
+ let totalChars = 0;
5230
+ for (const id of contentIds) {
5231
+ const items = getAllItemsById(index, id);
5232
+ for (const item of items) {
5233
+ try {
5234
+ if (item.type === "skill") {
5235
+ const skillPath = join20(CONTENT_ROOT, item.relativePath, "SKILL.md");
5236
+ const content = await readFile16(skillPath, "utf-8");
5237
+ totalChars += content.length;
5238
+ } else {
5239
+ const filePath = join20(CONTENT_ROOT, item.relativePath);
5240
+ const content = await readFile16(filePath, "utf-8");
5241
+ totalChars += content.length;
5242
+ }
5243
+ } catch {
5244
+ }
5245
+ }
5246
+ }
5247
+ return Math.ceil(totalChars / CHARS_PER_TOKEN);
5248
+ }
5249
+ async function syncWorkspaceRepos(workspaceRoot, options = {}) {
5250
+ const wsManifest = await readWorkspaceManifest(workspaceRoot);
5251
+ if (!wsManifest) {
5252
+ return { repos: [] };
5253
+ }
5254
+ const wsChecksum = createHash2("sha256").update(JSON.stringify(wsManifest)).digest("hex");
5255
+ const index = await buildContentIndex(CONTENT_ROOT);
5256
+ const protectedIds = new Set(
5257
+ index.items.filter((item) => item.protected).map((item) => item.id)
5258
+ );
5259
+ const targetRepos = options.repos?.length ? wsManifest.repos.filter((r) => options.repos.includes(r.path)) : wsManifest.repos.filter((r) => r.sync);
5260
+ const results = [];
5261
+ for (const repoEntry of targetRepos) {
4302
5262
  try {
4303
- const entries = await readdir7(dir);
4304
- if (entries.length === 0) {
4305
- await rm2(dir, { recursive: true });
5263
+ const result = await syncSingleRepo(
5264
+ workspaceRoot,
5265
+ wsManifest,
5266
+ wsChecksum,
5267
+ repoEntry,
5268
+ index,
5269
+ protectedIds,
5270
+ options
5271
+ );
5272
+ results.push(result);
5273
+ } catch (err) {
5274
+ results.push({
5275
+ path: repoEntry.path,
5276
+ added: [],
5277
+ removed: [],
5278
+ toolsSynced: [],
5279
+ action: "error",
5280
+ error: err instanceof Error ? err.message : String(err)
5281
+ });
5282
+ }
5283
+ }
5284
+ if (!options.dryRun) {
5285
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5286
+ for (const result of results) {
5287
+ if (result.action === "synced") {
5288
+ const entry = wsManifest.repos.find((r) => r.path === result.path);
5289
+ if (entry) entry.lastSync = now;
4306
5290
  }
4307
- } catch {
4308
5291
  }
5292
+ await writeWorkspaceManifest(workspaceRoot, wsManifest);
4309
5293
  }
5294
+ return { repos: results };
4310
5295
  }
4311
- function removeManagedFilesForPaths(manifest, paths) {
4312
- const pathSet = new Set(paths);
4313
- manifest.managedFiles = manifest.managedFiles.filter((f) => !pathSet.has(f));
5296
+ async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry, index, protectedIds, options) {
5297
+ const repoDir = join20(workspaceRoot, repoEntry.path);
5298
+ const repoAgentsDir = join20(repoDir, AGENTS_DIR);
5299
+ try {
5300
+ await access6(repoDir);
5301
+ } catch {
5302
+ return {
5303
+ path: repoEntry.path,
5304
+ added: [],
5305
+ removed: [],
5306
+ toolsSynced: [],
5307
+ action: "error",
5308
+ error: `Directory not found: ${repoEntry.path}`
5309
+ };
5310
+ }
5311
+ const resolved = resolveRepoConfig(wsManifest.defaults, repoEntry.overrides, protectedIds);
5312
+ const effectiveSelection = buildSelectionFromIds(resolved.contentIds, wsManifest.defaults.content, index.items);
5313
+ const existingManifest = await readManifest(repoDir);
5314
+ const previousIds = existingManifest?.content ? getAllContentIds(existingManifest.content) : /* @__PURE__ */ new Set();
5315
+ const toAdd = [...resolved.contentIds].filter((id) => !previousIds.has(id));
5316
+ const toRemove = [...previousIds].filter(
5317
+ (id) => !resolved.contentIds.has(id) && !existingManifest?.workspace?.localContent?.includes(id)
5318
+ );
5319
+ if (options.dryRun) {
5320
+ const estimatedTokens = await estimateTokensForContent(resolved.contentIds, index);
5321
+ return {
5322
+ path: repoEntry.path,
5323
+ added: toAdd,
5324
+ removed: toRemove,
5325
+ toolsSynced: resolved.tools,
5326
+ action: "dry-run",
5327
+ estimatedTokens
5328
+ };
5329
+ }
5330
+ options.onProgress?.(`Syncing ${repoEntry.name ?? repoEntry.path}...`);
5331
+ await mkdir6(repoAgentsDir, { recursive: true });
5332
+ await copySelectedContent(CONTENT_ROOT, repoAgentsDir, effectiveSelection, index);
5333
+ for (const id of toRemove) {
5334
+ const items = getAllItemsById(index, id);
5335
+ for (const item of items) {
5336
+ await removeContentItem(repoAgentsDir, item, { rootDir: repoDir });
5337
+ }
5338
+ }
5339
+ const canonicalAgentsMd = await generateCanonicalAgentsMd(repoAgentsDir);
5340
+ await safeWriteFile(join20(repoAgentsDir, "AGENTS.md"), canonicalAgentsMd, { force: true });
5341
+ const repoInfo = await analyzeRepo(repoDir);
5342
+ let gitOwner = repoEntry.owner ?? "";
5343
+ let gitRepo = repoEntry.repo ?? "";
5344
+ let gitBranch = repoEntry.defaultBranch ?? "";
5345
+ let gitPlatform = repoEntry.platform;
5346
+ if (!gitOwner && !gitRepo) {
5347
+ const identity = detectRepoGitIdentity(repoDir);
5348
+ gitOwner = identity.owner;
5349
+ gitRepo = identity.repo;
5350
+ gitBranch = gitBranch || identity.defaultBranch;
5351
+ gitPlatform = gitPlatform ?? identity.platform;
5352
+ }
5353
+ if (!gitOwner && !gitRepo && existingManifest) {
5354
+ gitOwner = existingManifest.owner;
5355
+ gitRepo = existingManifest.repo;
5356
+ }
5357
+ if (!gitBranch) gitBranch = "main";
5358
+ const manifest = createManifest({
5359
+ platform: gitPlatform ?? resolved.platform,
5360
+ owner: gitOwner,
5361
+ repo: gitRepo,
5362
+ namespace: gitOwner,
5363
+ project: gitRepo,
5364
+ defaultBranch: gitBranch,
5365
+ tools: resolved.tools,
5366
+ features: resolved.features,
5367
+ mcpServers: resolved.mcp.servers,
5368
+ content: effectiveSelection,
5369
+ languages: repoInfo.languages
5370
+ });
5371
+ manifest.workspace = {
5372
+ rootPath: relative3(repoDir, workspaceRoot),
5373
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
5374
+ syncVersion: HATCH3R_VERSION,
5375
+ workspaceChecksum: wsChecksum,
5376
+ excludedContent: resolved.excludedContent.length > 0 ? resolved.excludedContent : void 0,
5377
+ localContent: existingManifest?.workspace?.localContent
5378
+ };
5379
+ if (resolved.models) {
5380
+ manifest.models = resolved.models;
5381
+ }
5382
+ await writeManifest(repoDir, manifest);
5383
+ await safeWriteFile(join20(repoDir, "AGENTS.md"), AGENTS_MD_FULL, {
5384
+ managedContent: AGENTS_MD_INNER,
5385
+ appendIfNoBlock: true
5386
+ });
5387
+ addManagedFile(manifest, "AGENTS.md");
5388
+ const toolsSynced = [];
5389
+ for (const tool of resolved.tools) {
5390
+ try {
5391
+ const adapter = getAdapter(tool);
5392
+ const outputs = await adapter.generate(repoAgentsDir, manifest);
5393
+ for (const w of adapter.warnings) {
5394
+ options.onWarn?.(w);
5395
+ }
5396
+ for (const out of outputs) {
5397
+ await safeWriteFile(join20(repoDir, out.path), out.content, {
5398
+ managedContent: out.managedContent,
5399
+ appendIfNoBlock: true
5400
+ });
5401
+ addManagedFile(manifest, out.path);
5402
+ }
5403
+ toolsSynced.push(tool);
5404
+ } catch (err) {
5405
+ options.onWarn?.(
5406
+ `Failed to generate ${tool} output for ${repoEntry.path}: ${err instanceof Error ? err.message : String(err)}`
5407
+ );
5408
+ }
5409
+ }
5410
+ await writeManifest(repoDir, manifest);
5411
+ const integrityManifest = await generateIntegrityManifest(repoAgentsDir, HATCH3R_VERSION);
5412
+ await writeIntegrityManifest(repoAgentsDir, integrityManifest);
5413
+ if (manifest.features.mcp && manifest.mcp.servers.length > 0) {
5414
+ await ensureEnvMcp(repoDir, manifest.mcp.servers);
5415
+ await ensureGitignoreEntry(repoDir);
5416
+ }
5417
+ return {
5418
+ path: repoEntry.path,
5419
+ added: toAdd,
5420
+ removed: toRemove,
5421
+ toolsSynced,
5422
+ action: "synced"
5423
+ };
4314
5424
  }
4315
5425
 
4316
5426
  // src/cli/shared/constants.ts
@@ -4329,7 +5439,8 @@ var TOOL_DISPLAY_NAMES = {
4329
5439
  kiro: "Kiro",
4330
5440
  goose: "Goose",
4331
5441
  zed: "Zed",
4332
- "amazon-q": "Amazon Q"
5442
+ "amazon-q": "Amazon Q",
5443
+ antigravity: "Antigravity"
4333
5444
  };
4334
5445
  var TOOL_PROMPT_CHOICES = TOOLS.map((t) => ({
4335
5446
  name: TOOL_DISPLAY_NAMES[t],
@@ -4359,6 +5470,42 @@ var PLATFORM_MCP_SERVER = {
4359
5470
  "azure-devops": "azure-devops",
4360
5471
  gitlab: "gitlab"
4361
5472
  };
5473
+ var TOOL_COMMAND_SYNTAX = {
5474
+ cursor: "/",
5475
+ copilot: "/",
5476
+ claude: "/",
5477
+ opencode: "/",
5478
+ windsurf: "run workflow ",
5479
+ amp: "/",
5480
+ codex: "prompt with ",
5481
+ gemini: "/",
5482
+ cline: "run workflow ",
5483
+ aider: "prompt with ",
5484
+ kiro: "/",
5485
+ goose: "prompt with ",
5486
+ zed: "/",
5487
+ "amazon-q": "/",
5488
+ antigravity: "/"
5489
+ };
5490
+ function formatCommandHint(tools, commandName) {
5491
+ const allSlash = tools.every((t) => TOOL_COMMAND_SYNTAX[t] === "/");
5492
+ if (allSlash) {
5493
+ return `/${commandName}`;
5494
+ }
5495
+ return `the ${commandName} command`;
5496
+ }
5497
+ var TOOL_SECRET_NOTES = {
5498
+ cursor: "Cursor: auto-loads .env.mcp from project root",
5499
+ copilot: "VS Code / Copilot: auto-loads .env.mcp from project root",
5500
+ claude: "Claude Code: reads .env.mcp via shell sourcing (run `set -a && source .env.mcp && set +a` before starting)",
5501
+ windsurf: "Windsurf: auto-loads .env.mcp from project root",
5502
+ cline: "Cline / Roo Code: reads env from VS Code settings; copy values to .vscode/settings.json or use shell sourcing",
5503
+ amp: "Amp: reads env from shell; source .env.mcp in your shell profile",
5504
+ codex: "Codex CLI: reads env from shell; source .env.mcp before running",
5505
+ gemini: "Gemini CLI: reads env from shell; source .env.mcp before running",
5506
+ aider: "Aider: reads env from shell; source .env.mcp before running",
5507
+ opencode: "OpenCode: reads env from shell; source .env.mcp before running"
5508
+ };
4362
5509
  function sanitizeInput(value) {
4363
5510
  return value.replace(/[^a-zA-Z0-9._-]/g, "");
4364
5511
  }
@@ -4372,7 +5519,7 @@ function isWSL() {
4372
5519
  }
4373
5520
 
4374
5521
  // src/cli/commands/config.ts
4375
- var __dirname2 = dirname9(fileURLToPath2(import.meta.url));
5522
+ var __dirname3 = dirname11(fileURLToPath3(import.meta.url));
4376
5523
  function computeDiff(oldManifest, newTools, newFeatures, newMcp, newPlatform, newOwner, newRepo, newNamespace, newProject) {
4377
5524
  const oldToolSet = new Set(oldManifest.tools);
4378
5525
  const newToolSet = new Set(newTools);
@@ -4425,7 +5572,13 @@ async function configCommand() {
4425
5572
  if (!manifest) {
4426
5573
  error("No .agents/hatch.json found.");
4427
5574
  console.log(chalk5.dim(" Run `npx hatch3r init` to set up your project first.\n"));
4428
- throw new HatchError("No .agents/hatch.json found.", 1);
5575
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
5576
+ }
5577
+ if (manifest.workspace) {
5578
+ warn(
5579
+ `This repo is managed by workspace at ${manifest.workspace.rootPath}. Changes here may be overwritten on next workspace sync.`
5580
+ );
5581
+ console.log();
4429
5582
  }
4430
5583
  printCurrentConfig(manifest);
4431
5584
  const wslTheme = isWSL() ? { icon: { checked: chalk5.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
@@ -4499,7 +5652,7 @@ async function configCommand() {
4499
5652
  const tools = toolAnswers.tools;
4500
5653
  if (tools.length === 0) {
4501
5654
  error("At least one tool must be selected.");
4502
- throw new HatchError("At least one tool must be selected.", 1);
5655
+ throw new HatchError("At least one tool must be selected.", 1, "VALIDATION_ERROR");
4503
5656
  }
4504
5657
  const currentFeatureKeys = Object.keys(DEFAULT_FEATURES).filter((k) => manifest.features[k]);
4505
5658
  const featureAnswers = await inquirer2.prompt([
@@ -4560,8 +5713,12 @@ async function configCommand() {
4560
5713
  }
4561
5714
  ]);
4562
5715
  if (manageContent.manage) {
4563
- const contentRoot = findPackageRoot(__dirname2);
4564
- const agentsDir = join17(rootDir, AGENTS_DIR);
5716
+ info(
5717
+ chalk5.dim("Config adds/removes content items. To customize an item's behavior without ") + chalk5.dim("removing it, use .hatch3r/<type>/<id>.customize.yaml instead.")
5718
+ );
5719
+ console.log();
5720
+ const contentRoot = findPackageRoot(__dirname3);
5721
+ const agentsDir = join21(rootDir, AGENTS_DIR);
4565
5722
  const index = await buildContentIndex(contentRoot);
4566
5723
  const currentIds = /* @__PURE__ */ new Set();
4567
5724
  for (const ids of Object.values(manifest.content.items)) {
@@ -4581,6 +5738,63 @@ async function configCommand() {
4581
5738
  }
4582
5739
  ]);
4583
5740
  const newIds = new Set(contentAnswer.items);
5741
+ const pendingRemovals = [];
5742
+ for (const id of currentIds) {
5743
+ if (!newIds.has(id)) pendingRemovals.push(id);
5744
+ }
5745
+ if (pendingRemovals.length > 0) {
5746
+ const dependencyWarnings = [];
5747
+ for (const removedId of pendingRemovals) {
5748
+ const dependents = [];
5749
+ for (const keepId of contentAnswer.items) {
5750
+ const keepItem = index.byId.get(keepId);
5751
+ if (!keepItem) continue;
5752
+ try {
5753
+ const filePath = keepItem.type === "skill" ? join21(agentsDir, keepItem.relativePath, "SKILL.md") : join21(agentsDir, keepItem.relativePath);
5754
+ const content = await readFile17(filePath, "utf-8");
5755
+ const refs = extractContentReferences(content);
5756
+ if (refs.includes(removedId)) {
5757
+ dependents.push(keepId);
5758
+ }
5759
+ } catch {
5760
+ }
5761
+ }
5762
+ if (dependents.length > 0) {
5763
+ dependencyWarnings.push(
5764
+ `Removing "${removedId}" \u2014 referenced by: ${dependents.join(", ")}`
5765
+ );
5766
+ }
5767
+ }
5768
+ const proposedSelection = {
5769
+ ...manifest.content,
5770
+ items: {
5771
+ agents: [],
5772
+ skills: [],
5773
+ rules: [],
5774
+ commands: [],
5775
+ prompts: [],
5776
+ hooks: [],
5777
+ githubAgents: []
5778
+ }
5779
+ };
5780
+ for (const id of contentAnswer.items) {
5781
+ const proposedItem = index.byId.get(id);
5782
+ if (proposedItem) {
5783
+ const key = TYPE_TO_SELECTION_KEY[proposedItem.type];
5784
+ if (key) proposedSelection.items[key].push(proposedItem.id);
5785
+ }
5786
+ }
5787
+ const orchWarnings = validateOrchestrationDependencies(proposedSelection);
5788
+ dependencyWarnings.push(...orchWarnings);
5789
+ if (dependencyWarnings.length > 0) {
5790
+ console.log();
5791
+ warn("Dependency warnings for removed content:");
5792
+ for (const w of dependencyWarnings) {
5793
+ console.log(chalk5.dim(` ${w}`));
5794
+ }
5795
+ console.log();
5796
+ }
5797
+ }
4584
5798
  for (const id of contentAnswer.items) {
4585
5799
  if (!currentIds.has(id)) {
4586
5800
  const item = index.byId.get(id);
@@ -4618,7 +5832,7 @@ async function configCommand() {
4618
5832
  manifest.content.items = newItems;
4619
5833
  if (contentChanges.added.length > 0 || contentChanges.removed.length > 0) {
4620
5834
  const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
4621
- await safeWriteFile(join17(agentsDir, "AGENTS.md"), canonicalAgentsMd);
5835
+ await safeWriteFile(join21(agentsDir, "AGENTS.md"), canonicalAgentsMd);
4622
5836
  }
4623
5837
  }
4624
5838
  }
@@ -4685,7 +5899,7 @@ async function configCommand() {
4685
5899
  if (manifest.worktree?.enabled) {
4686
5900
  const wtContent = await generateWorktreeInclude(manifest, rootDir);
4687
5901
  const wtManaged = extractManagedContent(wtContent);
4688
- await safeWriteFile(join17(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
5902
+ await safeWriteFile(join21(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
4689
5903
  managedContent: wtManaged
4690
5904
  });
4691
5905
  }
@@ -4755,132 +5969,156 @@ async function configCommand() {
4755
5969
  }
4756
5970
  console.log();
4757
5971
  }
4758
- }
4759
-
4760
- // src/cli/commands/init.ts
4761
- import { access as access5, mkdir as mkdir6, readFile as readFile15 } from "fs/promises";
4762
- import { fileURLToPath as fileURLToPath3 } from "url";
4763
- import { dirname as dirname10, join as join19 } from "path";
4764
- import { execFileSync as execFileSync4 } from "child_process";
4765
- import chalk6 from "chalk";
4766
- import inquirer3 from "inquirer";
4767
-
4768
- // src/detect/repoAnalyzer.ts
4769
- import { access as access4, readFile as readFile14, readdir as readdir8 } from "fs/promises";
4770
- import { join as join18 } from "path";
4771
- async function analyzeRepo(rootDir) {
4772
- const [languages, pm, isMonorepo, hasExistingAgents, existingTools] = await Promise.all([
4773
- detectLanguages(rootDir),
4774
- detectPackageManager(rootDir),
4775
- detectMonorepo(rootDir),
4776
- detectExistingAgents(rootDir),
4777
- detectExistingTools(rootDir)
4778
- ]);
4779
- const packageManager = pm.name;
4780
- return {
4781
- languages,
4782
- packageManager,
4783
- isMonorepo,
4784
- hasExistingAgents,
4785
- existingTools,
4786
- rootDir
4787
- };
4788
- }
4789
- async function detectLanguages(rootDir) {
4790
- const languages = [];
4791
- const indicators = {
4792
- typescript: ["tsconfig.json", "tsconfig.base.json"],
4793
- javascript: ["jsconfig.json"],
4794
- python: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
4795
- rust: ["Cargo.toml", "Cargo.lock"],
4796
- go: ["go.mod", "go.sum"],
4797
- java: ["pom.xml", "build.gradle"],
4798
- kotlin: ["build.gradle.kts"],
4799
- ruby: ["Gemfile"],
4800
- php: ["composer.json"],
4801
- swift: ["Package.swift"],
4802
- dart: ["pubspec.yaml"],
4803
- elixir: ["mix.exs"]
4804
- };
4805
- for (const [lang, files] of Object.entries(indicators)) {
4806
- for (const file of files) {
4807
- if (await pathExists(join18(rootDir, file))) {
4808
- languages.push(lang);
4809
- break;
4810
- }
5972
+ if (diff.addedTools.length > 0 || diff.removedTools.length > 0) {
5973
+ console.log();
5974
+ info("Tool migration notes:");
5975
+ if (diff.removedTools.length > 0) {
5976
+ info(chalk5.dim(` Removed tool output archived to .hatch3r-archive/ (recoverable).`));
5977
+ info(chalk5.dim(` Customizations in .hatch3r/ are tool-agnostic and carry forward.`));
4811
5978
  }
4812
- }
4813
- try {
4814
- const rootEntries = await readdir8(rootDir);
4815
- if (rootEntries.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
4816
- languages.push("csharp");
5979
+ if (diff.addedTools.length > 0) {
5980
+ info(chalk5.dim(` New tool output generated. Restart your editor to pick up changes.`));
5981
+ info(chalk5.dim(` MCP secrets (.env.mcp) are shared across tools \u2014 no re-entry needed.`));
4817
5982
  }
4818
- } catch (err) {
4819
- if (err.code !== "ENOENT") throw err;
4820
- }
4821
- if (languages.length === 0) {
4822
- languages.push("unknown");
4823
- }
4824
- return languages;
4825
- }
4826
- async function detectMonorepo(rootDir) {
4827
- if (await pathExists(join18(rootDir, "pnpm-workspace.yaml"))) return true;
4828
- if (await pathExists(join18(rootDir, "lerna.json"))) return true;
4829
- if (await pathExists(join18(rootDir, "nx.json"))) return true;
4830
- if (await pathExists(join18(rootDir, "turbo.json"))) return true;
4831
- if (await pathExists(join18(rootDir, "pants.toml"))) return true;
4832
- try {
4833
- const pkgJson = await readFile14(join18(rootDir, "package.json"), "utf-8");
4834
- const pkg = JSON.parse(pkgJson);
4835
- if (pkg.workspaces) return true;
4836
- } catch (err) {
4837
- const isExpected = err.code === "ENOENT" || err instanceof SyntaxError;
4838
- if (!isExpected) throw err;
5983
+ console.log();
4839
5984
  }
4840
- return false;
4841
- }
4842
- async function detectExistingAgents(rootDir) {
4843
- return pathExists(join18(rootDir, ".agents"));
4844
- }
4845
- var TOOL_INDICATORS = [
4846
- { tool: "cursor", paths: [".cursor"] },
4847
- { tool: "copilot", paths: [join18(".github", "copilot-instructions.md")] },
4848
- { tool: "claude", paths: ["CLAUDE.md", ".claude"] },
4849
- { tool: "opencode", paths: ["opencode.json", "opencode.jsonc"] },
4850
- { tool: "windsurf", paths: [".windsurfrules"] },
4851
- { tool: "amp", paths: [".amp"] },
4852
- { tool: "codex", paths: [".codex"] },
4853
- { tool: "gemini", paths: [".gemini", "GEMINI.md"] },
4854
- { tool: "cline", paths: [".clinerules", ".roo", ".roomodes"] },
4855
- { tool: "aider", paths: [".aider", ".aider.conf.yml"] },
4856
- { tool: "kiro", paths: [".kiro"] },
4857
- { tool: "goose", paths: [".goosehints", ".goose"] },
4858
- { tool: "zed", paths: [".rules"] },
4859
- { tool: "amazon-q", paths: [".amazonq"] }
4860
- ];
4861
- async function detectExistingTools(rootDir) {
4862
- const results = await Promise.allSettled(
4863
- TOOL_INDICATORS.map(async ({ tool, paths }) => {
4864
- for (const p of paths) {
4865
- if (await pathExists(join18(rootDir, p))) return tool;
5985
+ const wsManifest = await readWorkspaceManifest(rootDir);
5986
+ if (wsManifest) {
5987
+ console.log();
5988
+ info(chalk5.bold("Workspace configuration"));
5989
+ const currentRepos = wsManifest.repos.map((r) => r.path);
5990
+ console.log(chalk5.dim(` Repos: ${currentRepos.join(", ") || "(none)"}`));
5991
+ console.log(chalk5.dim(` Sync strategy: ${wsManifest.syncStrategy}`));
5992
+ const { manageWorkspace } = await inquirer2.prompt([
5993
+ {
5994
+ type: "confirm",
5995
+ name: "manageWorkspace",
5996
+ message: "Configure workspace settings?",
5997
+ default: false
4866
5998
  }
4867
- return null;
4868
- })
4869
- );
4870
- return results.filter(
4871
- (r) => r.status === "fulfilled" && r.value !== null
4872
- ).map((r) => r.value);
4873
- }
4874
- async function pathExists(path) {
4875
- try {
4876
- await access4(path);
4877
- return true;
4878
- } catch (err) {
4879
- if (err.code !== "ENOENT") throw err;
4880
- return false;
5999
+ ]);
6000
+ if (manageWorkspace) {
6001
+ const detectedRepos = await detectSubRepos(rootDir);
6002
+ const existingPaths = new Set(wsManifest.repos.map((r) => r.path));
6003
+ const newRepos = detectedRepos.filter((r) => !existingPaths.has(r.path));
6004
+ if (newRepos.length > 0) {
6005
+ const { addRepos } = await inquirer2.prompt([
6006
+ {
6007
+ type: "checkbox",
6008
+ name: "addRepos",
6009
+ message: "New repos detected. Add to workspace?",
6010
+ choices: newRepos.map((r) => ({
6011
+ name: r.name,
6012
+ value: r.path,
6013
+ checked: false
6014
+ })),
6015
+ ...wslTheme && { theme: wslTheme }
6016
+ }
6017
+ ]);
6018
+ for (const path of addRepos) {
6019
+ wsManifest.repos.push({ path, name: path, sync: false });
6020
+ }
6021
+ }
6022
+ if (wsManifest.repos.length > 0) {
6023
+ const { syncRepos } = await inquirer2.prompt([
6024
+ {
6025
+ type: "checkbox",
6026
+ name: "syncRepos",
6027
+ message: "Select repos to sync:",
6028
+ choices: wsManifest.repos.map((r) => ({
6029
+ name: r.name ?? r.path,
6030
+ value: r.path,
6031
+ checked: r.sync
6032
+ })),
6033
+ ...wslTheme && { theme: wslTheme }
6034
+ }
6035
+ ]);
6036
+ const syncSet = new Set(syncRepos);
6037
+ for (const repo2 of wsManifest.repos) {
6038
+ repo2.sync = syncSet.has(repo2.path);
6039
+ }
6040
+ }
6041
+ if (wsManifest.repos.length > 0) {
6042
+ const { editIdentity } = await inquirer2.prompt([
6043
+ {
6044
+ type: "list",
6045
+ name: "editIdentity",
6046
+ message: "Repo git identities:",
6047
+ choices: [
6048
+ { name: "Keep current", value: "keep" },
6049
+ { name: "Re-detect all from git remotes", value: "detect" },
6050
+ { name: "Edit manually", value: "edit" }
6051
+ ],
6052
+ default: "keep"
6053
+ }
6054
+ ]);
6055
+ if (editIdentity === "detect") {
6056
+ for (const repo2 of wsManifest.repos) {
6057
+ const identity = detectRepoGitIdentity(join21(rootDir, repo2.path));
6058
+ repo2.owner = identity.owner || void 0;
6059
+ repo2.repo = identity.repo || void 0;
6060
+ repo2.defaultBranch = identity.defaultBranch || void 0;
6061
+ repo2.platform = identity.platform || void 0;
6062
+ }
6063
+ info("Re-detected git identities for all repos.");
6064
+ } else if (editIdentity === "edit") {
6065
+ for (const repo2 of wsManifest.repos) {
6066
+ console.log(chalk5.bold(`
6067
+ ${repo2.name ?? repo2.path}:`));
6068
+ const identity = await inquirer2.prompt([
6069
+ { type: "input", name: "owner", message: " Owner:", default: repo2.owner || void 0 },
6070
+ { type: "input", name: "repo", message: " Repo:", default: repo2.repo || void 0 },
6071
+ { type: "input", name: "defaultBranch", message: " Default branch:", default: repo2.defaultBranch || "main" }
6072
+ ]);
6073
+ repo2.owner = sanitizeInput(identity.owner) || void 0;
6074
+ repo2.repo = sanitizeInput(identity.repo) || void 0;
6075
+ repo2.defaultBranch = identity.defaultBranch.trim() || void 0;
6076
+ }
6077
+ }
6078
+ }
6079
+ const { strategy } = await inquirer2.prompt([
6080
+ {
6081
+ type: "list",
6082
+ name: "strategy",
6083
+ message: "Sync strategy:",
6084
+ choices: [
6085
+ { name: "Manual \u2014 sync sub-repos only with --repos flag", value: "manual" },
6086
+ { name: "On sync \u2014 auto-sync sub-repos when running hatch3r sync", value: "on-sync" }
6087
+ ],
6088
+ default: wsManifest.syncStrategy
6089
+ }
6090
+ ]);
6091
+ wsManifest.syncStrategy = strategy;
6092
+ await writeWorkspaceManifest(rootDir, wsManifest);
6093
+ const syncCount = wsManifest.repos.filter((r) => r.sync).length;
6094
+ if (syncCount > 0) {
6095
+ const { syncNow } = await inquirer2.prompt([
6096
+ {
6097
+ type: "confirm",
6098
+ name: "syncNow",
6099
+ message: `Sync ${syncCount} repo(s) now?`,
6100
+ default: false
6101
+ }
6102
+ ]);
6103
+ if (syncNow) {
6104
+ const wsSpinner = createSpinner(`Syncing ${syncCount} repo(s)...`);
6105
+ wsSpinner.start();
6106
+ const result = await syncWorkspaceRepos(rootDir, { onWarn: (msg) => warn(msg) });
6107
+ const succeeded = result.repos.filter((r) => r.action === "synced").length;
6108
+ wsSpinner.succeed(`Workspace sync: ${succeeded} repo(s) synced`);
6109
+ }
6110
+ }
6111
+ }
4881
6112
  }
4882
6113
  }
4883
6114
 
6115
+ // src/cli/commands/init.ts
6116
+ import { access as access7, mkdir as mkdir7, readFile as readFile18 } from "fs/promises";
6117
+ import { fileURLToPath as fileURLToPath4 } from "url";
6118
+ import { basename as basename2, dirname as dirname12, join as join22 } from "path";
6119
+ import chalk6 from "chalk";
6120
+ import inquirer3 from "inquirer";
6121
+
4884
6122
  // src/content/presets.ts
4885
6123
  var PRESETS = [
4886
6124
  {
@@ -4920,66 +6158,45 @@ function getPreset(id) {
4920
6158
  }
4921
6159
 
4922
6160
  // src/cli/commands/init.ts
4923
- var __dirname3 = dirname10(fileURLToPath3(import.meta.url));
4924
- var CONTENT_ROOT = findPackageRoot(__dirname3);
6161
+ var __dirname4 = dirname12(fileURLToPath4(import.meta.url));
6162
+ var CONTENT_ROOT2 = findPackageRoot(__dirname4);
4925
6163
  var DEFAULT_TOOLS = ["cursor"];
4926
6164
  var DEFAULT_FEATURE_KEYS = Object.keys(DEFAULT_FEATURES);
4927
6165
  var DEFAULT_MCP = ["playwright", "github", "context7"];
4928
- function parseGitRemote() {
4929
- try {
4930
- const url = execFileSync4("git", ["remote", "get-url", "origin"], {
4931
- stdio: "pipe"
4932
- }).toString().trim();
4933
- const sshMatch = url.match(/[:\/]([^/]+)\/([^/]+?)(?:\.git)?$/);
4934
- if (sshMatch) {
4935
- return { owner: sshMatch[1], repo: sshMatch[2] };
4936
- }
4937
- return { owner: "", repo: "" };
4938
- } catch (err) {
4939
- const e = err;
4940
- if (e.code === "ENOENT") return { owner: "", repo: "" };
4941
- if (e.status === 128) return { owner: "", repo: "" };
4942
- throw err;
4943
- }
4944
- }
4945
- function parseGitDefaultBranch() {
4946
- try {
4947
- const ref = execFileSync4("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], {
4948
- stdio: "pipe"
4949
- }).toString().trim();
4950
- if (ref && ref.startsWith("origin/")) {
4951
- return ref.replace(/^origin\//, "");
4952
- }
4953
- return "main";
4954
- } catch (err) {
4955
- const e = err;
4956
- if (e.code === "ENOENT") return "main";
4957
- if (e.status === 128) return "main";
4958
- throw err;
4959
- }
6166
+ function selectionHasBoardContent(selection) {
6167
+ return selection.items.commands.some((id) => id.startsWith("hatch3r-board"));
4960
6168
  }
4961
- function detectPlatformFromRemote(remoteUrl) {
4962
- if (remoteUrl.includes("dev.azure.com") || remoteUrl.includes("visualstudio.com")) return "azure-devops";
4963
- if (remoteUrl.includes("gitlab.com") || remoteUrl.includes("gitlab.")) return "gitlab";
4964
- return "github";
6169
+ function warnBoardPrerequisites(selection) {
6170
+ if (!selectionHasBoardContent(selection)) return;
6171
+ info(
6172
+ `Board commands selected. Prerequisites: ${chalk6.bold("GitHub Projects V2")} must be enabled and your PAT needs the ${chalk6.bold("project")} scope. See ${chalk6.dim("https://docs.github.com/en/issues/planning-and-tracking-with-projects")}`
6173
+ );
4965
6174
  }
4966
- function getGitRemoteUrl() {
4967
- try {
4968
- return execFileSync4("git", ["remote", "get-url", "origin"], { stdio: "pipe" }).toString().trim();
4969
- } catch {
4970
- return "";
6175
+ function deriveWorkspacePlatform(identities) {
6176
+ const counts = /* @__PURE__ */ new Map();
6177
+ for (const id of identities) {
6178
+ counts.set(id.platform, (counts.get(id.platform) ?? 0) + 1);
6179
+ }
6180
+ let best = "github";
6181
+ let max = 0;
6182
+ for (const [p, c] of counts) {
6183
+ if (c > max) {
6184
+ best = p;
6185
+ max = c;
6186
+ }
4971
6187
  }
6188
+ return best;
4972
6189
  }
4973
6190
  async function runInit(options) {
4974
6191
  const { rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection } = options;
4975
- const agentsDir = join19(rootDir, AGENTS_DIR);
6192
+ const agentsDir = join22(rootDir, AGENTS_DIR);
4976
6193
  const totalSteps = 4;
4977
6194
  const s1 = createSpinner(step(1, totalSteps, "Creating canonical files..."));
4978
6195
  s1.start();
4979
- await mkdir6(agentsDir, { recursive: true });
6196
+ await mkdir7(agentsDir, { recursive: true });
4980
6197
  const existingManifest = await readManifest(rootDir);
4981
- const index = await buildContentIndex(CONTENT_ROOT);
4982
- await copySelectedContent(CONTENT_ROOT, agentsDir, contentSelection, index);
6198
+ const index = await buildContentIndex(CONTENT_ROOT2);
6199
+ await copySelectedContent(CONTENT_ROOT2, agentsDir, contentSelection, index);
4983
6200
  if (existingManifest?.content) {
4984
6201
  const oldIds = getAllContentIds(existingManifest.content);
4985
6202
  const newIds = getAllContentIds(contentSelection);
@@ -4990,10 +6207,10 @@ async function runInit(options) {
4990
6207
  }
4991
6208
  }
4992
6209
  }
4993
- await mkdir6(join19(agentsDir, "learnings"), { recursive: true });
4994
- const mcpPath = join19(agentsDir, "mcp", "mcp.json");
6210
+ await mkdir7(join22(agentsDir, "learnings"), { recursive: true });
6211
+ const mcpPath = join22(agentsDir, "mcp", "mcp.json");
4995
6212
  try {
4996
- const mcpRaw = await readFile15(mcpPath, "utf-8");
6213
+ const mcpRaw = await readFile18(mcpPath, "utf-8");
4997
6214
  const mcpParsed = JSON.parse(mcpRaw);
4998
6215
  if (mcpParsed.mcpServers) {
4999
6216
  const selected = new Set(mcpServers);
@@ -5015,7 +6232,7 @@ async function runInit(options) {
5015
6232
  if (!isExpected) throw err;
5016
6233
  }
5017
6234
  const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
5018
- await safeWriteFile(join19(agentsDir, "AGENTS.md"), canonicalAgentsMd, { force: true });
6235
+ await safeWriteFile(join22(agentsDir, "AGENTS.md"), canonicalAgentsMd, { force: true });
5019
6236
  s1.succeed(step(1, totalSteps, `Canonical files created (${countSelectionItems(contentSelection)} items)`));
5020
6237
  const s2 = createSpinner(step(2, totalSteps, "Writing manifest..."));
5021
6238
  s2.start();
@@ -5026,7 +6243,7 @@ async function runInit(options) {
5026
6243
  step(3, totalSteps, `Generating ${tools.map((t) => TOOL_DISPLAY_NAMES[t] ?? t).join(", ")} output...`)
5027
6244
  );
5028
6245
  s3.start();
5029
- await safeWriteFile(join19(rootDir, "AGENTS.md"), AGENTS_MD_FULL, {
6246
+ await safeWriteFile(join22(rootDir, "AGENTS.md"), AGENTS_MD_FULL, {
5030
6247
  managedContent: AGENTS_MD_INNER,
5031
6248
  appendIfNoBlock: true
5032
6249
  });
@@ -5040,7 +6257,7 @@ async function runInit(options) {
5040
6257
  warn(w);
5041
6258
  }
5042
6259
  for (const out of outputs) {
5043
- await safeWriteFile(join19(rootDir, out.path), out.content, {
6260
+ await safeWriteFile(join22(rootDir, out.path), out.content, {
5044
6261
  managedContent: out.managedContent,
5045
6262
  appendIfNoBlock: true
5046
6263
  });
@@ -5059,7 +6276,7 @@ async function runInit(options) {
5059
6276
  }
5060
6277
  if (adapterFailures.length === tools.length) {
5061
6278
  s3.fail(step(3, totalSteps, "All adapters failed"));
5062
- throw new HatchError("All adapters failed", 1);
6279
+ throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
5063
6280
  }
5064
6281
  }
5065
6282
  s3.succeed(step(3, totalSteps, adapterFailures.length > 0 ? `Adapter output generated (${adapterFailures.length} failed)` : "Adapter output generated"));
@@ -5077,7 +6294,7 @@ async function runInit(options) {
5077
6294
  if (manifest.worktree?.enabled) {
5078
6295
  const wtContent = await generateWorktreeInclude(manifest, rootDir);
5079
6296
  const wtManaged = extractManagedContent(wtContent);
5080
- await safeWriteFile(join19(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
6297
+ await safeWriteFile(join22(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
5081
6298
  managedContent: wtManaged,
5082
6299
  appendIfNoBlock: true
5083
6300
  });
@@ -5125,22 +6342,21 @@ async function runInit(options) {
5125
6342
  const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
5126
6343
  summaryLines.push("");
5127
6344
  if (isGreenfield) {
5128
- summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold("/project-spec")} to define your new project`);
6345
+ summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold(formatCommandHint(tools, "project-spec"))} to define your new project`);
5129
6346
  } else {
5130
- summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold("/codebase-map")} to map your existing codebase`);
6347
+ summaryLines.push(`${chalk6.cyan("\u2192")} Run ${chalk6.bold(formatCommandHint(tools, "codebase-map"))} to map your existing codebase`);
5131
6348
  }
5132
- printBox("Hatch complete", summaryLines, "success");
5133
6349
  if (envResult && envResult.newVars.length > 0) {
5134
- warn(
5135
- `Add your secrets to .env.mcp: ${envResult.newVars.join(", ")}`
5136
- );
5137
- info(`Run this, then start or restart your editor: ${getSourceEnvMcpCommand()}`);
6350
+ summaryLines.push("");
6351
+ summaryLines.push(`${chalk6.yellow("!")} Add your secrets to ${chalk6.bold(".env.mcp")}: ${envResult.newVars.join(", ")}`);
6352
+ summaryLines.push(` Then run: ${chalk6.dim(getSourceEnvMcpCommand())}`);
5138
6353
  }
6354
+ printBox("Hatch complete", summaryLines, "success");
5139
6355
  }
5140
6356
  async function checkExisting(rootDir, skipPrompt, newSelection) {
5141
- const hatchJsonPath = join19(rootDir, AGENTS_DIR, "hatch.json");
6357
+ const hatchJsonPath = join22(rootDir, AGENTS_DIR, "hatch.json");
5142
6358
  try {
5143
- await access5(hatchJsonPath);
6359
+ await access7(hatchJsonPath);
5144
6360
  if (!skipPrompt) {
5145
6361
  let message = "Existing .agents/ found. This will overwrite managed files. Continue?";
5146
6362
  if (newSelection) {
@@ -5180,13 +6396,40 @@ function validateFlag(value, valid, fallback, name) {
5180
6396
  if (!value) return fallback;
5181
6397
  if (!valid.includes(value)) {
5182
6398
  error(`Invalid --${name}: "${value}". Valid: ${valid.join(", ")}`);
5183
- throw new HatchError(`Invalid --${name}: "${value}"`, 1);
6399
+ throw new HatchError(`Invalid --${name}: "${value}"`, 1, "VALIDATION_ERROR");
5184
6400
  }
5185
6401
  return value;
5186
6402
  }
5187
6403
  async function initCommand(opts = {}) {
5188
6404
  printBanner();
5189
6405
  const rootDir = process.cwd();
6406
+ if (!opts.workspace) {
6407
+ const suggestWs = await shouldSuggestWorkspace(rootDir);
6408
+ if (suggestWs) {
6409
+ const detectedRepos = await detectSubRepos(rootDir);
6410
+ if (opts.yes) {
6411
+ opts.workspace = true;
6412
+ info(chalk6.dim(`No git repo found. ${detectedRepos.length} git repo(s) detected in subdirectories \u2014 initializing as workspace.`));
6413
+ } else {
6414
+ info(`No git repo found, but ${detectedRepos.length} git repo(s) detected in subdirectories.`);
6415
+ const { useWorkspace } = await inquirer3.prompt([
6416
+ {
6417
+ type: "confirm",
6418
+ name: "useWorkspace",
6419
+ message: "Initialize as a multi-repo workspace?",
6420
+ default: true
6421
+ }
6422
+ ]);
6423
+ opts.workspace = useWorkspace;
6424
+ }
6425
+ }
6426
+ }
6427
+ if (opts.workspace) {
6428
+ const detectedRepos = await detectSubRepos(rootDir);
6429
+ const repoInfo2 = await analyzeRepo(rootDir);
6430
+ await runWorkspaceInit(rootDir, detectedRepos, repoInfo2, opts);
6431
+ return;
6432
+ }
5190
6433
  const detectSpinner = createSpinner("Detecting repository...");
5191
6434
  detectSpinner.start();
5192
6435
  const repoInfo = await analyzeRepo(rootDir);
@@ -5217,7 +6460,7 @@ async function initCommand(opts = {}) {
5217
6460
  if (invalid.length > 0) {
5218
6461
  error(`Invalid tool(s): ${invalid.join(", ")}`);
5219
6462
  console.log(chalk6.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
5220
- throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
6463
+ throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1, "VALIDATION_ERROR");
5221
6464
  }
5222
6465
  tools2 = rawTools;
5223
6466
  } else if (repoInfo.existingTools.length > 0) {
@@ -5234,12 +6477,13 @@ async function initCommand(opts = {}) {
5234
6477
  const projectType2 = validateFlag(opts.projectType, ["greenfield", "brownfield"], isGreenfield ? "greenfield" : "brownfield", "project-type");
5235
6478
  const teamSize2 = validateFlag(opts.teamSize, ["solo", "team"], "solo", "team-size");
5236
6479
  const preset = getPreset(presetId);
5237
- const index = await buildContentIndex(CONTENT_ROOT);
6480
+ const index = await buildContentIndex(CONTENT_ROOT2);
5238
6481
  const contentSelection2 = resolveSelection(preset, projectType2, teamSize2, index);
5239
6482
  const orchWarnings2 = validateOrchestrationDependencies(contentSelection2);
5240
6483
  for (const w of orchWarnings2) {
5241
6484
  warn(w);
5242
6485
  }
6486
+ warnBoardPrerequisites(contentSelection2);
5243
6487
  await checkExisting(rootDir, true, contentSelection2);
5244
6488
  await runInit({ rootDir, platform: platform2, owner: owner2, repo: repo2, namespace: namespace2, project: project2, defaultBranch: defaultBranch2, tools: tools2, features: features2, mcpServers: mcpServers2, repoInfo, contentSelection: contentSelection2 });
5245
6489
  return;
@@ -5304,42 +6548,53 @@ async function initCommand(opts = {}) {
5304
6548
  }
5305
6549
  ]);
5306
6550
  const defaultBranch = defaultBranchAnswers.defaultBranch.trim() || defaultBranchDefault;
6551
+ const filterIndex = await buildContentIndex(CONTENT_ROOT2);
5307
6552
  const isAutoGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
6553
+ const greenfieldExcl = countProjectTypeExclusions("greenfield", filterIndex.items);
6554
+ const brownfieldExcl = countProjectTypeExclusions("brownfield", filterIndex.items);
5308
6555
  const projectTypeAnswer = await inquirer3.prompt([
5309
6556
  {
5310
6557
  type: "list",
5311
6558
  name: "projectType",
5312
6559
  message: "Is this a new (greenfield) or existing (brownfield) project?",
5313
6560
  choices: [
5314
- { name: "Greenfield \u2014 new project from scratch", value: "greenfield" },
5315
- { name: "Brownfield \u2014 existing codebase", value: "brownfield" }
6561
+ { name: `Greenfield \u2014 new project from scratch${greenfieldExcl > 0 ? ` (filters out ${greenfieldExcl} brownfield-only item${greenfieldExcl === 1 ? "" : "s"})` : ""}`, value: "greenfield" },
6562
+ { name: `Brownfield \u2014 existing codebase${brownfieldExcl > 0 ? ` (filters out ${brownfieldExcl} greenfield-only item${brownfieldExcl === 1 ? "" : "s"})` : ""}`, value: "brownfield" }
5316
6563
  ],
5317
6564
  default: isAutoGreenfield ? "greenfield" : "brownfield"
5318
6565
  }
5319
6566
  ]);
5320
6567
  const projectType = projectTypeAnswer.projectType;
6568
+ const soloExcl = countTeamSizeExclusions("solo", filterIndex.items);
5321
6569
  const teamSizeAnswer = await inquirer3.prompt([
5322
6570
  {
5323
6571
  type: "list",
5324
6572
  name: "teamSize",
5325
6573
  message: "Solo developer or team collaboration?",
5326
6574
  choices: [
5327
- { name: "Solo \u2014 just me", value: "solo" },
6575
+ { name: `Solo \u2014 just me${soloExcl > 0 ? ` (filters out ${soloExcl} team-only item${soloExcl === 1 ? "" : "s"})` : ""}`, value: "solo" },
5328
6576
  { name: "Team \u2014 multiple contributors", value: "team" }
5329
6577
  ],
5330
6578
  default: "solo"
5331
6579
  }
5332
6580
  ]);
5333
6581
  const teamSize = teamSizeAnswer.teamSize;
6582
+ const totalItems = filterIndex.items.length;
5334
6583
  const presetAnswer = await inquirer3.prompt([
5335
6584
  {
5336
6585
  type: "list",
5337
6586
  name: "preset",
5338
6587
  message: "Select content profile:",
5339
- choices: PRESETS.map((p) => ({
5340
- name: `${p.name} \u2014 ${p.description}`,
5341
- value: p.id
5342
- })),
6588
+ choices: PRESETS.map((p) => {
6589
+ const excluded = countPresetExclusions(p, filterIndex);
6590
+ const estimated = p.id !== "custom" ? estimatePresetItemCount(p, projectType, teamSize, filterIndex) : 0;
6591
+ const countHint = estimated > 0 ? ` (~${estimated} items)` : "";
6592
+ const suffix = excluded > 0 ? ` (excludes ${excluded} of ${totalItems})` : "";
6593
+ return {
6594
+ name: `${p.name} \u2014 ${p.description}${countHint}${suffix}`,
6595
+ value: p.id
6596
+ };
6597
+ }),
5343
6598
  default: "standard"
5344
6599
  }
5345
6600
  ]);
@@ -5347,23 +6602,46 @@ async function initCommand(opts = {}) {
5347
6602
  const wslTheme = isWSL() ? { icon: { checked: chalk6.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
5348
6603
  let customSelections;
5349
6604
  if (selectedPreset.id === "custom") {
5350
- const contentIndex2 = await buildContentIndex(CONTENT_ROOT);
6605
+ const contentIndex = filterIndex;
5351
6606
  const tagGroups = /* @__PURE__ */ new Map();
5352
- for (const item of contentIndex2.items) {
6607
+ for (const item of contentIndex.items) {
5353
6608
  const primaryTag = item.tags[0] ?? "other";
5354
6609
  if (!tagGroups.has(primaryTag)) tagGroups.set(primaryTag, []);
5355
6610
  tagGroups.get(primaryTag).push(item);
5356
6611
  }
6612
+ const TAG_LABELS = {
6613
+ core: "Core",
6614
+ planning: "Planning",
6615
+ implementation: "Implementation",
6616
+ review: "Review",
6617
+ devops: "DevOps",
6618
+ maintenance: "Maintenance",
6619
+ greenfield: "Greenfield",
6620
+ brownfield: "Brownfield",
6621
+ board: "Board",
6622
+ security: "Security",
6623
+ a11y: "Accessibility",
6624
+ performance: "Performance",
6625
+ customize: "Customization",
6626
+ other: "Other"
6627
+ };
6628
+ const groupedChoices = [];
6629
+ for (const [tag, items] of tagGroups) {
6630
+ groupedChoices.push(new inquirer3.Separator(`\u2500\u2500 ${TAG_LABELS[tag] ?? tag} (${items.length}) \u2500\u2500`));
6631
+ for (const item of items) {
6632
+ groupedChoices.push({
6633
+ name: `${item.type}: ${item.id.replace(/^hatch3r-/, "")} \u2014 ${item.description.slice(0, 60)}`,
6634
+ value: item.id,
6635
+ checked: item.protected || item.tags.includes("core")
6636
+ });
6637
+ }
6638
+ }
5357
6639
  const customAnswer = await inquirer3.prompt([
5358
6640
  {
5359
6641
  type: "checkbox",
5360
6642
  name: "items",
5361
6643
  message: "Select content items:",
5362
- choices: contentIndex2.items.map((item) => ({
5363
- name: `${item.type}: ${item.id.replace(/^hatch3r-/, "")} \u2014 ${item.description.slice(0, 60)}`,
5364
- value: item.id,
5365
- checked: item.protected || item.tags.includes("core")
5366
- })),
6644
+ choices: groupedChoices,
5367
6645
  ...wslTheme && { theme: wslTheme }
5368
6646
  }
5369
6647
  ]);
@@ -5381,11 +6659,18 @@ async function initCommand(opts = {}) {
5381
6659
  }
5382
6660
  ]);
5383
6661
  const tools = toolAnswers.tools.length > 0 ? toolAnswers.tools : DEFAULT_TOOLS;
6662
+ const secretNotes = tools.map((t) => TOOL_SECRET_NOTES[t]).filter(Boolean);
6663
+ if (secretNotes.length > 0) {
6664
+ info(chalk6.dim("MCP secret loading by tool:"));
6665
+ for (const note of secretNotes) {
6666
+ info(chalk6.dim(` ${note}`));
6667
+ }
6668
+ }
5384
6669
  const featureAnswers = await inquirer3.prompt([
5385
6670
  {
5386
6671
  type: "checkbox",
5387
6672
  name: "features",
5388
- message: "Select features:",
6673
+ message: "Select features (MCP provides tool-server integration):",
5389
6674
  choices: FEATURE_CHOICES,
5390
6675
  default: DEFAULT_FEATURE_KEYS,
5391
6676
  ...wslTheme && { theme: wslTheme }
@@ -5417,35 +6702,380 @@ async function initCommand(opts = {}) {
5417
6702
  mcpServers.unshift(platformMcp);
5418
6703
  }
5419
6704
  }
5420
- const contentIndex = await buildContentIndex(CONTENT_ROOT);
5421
- const contentSelection = resolveSelection(selectedPreset, projectType, teamSize, contentIndex, customSelections);
6705
+ const contentSelection = resolveSelection(selectedPreset, projectType, teamSize, filterIndex, customSelections);
5422
6706
  const orchWarnings = validateOrchestrationDependencies(contentSelection);
5423
6707
  for (const w of orchWarnings) {
5424
6708
  warn(w);
5425
6709
  }
6710
+ warnBoardPrerequisites(contentSelection);
5426
6711
  await checkExisting(rootDir, false, contentSelection);
5427
6712
  await runInit({ rootDir, platform, owner, repo, namespace, project, defaultBranch, tools, features, mcpServers, repoInfo, contentSelection });
5428
6713
  }
6714
+ async function runWorkspaceInit(rootDir, detectedRepos, repoInfo, opts) {
6715
+ const headless = !!opts.yes;
6716
+ console.log();
6717
+ const wsSpinner = createSpinner("Detecting workspace repos...");
6718
+ wsSpinner.start();
6719
+ if (detectedRepos.length === 0) {
6720
+ wsSpinner.succeed("Workspace created (no sub-repos found)");
6721
+ const platform2 = "github";
6722
+ const tools2 = resolveToolsFromOpts(opts.tools, repoInfo);
6723
+ const features2 = { ...DEFAULT_FEATURES };
6724
+ const platformMcp = PLATFORM_MCP_SERVER[platform2];
6725
+ const mcpServers2 = features2.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
6726
+ const index = await buildContentIndex(CONTENT_ROOT2);
6727
+ const contentSelection2 = resolveSelection(getPreset("standard"), "brownfield", "solo", index);
6728
+ const wsManifest2 = createWorkspaceManifest(
6729
+ basename2(rootDir) || "workspace",
6730
+ { platform: platform2, tools: tools2, features: features2, mcp: { servers: mcpServers2 }, content: contentSelection2 },
6731
+ [],
6732
+ "manual"
6733
+ );
6734
+ await writeWorkspaceManifest(rootDir, wsManifest2);
6735
+ return;
6736
+ }
6737
+ const enriched = detectedRepos.map((r) => ({
6738
+ ...r,
6739
+ ...detectRepoGitIdentity(join22(rootDir, r.path))
6740
+ }));
6741
+ wsSpinner.succeed(`Workspace: ${detectedRepos.length} repo(s) detected`);
6742
+ console.log();
6743
+ console.log(chalk6.dim(" Repo Platform Owner/Repo Branch"));
6744
+ for (const r of enriched) {
6745
+ const name = (r.name ?? r.path).padEnd(16);
6746
+ if (r.owner && r.repo) {
6747
+ const platLabel = PLATFORM_DISPLAY_NAMES[r.platform].padEnd(14);
6748
+ const identity = `${r.owner}/${r.repo}`.padEnd(32);
6749
+ console.log(` ${name}${chalk6.dim(platLabel)}${chalk6.dim(identity)}${chalk6.dim(r.defaultBranch)}`);
6750
+ } else {
6751
+ console.log(` ${name}${chalk6.dim("(no remote detected)")}`);
6752
+ }
6753
+ }
6754
+ console.log();
6755
+ if (!headless) {
6756
+ const { acceptIdentity } = await inquirer3.prompt([
6757
+ {
6758
+ type: "confirm",
6759
+ name: "acceptIdentity",
6760
+ message: "Accept detected repo identities?",
6761
+ default: true
6762
+ }
6763
+ ]);
6764
+ if (!acceptIdentity) {
6765
+ for (const r of enriched) {
6766
+ console.log(chalk6.bold(`
6767
+ ${r.name ?? r.path}:`));
6768
+ const identity = await inquirer3.prompt([
6769
+ { type: "input", name: "owner", message: " Owner:", default: r.owner || void 0 },
6770
+ { type: "input", name: "repo", message: " Repo:", default: r.repo || void 0 },
6771
+ { type: "input", name: "defaultBranch", message: " Default branch:", default: r.defaultBranch || "main" }
6772
+ ]);
6773
+ r.owner = sanitizeInput(identity.owner);
6774
+ r.repo = sanitizeInput(identity.repo);
6775
+ r.defaultBranch = identity.defaultBranch.trim() || "main";
6776
+ }
6777
+ }
6778
+ }
6779
+ const platform = deriveWorkspacePlatform(enriched);
6780
+ let tools;
6781
+ let features;
6782
+ let mcpServers;
6783
+ let contentSelection;
6784
+ if (headless) {
6785
+ tools = resolveToolsFromOpts(opts.tools, repoInfo);
6786
+ features = { ...DEFAULT_FEATURES };
6787
+ const platformMcp = PLATFORM_MCP_SERVER[platform];
6788
+ mcpServers = features.mcp ? Array.from(/* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])) : [];
6789
+ const isGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
6790
+ const presetId = validateFlag(opts.preset, ["minimal", "standard", "full"], "standard", "preset");
6791
+ const projectType = validateFlag(opts.projectType, ["greenfield", "brownfield"], isGreenfield ? "greenfield" : "brownfield", "project-type");
6792
+ const teamSize = validateFlag(opts.teamSize, ["solo", "team"], "solo", "team-size");
6793
+ const preset = getPreset(presetId);
6794
+ const index = await buildContentIndex(CONTENT_ROOT2);
6795
+ contentSelection = resolveSelection(preset, projectType, teamSize, index);
6796
+ } else {
6797
+ const wslTheme = isWSL() ? { icon: { checked: chalk6.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
6798
+ const wsFilterIndex = await buildContentIndex(CONTENT_ROOT2);
6799
+ const isAutoGreenfield = repoInfo.languages.length === 1 && repoInfo.languages[0] === "unknown" && repoInfo.existingTools.length === 0 && !repoInfo.hasExistingAgents;
6800
+ const wsGreenfieldExcl = countProjectTypeExclusions("greenfield", wsFilterIndex.items);
6801
+ const wsBrownfieldExcl = countProjectTypeExclusions("brownfield", wsFilterIndex.items);
6802
+ const projectTypeAnswer = await inquirer3.prompt([
6803
+ {
6804
+ type: "list",
6805
+ name: "projectType",
6806
+ message: "Is this a new (greenfield) or existing (brownfield) project?",
6807
+ choices: [
6808
+ { name: `Greenfield \u2014 new project from scratch${wsGreenfieldExcl > 0 ? ` (filters out ${wsGreenfieldExcl} brownfield-only item${wsGreenfieldExcl === 1 ? "" : "s"})` : ""}`, value: "greenfield" },
6809
+ { name: `Brownfield \u2014 existing codebase${wsBrownfieldExcl > 0 ? ` (filters out ${wsBrownfieldExcl} greenfield-only item${wsBrownfieldExcl === 1 ? "" : "s"})` : ""}`, value: "brownfield" }
6810
+ ],
6811
+ default: isAutoGreenfield ? "greenfield" : "brownfield"
6812
+ }
6813
+ ]);
6814
+ const projectType = projectTypeAnswer.projectType;
6815
+ const wsSoloExcl = countTeamSizeExclusions("solo", wsFilterIndex.items);
6816
+ const teamSizeAnswer = await inquirer3.prompt([
6817
+ {
6818
+ type: "list",
6819
+ name: "teamSize",
6820
+ message: "Solo developer or team collaboration?",
6821
+ choices: [
6822
+ { name: `Solo \u2014 just me${wsSoloExcl > 0 ? ` (filters out ${wsSoloExcl} team-only item${wsSoloExcl === 1 ? "" : "s"})` : ""}`, value: "solo" },
6823
+ { name: "Team \u2014 multiple contributors", value: "team" }
6824
+ ],
6825
+ default: "solo"
6826
+ }
6827
+ ]);
6828
+ const teamSize = teamSizeAnswer.teamSize;
6829
+ const wsTotalItems = wsFilterIndex.items.length;
6830
+ const presetAnswer = await inquirer3.prompt([
6831
+ {
6832
+ type: "list",
6833
+ name: "preset",
6834
+ message: "Select content profile:",
6835
+ choices: PRESETS.map((p) => {
6836
+ const excluded = countPresetExclusions(p, wsFilterIndex);
6837
+ const wsEstimated = p.id !== "custom" ? estimatePresetItemCount(p, projectType, teamSize, wsFilterIndex) : 0;
6838
+ const wsCountHint = wsEstimated > 0 ? ` (~${wsEstimated} items)` : "";
6839
+ const suffix = excluded > 0 ? ` (excludes ${excluded} of ${wsTotalItems})` : "";
6840
+ return {
6841
+ name: `${p.name} \u2014 ${p.description}${wsCountHint}${suffix}`,
6842
+ value: p.id
6843
+ };
6844
+ }),
6845
+ default: "standard"
6846
+ }
6847
+ ]);
6848
+ const selectedPreset = getPreset(presetAnswer.preset);
6849
+ let customSelections;
6850
+ if (selectedPreset.id === "custom") {
6851
+ const contentIndex = wsFilterIndex;
6852
+ const wsTagGroups = /* @__PURE__ */ new Map();
6853
+ for (const item of contentIndex.items) {
6854
+ const primaryTag = item.tags[0] ?? "other";
6855
+ if (!wsTagGroups.has(primaryTag)) wsTagGroups.set(primaryTag, []);
6856
+ wsTagGroups.get(primaryTag).push(item);
6857
+ }
6858
+ const WS_TAG_LABELS = {
6859
+ core: "Core",
6860
+ planning: "Planning",
6861
+ implementation: "Implementation",
6862
+ review: "Review",
6863
+ devops: "DevOps",
6864
+ maintenance: "Maintenance",
6865
+ greenfield: "Greenfield",
6866
+ brownfield: "Brownfield",
6867
+ board: "Board",
6868
+ security: "Security",
6869
+ a11y: "Accessibility",
6870
+ performance: "Performance",
6871
+ customize: "Customization",
6872
+ other: "Other"
6873
+ };
6874
+ const wsGroupedChoices = [];
6875
+ for (const [tag, items] of wsTagGroups) {
6876
+ wsGroupedChoices.push(new inquirer3.Separator(`\u2500\u2500 ${WS_TAG_LABELS[tag] ?? tag} (${items.length}) \u2500\u2500`));
6877
+ for (const item of items) {
6878
+ wsGroupedChoices.push({
6879
+ name: `${item.type}: ${item.id.replace(/^hatch3r-/, "")} \u2014 ${item.description.slice(0, 60)}`,
6880
+ value: item.id,
6881
+ checked: item.protected || item.tags.includes("core")
6882
+ });
6883
+ }
6884
+ }
6885
+ const customAnswer = await inquirer3.prompt([
6886
+ {
6887
+ type: "checkbox",
6888
+ name: "items",
6889
+ message: "Select content items:",
6890
+ choices: wsGroupedChoices,
6891
+ ...wslTheme && { theme: wslTheme }
6892
+ }
6893
+ ]);
6894
+ customSelections = customAnswer.items;
6895
+ }
6896
+ const toolDefaults = repoInfo.existingTools.length > 0 ? repoInfo.existingTools : DEFAULT_TOOLS;
6897
+ const toolAnswers = await inquirer3.prompt([
6898
+ {
6899
+ type: "checkbox",
6900
+ name: "tools",
6901
+ message: "Select tools to configure:",
6902
+ choices: TOOL_PROMPT_CHOICES,
6903
+ default: toolDefaults,
6904
+ ...wslTheme && { theme: wslTheme }
6905
+ }
6906
+ ]);
6907
+ tools = toolAnswers.tools.length > 0 ? toolAnswers.tools : DEFAULT_TOOLS;
6908
+ const wsSecretNotes = tools.map((t) => TOOL_SECRET_NOTES[t]).filter(Boolean);
6909
+ if (wsSecretNotes.length > 0) {
6910
+ info(chalk6.dim("MCP secret loading by tool:"));
6911
+ for (const note of wsSecretNotes) {
6912
+ info(chalk6.dim(` ${note}`));
6913
+ }
6914
+ }
6915
+ const featureAnswers = await inquirer3.prompt([
6916
+ {
6917
+ type: "checkbox",
6918
+ name: "features",
6919
+ message: "Select features:",
6920
+ choices: FEATURE_CHOICES,
6921
+ default: DEFAULT_FEATURE_KEYS,
6922
+ ...wslTheme && { theme: wslTheme }
6923
+ }
6924
+ ]);
6925
+ const selectedFeatures = featureAnswers.features;
6926
+ features = { ...DEFAULT_FEATURES };
6927
+ for (const k of Object.keys(features)) {
6928
+ features[k] = selectedFeatures.includes(k);
6929
+ }
6930
+ mcpServers = [];
6931
+ if (features.mcp) {
6932
+ const platformMcp = PLATFORM_MCP_SERVER[platform];
6933
+ const defaultMcpForPlatform = Array.from(
6934
+ /* @__PURE__ */ new Set([platformMcp, ...DEFAULT_MCP.filter((s) => s !== "github")])
6935
+ );
6936
+ const mcpAnswers = await inquirer3.prompt([
6937
+ {
6938
+ type: "checkbox",
6939
+ name: "mcp",
6940
+ message: "Select MCP servers:",
6941
+ choices: MCP_CHOICES,
6942
+ default: defaultMcpForPlatform,
6943
+ ...wslTheme && { theme: wslTheme }
6944
+ }
6945
+ ]);
6946
+ mcpServers = mcpAnswers.mcp ?? [];
6947
+ if (!mcpServers.includes(platformMcp)) {
6948
+ mcpServers.unshift(platformMcp);
6949
+ }
6950
+ }
6951
+ contentSelection = resolveSelection(selectedPreset, projectType, teamSize, wsFilterIndex, customSelections);
6952
+ }
6953
+ const orchWarnings = validateOrchestrationDependencies(contentSelection);
6954
+ for (const w of orchWarnings) {
6955
+ warn(w);
6956
+ }
6957
+ warnBoardPrerequisites(contentSelection);
6958
+ await checkExisting(rootDir, headless, contentSelection);
6959
+ await runInit({
6960
+ rootDir,
6961
+ platform,
6962
+ owner: "",
6963
+ repo: "",
6964
+ namespace: "",
6965
+ project: "",
6966
+ defaultBranch: "",
6967
+ tools,
6968
+ features,
6969
+ mcpServers,
6970
+ repoInfo,
6971
+ contentSelection
6972
+ });
6973
+ let repoEntries;
6974
+ if (headless) {
6975
+ repoEntries = enriched.map((r) => ({
6976
+ path: r.path,
6977
+ name: r.name,
6978
+ sync: false,
6979
+ owner: r.owner || void 0,
6980
+ repo: r.repo || void 0,
6981
+ defaultBranch: r.defaultBranch || void 0,
6982
+ platform: r.platform || void 0
6983
+ }));
6984
+ } else {
6985
+ const wslTheme = isWSL() ? { icon: { checked: chalk6.green("[x]"), unchecked: "[ ]", cursor: ">" } } : void 0;
6986
+ const { syncRepos } = await inquirer3.prompt([
6987
+ {
6988
+ type: "checkbox",
6989
+ name: "syncRepos",
6990
+ message: "Select repos to sync workspace content to:",
6991
+ choices: enriched.map((r) => ({
6992
+ name: `${r.name}${r.hasHatch3r ? chalk6.dim(" (has existing hatch3r)") : ""}`,
6993
+ value: r.path,
6994
+ checked: false
6995
+ })),
6996
+ ...wslTheme && { theme: wslTheme }
6997
+ }
6998
+ ]);
6999
+ const syncSet = new Set(syncRepos);
7000
+ repoEntries = enriched.map((r) => ({
7001
+ path: r.path,
7002
+ name: r.name,
7003
+ sync: syncSet.has(r.path),
7004
+ owner: r.owner || void 0,
7005
+ repo: r.repo || void 0,
7006
+ defaultBranch: r.defaultBranch || void 0,
7007
+ platform: r.platform || void 0
7008
+ }));
7009
+ }
7010
+ const dirName = basename2(rootDir) || "workspace";
7011
+ const wsManifest = createWorkspaceManifest(
7012
+ dirName,
7013
+ { platform, tools, features, mcp: { servers: mcpServers }, content: contentSelection },
7014
+ repoEntries,
7015
+ "manual"
7016
+ );
7017
+ await writeWorkspaceManifest(rootDir, wsManifest);
7018
+ const syncCount = repoEntries.filter((r) => r.sync).length;
7019
+ if (syncCount > 0) {
7020
+ const syncSpinner = createSpinner(`Syncing ${syncCount} repo(s)...`);
7021
+ syncSpinner.start();
7022
+ const result = await syncWorkspaceRepos(rootDir, {
7023
+ onWarn: (msg) => warn(msg)
7024
+ });
7025
+ const succeeded = result.repos.filter((r) => r.action === "synced").length;
7026
+ const failed = result.repos.filter((r) => r.action === "error").length;
7027
+ if (failed > 0) {
7028
+ syncSpinner.warn(`Workspace sync: ${succeeded} synced, ${failed} failed`);
7029
+ for (const r of result.repos.filter((r2) => r2.action === "error")) {
7030
+ error(` ${r.path}: ${r.error}`);
7031
+ }
7032
+ } else {
7033
+ syncSpinner.succeed(`Workspace sync: ${succeeded} repo(s) synced`);
7034
+ }
7035
+ }
7036
+ console.log();
7037
+ const wsLines = [
7038
+ label("Mode", "workspace"),
7039
+ label("Repos", `${repoEntries.length} registered, ${syncCount} synced`),
7040
+ label("Strategy", "manual (use hatch3r sync --repos to propagate)"),
7041
+ label("Manifest", `${AGENTS_DIR}/workspace.json`)
7042
+ ];
7043
+ printBox("Workspace ready", wsLines, "success");
7044
+ }
7045
+ function resolveToolsFromOpts(toolsFlag, repoInfo) {
7046
+ if (toolsFlag) {
7047
+ const rawTools = toolsFlag.split(",").map((t) => t.trim());
7048
+ const invalid = rawTools.filter((t) => !VALID_TOOLS.has(t));
7049
+ if (invalid.length > 0) {
7050
+ error(`Invalid tool(s): ${invalid.join(", ")}`);
7051
+ console.log(chalk6.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
7052
+ throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
7053
+ }
7054
+ return rawTools;
7055
+ }
7056
+ if (repoInfo.existingTools.length > 0) return repoInfo.existingTools;
7057
+ return DEFAULT_TOOLS;
7058
+ }
5429
7059
 
5430
7060
  // src/cli/commands/sync.ts
5431
- import { stat as stat3, readdir as readdir9 } from "fs/promises";
5432
- import { join as join20 } from "path";
7061
+ import { stat as stat4, readdir as readdir10 } from "fs/promises";
7062
+ import { join as join23 } from "path";
5433
7063
  import { execFileSync as execFileSync5 } from "child_process";
5434
7064
  import chalk7 from "chalk";
5435
7065
  async function checkSpecFreshness(rootDir) {
5436
- const specsDir = join20(rootDir, "docs", "specs");
7066
+ const specsDir = join23(rootDir, "docs", "specs");
5437
7067
  try {
5438
- await stat3(specsDir);
7068
+ await stat4(specsDir);
5439
7069
  } catch {
5440
7070
  return;
5441
7071
  }
5442
7072
  let oldestSpecMtime = Date.now();
5443
7073
  try {
5444
- const entries = await readdir9(specsDir, { withFileTypes: true, recursive: true });
7074
+ const entries = await readdir10(specsDir, { withFileTypes: true, recursive: true });
5445
7075
  for (const entry of entries) {
5446
7076
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5447
7077
  const parentPath = entry.parentPath ?? entry.path ?? specsDir;
5448
- const fileStat = await stat3(join20(parentPath, entry.name));
7078
+ const fileStat = await stat4(join23(parentPath, entry.name));
5449
7079
  if (fileStat.mtimeMs < oldestSpecMtime) {
5450
7080
  oldestSpecMtime = fileStat.mtimeMs;
5451
7081
  }
@@ -5467,15 +7097,15 @@ async function checkSpecFreshness(rootDir) {
5467
7097
  } catch {
5468
7098
  }
5469
7099
  }
5470
- async function syncCommand() {
7100
+ async function syncCommand(opts = {}) {
5471
7101
  printBanner(true);
5472
7102
  const rootDir = process.cwd();
5473
- const agentsDir = join20(rootDir, AGENTS_DIR);
7103
+ const agentsDir = join23(rootDir, AGENTS_DIR);
5474
7104
  const manifest = await readManifest(rootDir);
5475
7105
  if (!manifest) {
5476
7106
  error("No .agents/hatch.json found.");
5477
7107
  console.log(chalk7.dim(" Run `npx hatch3r init` to set up your project first.\n"));
5478
- throw new HatchError("No .agents/hatch.json found.", 1);
7108
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
5479
7109
  }
5480
7110
  const m = manifest;
5481
7111
  const integrityResults = await verifyIntegrity(agentsDir);
@@ -5497,28 +7127,32 @@ async function syncCommand() {
5497
7127
  let currentStep = 0;
5498
7128
  const s1 = createSpinner(step(++currentStep, totalSteps, "Syncing AGENTS.md..."));
5499
7129
  s1.start();
5500
- const agentsMdResult = await safeWriteFile(join20(rootDir, "AGENTS.md"), AGENTS_MD_FULL, {
7130
+ const agentsMdResult = await safeWriteFile(join23(rootDir, "AGENTS.md"), AGENTS_MD_FULL, {
5501
7131
  managedContent: AGENTS_MD_INNER
5502
7132
  });
5503
7133
  if (agentsMdResult.warning) warn(agentsMdResult.warning);
5504
7134
  results.push({ path: "AGENTS.md", action: agentsMdResult.action });
5505
7135
  const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
5506
- const canonicalResult = await safeWriteFile(join20(agentsDir, "AGENTS.md"), canonicalAgentsMd);
7136
+ const canonicalResult = await safeWriteFile(join23(agentsDir, "AGENTS.md"), canonicalAgentsMd);
5507
7137
  if (canonicalResult.warning) warn(canonicalResult.warning);
5508
7138
  results.push({ path: `${AGENTS_DIR}/AGENTS.md`, action: canonicalResult.action });
5509
7139
  s1.succeed(step(currentStep, totalSteps, "AGENTS.md synced"));
7140
+ const generationMode = opts.minimal ? "minimal" : "standard";
7141
+ if (opts.minimal) {
7142
+ info("Minimal generation mode: output will be stripped-down to reduce token usage.");
7143
+ }
5510
7144
  const adapterFailures = [];
5511
7145
  for (const tool of m.tools) {
5512
7146
  const s = createSpinner(step(++currentStep, totalSteps, `Generating ${tool} output...`));
5513
7147
  s.start();
5514
7148
  try {
5515
7149
  const adapter = getAdapter(tool);
5516
- const outputs = await adapter.generate(agentsDir, m);
7150
+ const outputs = await adapter.generate(agentsDir, m, generationMode);
5517
7151
  for (const w of adapter.warnings) {
5518
7152
  warn(w);
5519
7153
  }
5520
7154
  for (const out of outputs) {
5521
- const fullPath = join20(rootDir, out.path);
7155
+ const fullPath = join23(rootDir, out.path);
5522
7156
  if (out.managedContent) {
5523
7157
  const result = await safeWriteFile(fullPath, out.content, {
5524
7158
  managedContent: out.managedContent
@@ -5545,7 +7179,7 @@ async function syncCommand() {
5545
7179
  error(`Failed to generate ${f.tool}: ${f.error}`);
5546
7180
  }
5547
7181
  if (adapterFailures.length === m.tools.length) {
5548
- throw new HatchError("All adapters failed", 1);
7182
+ throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
5549
7183
  }
5550
7184
  }
5551
7185
  for (const tool of m.tools) {
@@ -5558,7 +7192,7 @@ async function syncCommand() {
5558
7192
  const wtContent = await generateWorktreeInclude(m, rootDir);
5559
7193
  const wtManaged = extractManagedContent(wtContent);
5560
7194
  const wtResult = await safeWriteFile(
5561
- join20(rootDir, WORKTREE_INCLUDE_FILE),
7195
+ join23(rootDir, WORKTREE_INCLUDE_FILE),
5562
7196
  wtContent,
5563
7197
  { managedContent: wtManaged }
5564
7198
  );
@@ -5578,6 +7212,9 @@ async function syncCommand() {
5578
7212
  info(`Run this, then start or restart your editor: ${getSourceEnvMcpCommand()}`);
5579
7213
  }
5580
7214
  }
7215
+ const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
7216
+ await writeIntegrityManifest(agentsDir, integrityManifest);
7217
+ await pruneArchives(rootDir);
5581
7218
  await checkSpecFreshness(rootDir);
5582
7219
  console.log();
5583
7220
  const icons = {
@@ -5590,11 +7227,55 @@ async function syncCommand() {
5590
7227
  return `${icon} ${r.path} ${chalk7.dim(`(${r.action})`)}`;
5591
7228
  });
5592
7229
  printBox("Sync complete", summaryLines, "success");
7230
+ const wsManifest = await readWorkspaceManifest(rootDir);
7231
+ if (!wsManifest) return;
7232
+ const syncReposRequested = opts.repos !== void 0;
7233
+ const syncOnSync = wsManifest.syncStrategy === "on-sync";
7234
+ const syncableCount = wsManifest.repos.filter((r) => r.sync).length;
7235
+ if (!syncReposRequested && !syncOnSync) {
7236
+ if (syncableCount > 0) {
7237
+ info(`Workspace: ${syncableCount} repo(s) available for sync. Run ${chalk7.bold("hatch3r sync --repos")} to propagate.`);
7238
+ }
7239
+ return;
7240
+ }
7241
+ const repoPaths = Array.isArray(opts.repos) ? opts.repos : void 0;
7242
+ console.log();
7243
+ const wsSpinner = createSpinner(
7244
+ opts.dryRun ? "Workspace sync (dry run)..." : `Syncing workspace to ${repoPaths ? repoPaths.length : syncableCount} repo(s)...`
7245
+ );
7246
+ wsSpinner.start();
7247
+ const wsResult = await syncWorkspaceRepos(rootDir, {
7248
+ repos: repoPaths,
7249
+ dryRun: opts.dryRun,
7250
+ force: opts.force,
7251
+ onWarn: (msg) => warn(msg)
7252
+ });
7253
+ if (opts.dryRun) {
7254
+ wsSpinner.succeed("Workspace sync (dry run)");
7255
+ for (const r of wsResult.repos) {
7256
+ const changes = [];
7257
+ if (r.added.length > 0) changes.push(`+${r.added.length} content`);
7258
+ if (r.removed.length > 0) changes.push(`-${r.removed.length} content`);
7259
+ if (r.toolsSynced.length > 0) changes.push(`${r.toolsSynced.length} tools`);
7260
+ info(` ${r.path}: ${changes.length > 0 ? changes.join(", ") : "up to date"}`);
7261
+ }
7262
+ return;
7263
+ }
7264
+ const succeeded = wsResult.repos.filter((r) => r.action === "synced").length;
7265
+ const failed = wsResult.repos.filter((r) => r.action === "error").length;
7266
+ if (failed > 0) {
7267
+ wsSpinner.warn(`Workspace sync: ${succeeded} synced, ${failed} failed`);
7268
+ for (const r of wsResult.repos.filter((r2) => r2.action === "error")) {
7269
+ error(` ${r.path}: ${r.error}`);
7270
+ }
7271
+ } else {
7272
+ wsSpinner.succeed(`Workspace sync: ${succeeded} repo(s) synced`);
7273
+ }
5593
7274
  }
5594
7275
 
5595
7276
  // src/cli/commands/validate.ts
5596
- import { readdir as readdir10, readFile as readFile16, access as access6 } from "fs/promises";
5597
- import { join as join21, posix as posix2 } from "path";
7277
+ import { readdir as readdir11, readFile as readFile19, access as access8 } from "fs/promises";
7278
+ import { join as join24, posix as posix2 } from "path";
5598
7279
  import chalk8 from "chalk";
5599
7280
  import { parse as parseYaml4 } from "yaml";
5600
7281
  var DEFAULT_KNOWN_AGENTS = /* @__PURE__ */ new Set([
@@ -5630,7 +7311,7 @@ async function validateManifest2(rootDir, manifest, result) {
5630
7311
  if (!manifest.tools || manifest.tools.length === 0) result.warnings.push("hatch.json: no tools configured");
5631
7312
  for (const managedFile of manifest.managedFiles ?? []) {
5632
7313
  try {
5633
- await access6(join21(rootDir, managedFile));
7314
+ await access8(join24(rootDir, managedFile));
5634
7315
  } catch (err) {
5635
7316
  if (err.code !== "ENOENT") throw err;
5636
7317
  result.warnings.push(`Managed file missing from disk: ${managedFile}`);
@@ -5642,7 +7323,7 @@ async function validateDirectories(agentsDir, result) {
5642
7323
  const optionalDirs = ["commands", "prompts", "mcp", "policy", "github-agents"];
5643
7324
  for (const dir of requiredDirs) {
5644
7325
  try {
5645
- await access6(join21(agentsDir, dir));
7326
+ await access8(join24(agentsDir, dir));
5646
7327
  } catch (err) {
5647
7328
  if (err.code !== "ENOENT") throw err;
5648
7329
  result.errors.push(`Required directory missing: .agents/${dir}/`);
@@ -5650,7 +7331,7 @@ async function validateDirectories(agentsDir, result) {
5650
7331
  }
5651
7332
  for (const dir of optionalDirs) {
5652
7333
  try {
5653
- await access6(join21(agentsDir, dir));
7334
+ await access8(join24(agentsDir, dir));
5654
7335
  } catch (err) {
5655
7336
  if (err.code !== "ENOENT") throw err;
5656
7337
  result.warnings.push(`Optional directory missing: .agents/${dir}/`);
@@ -5661,13 +7342,13 @@ async function validateFrontmatter(agentsDir, result) {
5661
7342
  const requiredDirs = ["agents", "skills", "rules"];
5662
7343
  const optionalDirs = ["commands", "prompts", "mcp", "policy", "github-agents"];
5663
7344
  for (const dir of [...requiredDirs, ...optionalDirs]) {
5664
- const dirPath = join21(agentsDir, dir);
7345
+ const dirPath = join24(agentsDir, dir);
5665
7346
  try {
5666
- const entries = await readdir10(dirPath, { withFileTypes: true });
7347
+ const entries = await readdir11(dirPath, { withFileTypes: true });
5667
7348
  for (const entry of entries) {
5668
7349
  if (entry.isFile() && entry.name.endsWith(".md")) {
5669
- const filePath = join21(dirPath, entry.name);
5670
- const content = await readFile16(filePath, "utf-8");
7350
+ const filePath = join24(dirPath, entry.name);
7351
+ const content = await readFile19(filePath, "utf-8");
5671
7352
  if (!content.startsWith("---")) {
5672
7353
  result.warnings.push(`Missing frontmatter: .agents/${dir}/${entry.name}`);
5673
7354
  } else {
@@ -5686,9 +7367,9 @@ async function validateFrontmatter(agentsDir, result) {
5686
7367
  }
5687
7368
  }
5688
7369
  } else if (entry.isDirectory()) {
5689
- const skillPath = join21(dirPath, entry.name, "SKILL.md");
7370
+ const skillPath = join24(dirPath, entry.name, "SKILL.md");
5690
7371
  try {
5691
- await access6(skillPath);
7372
+ await access8(skillPath);
5692
7373
  } catch (err) {
5693
7374
  if (err.code !== "ENOENT") throw err;
5694
7375
  result.warnings.push(`Skill directory missing SKILL.md: .agents/${dir}/${entry.name}/`);
@@ -5700,7 +7381,7 @@ async function validateFrontmatter(agentsDir, result) {
5700
7381
  }
5701
7382
  }
5702
7383
  try {
5703
- await access6(join21(agentsDir, "AGENTS.md"));
7384
+ await access8(join24(agentsDir, "AGENTS.md"));
5704
7385
  } catch (err) {
5705
7386
  if (err.code !== "ENOENT") throw err;
5706
7387
  result.warnings.push("Missing .agents/AGENTS.md");
@@ -5719,22 +7400,22 @@ async function validateManagedFilePrefixes(manifest, result) {
5719
7400
  }
5720
7401
  async function validateHooks(agentsDir, manifest, result) {
5721
7402
  if (!manifest.features.hooks) return;
5722
- const hooksDir = join21(agentsDir, "hooks");
7403
+ const hooksDir = join24(agentsDir, "hooks");
5723
7404
  try {
5724
- const hookFiles = await readdir10(hooksDir);
7405
+ const hookFiles = await readdir11(hooksDir);
5725
7406
  const mdHooks = hookFiles.filter((f) => f.endsWith(".md"));
5726
7407
  if (mdHooks.length === 0) {
5727
7408
  result.warnings.push("Hooks feature enabled but no hook definitions found in .agents/hooks/");
5728
7409
  }
5729
7410
  let agentFiles;
5730
7411
  try {
5731
- const agentEntries = await readdir10(join21(agentsDir, "agents"));
7412
+ const agentEntries = await readdir11(join24(agentsDir, "agents"));
5732
7413
  agentFiles = new Set(agentEntries.filter((f) => f.endsWith(".md")));
5733
7414
  } catch (err) {
5734
7415
  if (err.code !== "ENOENT") throw err;
5735
7416
  }
5736
7417
  for (const hookFile of mdHooks) {
5737
- const hookContent = await readFile16(join21(hooksDir, hookFile), "utf-8");
7418
+ const hookContent = await readFile19(join24(hooksDir, hookFile), "utf-8");
5738
7419
  if (!hookContent.startsWith("---")) {
5739
7420
  result.warnings.push(`Hook missing frontmatter: .agents/hooks/${hookFile}`);
5740
7421
  continue;
@@ -5766,9 +7447,9 @@ async function validateHooks(agentsDir, manifest, result) {
5766
7447
  }
5767
7448
  async function validateMcp(agentsDir, manifest, result) {
5768
7449
  if (!manifest.features.mcp || manifest.mcp.servers.length === 0) return;
5769
- const mcpPath = join21(agentsDir, "mcp", "mcp.json");
7450
+ const mcpPath = join24(agentsDir, "mcp", "mcp.json");
5770
7451
  try {
5771
- const mcpContent = await readFile16(mcpPath, "utf-8");
7452
+ const mcpContent = await readFile19(mcpPath, "utf-8");
5772
7453
  const mcpParsed = JSON.parse(mcpContent);
5773
7454
  if (!mcpParsed.mcpServers || typeof mcpParsed.mcpServers !== "object") {
5774
7455
  result.errors.push("MCP config missing 'mcpServers' key");
@@ -5794,17 +7475,101 @@ async function validateModels(manifest, result) {
5794
7475
  }
5795
7476
  }
5796
7477
  }
7478
+ async function validateCustomizeYaml(rootDir, result) {
7479
+ const VALID_FIELDS = /* @__PURE__ */ new Set(["model", "scope", "description", "enabled"]);
7480
+ const FIELD_TYPES = {
7481
+ model: "string",
7482
+ scope: "string",
7483
+ description: "string",
7484
+ enabled: "boolean"
7485
+ };
7486
+ for (const { dir } of CUSTOMIZATION_TYPES) {
7487
+ const customDir = join24(rootDir, ".hatch3r", dir);
7488
+ let files;
7489
+ try {
7490
+ files = await readdir11(customDir);
7491
+ } catch (err) {
7492
+ if (err.code === "ENOENT") continue;
7493
+ throw err;
7494
+ }
7495
+ const yamlFiles = files.filter((f) => f.endsWith(".customize.yaml"));
7496
+ for (const file of yamlFiles) {
7497
+ const filePath = join24(customDir, file);
7498
+ const itemId = file.replace(".customize.yaml", "");
7499
+ let raw;
7500
+ try {
7501
+ raw = await readFile19(filePath, "utf-8");
7502
+ } catch {
7503
+ continue;
7504
+ }
7505
+ if (Buffer.byteLength(raw, "utf-8") > 10240) {
7506
+ result.warnings.push(
7507
+ `.customize.yaml for "${itemId}" exceeds 10KB limit and will be skipped during generation`
7508
+ );
7509
+ continue;
7510
+ }
7511
+ let parsed;
7512
+ try {
7513
+ parsed = parseYaml4(raw);
7514
+ } catch {
7515
+ result.errors.push(
7516
+ `Invalid YAML syntax in .hatch3r/${dir}/${file}`
7517
+ );
7518
+ continue;
7519
+ }
7520
+ if (!parsed || typeof parsed !== "object") {
7521
+ result.warnings.push(
7522
+ `.customize.yaml for "${itemId}" is empty or not an object`
7523
+ );
7524
+ continue;
7525
+ }
7526
+ for (const key of Object.keys(parsed)) {
7527
+ if (!VALID_FIELDS.has(key)) {
7528
+ result.warnings.push(
7529
+ `.hatch3r/${dir}/${file}: unknown field "${key}" (valid: ${[...VALID_FIELDS].join(", ")})`
7530
+ );
7531
+ }
7532
+ }
7533
+ for (const [key, expectedType] of Object.entries(FIELD_TYPES)) {
7534
+ if (key in parsed && parsed[key] !== void 0 && parsed[key] !== null) {
7535
+ const actualType = typeof parsed[key];
7536
+ if (actualType !== expectedType) {
7537
+ result.warnings.push(
7538
+ `.hatch3r/${dir}/${file}: field "${key}" should be ${expectedType} but is ${actualType}`
7539
+ );
7540
+ }
7541
+ }
7542
+ }
7543
+ for (const field of ["description", "scope"]) {
7544
+ const value = parsed[field];
7545
+ if (typeof value === "string") {
7546
+ const violations = scanForDeniedPatterns(value);
7547
+ for (const v of violations) {
7548
+ result.warnings.push(
7549
+ `.hatch3r/${dir}/${file}: field "${field}" contains denied pattern: ${v}`
7550
+ );
7551
+ }
7552
+ }
7553
+ }
7554
+ const type = dir;
7555
+ const readResult = await readCustomizationWithWarnings(rootDir, type, itemId);
7556
+ for (const w of readResult.warnings) {
7557
+ result.warnings.push(w);
7558
+ }
7559
+ }
7560
+ }
7561
+ }
5797
7562
  async function validateCustomizations(rootDir, agentsDir, manifest, result) {
5798
7563
  for (const { dir, canonical } of CUSTOMIZATION_TYPES) {
5799
- const customDir = join21(rootDir, ".hatch3r", dir);
7564
+ const customDir = join24(rootDir, ".hatch3r", dir);
5800
7565
  try {
5801
- const customFiles = await readdir10(customDir);
7566
+ const customFiles = await readdir11(customDir);
5802
7567
  for (const file of customFiles) {
5803
7568
  if (file.endsWith(".customize.yaml")) {
5804
7569
  const itemId = file.replace(".customize.yaml", "");
5805
- const canonicalPath = canonical === "skills" ? join21(agentsDir, canonical, itemId) : join21(agentsDir, canonical, `${itemId}.md`);
7570
+ const canonicalPath = canonical === "skills" ? join24(agentsDir, canonical, itemId) : join24(agentsDir, canonical, `${itemId}.md`);
5806
7571
  try {
5807
- await access6(canonicalPath);
7572
+ await access8(canonicalPath);
5808
7573
  } catch (err) {
5809
7574
  if (err.code !== "ENOENT") throw err;
5810
7575
  result.warnings.push(`Customization file for non-existent ${canonical.slice(0, -1)}: .hatch3r/${dir}/${file}`);
@@ -5830,9 +7595,9 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
5830
7595
  for (const [key, cfg] of Object.entries(contentDirs)) {
5831
7596
  const ids = manifest.content.items[key];
5832
7597
  for (const id of ids) {
5833
- const checkPath = cfg.strategy === "subdir" ? join21(agentsDir, cfg.dir, id, "SKILL.md") : join21(agentsDir, cfg.dir, `${id}.md`);
7598
+ const checkPath = cfg.strategy === "subdir" ? join24(agentsDir, cfg.dir, id, "SKILL.md") : join24(agentsDir, cfg.dir, `${id}.md`);
5834
7599
  try {
5835
- await access6(checkPath);
7600
+ await access8(checkPath);
5836
7601
  } catch {
5837
7602
  result.warnings.push(`Content "${id}" (${key}) in manifest but missing from .agents/${cfg.dir}/`);
5838
7603
  }
@@ -5843,9 +7608,9 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
5843
7608
  for (const id of ids) allContentIds.add(id);
5844
7609
  }
5845
7610
  for (const { dir } of CUSTOMIZATION_TYPES) {
5846
- const customDir = join21(rootDir, ".hatch3r", dir);
7611
+ const customDir = join24(rootDir, ".hatch3r", dir);
5847
7612
  try {
5848
- const files = await readdir10(customDir);
7613
+ const files = await readdir11(customDir);
5849
7614
  for (const f of files.filter((f2) => f2.endsWith(".customize.yaml") || f2.endsWith(".customize.md"))) {
5850
7615
  const itemId = f.replace(/\.customize\.(yaml|md)$/, "");
5851
7616
  if (!allContentIds.has(itemId) && !allContentIds.has(`${HATCH3R_PREFIX}${itemId}`)) {
@@ -5857,12 +7622,12 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
5857
7622
  }
5858
7623
  }
5859
7624
  }
5860
- const learningsDir = join21(agentsDir, "learnings");
7625
+ const learningsDir = join24(agentsDir, "learnings");
5861
7626
  try {
5862
- const learningFiles = await readdir10(learningsDir);
7627
+ const learningFiles = await readdir11(learningsDir);
5863
7628
  const mdFiles = learningFiles.filter((f) => f.endsWith(".md"));
5864
7629
  for (const file of mdFiles) {
5865
- const content = await readFile16(join21(learningsDir, file), "utf-8");
7630
+ const content = await readFile19(join24(learningsDir, file), "utf-8");
5866
7631
  const violations = scanForDeniedPatterns(content);
5867
7632
  if (violations.length > 0) {
5868
7633
  for (const v of violations) {
@@ -5877,18 +7642,18 @@ async function validateContentConsistency(rootDir, agentsDir, manifest, result)
5877
7642
  async function validateCommand() {
5878
7643
  printBanner(true);
5879
7644
  const rootDir = process.cwd();
5880
- const agentsDir = join21(rootDir, AGENTS_DIR);
7645
+ const agentsDir = join24(rootDir, AGENTS_DIR);
5881
7646
  const result = { errors: [], warnings: [] };
5882
7647
  const spinner = createSpinner("Validating .agents/ structure...");
5883
7648
  spinner.start();
5884
7649
  try {
5885
- await access6(agentsDir);
7650
+ await access8(agentsDir);
5886
7651
  } catch (err) {
5887
7652
  if (err.code !== "ENOENT") throw err;
5888
7653
  spinner.fail("Validation failed");
5889
7654
  error(".agents/ directory not found. Run `hatch3r init` first.");
5890
7655
  console.log();
5891
- throw new HatchError(".agents/ directory not found.", 1);
7656
+ throw new HatchError(".agents/ directory not found.", 1, "CONFIG_ERROR");
5892
7657
  }
5893
7658
  const manifest = await readManifest(rootDir);
5894
7659
  await validateManifest2(rootDir, manifest, result);
@@ -5900,6 +7665,7 @@ async function validateCommand() {
5900
7665
  await validateMcp(agentsDir, manifest, result);
5901
7666
  await validateModels(manifest, result);
5902
7667
  await validateCustomizations(rootDir, agentsDir, manifest, result);
7668
+ await validateCustomizeYaml(rootDir, result);
5903
7669
  await validateContentConsistency(rootDir, agentsDir, manifest, result);
5904
7670
  try {
5905
7671
  const index = await buildContentIndex(agentsDir);
@@ -5909,6 +7675,18 @@ async function validateCommand() {
5909
7675
  result.warnings.push(w);
5910
7676
  }
5911
7677
  }
7678
+ const EXPECTED_CROSS_TYPE_PAIRS = /* @__PURE__ */ new Set(["command", "skill"]);
7679
+ for (const collision of index.collisions) {
7680
+ if (collision.kind === "cross-type") {
7681
+ const types = /* @__PURE__ */ new Set([collision.existingType, collision.duplicateType]);
7682
+ if (types.size === 2 && [...types].every((t) => EXPECTED_CROSS_TYPE_PAIRS.has(t))) {
7683
+ continue;
7684
+ }
7685
+ }
7686
+ result.warnings.push(
7687
+ `Content ID collision: "${collision.id}" exists as ${collision.existingType} (${collision.existingPath}) and ${collision.duplicateType} (${collision.duplicatePath})`
7688
+ );
7689
+ }
5912
7690
  } catch {
5913
7691
  }
5914
7692
  if (manifest.content) {
@@ -5919,8 +7697,22 @@ async function validateCommand() {
5919
7697
  }
5920
7698
  }
5921
7699
  spinner.stop();
7700
+ let hasCustomizations = false;
7701
+ for (const { dir } of CUSTOMIZATION_TYPES) {
7702
+ try {
7703
+ const files = await readdir11(join24(rootDir, ".hatch3r", dir));
7704
+ if (files.some((f) => f.endsWith(".customize.yaml") || f.endsWith(".customize.md"))) {
7705
+ hasCustomizations = true;
7706
+ break;
7707
+ }
7708
+ } catch {
7709
+ }
7710
+ }
5922
7711
  if (result.errors.length === 0 && result.warnings.length === 0) {
5923
7712
  printBox("Validation", [chalk8.green("All checks passed")], "success");
7713
+ if (hasCustomizations) {
7714
+ printCustomizationHint();
7715
+ }
5924
7716
  return;
5925
7717
  }
5926
7718
  console.log();
@@ -5942,7 +7734,7 @@ async function validateCommand() {
5942
7734
  `${chalk8.yellow("\u26A0")} ${result.warnings.length} warning(s)`
5943
7735
  ];
5944
7736
  printBox("Validation failed", summaryLines, "error");
5945
- throw new HatchError("Validation failed", 1);
7737
+ throw new HatchError("Validation failed", 1, "VALIDATION_ERROR");
5946
7738
  } else {
5947
7739
  const summaryLines = [
5948
7740
  `${chalk8.green("\u2714")} 0 errors`,
@@ -5950,15 +7742,29 @@ async function validateCommand() {
5950
7742
  ];
5951
7743
  printBox("Validation passed", summaryLines, "success");
5952
7744
  }
7745
+ if (hasCustomizations) {
7746
+ printCustomizationHint();
7747
+ }
7748
+ }
7749
+ function printCustomizationHint() {
7750
+ console.log();
7751
+ info(chalk8.bold("Customization mechanisms detected. Quick reference:"));
7752
+ console.log(chalk8.dim(" 1. hatch3r- prefix: Files prefixed with hatch3r- are managed by hatch3r and"));
7753
+ console.log(chalk8.dim(" overwritten on update. Do not edit these directly."));
7754
+ console.log(chalk8.dim(" 2. Managed blocks: Sections between <!-- MANAGED-BLOCK:BEGIN --> and"));
7755
+ console.log(chalk8.dim(" <!-- MANAGED-BLOCK:END --> are auto-updated. Add content outside these markers."));
7756
+ console.log(chalk8.dim(" 3. .customize.yaml/.md: Place in .hatch3r/{type}/ to override model, scope,"));
7757
+ console.log(chalk8.dim(" description, or disable items. Use .customize.md for content additions."));
7758
+ console.log(chalk8.dim(" See: https://docs.hatch3r.com/docs/guides/customization"));
5953
7759
  }
5954
7760
 
5955
7761
  // src/cli/commands/verify.ts
5956
- import { join as join22 } from "path";
7762
+ import { join as join25 } from "path";
5957
7763
  import chalk9 from "chalk";
5958
7764
  async function verifyCommand() {
5959
7765
  printBanner(true);
5960
7766
  const rootDir = process.cwd();
5961
- const agentsDir = join22(rootDir, AGENTS_DIR);
7767
+ const agentsDir = join25(rootDir, AGENTS_DIR);
5962
7768
  const spinner = createSpinner("Verifying file integrity...");
5963
7769
  spinner.start();
5964
7770
  const manifest = await readIntegrityManifest(agentsDir);
@@ -5966,7 +7772,7 @@ async function verifyCommand() {
5966
7772
  spinner.fail("No integrity manifest found");
5967
7773
  error("Missing .agents/.integrity.json \u2014 run `hatch3r init` or `hatch3r update` to generate it.");
5968
7774
  console.log();
5969
- throw new HatchError("Missing .agents/.integrity.json", 1);
7775
+ throw new HatchError("Missing .agents/.integrity.json", 1, "INTEGRITY_ERROR");
5970
7776
  }
5971
7777
  const results = await verifyIntegrity(agentsDir);
5972
7778
  spinner.stop();
@@ -6015,30 +7821,30 @@ async function verifyCommand() {
6015
7821
  info(`Modified files may have been tampered with. Run ${chalk9.bold("hatch3r update")} to restore originals.`);
6016
7822
  }
6017
7823
  console.log();
6018
- throw new HatchError("Integrity check failed", 1);
7824
+ throw new HatchError("Integrity check failed", 1, "INTEGRITY_ERROR");
6019
7825
  } else {
6020
7826
  printBox("Integrity check passed", summaryLines, "success");
6021
7827
  }
6022
7828
  }
6023
7829
 
6024
7830
  // src/cli/commands/status.ts
6025
- import { readFile as readFile17, readdir as readdir11, stat as stat4 } from "fs/promises";
6026
- import { join as join23 } from "path";
7831
+ import { readFile as readFile20, readdir as readdir12, stat as stat5 } from "fs/promises";
7832
+ import { join as join26 } from "path";
6027
7833
  import chalk10 from "chalk";
6028
7834
  async function dirCharCount(dir) {
6029
7835
  let total = 0;
6030
7836
  let entries;
6031
7837
  try {
6032
- entries = await readdir11(dir, { withFileTypes: true });
7838
+ entries = await readdir12(dir, { withFileTypes: true });
6033
7839
  } catch {
6034
7840
  return 0;
6035
7841
  }
6036
7842
  for (const entry of entries) {
6037
- const fullPath = join23(dir, entry.name);
7843
+ const fullPath = join26(dir, entry.name);
6038
7844
  if (entry.isDirectory()) {
6039
7845
  total += await dirCharCount(fullPath);
6040
7846
  } else if (entry.isFile()) {
6041
- const info2 = await stat4(fullPath);
7847
+ const info2 = await stat5(fullPath);
6042
7848
  total += info2.size;
6043
7849
  }
6044
7850
  }
@@ -6047,12 +7853,12 @@ async function dirCharCount(dir) {
6047
7853
  async function statusCommand() {
6048
7854
  printBanner(true);
6049
7855
  const rootDir = process.cwd();
6050
- const agentsDir = join23(rootDir, AGENTS_DIR);
7856
+ const agentsDir = join26(rootDir, AGENTS_DIR);
6051
7857
  const manifest = await readManifest(rootDir);
6052
7858
  if (!manifest) {
6053
7859
  error("No .agents/hatch.json found.");
6054
7860
  console.log(chalk10.dim(" Run `npx hatch3r init` to set up your project first.\n"));
6055
- throw new HatchError("No .agents/hatch.json found.", 1);
7861
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
6056
7862
  }
6057
7863
  const spinner = createSpinner("Checking sync status...");
6058
7864
  spinner.start();
@@ -6063,9 +7869,9 @@ async function statusCommand() {
6063
7869
  const outputs = await adapter.generate(agentsDir, manifest);
6064
7870
  fileLines.push(chalk10.bold(`${tool}:`));
6065
7871
  for (const out of outputs) {
6066
- const destPath = join23(rootDir, out.path);
7872
+ const destPath = join26(rootDir, out.path);
6067
7873
  try {
6068
- const existing = await readFile17(destPath, "utf-8");
7874
+ const existing = await readFile20(destPath, "utf-8");
6069
7875
  const existingBlock = extractManagedBlock(existing);
6070
7876
  const expectedBlock = out.managedContent ?? extractManagedBlock(out.content);
6071
7877
  if (existingBlock !== null && expectedBlock !== null ? existingBlock === expectedBlock : existing === out.content) {
@@ -6107,6 +7913,36 @@ async function statusCommand() {
6107
7913
  info(`Run ${chalk10.bold("hatch3r sync")} to regenerate drifted/missing files.`);
6108
7914
  console.log();
6109
7915
  }
7916
+ const wsManifest = await readWorkspaceManifest(rootDir);
7917
+ if (wsManifest && wsManifest.repos.length > 0) {
7918
+ const wsLines = [];
7919
+ for (const repo of wsManifest.repos) {
7920
+ const icon = repo.sync ? chalk10.green("\u2713") : chalk10.dim("\u25CB");
7921
+ let detail;
7922
+ if (!repo.sync) {
7923
+ detail = chalk10.dim("sync disabled");
7924
+ } else if (repo.lastSync) {
7925
+ const elapsed = Math.max(0, Date.now() - new Date(repo.lastSync).getTime());
7926
+ const hours = Math.floor(elapsed / (1e3 * 60 * 60));
7927
+ const timeAgo = hours < 1 ? "just now" : hours < 24 ? `${hours}h ago` : `${Math.floor(hours / 24)}d ago`;
7928
+ detail = `synced ${timeAgo}`;
7929
+ } else {
7930
+ detail = chalk10.yellow("never synced");
7931
+ }
7932
+ const identity = repo.owner && repo.repo ? chalk10.dim(`${repo.owner}/${repo.repo}`) : "";
7933
+ const branch = repo.defaultBranch ? chalk10.dim(`[${repo.defaultBranch}]`) : "";
7934
+ const identityPart = identity || branch ? ` ${identity} ${branch}` : "";
7935
+ wsLines.push(`${icon} ${repo.name ?? repo.path}${identityPart} ${chalk10.dim(`(${detail})`)}`);
7936
+ }
7937
+ printBox(`Workspace: ${wsManifest.name} (${wsManifest.repos.length} repos)`, wsLines, "info");
7938
+ }
7939
+ if (manifest.workspace) {
7940
+ const wsInfo = [
7941
+ `Managed by workspace at ${chalk10.bold(manifest.workspace.rootPath)}`,
7942
+ `Last synced: ${manifest.workspace.lastSync ? new Date(manifest.workspace.lastSync).toLocaleString() : "never"}`
7943
+ ];
7944
+ printBox("Workspace member", wsInfo, "info");
7945
+ }
6110
7946
  }
6111
7947
 
6112
7948
  // src/cli/index.ts
@@ -6117,15 +7953,67 @@ program.name("hatch3r").description(
6117
7953
  program.command("init").description("Install a complete agent setup into the current repo").option(
6118
7954
  "--tools <tools>",
6119
7955
  `Comma-separated tools (${TOOL_CHOICES})`
6120
- ).option("--yes", "Skip interactive prompts, use defaults").option("--preset <preset>", "Content preset: minimal, standard, full").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").action(initCommand);
6121
- program.command("sync").description("Re-generate tool outputs from canonical .agents/ state").action(syncCommand);
7956
+ ).option("--yes", "Skip interactive prompts, use defaults").option("--preset <preset>", "Content preset: minimal, standard, full").option("--project-type <type>", "Project type: greenfield, brownfield").option("--team-size <size>", "Team size: solo, team").option("--workspace", "Initialize as a multi-repo workspace").action(initCommand);
7957
+ program.command("sync").description("Re-generate tool outputs from canonical .agents/ state").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("--force", "Overwrite locally modified files in sub-repos").option("--minimal", "Generate stripped-down output (no comments, minimal formatting) to reduce token usage").action(syncCommand);
6122
7958
  program.command("status").description("Check sync status between canonical .agents/ and generated files").action(statusCommand);
6123
- program.command("update").description("Pull latest hatch3r templates with safe merge").action(updateCommand);
7959
+ program.command("update").description("Pull latest hatch3r templates with safe merge").option("--yes", "Skip interactive prompts, use defaults").action(updateCommand);
6124
7960
  program.command("validate").description("Validate the canonical .agents/ structure").action(validateCommand);
6125
7961
  program.command("verify").description("Verify integrity of canonical agent files").action(verifyCommand);
6126
7962
  program.command("config").description("Reconfigure tools, MCP servers, features, and platform").action(configCommand);
6127
7963
  program.command("add [pack]").description("Install a community pack (coming soon)").action(addCommand);
6128
7964
  program.command("worktree-setup [worktree-path]").description("Set up gitignored files in a git worktree").option("--from <path>", "Main repo path (auto-detected by default)").option("--dry-run", "Show what would be done without changes").option("--force", "Overwrite existing files in the worktree").action(worktreeSetupCommand);
7965
+ var AGENT_COMMAND_NAMES = /* @__PURE__ */ new Set([
7966
+ "review",
7967
+ "workflow",
7968
+ "project-spec",
7969
+ "codebase-map",
7970
+ "debug",
7971
+ "release",
7972
+ "refactor-plan",
7973
+ "test-plan",
7974
+ "bug-plan",
7975
+ "roadmap",
7976
+ "onboard",
7977
+ "recipe",
7978
+ "board-init",
7979
+ "board-pickup",
7980
+ "board-groom",
7981
+ "board-refresh",
7982
+ "security-audit",
7983
+ "dep-audit",
7984
+ "benchmark",
7985
+ "healthcheck",
7986
+ "context-health",
7987
+ "learn",
7988
+ "revision",
7989
+ "cost-tracking",
7990
+ "api-spec",
7991
+ "hooks",
7992
+ "quick-change",
7993
+ "command-customize"
7994
+ ]);
7995
+ program.on("command:*", (operands) => {
7996
+ const cmd = operands[0];
7997
+ if (cmd && AGENT_COMMAND_NAMES.has(cmd)) {
7998
+ console.error(
7999
+ `
8000
+ "${cmd}" is a hatch3r agent command meant to be run inside your AI editor (e.g. /${cmd}).
8001
+ It cannot be invoked from the terminal CLI.
8002
+
8003
+ To use agent commands, open your project in Cursor, Claude Code, or another supported tool
8004
+ and type /${cmd} in the AI chat.
8005
+ `
8006
+ );
8007
+ } else {
8008
+ console.error(
8009
+ `
8010
+ Unknown command: ${cmd}
8011
+ Run "hatch3r --help" for available commands.
8012
+ `
8013
+ );
8014
+ }
8015
+ process.exit(1);
8016
+ });
6129
8017
  var nodeVersion = parseInt(process.version.slice(1), 10);
6130
8018
  if (nodeVersion < 22) {
6131
8019
  console.error(