hatch3r 1.3.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 (62) hide show
  1. package/README.md +2 -1
  2. package/agents/hatch3r-a11y-auditor.md +7 -11
  3. package/agents/hatch3r-architect.md +7 -11
  4. package/agents/hatch3r-ci-watcher.md +7 -10
  5. package/agents/hatch3r-context-rules.md +5 -7
  6. package/agents/hatch3r-dependency-auditor.md +7 -13
  7. package/agents/hatch3r-devops.md +7 -13
  8. package/agents/hatch3r-docs-writer.md +7 -11
  9. package/agents/hatch3r-fixer.md +2 -8
  10. package/agents/hatch3r-implementer.md +2 -8
  11. package/agents/hatch3r-learnings-loader.md +5 -7
  12. package/agents/hatch3r-lint-fixer.md +7 -9
  13. package/agents/hatch3r-perf-profiler.md +7 -11
  14. package/agents/hatch3r-researcher.md +6 -8
  15. package/agents/hatch3r-reviewer.md +7 -10
  16. package/agents/hatch3r-security-auditor.md +7 -12
  17. package/agents/hatch3r-test-writer.md +7 -11
  18. package/agents/shared/external-knowledge.md +21 -0
  19. package/agents/shared/quality-charter.md +78 -0
  20. package/commands/board/pickup-azure-devops.md +4 -0
  21. package/commands/board/pickup-delegation-multi.md +3 -0
  22. package/commands/board/pickup-delegation.md +3 -0
  23. package/commands/board/pickup-github.md +4 -0
  24. package/commands/board/pickup-gitlab.md +4 -0
  25. package/commands/board/pickup-post-impl.md +8 -1
  26. package/commands/board/shared-azure-devops.md +13 -3
  27. package/commands/board/shared-github.md +1 -0
  28. package/commands/board/shared-gitlab.md +9 -2
  29. package/commands/hatch3r-agent-customize.md +5 -1
  30. package/commands/hatch3r-board-groom.md +55 -2
  31. package/commands/hatch3r-board-init.md +5 -2
  32. package/commands/hatch3r-board-shared.md +37 -2
  33. package/commands/hatch3r-command-customize.md +4 -0
  34. package/commands/hatch3r-hooks.md +1 -1
  35. package/commands/hatch3r-quick-change.md +29 -3
  36. package/commands/hatch3r-revision.md +136 -16
  37. package/commands/hatch3r-rule-customize.md +4 -0
  38. package/commands/hatch3r-skill-customize.md +4 -0
  39. package/commands/hatch3r-workflow.md +10 -1
  40. package/dist/cli/index.js +522 -360
  41. package/dist/cli/index.js.map +1 -1
  42. package/package.json +12 -9
  43. package/rules/hatch3r-agent-orchestration-detail.md +159 -0
  44. package/rules/hatch3r-agent-orchestration-detail.mdc +156 -0
  45. package/rules/hatch3r-agent-orchestration.md +91 -330
  46. package/rules/hatch3r-agent-orchestration.mdc +127 -149
  47. package/rules/hatch3r-code-standards.mdc +10 -2
  48. package/rules/hatch3r-component-conventions.mdc +0 -1
  49. package/rules/hatch3r-deep-context.mdc +30 -8
  50. package/rules/hatch3r-dependency-management.mdc +17 -5
  51. package/rules/hatch3r-i18n.mdc +0 -1
  52. package/rules/hatch3r-migrations.mdc +12 -1
  53. package/rules/hatch3r-observability.mdc +289 -0
  54. package/rules/hatch3r-security-patterns.mdc +11 -0
  55. package/rules/hatch3r-testing.mdc +1 -1
  56. package/rules/hatch3r-theming.mdc +0 -1
  57. package/rules/hatch3r-tooling-hierarchy.mdc +18 -4
  58. package/skills/hatch3r-agent-customize/SKILL.md +4 -72
  59. package/skills/hatch3r-command-customize/SKILL.md +4 -62
  60. package/skills/hatch3r-customize/SKILL.md +117 -0
  61. package/skills/hatch3r-rule-customize/SKILL.md +4 -65
  62. 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.3.0";
15
+ var HATCH3R_VERSION = "1.4.0";
16
16
 
17
17
  // src/cli/shared/ui.ts
18
18
  var CYAN = chalk.hex("#06b6d4");
@@ -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
  };
@@ -538,7 +539,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
538
539
  error("Worktree path is required when running from the main repo.");
539
540
  console.log(chalk3.dim(" Usage: hatch3r worktree-setup <worktree-path>"));
540
541
  console.log(chalk3.dim(" Or run this command from inside a worktree.\n"));
541
- throw new HatchError("Missing worktree path", 1);
542
+ throw new HatchError("Missing worktree path", 1, "VALIDATION_ERROR");
542
543
  }
543
544
  targetRoot = join3(cwd, worktreePath);
544
545
  }
@@ -550,7 +551,7 @@ async function worktreeSetupCommand(worktreePath, opts = {}) {
550
551
  if (err.code === "ENOENT") {
551
552
  error(`No ${WORKTREE_INCLUDE_FILE} found in ${mainRoot}`);
552
553
  console.log(chalk3.dim(" Run `hatch3r init` or `hatch3r sync` to generate it.\n"));
553
- throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1);
554
+ throw new HatchError(`Missing ${WORKTREE_INCLUDE_FILE}`, 1, "FS_ERROR");
554
555
  }
555
556
  throw err;
556
557
  }
@@ -631,7 +632,8 @@ import {
631
632
  access,
632
633
  rename,
633
634
  unlink as unlink2,
634
- open
635
+ open,
636
+ copyFile as copyFile2
635
637
  } from "fs/promises";
636
638
  import { dirname as dirname3, basename } from "path";
637
639
  import { randomBytes as randomBytes2 } from "crypto";
@@ -778,7 +780,16 @@ var DENY_PATTERNS = [
778
780
  /override\s+(all\s+)?security/i,
779
781
  /(?:atob|Buffer\.from)\s*\([^)]*(?:eval|exec|require)/i,
780
782
  /(?:chmod|chown)\s+[0-7]{3,4}/i,
781
- /(?: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
782
793
  ];
783
794
  var ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\uFEFF\u00AD]/g;
784
795
  var MAX_CUSTOMIZE_MD_BYTES = 10240;
@@ -1012,11 +1023,13 @@ async function safeWriteFile(filePath, content, options = {}) {
1012
1023
  try {
1013
1024
  merged = insertManagedBlock(existingContent, options.managedContent);
1014
1025
  } catch {
1026
+ const bakPath = filePath + ".bak";
1027
+ await copyFile2(filePath, bakPath);
1015
1028
  await atomicWriteFile(filePath, content);
1016
1029
  return {
1017
1030
  path: filePath,
1018
1031
  action: "updated",
1019
- warning: `Auto-repaired corrupted managed block in ${filePath}`
1032
+ warning: `Auto-repaired corrupted managed block in ${filePath} (backup saved to ${bakPath})`
1020
1033
  };
1021
1034
  }
1022
1035
  await atomicWriteFile(filePath, merged);
@@ -1322,10 +1335,10 @@ async function ensureEnvMcp(rootDir, servers) {
1322
1335
  }
1323
1336
 
1324
1337
  // src/cli/commands/update.ts
1325
- 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";
1326
1339
  import { execFileSync as execFileSync3 } from "child_process";
1327
1340
  import { fileURLToPath } from "url";
1328
- import { dirname as dirname7, join as join15 } from "path";
1341
+ import { dirname as dirname8, join as join16 } from "path";
1329
1342
  import chalk4 from "chalk";
1330
1343
  import inquirer from "inquirer";
1331
1344
 
@@ -1425,7 +1438,7 @@ async function readGlobMd(baseDir, fileType) {
1425
1438
  let entries;
1426
1439
  try {
1427
1440
  const all = await readdir(baseDir, { recursive: true });
1428
- entries = all.filter((f) => f.endsWith(".md"));
1441
+ entries = all.filter((f) => f.endsWith(".md")).sort();
1429
1442
  } catch (err) {
1430
1443
  if (err.code !== "ENOENT") throw err;
1431
1444
  return [];
@@ -1461,7 +1474,7 @@ async function readGlobMd(baseDir, fileType) {
1461
1474
  async function readSkillSubdirs(baseDir) {
1462
1475
  let dirents;
1463
1476
  try {
1464
- dirents = await readdir(baseDir, { withFileTypes: true });
1477
+ dirents = (await readdir(baseDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1465
1478
  } catch (err) {
1466
1479
  if (err.code !== "ENOENT") throw err;
1467
1480
  return [];
@@ -2159,7 +2172,7 @@ var AmazonQAdapter = class extends BaseAdapter {
2159
2172
  if (mcp && Object.keys(mcp).length > 0) {
2160
2173
  const entries = this.buildStdMcpEntries(mcp);
2161
2174
  if (Object.keys(entries).length > 0) {
2162
- 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)));
2163
2176
  }
2164
2177
  }
2165
2178
  return results;
@@ -2178,9 +2191,9 @@ var AmpAdapter = class extends BaseAdapter {
2178
2191
  text: `**Recommended model:** \`${m}\`. Use Smart mode for Opus, Rush for Haiku, Deep for Codex.`
2179
2192
  }))
2180
2193
  ].join("\n");
2181
- results.push(output(".amp/AGENTS.md", wrapInManagedBlock(inner), inner));
2194
+ results.push(output("AGENTS.md", wrapInManagedBlock(inner), inner));
2182
2195
  results.push(
2183
- ...await this.processSkillsRaw(ctx, (id) => `.amp/skills/${toPrefixedId(id)}/SKILL.md`)
2196
+ ...await this.processSkillsRaw(ctx, (id) => `.agents/skills/${toPrefixedId(id)}/SKILL.md`)
2184
2197
  );
2185
2198
  const mcp = await this.readFilteredMcp(ctx);
2186
2199
  if (mcp && Object.keys(mcp).length > 0) {
@@ -2984,7 +2997,7 @@ var CursorAdapter = class extends BaseAdapter {
2984
2997
  const lines = [`name: ${agent.id}`, `description: ${desc}`];
2985
2998
  if (model) lines.push(`model: ${model}`);
2986
2999
  if (agent.readonly) lines.push("readonly: true");
2987
- if (agent.background) lines.push("background: true");
3000
+ if (agent.background) lines.push("is_background: true");
2988
3001
  const fm = `---
2989
3002
  ${lines.join("\n")}
2990
3003
  ---`;
@@ -3427,7 +3440,7 @@ function isGlobPattern(scope) {
3427
3440
  function ruleTrigger(scope) {
3428
3441
  if (!scope) return "model_decision";
3429
3442
  if (scope === "always") return "always_on";
3430
- return "glob_pattern";
3443
+ return "glob";
3431
3444
  }
3432
3445
  var WindsurfAdapter = class extends BaseAdapter {
3433
3446
  name = "windsurf";
@@ -3464,7 +3477,7 @@ var WindsurfAdapter = class extends BaseAdapter {
3464
3477
  if (skip) continue;
3465
3478
  const scope = overrides.scope ?? rule.scope;
3466
3479
  const trigger = ruleTrigger(scope);
3467
- const globScope = trigger === "glob_pattern" && scope ? isGlobPattern(scope) ? scope : `${scope}/**` : void 0;
3480
+ const globScope = trigger === "glob" && scope ? isGlobPattern(scope) ? scope : `${scope}/**` : void 0;
3468
3481
  const fm = `---
3469
3482
  trigger: ${trigger}${globScope ? `
3470
3483
  globs: "${globScope}"` : ""}
@@ -3649,7 +3662,7 @@ function validateIntegrityManifest(data) {
3649
3662
  for (const val of Object.values(obj.files)) {
3650
3663
  if (typeof val !== "string") return false;
3651
3664
  }
3652
- if ("checksum" in obj && typeof obj.checksum !== "string") return false;
3665
+ if (typeof obj.checksum !== "string") return false;
3653
3666
  return true;
3654
3667
  }
3655
3668
  async function readIntegrityManifest(agentsDir) {
@@ -3670,12 +3683,10 @@ async function verifyIntegrity(agentsDir) {
3670
3683
  return [];
3671
3684
  }
3672
3685
  const results = [];
3673
- if (manifest.checksum !== void 0) {
3674
- const expected = createHash("sha256").update(JSON.stringify(manifest.files)).digest("hex");
3675
- if (manifest.checksum !== expected) {
3676
- results.push({ file: INTEGRITY_FILE, status: "tampered" });
3677
- return results;
3678
- }
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;
3679
3690
  }
3680
3691
  const manifestFiles = new Set(Object.keys(manifest.files));
3681
3692
  for (const [filePath, expectedHash] of Object.entries(manifest.files)) {
@@ -3723,17 +3734,209 @@ async function verifyIntegrity(agentsDir) {
3723
3734
  return results;
3724
3735
  }
3725
3736
 
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
+ }
3778
+ }
3779
+ return null;
3780
+ }
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
+ }
3788
+ }
3789
+ return content.trim();
3790
+ }
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;
3835
+ let content;
3836
+ try {
3837
+ content = await readFile12(absPath, "utf-8");
3838
+ } catch {
3839
+ continue;
3840
+ }
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
+ }
3858
+ }
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);
3870
+ }
3871
+ await cleanEmptyDirs(rootDir, filesToArchive);
3872
+ return { archivedFiles, migrations };
3873
+ }
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
+
3726
3929
  // src/content/index.ts
3727
- import { readFile as readFile12, readdir as readdir5, cp, mkdir as mkdir3, rm } from "fs/promises";
3728
- import { join as join14, dirname as dirname6, normalize, isAbsolute } from "path";
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";
3729
3932
  function assertSafePath(relativePath, label2) {
3730
3933
  const sanitized = relativePath.replace(/\0/g, "");
3731
3934
  const normalized = normalize(sanitized);
3732
3935
  if (normalized.startsWith("..") || isAbsolute(normalized)) {
3733
- throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1);
3936
+ throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3734
3937
  }
3735
3938
  if (sanitized !== relativePath) {
3736
- throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1);
3939
+ throw new HatchError(`Unsafe path detected in ${label2}: ${relativePath}`, 1, "FS_ERROR");
3737
3940
  }
3738
3941
  }
3739
3942
  function extractContentReferences(content) {
@@ -3751,8 +3954,8 @@ async function validateCrossReferences(contentRoot, index) {
3751
3954
  for (const item of index.items) {
3752
3955
  let content;
3753
3956
  try {
3754
- const filePath = item.type === "skill" ? join14(contentRoot, item.relativePath, "SKILL.md") : join14(contentRoot, `${item.relativePath}`);
3755
- content = await readFile12(filePath, "utf-8");
3957
+ const filePath = item.type === "skill" ? join15(contentRoot, item.relativePath, "SKILL.md") : join15(contentRoot, `${item.relativePath}`);
3958
+ content = await readFile13(filePath, "utf-8");
3756
3959
  } catch {
3757
3960
  continue;
3758
3961
  }
@@ -3789,6 +3992,12 @@ function validateOrchestrationDependencies(selection) {
3789
3992
  }
3790
3993
  return warnings;
3791
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
+ }
3792
4001
  var CONTENT_TYPE_CONFIGS = [
3793
4002
  { dir: "agents", type: "agent", strategy: "glob" },
3794
4003
  { dir: "commands", type: "command", strategy: "glob" },
@@ -3801,20 +4010,20 @@ var CONTENT_TYPE_CONFIGS = [
3801
4010
  async function buildContentIndex(contentRoot) {
3802
4011
  const items = [];
3803
4012
  for (const config of CONTENT_TYPE_CONFIGS) {
3804
- const dirPath = join14(contentRoot, config.dir);
4013
+ const dirPath = join15(contentRoot, config.dir);
3805
4014
  if (config.strategy === "subdirectory") {
3806
4015
  let dirents;
3807
4016
  try {
3808
- dirents = await readdir5(dirPath, { withFileTypes: true });
4017
+ dirents = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3809
4018
  } catch (err) {
3810
4019
  if (err.code === "ENOENT") continue;
3811
4020
  throw err;
3812
4021
  }
3813
4022
  for (const dirent of dirents) {
3814
4023
  if (!dirent.isDirectory()) continue;
3815
- const skillPath = join14(dirPath, dirent.name, "SKILL.md");
4024
+ const skillPath = join15(dirPath, dirent.name, "SKILL.md");
3816
4025
  try {
3817
- const raw = await readFile12(skillPath, "utf-8");
4026
+ const raw = await readFile13(skillPath, "utf-8");
3818
4027
  const { metadata } = parseFrontmatter(raw);
3819
4028
  const id = metadata.id || metadata.name || dirent.name;
3820
4029
  items.push({
@@ -3823,7 +4032,7 @@ async function buildContentIndex(contentRoot) {
3823
4032
  description: metadata.description ?? "",
3824
4033
  tags: metadata.tags ?? [],
3825
4034
  protected: metadata.protected,
3826
- relativePath: join14(config.dir, dirent.name)
4035
+ relativePath: join15(config.dir, dirent.name)
3827
4036
  });
3828
4037
  } catch (err) {
3829
4038
  if (err.code !== "ENOENT") throw err;
@@ -3832,15 +4041,15 @@ async function buildContentIndex(contentRoot) {
3832
4041
  } else {
3833
4042
  let entries;
3834
4043
  try {
3835
- const all = await readdir5(dirPath);
3836
- entries = all.filter((f) => f.endsWith(".md"));
4044
+ const all = await readdir6(dirPath);
4045
+ entries = all.filter((f) => f.endsWith(".md")).sort();
3837
4046
  } catch (err) {
3838
4047
  if (err.code === "ENOENT") continue;
3839
4048
  throw err;
3840
4049
  }
3841
4050
  for (const file of entries) {
3842
- const filePath = join14(dirPath, file);
3843
- const raw = await readFile12(filePath, "utf-8");
4051
+ const filePath = join15(dirPath, file);
4052
+ const raw = await readFile13(filePath, "utf-8");
3844
4053
  const { metadata } = parseFrontmatter(raw);
3845
4054
  const id = metadata.id || metadata.name || file.replace(/\.md$/, "");
3846
4055
  const item = {
@@ -3849,13 +4058,13 @@ async function buildContentIndex(contentRoot) {
3849
4058
  description: metadata.description ?? "",
3850
4059
  tags: metadata.tags ?? [],
3851
4060
  protected: metadata.protected,
3852
- relativePath: join14(config.dir, file)
4061
+ relativePath: join15(config.dir, file)
3853
4062
  };
3854
4063
  if (config.type === "rule") {
3855
4064
  const mdcFile = file.replace(/\.md$/, ".mdc");
3856
4065
  try {
3857
- await readFile12(join14(dirPath, mdcFile), "utf-8");
3858
- item.companionPath = join14(config.dir, mdcFile);
4066
+ await readFile13(join15(dirPath, mdcFile), "utf-8");
4067
+ item.companionPath = join15(config.dir, mdcFile);
3859
4068
  } catch {
3860
4069
  }
3861
4070
  }
@@ -3865,10 +4074,12 @@ async function buildContentIndex(contentRoot) {
3865
4074
  }
3866
4075
  const byType = {};
3867
4076
  const byId = /* @__PURE__ */ new Map();
4077
+ const byTypeAndId = /* @__PURE__ */ new Map();
3868
4078
  const collisions = [];
3869
4079
  for (const item of items) {
3870
4080
  if (!byType[item.type]) byType[item.type] = [];
3871
4081
  byType[item.type].push(item);
4082
+ byTypeAndId.set(typeIdKey(item.type, item.id), item);
3872
4083
  const existing = byId.get(item.id);
3873
4084
  if (existing) {
3874
4085
  const kind = existing.type !== item.type ? "cross-type" : "same-type";
@@ -3882,7 +4093,7 @@ async function buildContentIndex(contentRoot) {
3882
4093
  });
3883
4094
  if (kind === "cross-type") {
3884
4095
  console.warn(
3885
- `[hatch3r] Content ID collision: "${item.id}" exists as both ${existing.type} (${existing.relativePath}) and ${item.type} (${item.relativePath}). The ${item.type} entry will shadow the ${existing.type} entry in ID lookups.`
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.`
3886
4097
  );
3887
4098
  } else {
3888
4099
  console.warn(
@@ -3892,7 +4103,7 @@ async function buildContentIndex(contentRoot) {
3892
4103
  }
3893
4104
  byId.set(item.id, item);
3894
4105
  }
3895
- return { items, byType, byId, collisions };
4106
+ return { items, byType, byId, byTypeAndId, collisions };
3896
4107
  }
3897
4108
  var TYPE_TO_SELECTION_KEY = {
3898
4109
  agent: "agents",
@@ -4022,21 +4233,21 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
4022
4233
  if (item.companionPath) {
4023
4234
  assertSafePath(item.companionPath, "copySelectedContent companion");
4024
4235
  }
4025
- const srcPath = join14(contentRoot, item.relativePath);
4026
- const destPath = join14(agentsDir, item.relativePath);
4236
+ const srcPath = join15(contentRoot, item.relativePath);
4237
+ const destPath = join15(agentsDir, item.relativePath);
4027
4238
  if (item.type === "skill") {
4028
- await mkdir3(destPath, { recursive: true });
4029
- await cp(srcPath, destPath, { recursive: true, force: true });
4239
+ await mkdir4(destPath, { recursive: true });
4240
+ await cp2(srcPath, destPath, { recursive: true, force: true });
4030
4241
  copied.push(item.relativePath);
4031
4242
  } else {
4032
- await mkdir3(dirname6(destPath), { recursive: true });
4033
- await cp(srcPath, destPath, { force: true });
4243
+ await mkdir4(dirname7(destPath), { recursive: true });
4244
+ await cp2(srcPath, destPath, { force: true });
4034
4245
  copied.push(item.relativePath);
4035
4246
  if (item.companionPath) {
4036
- const mdcSrc = join14(contentRoot, item.companionPath);
4037
- const mdcDest = join14(agentsDir, item.companionPath);
4247
+ const mdcSrc = join15(contentRoot, item.companionPath);
4248
+ const mdcDest = join15(agentsDir, item.companionPath);
4038
4249
  try {
4039
- await cp(mdcSrc, mdcDest, { force: true });
4250
+ await cp2(mdcSrc, mdcDest, { force: true });
4040
4251
  copied.push(item.companionPath);
4041
4252
  } catch (err) {
4042
4253
  if (err.code !== "ENOENT") throw err;
@@ -4047,31 +4258,31 @@ async function copySelectedContent(contentRoot, agentsDir, selection, index) {
4047
4258
  for (const config of CONTENT_TYPE_CONFIGS) {
4048
4259
  if (config.strategy !== "glob") continue;
4049
4260
  try {
4050
- const dirEntries = await readdir5(join14(contentRoot, config.dir), { withFileTypes: true });
4261
+ const dirEntries = await readdir6(join15(contentRoot, config.dir), { withFileTypes: true });
4051
4262
  for (const entry of dirEntries) {
4052
4263
  if (!entry.isDirectory() || entry.name.startsWith("hatch3r-")) continue;
4053
- const subSrc = join14(contentRoot, config.dir, entry.name);
4054
- const subDest = join14(agentsDir, config.dir, entry.name);
4055
- await mkdir3(subDest, { recursive: true });
4056
- await cp(subSrc, subDest, { recursive: true, force: true });
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 });
4057
4268
  }
4058
4269
  } catch (err) {
4059
4270
  if (err.code !== "ENOENT") throw err;
4060
4271
  }
4061
4272
  }
4062
4273
  try {
4063
- const checksSrc = join14(contentRoot, "checks");
4064
- const checksDest = join14(agentsDir, "checks");
4065
- await mkdir3(checksDest, { recursive: true });
4066
- 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 });
4067
4278
  } catch (err) {
4068
4279
  if (err.code !== "ENOENT") throw err;
4069
4280
  }
4070
4281
  try {
4071
- const mcpSrc = join14(contentRoot, "mcp");
4072
- const mcpDest = join14(agentsDir, "mcp");
4073
- await mkdir3(mcpDest, { recursive: true });
4074
- 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 });
4075
4286
  } catch (err) {
4076
4287
  if (err.code !== "ENOENT") throw err;
4077
4288
  }
@@ -4088,16 +4299,16 @@ async function buildSelectionsFromDisk(agentsDir) {
4088
4299
  githubAgents: []
4089
4300
  };
4090
4301
  for (const config of CONTENT_TYPE_CONFIGS) {
4091
- const dirPath = join14(agentsDir, config.dir);
4302
+ const dirPath = join15(agentsDir, config.dir);
4092
4303
  const key = TYPE_TO_SELECTION_KEY[config.type];
4093
4304
  if (!key) continue;
4094
4305
  if (config.strategy === "subdirectory") {
4095
4306
  try {
4096
- const dirents = await readdir5(dirPath, { withFileTypes: true });
4307
+ const dirents = await readdir6(dirPath, { withFileTypes: true });
4097
4308
  for (const d of dirents) {
4098
4309
  if (!d.isDirectory()) continue;
4099
4310
  try {
4100
- 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");
4101
4312
  const { metadata } = parseFrontmatter(raw);
4102
4313
  items[key].push(metadata.id || metadata.name || d.name);
4103
4314
  } catch {
@@ -4107,9 +4318,9 @@ async function buildSelectionsFromDisk(agentsDir) {
4107
4318
  }
4108
4319
  } else {
4109
4320
  try {
4110
- const files = await readdir5(dirPath);
4321
+ const files = await readdir6(dirPath);
4111
4322
  for (const f of files.filter((f2) => f2.endsWith(".md"))) {
4112
- const raw = await readFile12(join14(dirPath, f), "utf-8");
4323
+ const raw = await readFile13(join15(dirPath, f), "utf-8");
4113
4324
  const { metadata } = parseFrontmatter(raw);
4114
4325
  items[key].push(metadata.id || metadata.name || f.replace(/\.md$/, ""));
4115
4326
  }
@@ -4129,20 +4340,20 @@ async function addContentItem(contentRoot, agentsDir, item) {
4129
4340
  if (item.companionPath) {
4130
4341
  assertSafePath(item.companionPath, "addContentItem companion");
4131
4342
  }
4132
- const srcPath = join14(contentRoot, item.relativePath);
4133
- const destPath = join14(agentsDir, item.relativePath);
4343
+ const srcPath = join15(contentRoot, item.relativePath);
4344
+ const destPath = join15(agentsDir, item.relativePath);
4134
4345
  try {
4135
4346
  if (item.type === "skill") {
4136
- await mkdir3(destPath, { recursive: true });
4137
- await cp(srcPath, destPath, { recursive: true, force: true });
4347
+ await mkdir4(destPath, { recursive: true });
4348
+ await cp2(srcPath, destPath, { recursive: true, force: true });
4138
4349
  } else {
4139
- await mkdir3(dirname6(destPath), { recursive: true });
4140
- await cp(srcPath, destPath, { force: true });
4350
+ await mkdir4(dirname7(destPath), { recursive: true });
4351
+ await cp2(srcPath, destPath, { force: true });
4141
4352
  if (item.companionPath) {
4142
4353
  try {
4143
- await cp(
4144
- join14(contentRoot, item.companionPath),
4145
- join14(agentsDir, item.companionPath),
4354
+ await cp2(
4355
+ join15(contentRoot, item.companionPath),
4356
+ join15(agentsDir, item.companionPath),
4146
4357
  { force: true }
4147
4358
  );
4148
4359
  } catch (err) {
@@ -4154,7 +4365,8 @@ async function addContentItem(contentRoot, agentsDir, item) {
4154
4365
  if (err.code === "ENOENT") {
4155
4366
  throw new HatchError(
4156
4367
  `Content "${item.id}" (${item.type}) not found in package at ${item.relativePath}. It may have been renamed or removed in this hatch3r version.`,
4157
- 1
4368
+ 1,
4369
+ "FS_ERROR"
4158
4370
  );
4159
4371
  }
4160
4372
  throw err;
@@ -4165,13 +4377,13 @@ async function removeContentItem(agentsDir, item, options) {
4165
4377
  if (item.companionPath) {
4166
4378
  assertSafePath(item.companionPath, "removeContentItem companion");
4167
4379
  }
4168
- const destPath = join14(agentsDir, item.relativePath);
4380
+ const destPath = join15(agentsDir, item.relativePath);
4169
4381
  if (item.type === "skill") {
4170
- await rm(destPath, { recursive: true, force: true });
4382
+ await rm2(destPath, { recursive: true, force: true });
4171
4383
  } else {
4172
- await rm(destPath, { force: true });
4384
+ await rm2(destPath, { force: true });
4173
4385
  if (item.companionPath) {
4174
- await rm(join14(agentsDir, item.companionPath), { force: true });
4386
+ await rm2(join15(agentsDir, item.companionPath), { force: true });
4175
4387
  }
4176
4388
  }
4177
4389
  if (options?.rootDir) {
@@ -4183,10 +4395,10 @@ async function removeContentItem(agentsDir, item, options) {
4183
4395
  };
4184
4396
  const customDir = typeToDir[item.type];
4185
4397
  if (customDir) {
4186
- const yamlPath = join14(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.yaml`);
4187
- const mdPath = join14(options.rootDir, ".hatch3r", customDir, `${item.id}.customize.md`);
4188
- await rm(yamlPath, { force: true });
4189
- 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 });
4190
4402
  }
4191
4403
  }
4192
4404
  }
@@ -4218,40 +4430,40 @@ function selectionSummary(selection) {
4218
4430
  }
4219
4431
 
4220
4432
  // src/cli/commands/update.ts
4221
- var __dirname = dirname7(fileURLToPath(import.meta.url));
4433
+ var __dirname = dirname8(fileURLToPath(import.meta.url));
4222
4434
  var CONTENT_DIRS = ["agents", "commands", "rules", "skills", "prompts", "github-agents", "mcp", "hooks"];
4223
4435
  var ALWAYS_COPY_FILES = /* @__PURE__ */ new Set(["mcp.json"]);
4224
4436
  async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selectedIds) {
4225
4437
  const copied = [];
4226
4438
  let entries;
4227
4439
  try {
4228
- entries = await readdir6(srcDir, { withFileTypes: true });
4440
+ entries = await readdir7(srcDir, { withFileTypes: true });
4229
4441
  } catch (err) {
4230
4442
  if (err.code === "ENOENT") return [];
4231
4443
  throw err;
4232
4444
  }
4233
4445
  for (const entry of entries) {
4234
- const srcPath = join15(srcDir, entry.name);
4235
- const destPath = join15(destDir, entry.name);
4446
+ const srcPath = join16(srcDir, entry.name);
4447
+ const destPath = join16(destDir, entry.name);
4236
4448
  if (entry.isDirectory()) {
4237
4449
  if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
4238
4450
  if (!selectedIds.has(entry.name)) continue;
4239
4451
  }
4240
- await mkdir4(destPath, { recursive: true });
4452
+ await mkdir5(destPath, { recursive: true });
4241
4453
  const subCopied = await copyHatch3rFiles(
4242
4454
  srcPath,
4243
4455
  destPath,
4244
4456
  insideHatch3rDir || !entry.name.startsWith(HATCH3R_PREFIX),
4245
4457
  selectedIds
4246
4458
  );
4247
- copied.push(...subCopied.map((p) => join15(entry.name, p)));
4459
+ copied.push(...subCopied.map((p) => join16(entry.name, p)));
4248
4460
  } else if (entry.name.startsWith(HATCH3R_PREFIX) || insideHatch3rDir || ALWAYS_COPY_FILES.has(entry.name)) {
4249
4461
  if (selectedIds && entry.name.startsWith(HATCH3R_PREFIX)) {
4250
4462
  const baseId = entry.name.replace(/\.(md|mdc)$/, "");
4251
4463
  if (!selectedIds.has(baseId)) continue;
4252
4464
  }
4253
- await mkdir4(dirname7(destPath), { recursive: true });
4254
- await cp2(srcPath, destPath, { force: true });
4465
+ await mkdir5(dirname8(destPath), { recursive: true });
4466
+ await cp3(srcPath, destPath, { force: true });
4255
4467
  copied.push(entry.name);
4256
4468
  }
4257
4469
  }
@@ -4260,7 +4472,7 @@ async function copyHatch3rFiles(srcDir, destDir, insideHatch3rDir = false, selec
4260
4472
  async function runUpdate(rootDir, manifest, options = {}) {
4261
4473
  const offset = options.stepOffset ?? 0;
4262
4474
  const total = options.totalSteps ?? 4;
4263
- const agentsDir = join15(rootDir, AGENTS_DIR);
4475
+ const agentsDir = join16(rootDir, AGENTS_DIR);
4264
4476
  let contentRoot = findPackageRoot(__dirname);
4265
4477
  const pm = await detectPackageManager(rootDir);
4266
4478
  const s0 = createSpinner(step(offset + 1, total, "Updating package..."));
@@ -4274,7 +4486,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
4274
4486
  const msg = isTimeout ? "Package update timed out after 30s. Check network connectivity and retry." : err instanceof Error ? err.message : String(err);
4275
4487
  s0.fail(step(offset + 1, total, "Failed to update package"));
4276
4488
  error(msg);
4277
- throw new HatchError(msg, 1);
4489
+ throw new HatchError(msg, 1, isTimeout ? "NETWORK_ERROR" : "UNKNOWN_ERROR");
4278
4490
  }
4279
4491
  s0.succeed(step(offset + 1, total, "Package updated"));
4280
4492
  const s1 = createSpinner(step(offset + 2, total, "Updating canonical files..."));
@@ -4288,16 +4500,16 @@ async function runUpdate(rootDir, manifest, options = {}) {
4288
4500
  }
4289
4501
  const copied = [];
4290
4502
  for (const dir of CONTENT_DIRS) {
4291
- const srcDir = join15(contentRoot, dir);
4503
+ const srcDir = join16(contentRoot, dir);
4292
4504
  try {
4293
- const dirCopied = await copyHatch3rFiles(srcDir, join15(agentsDir, dir), false, selectedIds);
4294
- 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)));
4295
4507
  } catch (err) {
4296
4508
  if (err.code !== "ENOENT") throw err;
4297
4509
  }
4298
4510
  }
4299
4511
  const canonicalAgentsMd = await generateCanonicalAgentsMd(agentsDir);
4300
- await safeWriteFile(join15(agentsDir, "AGENTS.md"), canonicalAgentsMd);
4512
+ await safeWriteFile(join16(agentsDir, "AGENTS.md"), canonicalAgentsMd);
4301
4513
  s1.succeed(step(offset + 2, total, `Updated ${copied.length} canonical files`));
4302
4514
  const s2 = createSpinner(step(offset + 3, total, "Re-syncing adapter output..."));
4303
4515
  s2.start();
@@ -4310,7 +4522,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
4310
4522
  warn(w);
4311
4523
  }
4312
4524
  for (const out of outputs) {
4313
- const fullPath = join15(rootDir, out.path);
4525
+ const fullPath = join16(rootDir, out.path);
4314
4526
  if (out.managedContent) {
4315
4527
  await safeWriteFile(fullPath, out.content, {
4316
4528
  managedContent: out.managedContent
@@ -4332,7 +4544,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
4332
4544
  }
4333
4545
  if (adapterFailures.length === manifest.tools.length) {
4334
4546
  s2.fail(step(offset + 3, total, "All adapters failed"));
4335
- throw new HatchError("All adapters failed", 1);
4547
+ throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
4336
4548
  }
4337
4549
  }
4338
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)`));
@@ -4342,6 +4554,7 @@ async function runUpdate(rootDir, manifest, options = {}) {
4342
4554
  await writeManifest(rootDir, manifest);
4343
4555
  const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
4344
4556
  await writeIntegrityManifest(agentsDir, integrityManifest);
4557
+ await pruneArchives(rootDir);
4345
4558
  s3.succeed(step(offset + 4, total, "Manifest updated"));
4346
4559
  return {
4347
4560
  copiedFiles: copied.length,
@@ -4354,35 +4567,40 @@ var MIGRATION_CHECKPOINTS = [
4354
4567
  {
4355
4568
  id: "content-selections-init",
4356
4569
  condition: async (manifest) => manifest.content === void 0,
4357
- execute: async (manifest, rootDir) => {
4358
- const agentsDir = join15(rootDir, AGENTS_DIR);
4570
+ execute: async (manifest, rootDir, headless) => {
4571
+ const agentsDir = join16(rootDir, AGENTS_DIR);
4359
4572
  const content = await buildSelectionsFromDisk(agentsDir);
4360
- const { projectType } = await inquirer.prompt([
4361
- {
4362
- type: "list",
4363
- name: "projectType",
4364
- message: "For content tracking \u2014 is this a greenfield or brownfield project?",
4365
- choices: [
4366
- { name: "Greenfield \u2014 new project", value: "greenfield" },
4367
- { name: "Brownfield \u2014 existing codebase", value: "brownfield" }
4368
- ],
4369
- default: "brownfield"
4370
- }
4371
- ]);
4372
- const { teamSize } = await inquirer.prompt([
4373
- {
4374
- type: "list",
4375
- name: "teamSize",
4376
- message: "Solo developer or team?",
4377
- choices: [
4378
- { name: "Solo", value: "solo" },
4379
- { name: "Team", value: "team" }
4380
- ],
4381
- default: "team"
4382
- }
4383
- ]);
4384
- content.projectType = projectType;
4385
- 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
+ }
4386
4604
  return {
4387
4605
  manifest: { ...manifest, content },
4388
4606
  notices: ["Migrated to explicit content tracking (all existing items preserved)"]
@@ -4392,20 +4610,26 @@ var MIGRATION_CHECKPOINTS = [
4392
4610
  {
4393
4611
  id: "platform-selection",
4394
4612
  condition: async (manifest) => !manifest.platform,
4395
- execute: async (manifest) => {
4396
- const { platform } = await inquirer.prompt([
4397
- {
4398
- type: "list",
4399
- name: "platform",
4400
- message: "hatch3r now supports multiple platforms. Select your platform:",
4401
- choices: [
4402
- { name: "GitHub", value: "github" },
4403
- { name: "Azure DevOps", value: "azure-devops" },
4404
- { name: "GitLab", value: "gitlab" }
4405
- ],
4406
- default: "github"
4407
- }
4408
- ]);
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
+ }
4409
4633
  const updated = { ...manifest, platform };
4410
4634
  const notices = [];
4411
4635
  if (platform === "github") {
@@ -4433,12 +4657,12 @@ var MIGRATION_CHECKPOINTS = [
4433
4657
  {
4434
4658
  id: "customize-yaml-size",
4435
4659
  condition: async (_manifest, rootDir) => {
4436
- const agentsDir = join15(rootDir, AGENTS_DIR);
4660
+ const agentsDir = join16(rootDir, AGENTS_DIR);
4437
4661
  try {
4438
- const entries = await readdir6(agentsDir, { recursive: true });
4662
+ const entries = await readdir7(agentsDir, { recursive: true });
4439
4663
  for (const entry of entries) {
4440
4664
  if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
4441
- const s = await stat(join15(agentsDir, entry));
4665
+ const s = await stat2(join16(agentsDir, entry));
4442
4666
  if (s.size > 10240) return true;
4443
4667
  }
4444
4668
  }
@@ -4447,14 +4671,14 @@ var MIGRATION_CHECKPOINTS = [
4447
4671
  }
4448
4672
  return false;
4449
4673
  },
4450
- execute: async (manifest, rootDir) => {
4674
+ execute: async (manifest, rootDir, _headless) => {
4451
4675
  const notices = [];
4452
- const agentsDir = join15(rootDir, AGENTS_DIR);
4676
+ const agentsDir = join16(rootDir, AGENTS_DIR);
4453
4677
  try {
4454
- const entries = await readdir6(agentsDir, { recursive: true });
4678
+ const entries = await readdir7(agentsDir, { recursive: true });
4455
4679
  for (const entry of entries) {
4456
4680
  if (typeof entry === "string" && entry.endsWith(".customize.yaml")) {
4457
- const s = await stat(join15(agentsDir, entry));
4681
+ const s = await stat2(join16(agentsDir, entry));
4458
4682
  if (s.size > 10240) {
4459
4683
  notices.push(`Large customize file detected: ${entry} (${Math.round(s.size / 1024)}KB) \u2014 consider splitting`);
4460
4684
  }
@@ -4473,18 +4697,24 @@ var MIGRATION_CHECKPOINTS = [
4473
4697
  const worktreeCapableTools = /* @__PURE__ */ new Set(["claude"]);
4474
4698
  return manifest.tools.some((t) => worktreeCapableTools.has(t));
4475
4699
  },
4476
- execute: async (manifest, rootDir) => {
4477
- const { enabled } = await inquirer.prompt([{
4478
- type: "confirm",
4479
- name: "enabled",
4480
- message: "hatch3r now supports worktree file isolation for parallel agent sessions. Enable it?",
4481
- default: true
4482
- }]);
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
+ }
4483
4713
  const updated = { ...manifest, worktree: { enabled } };
4484
4714
  const notices = [];
4485
4715
  if (enabled) {
4486
4716
  const wtContent = await generateWorktreeInclude(updated, rootDir);
4487
- await safeWriteFile(join15(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
4717
+ await safeWriteFile(join16(rootDir, WORKTREE_INCLUDE_FILE), wtContent, {
4488
4718
  appendIfNoBlock: true
4489
4719
  });
4490
4720
  notices.push("Worktree isolation enabled \u2014 .worktreeinclude generated");
@@ -4495,12 +4725,12 @@ var MIGRATION_CHECKPOINTS = [
4495
4725
  }
4496
4726
  }
4497
4727
  ];
4498
- async function runMigrationCheckpoints(manifest, rootDir) {
4728
+ async function runMigrationCheckpoints(manifest, rootDir, headless = false) {
4499
4729
  let current = manifest;
4500
4730
  const allNotices = [];
4501
4731
  for (const checkpoint of MIGRATION_CHECKPOINTS) {
4502
4732
  if (await checkpoint.condition(current, rootDir)) {
4503
- const { manifest: updated, notices } = await checkpoint.execute(current, rootDir);
4733
+ const { manifest: updated, notices } = await checkpoint.execute(current, rootDir, headless);
4504
4734
  current = updated;
4505
4735
  allNotices.push(...notices);
4506
4736
  }
@@ -4514,9 +4744,10 @@ async function updateCommand(_opts) {
4514
4744
  if (!manifest) {
4515
4745
  error("No .agents/hatch.json found.");
4516
4746
  console.log(chalk4.dim(" Run `npx hatch3r init` to set up your project first.\n"));
4517
- throw new HatchError("No .agents/hatch.json found.", 1);
4747
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
4518
4748
  }
4519
- const { manifest: migrated, allNotices } = await runMigrationCheckpoints(manifest, rootDir);
4749
+ const headless = !!_opts?.yes;
4750
+ const { manifest: migrated, allNotices } = await runMigrationCheckpoints(manifest, rootDir, headless);
4520
4751
  const m = migrated;
4521
4752
  for (const notice of allNotices) {
4522
4753
  warn(notice);
@@ -4537,177 +4768,22 @@ async function updateCommand(_opts) {
4537
4768
  ], "success");
4538
4769
  }
4539
4770
 
4540
- // src/archive/index.ts
4541
- 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";
4542
- import { dirname as dirname8, join as join16, sep } from "path";
4543
- function toPosixPath(p) {
4544
- return sep === "\\" ? p.replaceAll("\\", "/") : p;
4545
- }
4546
- var ARCHIVE_DIR = ".hatch3r-archive";
4547
- var TOOL_PATH_PREFIXES = {
4548
- cursor: [".cursor/"],
4549
- claude: [".claude/", "CLAUDE.md", ".mcp.json"],
4550
- copilot: [".github/copilot-instructions.md", ".github/workflows/copilot-setup-steps.yml", ".vscode/mcp.json"],
4551
- windsurf: [".windsurf/", ".windsurfrules"],
4552
- amp: [".amp/"],
4553
- codex: [".codex/"],
4554
- gemini: [".gemini/", "GEMINI.md"],
4555
- cline: [".roo/", ".roomodes"],
4556
- aider: ["CONVENTIONS.md", ".aider.conf.yml"],
4557
- kiro: [".kiro/"],
4558
- opencode: ["opencode.json"],
4559
- goose: [".goosehints"],
4560
- zed: [".rules"],
4561
- "amazon-q": [".amazonq/"],
4562
- antigravity: [".antigravity/"]
4563
- };
4564
- var PATH_PATTERNS = [
4565
- { pattern: /\/rules\/([^/]+)\.(mdc|md)$/, type: "rules" },
4566
- { pattern: /\/agents\/([^/]+)\.md$/, type: "agents" },
4567
- { pattern: /\/skills\/([^/]+)\/SKILL\.md$/, type: "skills" },
4568
- { pattern: /\/commands\/([^/]+)\.md$/, type: "commands" }
4569
- ];
4570
- function parseOutputPath(filePath) {
4571
- for (const { pattern, type } of PATH_PATTERNS) {
4572
- const match = filePath.match(pattern);
4573
- if (match) {
4574
- let id = match[1];
4575
- if (id.startsWith(HATCH3R_PREFIX)) {
4576
- id = id.slice(HATCH3R_PREFIX.length);
4577
- }
4578
- id = sanitizeId(id);
4579
- if (id.length > 0) return { type, id };
4580
- }
4581
- }
4582
- return null;
4583
- }
4584
- function stripFrontmatter(content) {
4585
- const trimmed = content.trimStart();
4586
- if (trimmed.startsWith("---")) {
4587
- const endIdx = trimmed.indexOf("\n---", 3);
4588
- if (endIdx !== -1) {
4589
- return trimmed.slice(endIdx + 4).trim();
4590
- }
4591
- }
4592
- return content.trim();
4593
- }
4594
- async function fileExists2(path) {
4595
- try {
4596
- await access3(path);
4597
- return true;
4598
- } catch {
4599
- return false;
4600
- }
4601
- }
4602
- async function collectToolFiles(rootDir, tool) {
4603
- const prefixes = TOOL_PATH_PREFIXES[tool];
4604
- if (!prefixes) return [];
4605
- const files = [];
4606
- for (const prefix of prefixes) {
4607
- const absPath = join16(rootDir, prefix);
4608
- if (prefix.endsWith("/")) {
4609
- try {
4610
- const entries = await readdir7(absPath, { recursive: true, withFileTypes: true });
4611
- for (const entry of entries) {
4612
- if (entry.isFile()) {
4613
- const parent = entry.parentPath ?? entry.path ?? absPath;
4614
- const relPath = toPosixPath(join16(prefix, parent.slice(absPath.length), entry.name));
4615
- files.push(relPath);
4616
- }
4617
- }
4618
- } catch {
4619
- }
4620
- } else if (await fileExists2(absPath)) {
4621
- files.push(prefix);
4622
- }
4623
- }
4624
- return files;
4625
- }
4626
- async function archiveToolOutputs(rootDir, tool) {
4627
- const filesToArchive = await collectToolFiles(rootDir, tool);
4628
- if (filesToArchive.length === 0) {
4629
- return { archivedFiles: [], migrations: [] };
4630
- }
4631
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4632
- const archiveBase = join16(rootDir, ARCHIVE_DIR, tool, timestamp);
4633
- const archivedFiles = [];
4634
- const migrations = [];
4635
- for (const relPath of filesToArchive) {
4636
- const absPath = join16(rootDir, relPath);
4637
- if (!await fileExists2(absPath)) continue;
4638
- let content;
4639
- try {
4640
- content = await readFile13(absPath, "utf-8");
4641
- } catch {
4642
- continue;
4643
- }
4644
- if (hasManagedBlock(content)) {
4645
- const customContent = stripFrontmatter(extractCustomContent(content));
4646
- if (customContent.length > 0) {
4647
- const parsed = parseOutputPath(relPath);
4648
- if (parsed) {
4649
- const customizePath = join16(rootDir, ".hatch3r", parsed.type, `${parsed.id}.customize.md`);
4650
- if (!await fileExists2(customizePath)) {
4651
- await mkdir5(dirname8(customizePath), { recursive: true });
4652
- await writeFile3(customizePath, customContent + "\n", "utf-8");
4653
- migrations.push({
4654
- from: relPath,
4655
- to: `.hatch3r/${parsed.type}/${parsed.id}.customize.md`,
4656
- type: parsed.type,
4657
- id: parsed.id
4658
- });
4659
- }
4660
- }
4661
- }
4662
- }
4663
- const archiveDest = join16(archiveBase, relPath);
4664
- await mkdir5(dirname8(archiveDest), { recursive: true });
4665
- await cp3(absPath, archiveDest);
4666
- const srcStat = await stat2(absPath);
4667
- const destStat = await stat2(archiveDest);
4668
- if (destStat.size !== srcStat.size) {
4669
- throw new Error(`Archive copy size mismatch for ${relPath}: source=${srcStat.size}, dest=${destStat.size}`);
4670
- }
4671
- await rm2(absPath);
4672
- archivedFiles.push(relPath);
4673
- }
4674
- await cleanEmptyDirs(rootDir, filesToArchive);
4675
- return { archivedFiles, migrations };
4676
- }
4677
- async function cleanEmptyDirs(rootDir, paths) {
4678
- const dirs = /* @__PURE__ */ new Set();
4679
- for (const p of paths) {
4680
- let dir = dirname8(join16(rootDir, p));
4681
- while (dir !== rootDir && dir.length > rootDir.length) {
4682
- dirs.add(dir);
4683
- dir = dirname8(dir);
4684
- }
4685
- }
4686
- const sorted = [...dirs].sort((a, b) => b.length - a.length);
4687
- for (const dir of sorted) {
4688
- try {
4689
- const entries = await readdir7(dir);
4690
- if (entries.length === 0) {
4691
- await rm2(dir, { recursive: true });
4692
- }
4693
- } catch {
4694
- }
4695
- }
4696
- }
4697
- function removeManagedFilesForPaths(manifest, paths) {
4698
- const pathSet = new Set(paths);
4699
- manifest.managedFiles = manifest.managedFiles.filter((f) => !pathSet.has(f));
4700
- }
4701
-
4702
4771
  // src/workspace/manifest.ts
4703
4772
  import { readFile as readFile14 } from "fs/promises";
4704
- import { join as join17 } from "path";
4773
+ import { join as join17, normalize as normalize2, isAbsolute as isAbsolute2 } from "path";
4705
4774
 
4706
4775
  // src/workspace/types.ts
4707
4776
  var WORKSPACE_MANIFEST_FILE = "workspace.json";
4708
4777
  var WORKSPACE_MANIFEST_VERSION = "1.0.0";
4709
4778
 
4710
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;
4786
+ }
4711
4787
  function validateWorkspaceManifest(data) {
4712
4788
  if (!data || typeof data !== "object") return false;
4713
4789
  const obj = data;
@@ -4736,6 +4812,7 @@ function validateWorkspaceManifest(data) {
4736
4812
  if (!repo || typeof repo !== "object") return false;
4737
4813
  const r = repo;
4738
4814
  if (typeof r.path !== "string") return false;
4815
+ if (isUnsafeRepoPath(r.path)) return false;
4739
4816
  if (typeof r.sync !== "boolean") return false;
4740
4817
  if (r.owner !== void 0 && typeof r.owner !== "string") return false;
4741
4818
  if (r.repo !== void 0 && typeof r.repo !== "string") return false;
@@ -4761,13 +4838,15 @@ async function readWorkspaceManifest(rootDir) {
4761
4838
  } catch (err) {
4762
4839
  throw new HatchError(
4763
4840
  `Malformed JSON in ${manifestPath}: ${err instanceof Error ? err.message : String(err)}`,
4764
- 1
4841
+ 1,
4842
+ "CONFIG_ERROR"
4765
4843
  );
4766
4844
  }
4767
4845
  if (!validateWorkspaceManifest(parsed)) {
4768
4846
  throw new HatchError(
4769
4847
  `Invalid workspace manifest in ${manifestPath}: required fields missing or malformed.`,
4770
- 1
4848
+ 1,
4849
+ "VALIDATION_ERROR"
4771
4850
  );
4772
4851
  }
4773
4852
  return parsed;
@@ -4849,17 +4928,19 @@ import { dirname as dirname10 } from "path";
4849
4928
  import { access as access5, readFile as readFile15, readdir as readdir9 } from "fs/promises";
4850
4929
  import { join as join19 } from "path";
4851
4930
  async function analyzeRepo(rootDir) {
4852
- const [languages, pm, isMonorepo, hasExistingAgents, existingTools] = await Promise.all([
4931
+ const [languages, pm, isMonorepo, hasExistingAgents, existingTools, frameworks] = await Promise.all([
4853
4932
  detectLanguages(rootDir),
4854
4933
  detectPackageManager(rootDir),
4855
4934
  detectMonorepo(rootDir),
4856
4935
  detectExistingAgents(rootDir),
4857
- detectExistingTools(rootDir)
4936
+ detectExistingTools(rootDir),
4937
+ detectFrameworks(rootDir)
4858
4938
  ]);
4859
4939
  const packageManager = pm.name;
4860
4940
  return {
4861
4941
  languages,
4862
4942
  packageManager,
4943
+ frameworks,
4863
4944
  isMonorepo,
4864
4945
  hasExistingAgents,
4865
4946
  existingTools,
@@ -4951,6 +5032,71 @@ async function detectExistingTools(rootDir) {
4951
5032
  (r) => r.status === "fulfilled" && r.value !== null
4952
5033
  ).map((r) => r.value);
4953
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);
5075
+ }
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);
5087
+ }
5088
+ }
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);
5096
+ }
5097
+ }
5098
+ return [...detected];
5099
+ }
4954
5100
  async function pathExists(path) {
4955
5101
  try {
4956
5102
  await access5(path);
@@ -5082,19 +5228,20 @@ var CHARS_PER_TOKEN = 4;
5082
5228
  async function estimateTokensForContent(contentIds, index) {
5083
5229
  let totalChars = 0;
5084
5230
  for (const id of contentIds) {
5085
- const item = index.byId.get(id);
5086
- if (!item) continue;
5087
- try {
5088
- if (item.type === "skill") {
5089
- const skillPath = join20(CONTENT_ROOT, item.relativePath, "SKILL.md");
5090
- const content = await readFile16(skillPath, "utf-8");
5091
- totalChars += content.length;
5092
- } else {
5093
- const filePath = join20(CONTENT_ROOT, item.relativePath);
5094
- const content = await readFile16(filePath, "utf-8");
5095
- totalChars += content.length;
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 {
5096
5244
  }
5097
- } catch {
5098
5245
  }
5099
5246
  }
5100
5247
  return Math.ceil(totalChars / CHARS_PER_TOKEN);
@@ -5184,8 +5331,8 @@ async function syncSingleRepo(workspaceRoot, wsManifest, wsChecksum, repoEntry,
5184
5331
  await mkdir6(repoAgentsDir, { recursive: true });
5185
5332
  await copySelectedContent(CONTENT_ROOT, repoAgentsDir, effectiveSelection, index);
5186
5333
  for (const id of toRemove) {
5187
- const item = index.byId.get(id);
5188
- if (item) {
5334
+ const items = getAllItemsById(index, id);
5335
+ for (const item of items) {
5189
5336
  await removeContentItem(repoAgentsDir, item, { rootDir: repoDir });
5190
5337
  }
5191
5338
  }
@@ -5425,7 +5572,7 @@ async function configCommand() {
5425
5572
  if (!manifest) {
5426
5573
  error("No .agents/hatch.json found.");
5427
5574
  console.log(chalk5.dim(" Run `npx hatch3r init` to set up your project first.\n"));
5428
- throw new HatchError("No .agents/hatch.json found.", 1);
5575
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
5429
5576
  }
5430
5577
  if (manifest.workspace) {
5431
5578
  warn(
@@ -5505,7 +5652,7 @@ async function configCommand() {
5505
5652
  const tools = toolAnswers.tools;
5506
5653
  if (tools.length === 0) {
5507
5654
  error("At least one tool must be selected.");
5508
- 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");
5509
5656
  }
5510
5657
  const currentFeatureKeys = Object.keys(DEFAULT_FEATURES).filter((k) => manifest.features[k]);
5511
5658
  const featureAnswers = await inquirer2.prompt([
@@ -6129,7 +6276,7 @@ async function runInit(options) {
6129
6276
  }
6130
6277
  if (adapterFailures.length === tools.length) {
6131
6278
  s3.fail(step(3, totalSteps, "All adapters failed"));
6132
- throw new HatchError("All adapters failed", 1);
6279
+ throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
6133
6280
  }
6134
6281
  }
6135
6282
  s3.succeed(step(3, totalSteps, adapterFailures.length > 0 ? `Adapter output generated (${adapterFailures.length} failed)` : "Adapter output generated"));
@@ -6249,7 +6396,7 @@ function validateFlag(value, valid, fallback, name) {
6249
6396
  if (!value) return fallback;
6250
6397
  if (!valid.includes(value)) {
6251
6398
  error(`Invalid --${name}: "${value}". Valid: ${valid.join(", ")}`);
6252
- throw new HatchError(`Invalid --${name}: "${value}"`, 1);
6399
+ throw new HatchError(`Invalid --${name}: "${value}"`, 1, "VALIDATION_ERROR");
6253
6400
  }
6254
6401
  return value;
6255
6402
  }
@@ -6313,7 +6460,7 @@ async function initCommand(opts = {}) {
6313
6460
  if (invalid.length > 0) {
6314
6461
  error(`Invalid tool(s): ${invalid.join(", ")}`);
6315
6462
  console.log(chalk6.dim(` Valid tools: ${[...VALID_TOOLS].join(", ")}`));
6316
- throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1);
6463
+ throw new HatchError(`Invalid tool(s): ${invalid.join(", ")}`, 1, "VALIDATION_ERROR");
6317
6464
  }
6318
6465
  tools2 = rawTools;
6319
6466
  } else if (repoInfo.existingTools.length > 0) {
@@ -6958,7 +7105,7 @@ async function syncCommand(opts = {}) {
6958
7105
  if (!manifest) {
6959
7106
  error("No .agents/hatch.json found.");
6960
7107
  console.log(chalk7.dim(" Run `npx hatch3r init` to set up your project first.\n"));
6961
- throw new HatchError("No .agents/hatch.json found.", 1);
7108
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
6962
7109
  }
6963
7110
  const m = manifest;
6964
7111
  const integrityResults = await verifyIntegrity(agentsDir);
@@ -7032,7 +7179,7 @@ async function syncCommand(opts = {}) {
7032
7179
  error(`Failed to generate ${f.tool}: ${f.error}`);
7033
7180
  }
7034
7181
  if (adapterFailures.length === m.tools.length) {
7035
- throw new HatchError("All adapters failed", 1);
7182
+ throw new HatchError("All adapters failed", 1, "ADAPTER_ERROR");
7036
7183
  }
7037
7184
  }
7038
7185
  for (const tool of m.tools) {
@@ -7065,6 +7212,9 @@ async function syncCommand(opts = {}) {
7065
7212
  info(`Run this, then start or restart your editor: ${getSourceEnvMcpCommand()}`);
7066
7213
  }
7067
7214
  }
7215
+ const integrityManifest = await generateIntegrityManifest(agentsDir, HATCH3R_VERSION);
7216
+ await writeIntegrityManifest(agentsDir, integrityManifest);
7217
+ await pruneArchives(rootDir);
7068
7218
  await checkSpecFreshness(rootDir);
7069
7219
  console.log();
7070
7220
  const icons = {
@@ -7503,7 +7653,7 @@ async function validateCommand() {
7503
7653
  spinner.fail("Validation failed");
7504
7654
  error(".agents/ directory not found. Run `hatch3r init` first.");
7505
7655
  console.log();
7506
- throw new HatchError(".agents/ directory not found.", 1);
7656
+ throw new HatchError(".agents/ directory not found.", 1, "CONFIG_ERROR");
7507
7657
  }
7508
7658
  const manifest = await readManifest(rootDir);
7509
7659
  await validateManifest2(rootDir, manifest, result);
@@ -7525,6 +7675,18 @@ async function validateCommand() {
7525
7675
  result.warnings.push(w);
7526
7676
  }
7527
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
+ }
7528
7690
  } catch {
7529
7691
  }
7530
7692
  if (manifest.content) {
@@ -7572,7 +7734,7 @@ async function validateCommand() {
7572
7734
  `${chalk8.yellow("\u26A0")} ${result.warnings.length} warning(s)`
7573
7735
  ];
7574
7736
  printBox("Validation failed", summaryLines, "error");
7575
- throw new HatchError("Validation failed", 1);
7737
+ throw new HatchError("Validation failed", 1, "VALIDATION_ERROR");
7576
7738
  } else {
7577
7739
  const summaryLines = [
7578
7740
  `${chalk8.green("\u2714")} 0 errors`,
@@ -7610,7 +7772,7 @@ async function verifyCommand() {
7610
7772
  spinner.fail("No integrity manifest found");
7611
7773
  error("Missing .agents/.integrity.json \u2014 run `hatch3r init` or `hatch3r update` to generate it.");
7612
7774
  console.log();
7613
- throw new HatchError("Missing .agents/.integrity.json", 1);
7775
+ throw new HatchError("Missing .agents/.integrity.json", 1, "INTEGRITY_ERROR");
7614
7776
  }
7615
7777
  const results = await verifyIntegrity(agentsDir);
7616
7778
  spinner.stop();
@@ -7659,7 +7821,7 @@ async function verifyCommand() {
7659
7821
  info(`Modified files may have been tampered with. Run ${chalk9.bold("hatch3r update")} to restore originals.`);
7660
7822
  }
7661
7823
  console.log();
7662
- throw new HatchError("Integrity check failed", 1);
7824
+ throw new HatchError("Integrity check failed", 1, "INTEGRITY_ERROR");
7663
7825
  } else {
7664
7826
  printBox("Integrity check passed", summaryLines, "success");
7665
7827
  }
@@ -7696,7 +7858,7 @@ async function statusCommand() {
7696
7858
  if (!manifest) {
7697
7859
  error("No .agents/hatch.json found.");
7698
7860
  console.log(chalk10.dim(" Run `npx hatch3r init` to set up your project first.\n"));
7699
- throw new HatchError("No .agents/hatch.json found.", 1);
7861
+ throw new HatchError("No .agents/hatch.json found.", 1, "CONFIG_ERROR");
7700
7862
  }
7701
7863
  const spinner = createSpinner("Checking sync status...");
7702
7864
  spinner.start();
@@ -7794,7 +7956,7 @@ program.command("init").description("Install a complete agent setup into the cur
7794
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);
7795
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);
7796
7958
  program.command("status").description("Check sync status between canonical .agents/ and generated files").action(statusCommand);
7797
- 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);
7798
7960
  program.command("validate").description("Validate the canonical .agents/ structure").action(validateCommand);
7799
7961
  program.command("verify").description("Verify integrity of canonical agent files").action(verifyCommand);
7800
7962
  program.command("config").description("Reconfigure tools, MCP servers, features, and platform").action(configCommand);