@haus-tech/haus-workflow 0.18.1 → 0.18.2

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 (59) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +159 -164
  4. package/library/catalog/manifest.json +1 -1
  5. package/package.json +4 -8
  6. package/tests/README.md +0 -54
  7. package/tests/fixtures/catalog/agents/code-reviewer.md +0 -15
  8. package/tests/fixtures/catalog/agents/docs-researcher.md +0 -15
  9. package/tests/fixtures/catalog/agents/planner.md +0 -15
  10. package/tests/fixtures/catalog/agents/security-reviewer.md +0 -15
  11. package/tests/fixtures/catalog/agents/test-reviewer.md +0 -15
  12. package/tests/fixtures/catalog/manifest.json +0 -1065
  13. package/tests/fixtures/catalog/policy-gates-manifest.json +0 -120
  14. package/tests/fixtures/catalog/skills/auth-oidc-azure-bankid-patterns/SKILL.md +0 -14
  15. package/tests/fixtures/catalog/skills/bullmq-patterns/SKILL.md +0 -14
  16. package/tests/fixtures/catalog/skills/database-patterns/SKILL.md +0 -14
  17. package/tests/fixtures/catalog/skills/dotnet-patterns/SKILL.md +0 -14
  18. package/tests/fixtures/catalog/skills/dotnet-service-patterns/SKILL.md +0 -14
  19. package/tests/fixtures/catalog/skills/eslint-setup/SKILL.md +0 -14
  20. package/tests/fixtures/catalog/skills/expo-react-native-patterns/SKILL.md +0 -14
  21. package/tests/fixtures/catalog/skills/global-engineering-rules/SKILL.md +0 -14
  22. package/tests/fixtures/catalog/skills/i18next-patterns/SKILL.md +0 -14
  23. package/tests/fixtures/catalog/skills/jest-patterns/SKILL.md +0 -14
  24. package/tests/fixtures/catalog/skills/laravel-nova-patterns/SKILL.md +0 -14
  25. package/tests/fixtures/catalog/skills/laravel-patterns/SKILL.md +0 -14
  26. package/tests/fixtures/catalog/skills/nestjs-graphql-patterns/SKILL.md +0 -14
  27. package/tests/fixtures/catalog/skills/nextauth-patterns/SKILL.md +0 -14
  28. package/tests/fixtures/catalog/skills/nextjs-patterns/SKILL.md +0 -14
  29. package/tests/fixtures/catalog/skills/nx21-monorepo-patterns/SKILL.md +0 -14
  30. package/tests/fixtures/catalog/skills/package-manager-yarn4-pnpm89/SKILL.md +0 -14
  31. package/tests/fixtures/catalog/skills/phpunit-patterns/SKILL.md +0 -14
  32. package/tests/fixtures/catalog/skills/playwright-patterns/SKILL.md +0 -14
  33. package/tests/fixtures/catalog/skills/prettier-setup/SKILL.md +0 -14
  34. package/tests/fixtures/catalog/skills/prisma-patterns/SKILL.md +0 -14
  35. package/tests/fixtures/catalog/skills/production-readiness-review/SKILL.md +0 -14
  36. package/tests/fixtures/catalog/skills/qliro-patterns/SKILL.md +0 -14
  37. package/tests/fixtures/catalog/skills/radix-shadcn-patterns/SKILL.md +0 -14
  38. package/tests/fixtures/catalog/skills/react-router-v7-patterns/SKILL.md +0 -14
  39. package/tests/fixtures/catalog/skills/react19-patterns/SKILL.md +0 -14
  40. package/tests/fixtures/catalog/skills/sanity-patterns/SKILL.md +0 -14
  41. package/tests/fixtures/catalog/skills/security-review/SKILL.md +0 -14
  42. package/tests/fixtures/catalog/skills/sentry-patterns/SKILL.md +0 -14
  43. package/tests/fixtures/catalog/skills/storybook-patterns/SKILL.md +0 -14
  44. package/tests/fixtures/catalog/skills/strapi-patterns/SKILL.md +0 -14
  45. package/tests/fixtures/catalog/skills/stripe-patterns/SKILL.md +0 -14
  46. package/tests/fixtures/catalog/skills/supabase-patterns/SKILL.md +0 -14
  47. package/tests/fixtures/catalog/skills/tailwind-scss-patterns/SKILL.md +0 -14
  48. package/tests/fixtures/catalog/skills/tanstack-query-router-patterns/SKILL.md +0 -14
  49. package/tests/fixtures/catalog/skills/testing-library-patterns/SKILL.md +0 -14
  50. package/tests/fixtures/catalog/skills/turbo-monorepo-patterns/SKILL.md +0 -14
  51. package/tests/fixtures/catalog/skills/typescript5-patterns/SKILL.md +0 -14
  52. package/tests/fixtures/catalog/skills/vendure-app-patterns/SKILL.md +0 -14
  53. package/tests/fixtures/catalog/skills/vendure-plugin-patterns/SKILL.md +0 -14
  54. package/tests/fixtures/catalog/skills/vite8-patterns/SKILL.md +0 -14
  55. package/tests/fixtures/catalog/skills/vitest-patterns/SKILL.md +0 -14
  56. package/tests/fixtures/catalog/skills/vue-patterns/SKILL.md +0 -14
  57. package/tests/fixtures/catalog/skills/wordpress-acf-elementor-jetengine-patterns/SKILL.md +0 -14
  58. package/tests/fixtures/catalog/skills/wordpress-bedrock-patterns/SKILL.md +0 -14
  59. package/tests/fixtures/catalog/skills/wordpress-patterns/SKILL.md +0 -14
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { Command } from "commander";
8
8
  // src/commands/apply.ts
9
9
  import path13 from "path";
10
10
  import checkbox from "@inquirer/checkbox";
11
- import fs11 from "fs-extra";
11
+ import fs12 from "fs-extra";
12
12
 
13
13
  // src/catalog/remote-catalog.ts
14
14
  import os from "os";
@@ -254,6 +254,13 @@ async function writeJson(file, value) {
254
254
  await fs2.writeFile(file, `${JSON.stringify(value, null, 2)}
255
255
  `, "utf8");
256
256
  }
257
+ async function pruneEmptyDir(dir) {
258
+ try {
259
+ const entries = await fs2.readdir(dir);
260
+ if (entries.length === 0) await fs2.remove(dir);
261
+ } catch {
262
+ }
263
+ }
257
264
  async function readText(file) {
258
265
  try {
259
266
  return await fs2.readFile(file, "utf8");
@@ -647,7 +654,7 @@ async function applyProjectSettingsMerge(root) {
647
654
 
648
655
  // src/claude/write-claude-files.ts
649
656
  import path12 from "path";
650
- import fs10 from "fs-extra";
657
+ import fs11 from "fs-extra";
651
658
 
652
659
  // src/catalog/load-catalog.ts
653
660
  import path6 from "path";
@@ -728,6 +735,22 @@ async function hashInstalledPaths(root, relPaths) {
728
735
  return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
729
736
  }
730
737
 
738
+ // src/claude/load-hooks-config.ts
739
+ import path8 from "path";
740
+ var CONFIG_PATH = ".haus-workflow/config.json";
741
+ var DEFAULT_HOOKS_CONFIG = {
742
+ hooks: {
743
+ context: { enabled: false }
744
+ }
745
+ };
746
+ async function isHookEnabled(root, key) {
747
+ const cfg = await readJson(path8.join(root, CONFIG_PATH));
748
+ return cfg?.hooks?.[key]?.enabled === true;
749
+ }
750
+
751
+ // src/claude/managed-write.ts
752
+ import fs5 from "fs-extra";
753
+
731
754
  // src/utils/diff.ts
732
755
  import { createTwoFilesPatch } from "diff";
733
756
  function hasTextChanged(before, after) {
@@ -750,21 +773,35 @@ function summarizeDiff(diffText) {
750
773
  return { additions, deletions };
751
774
  }
752
775
 
753
- // src/claude/load-hooks-config.ts
754
- import path8 from "path";
755
- var CONFIG_PATH = ".haus-workflow/config.json";
756
- var DEFAULT_HOOKS_CONFIG = {
757
- hooks: {
758
- context: { enabled: false }
776
+ // src/claude/managed-write.ts
777
+ async function writeManagedText(root, filePath, nextText, dryRun) {
778
+ const prev = await fs5.pathExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
779
+ const printable = displayPath(root, filePath);
780
+ if (dryRun) {
781
+ if (!prev) {
782
+ log(createUnifiedDiff(printable, "", nextText));
783
+ } else if (hasTextChanged(prev, nextText)) {
784
+ log(createUnifiedDiff(printable, prev, nextText));
785
+ } else {
786
+ log(`${printable}: unchanged`);
787
+ }
788
+ return;
759
789
  }
760
- };
761
- async function isHookEnabled(root, key) {
762
- const cfg = await readJson(path8.join(root, CONFIG_PATH));
763
- return cfg?.hooks?.[key]?.enabled === true;
790
+ if (hasTextChanged(prev, nextText) && prev.length > 0) {
791
+ const diffText = createUnifiedDiff(printable, prev, nextText);
792
+ const summary = summarizeDiff(diffText);
793
+ log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
794
+ }
795
+ await writeText(filePath, nextText);
796
+ }
797
+ async function writeManagedJson(root, filePath, value, dryRun) {
798
+ const nextText = `${JSON.stringify(value, null, 2)}
799
+ `;
800
+ await writeManagedText(root, filePath, nextText, dryRun);
764
801
  }
765
802
 
766
803
  // src/claude/verify-hooks-contract.ts
767
- import fs5 from "fs-extra";
804
+ import fs6 from "fs-extra";
768
805
 
769
806
  // src/claude/load-hooks.ts
770
807
  var CANONICAL_HOOKS = {
@@ -858,7 +895,7 @@ async function assertPostApplySettingsHausContract(root) {
858
895
  }
859
896
  async function verifyProjectSettingsHooksContract(root) {
860
897
  const settingsPath = claudePath(root, "settings.json");
861
- if (!await fs5.pathExists(settingsPath)) {
898
+ if (!await fs6.pathExists(settingsPath)) {
862
899
  return {
863
900
  ok: true,
864
901
  skipped: true,
@@ -886,7 +923,7 @@ async function verifyProjectSettingsHooksContract(root) {
886
923
 
887
924
  // src/claude/write-root-claude-md.ts
888
925
  import path9 from "path";
889
- import fs6 from "fs-extra";
926
+ import fs7 from "fs-extra";
890
927
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
891
928
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
892
929
  var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
@@ -898,8 +935,9 @@ ${BLOCK_END}`;
898
935
  }
899
936
  function stripHausBlock(existing) {
900
937
  const beginIdx = existing.indexOf(BLOCK_BEGIN);
901
- const endIdx = existing.indexOf(BLOCK_END);
902
- if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) return existing;
938
+ if (beginIdx === -1) return existing;
939
+ const endIdx = existing.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
940
+ if (endIdx === -1) return existing;
903
941
  const before = existing.slice(0, beginIdx);
904
942
  const after = existing.slice(endIdx + BLOCK_END.length);
905
943
  const merged = `${before}${after}`.replace(/\n{3,}/g, "\n\n").trimEnd();
@@ -908,8 +946,8 @@ function stripHausBlock(existing) {
908
946
  }
909
947
  function injectHausBlock(existing, block) {
910
948
  const beginIdx = existing.indexOf(BLOCK_BEGIN);
911
- const endIdx = existing.indexOf(BLOCK_END);
912
- if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
949
+ const endIdx = beginIdx === -1 ? -1 : existing.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
950
+ if (beginIdx !== -1 && endIdx !== -1) {
913
951
  const before = existing.slice(0, beginIdx);
914
952
  const after = existing.slice(endIdx + BLOCK_END.length);
915
953
  return `${before}${block}${after}`;
@@ -927,35 +965,19 @@ ${block}
927
965
  async function writeRootClaudeMd(root, dryRun) {
928
966
  const filePath = path9.join(root, "CLAUDE.md");
929
967
  const block = buildImportBlock();
930
- const prev = await fs6.pathExists(filePath) ? await fs6.readFile(filePath, "utf8") : "";
968
+ const prev = await fs7.pathExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
931
969
  const next = injectHausBlock(prev, block);
932
- const printable = displayPath(root, filePath);
933
- if (dryRun) {
934
- if (!prev) {
935
- log(createUnifiedDiff(printable, "", next));
936
- } else if (hasTextChanged(prev, next)) {
937
- log(createUnifiedDiff(printable, prev, next));
938
- } else {
939
- log(`${printable}: unchanged`);
940
- }
941
- return filePath;
942
- }
943
- if (hasTextChanged(prev, next) && prev.length > 0) {
944
- const diffText = createUnifiedDiff(printable, prev, next);
945
- const summary = summarizeDiff(diffText);
946
- log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
947
- }
948
- await writeText(filePath, next);
970
+ await writeManagedText(root, filePath, next, dryRun);
949
971
  return filePath;
950
972
  }
951
973
 
952
974
  // src/claude/write-workflow-config.ts
953
975
  import path11 from "path";
954
- import fs8 from "fs-extra";
976
+ import fs9 from "fs-extra";
955
977
 
956
978
  // src/claude/derive-workflow-config.ts
957
979
  import path10 from "path";
958
- import fs7 from "fs-extra";
980
+ import fs8 from "fs-extra";
959
981
  function binCmd(pm, bin, args) {
960
982
  const tail = args ? ` ${args}` : "";
961
983
  if (pm === "yarn") return `yarn ${bin}${tail}`;
@@ -974,7 +996,7 @@ async function deriveWorkflowConfig(root, ctx) {
974
996
  return null;
975
997
  };
976
998
  const hasDep = (name) => deps.has(name);
977
- const exists = (rel) => fs7.pathExistsSync(path10.join(root, rel));
999
+ const exists = (rel) => fs8.pathExistsSync(path10.join(root, rel));
978
1000
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
979
1001
  const hasCypress = hasDep("cypress");
980
1002
  const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
@@ -1043,7 +1065,7 @@ var FALLBACK_CONTEXT = {
1043
1065
  async function writeWorkflowConfig(root, dryRun, opts = {}) {
1044
1066
  const destPath = hausPath(root, "workflow-config.md");
1045
1067
  const printable = displayPath(root, destPath);
1046
- const exists = await fs8.pathExists(destPath);
1068
+ const exists = await fs9.pathExists(destPath);
1047
1069
  if (exists && !opts.refill) {
1048
1070
  if (dryRun) log(printable + ": exists (project-owned, skipping)");
1049
1071
  return null;
@@ -1055,7 +1077,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1055
1077
  };
1056
1078
  const values = await deriveWorkflowConfig(root, ctx);
1057
1079
  if (exists) {
1058
- const current = await fs8.readFile(destPath, "utf8");
1080
+ const current = await fs9.readFile(destPath, "utf8");
1059
1081
  const refilled = refillContent(current, values);
1060
1082
  if (refilled === current) {
1061
1083
  if (dryRun) log(printable + ": no blank fields to refill");
@@ -1077,7 +1099,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1077
1099
  }
1078
1100
 
1079
1101
  // src/claude/write-workflow.ts
1080
- import fs9 from "fs-extra";
1102
+ import fs10 from "fs-extra";
1081
1103
 
1082
1104
  // src/claude/managed-template.ts
1083
1105
  function normaliseLF(content2) {
@@ -1110,8 +1132,8 @@ async function writeWorkflow(root, pkgVersion, dryRun) {
1110
1132
  ${templateContent}`;
1111
1133
  const destPath = hausPath(root, "WORKFLOW.md");
1112
1134
  const printable = displayPath(root, destPath);
1113
- if (await fs9.pathExists(destPath)) {
1114
- const existing = await fs9.readFile(destPath, "utf8");
1135
+ if (await fs10.pathExists(destPath)) {
1136
+ const existing = await fs10.readFile(destPath, "utf8");
1115
1137
  const firstLine = existing.split("\n")[0] ?? "";
1116
1138
  const parsed = parseHausManagedHeader(firstLine);
1117
1139
  if (!parsed) {
@@ -1133,7 +1155,7 @@ ${templateContent}`;
1133
1155
  }
1134
1156
  }
1135
1157
  if (dryRun) {
1136
- const prev = await fs9.pathExists(destPath) ? await fs9.readFile(destPath, "utf8") : "";
1158
+ const prev = await fs10.pathExists(destPath) ? await fs10.readFile(destPath, "utf8") : "";
1137
1159
  if (!prev) {
1138
1160
  log(createUnifiedDiff(printable, "", next));
1139
1161
  } else {
@@ -1192,7 +1214,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1192
1214
  await assertPostApplySettingsHausContract(root);
1193
1215
  }
1194
1216
  const configPath = hausPath(root, "config.json");
1195
- if (!await fs10.pathExists(configPath)) {
1217
+ if (!await fs11.pathExists(configPath)) {
1196
1218
  await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
1197
1219
  }
1198
1220
  await writeManagedText(
@@ -1242,15 +1264,15 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1242
1264
  const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
1243
1265
  const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : item.type === "command" ? "commands" : "skills";
1244
1266
  const destination = claudePath(root, target, path12.basename(sourcePath));
1245
- if (await fs10.pathExists(sourcePath)) {
1267
+ if (await fs11.pathExists(sourcePath)) {
1246
1268
  if (dryRun) {
1247
- const exists = await fs10.pathExists(destination);
1269
+ const exists = await fs11.pathExists(destination);
1248
1270
  log(
1249
1271
  `${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
1250
1272
  );
1251
1273
  } else {
1252
- await fs10.ensureDir(path12.dirname(destination));
1253
- await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1274
+ await fs11.ensureDir(path12.dirname(destination));
1275
+ await fs11.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1254
1276
  }
1255
1277
  files.push(destination);
1256
1278
  const current = installedPathsByItem.get(item.id) ?? [];
@@ -1314,7 +1336,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1314
1336
  if (relPaths.length === 0) continue;
1315
1337
  const existing = [];
1316
1338
  for (const rel of relPaths) {
1317
- if (await fs10.pathExists(path12.join(root, rel))) existing.push(rel);
1339
+ if (await fs11.pathExists(path12.join(root, rel))) existing.push(rel);
1318
1340
  }
1319
1341
  if (existing.length === 0) continue;
1320
1342
  if (entry.hash === void 0) {
@@ -1336,44 +1358,12 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1336
1358
  log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
1337
1359
  continue;
1338
1360
  }
1339
- await fs10.remove(abs);
1361
+ await fs11.remove(abs);
1340
1362
  await pruneEmptyDir(path12.dirname(abs));
1341
1363
  log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
1342
1364
  }
1343
1365
  }
1344
1366
  }
1345
- async function pruneEmptyDir(dir) {
1346
- try {
1347
- const entries = await fs10.readdir(dir);
1348
- if (entries.length === 0) await fs10.remove(dir);
1349
- } catch {
1350
- }
1351
- }
1352
- async function writeManagedText(root, filePath, nextText, dryRun) {
1353
- const prev = await fs10.pathExists(filePath) ? await fs10.readFile(filePath, "utf8") : "";
1354
- const printable = displayPath(root, filePath);
1355
- if (dryRun) {
1356
- if (!prev) {
1357
- log(createUnifiedDiff(printable, "", nextText));
1358
- } else if (hasTextChanged(prev, nextText)) {
1359
- log(createUnifiedDiff(printable, prev, nextText));
1360
- } else {
1361
- log(`${printable}: unchanged`);
1362
- }
1363
- return;
1364
- }
1365
- if (hasTextChanged(prev, nextText) && prev.length > 0) {
1366
- const diffText = createUnifiedDiff(printable, prev, nextText);
1367
- const summary = summarizeDiff(diffText);
1368
- log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
1369
- }
1370
- await writeText(filePath, nextText);
1371
- }
1372
- async function writeManagedJson(root, filePath, value, dryRun) {
1373
- const nextText = `${JSON.stringify(value, null, 2)}
1374
- `;
1375
- await writeManagedText(root, filePath, nextText, dryRun);
1376
- }
1377
1367
 
1378
1368
  // src/commands/apply.ts
1379
1369
  async function cacheHasItems() {
@@ -1437,8 +1427,8 @@ async function runApply(options) {
1437
1427
  }
1438
1428
  }
1439
1429
  async function isHausProject(root) {
1440
- if (await fs11.pathExists(hausPath(root, "recommendation.json"))) return true;
1441
- if (await fs11.pathExists(claudePath(root, "settings.json"))) {
1430
+ if (await fs12.pathExists(hausPath(root, "recommendation.json"))) return true;
1431
+ if (await fs12.pathExists(claudePath(root, "settings.json"))) {
1442
1432
  const settings = await readProjectSettings(root);
1443
1433
  if (settings._haus != null) return true;
1444
1434
  }
@@ -2067,7 +2057,7 @@ function compareVersions(a, b) {
2067
2057
 
2068
2058
  // src/scanner/detect-package-manager.ts
2069
2059
  import path15 from "path";
2070
- import fs12 from "fs-extra";
2060
+ import fs13 from "fs-extra";
2071
2061
  function detectPackageManager(root, packageManagerField) {
2072
2062
  const field = String(packageManagerField ?? "").trim();
2073
2063
  if (field.startsWith("yarn@")) {
@@ -2085,9 +2075,9 @@ function detectPackageManager(root, packageManagerField) {
2085
2075
  if (satisfiesVersion(version, ">=9")) return "npm";
2086
2076
  return "unknown";
2087
2077
  }
2088
- if (fs12.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2089
- if (fs12.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2090
- if (fs12.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2078
+ if (fs13.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2079
+ if (fs13.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2080
+ if (fs13.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2091
2081
  return "unknown";
2092
2082
  }
2093
2083
 
@@ -2620,7 +2610,7 @@ async function runContext(options) {
2620
2610
 
2621
2611
  // src/commands/doctor.ts
2622
2612
  import path19 from "path";
2623
- import fs13 from "fs-extra";
2613
+ import fs14 from "fs-extra";
2624
2614
 
2625
2615
  // src/update/npm-version.ts
2626
2616
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
@@ -2728,7 +2718,7 @@ async function runDoctor(options) {
2728
2718
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
2729
2719
  const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
2730
2720
  for (const target of importTargets) {
2731
- if (!await fs13.pathExists(hausPath(root, target))) {
2721
+ if (!await fs14.pathExists(hausPath(root, target))) {
2732
2722
  flag(
2733
2723
  `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
2734
2724
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
@@ -2739,7 +2729,7 @@ async function runDoctor(options) {
2739
2729
  }
2740
2730
  }
2741
2731
  const workflowPath = hausPath(root, "WORKFLOW.md");
2742
- const workflowExists = await fs13.pathExists(workflowPath);
2732
+ const workflowExists = await fs14.pathExists(workflowPath);
2743
2733
  if (!workflowExists) {
2744
2734
  flag(
2745
2735
  "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
@@ -2761,7 +2751,7 @@ async function runDoctor(options) {
2761
2751
  "templates",
2762
2752
  "agentic-workflow-standard.md"
2763
2753
  );
2764
- const templatePath = await fs13.pathExists(cachePath) ? cachePath : bundledPath;
2754
+ const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
2765
2755
  const templateContent = await readText(templatePath);
2766
2756
  if (storedHashMatch && templateContent) {
2767
2757
  const currentHash = hashText(normaliseLF(templateContent));
@@ -2780,7 +2770,7 @@ async function runDoctor(options) {
2780
2770
  }
2781
2771
  }
2782
2772
  const workflowConfigPath = hausPath(root, "workflow-config.md");
2783
- const workflowConfigExists = await fs13.pathExists(workflowConfigPath);
2773
+ const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
2784
2774
  if (!workflowConfigExists) {
2785
2775
  flag(
2786
2776
  "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
@@ -2788,7 +2778,7 @@ async function runDoctor(options) {
2788
2778
  "haus apply --write"
2789
2779
  );
2790
2780
  } else {
2791
- const cfg = await fs13.readFile(workflowConfigPath, "utf8");
2781
+ const cfg = await fs14.readFile(workflowConfigPath, "utf8");
2792
2782
  const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
2793
2783
  if (unfilled > 0) {
2794
2784
  flag(
@@ -2963,7 +2953,7 @@ async function runGuard(kind, _options) {
2963
2953
 
2964
2954
  // src/commands/init.ts
2965
2955
  import path20 from "path";
2966
- import fs14 from "fs-extra";
2956
+ import fs15 from "fs-extra";
2967
2957
 
2968
2958
  // src/utils/prompts.ts
2969
2959
  import { stdin as input, stdout as output } from "process";
@@ -3186,7 +3176,8 @@ async function recommend(root, context) {
3186
3176
  (t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
3187
3177
  );
3188
3178
  if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
3189
- const changedMatch = changedFiles.find((f) => f.includes(item.id.split(".").pop() ?? ""));
3179
+ const idSegment = item.id.split(".").pop() ?? "";
3180
+ const changedMatch = idSegment ? changedFiles.find((f) => f.includes(idSegment)) : void 0;
3190
3181
  if (changedMatch)
3191
3182
  push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
3192
3183
  const requiresAny = item.requiresAny ?? [];
@@ -3350,7 +3341,7 @@ async function runSetupProject(options) {
3350
3341
  async function runInit(options) {
3351
3342
  const root = process.cwd();
3352
3343
  const hausDir = path20.join(root, ".haus-workflow");
3353
- const alreadyInit = await fs14.pathExists(hausDir);
3344
+ const alreadyInit = await fs15.pathExists(hausDir);
3354
3345
  if (alreadyInit) {
3355
3346
  log("Haus AI already initialized in this project.");
3356
3347
  log("Run `haus setup-project` to reconfigure.");
@@ -3363,7 +3354,7 @@ async function runInit(options) {
3363
3354
  // src/install/apply.ts
3364
3355
  import crypto2 from "crypto";
3365
3356
  import path21 from "path";
3366
- import fs15 from "fs-extra";
3357
+ import fs16 from "fs-extra";
3367
3358
 
3368
3359
  // src/install/header.ts
3369
3360
  var MD_PREFIX = "<!-- HAUS-MANAGED";
@@ -3452,7 +3443,7 @@ function hashContent(content2) {
3452
3443
  function sourceVersion() {
3453
3444
  try {
3454
3445
  const pkgPath = path21.join(packageRoot(), "package.json");
3455
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
3446
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
3456
3447
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
3457
3448
  } catch {
3458
3449
  return "haus@0.0.0";
@@ -3464,10 +3455,10 @@ function globalSrcDir() {
3464
3455
  function collectSourceFiles(srcDir, claudeDir) {
3465
3456
  const entries = [];
3466
3457
  const skillsDir = path21.join(srcDir, "skills");
3467
- if (fs15.pathExistsSync(skillsDir)) {
3468
- for (const skillName of fs15.readdirSync(skillsDir)) {
3458
+ if (fs16.pathExistsSync(skillsDir)) {
3459
+ for (const skillName of fs16.readdirSync(skillsDir)) {
3469
3460
  const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
3470
- if (fs15.pathExistsSync(skillFile)) {
3461
+ if (fs16.pathExistsSync(skillFile)) {
3471
3462
  entries.push({
3472
3463
  stableId: `skill.${skillName}`,
3473
3464
  srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
@@ -3477,8 +3468,8 @@ function collectSourceFiles(srcDir, claudeDir) {
3477
3468
  }
3478
3469
  }
3479
3470
  const commandsDir = path21.join(srcDir, "commands");
3480
- if (fs15.pathExistsSync(commandsDir)) {
3481
- for (const fileName of fs15.readdirSync(commandsDir)) {
3471
+ if (fs16.pathExistsSync(commandsDir)) {
3472
+ for (const fileName of fs16.readdirSync(commandsDir)) {
3482
3473
  if (!fileName.endsWith(".md")) continue;
3483
3474
  const commandName = fileName.slice(0, -".md".length);
3484
3475
  entries.push({
@@ -3528,7 +3519,7 @@ async function applyInstall(options = {}) {
3528
3519
  }
3529
3520
  continue;
3530
3521
  }
3531
- const destExists = fs15.pathExistsSync(entry.destPath);
3522
+ const destExists = fs16.pathExistsSync(entry.destPath);
3532
3523
  if (destExists) {
3533
3524
  const currentContent = await readText(entry.destPath);
3534
3525
  if (currentContent !== void 0) {
@@ -3575,13 +3566,13 @@ async function applyInstall(options = {}) {
3575
3566
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
3576
3567
  for (const entry of existingManifest.files) {
3577
3568
  if (currentDestPaths.has(entry.destPath)) continue;
3578
- if (!fs15.pathExistsSync(entry.destPath)) continue;
3569
+ if (!fs16.pathExistsSync(entry.destPath)) continue;
3579
3570
  const content2 = await readText(entry.destPath);
3580
3571
  if (!content2) continue;
3581
3572
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
3582
3573
  const currentHash = hashContent(content2);
3583
3574
  if (hasHeader && currentHash === entry.hash) {
3584
- if (!dryRun) await fs15.remove(entry.destPath);
3575
+ if (!dryRun) await fs16.remove(entry.destPath);
3585
3576
  result.deleted.push(entry.destPath);
3586
3577
  } else {
3587
3578
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -3709,7 +3700,7 @@ async function runScan(options) {
3709
3700
 
3710
3701
  // src/commands/undo.ts
3711
3702
  import path22 from "path";
3712
- import fs16 from "fs-extra";
3703
+ import fs17 from "fs-extra";
3713
3704
 
3714
3705
  // src/claude/managed-paths.ts
3715
3706
  var PROJECT_MANAGED_CLAUDE_REL = [
@@ -3740,25 +3731,25 @@ async function collectManagedPaths(root) {
3740
3731
  }
3741
3732
  const existing = [];
3742
3733
  for (const abs of paths) {
3743
- if (await fs16.pathExists(abs)) existing.push(abs);
3734
+ if (await fs17.pathExists(abs)) existing.push(abs);
3744
3735
  }
3745
3736
  return existing;
3746
3737
  }
3747
3738
  async function settingsHasHausContent(root) {
3748
3739
  const settingsPath = claudePath(root, "settings.json");
3749
- if (!await fs16.pathExists(settingsPath)) return false;
3740
+ if (!await fs17.pathExists(settingsPath)) return false;
3750
3741
  const settings = await readProjectSettings(root);
3751
3742
  return settings._haus != null;
3752
3743
  }
3753
3744
  async function claudeMdHasHausBlock(root) {
3754
3745
  const filePath = path22.join(root, "CLAUDE.md");
3755
- if (!await fs16.pathExists(filePath)) return false;
3756
- const text = await fs16.readFile(filePath, "utf8");
3746
+ if (!await fs17.pathExists(filePath)) return false;
3747
+ const text = await fs17.readFile(filePath, "utf8");
3757
3748
  return text.includes(BLOCK_BEGIN);
3758
3749
  }
3759
3750
  async function stripProjectSettings(root) {
3760
3751
  const settingsPath = claudePath(root, "settings.json");
3761
- if (!await fs16.pathExists(settingsPath)) return false;
3752
+ if (!await fs17.pathExists(settingsPath)) return false;
3762
3753
  let settings = await readProjectSettings(root);
3763
3754
  settings = stripHausAllow(stripHausDeny(stripHausHooks(settings)));
3764
3755
  const hasContent = Object.keys(settings).length > 0;
@@ -3767,29 +3758,29 @@ async function stripProjectSettings(root) {
3767
3758
  log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
3768
3759
  return true;
3769
3760
  }
3770
- await fs16.remove(settingsPath);
3761
+ await fs17.remove(settingsPath);
3771
3762
  log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
3772
3763
  return true;
3773
3764
  }
3774
3765
  async function stripRootClaudeMd(root) {
3775
3766
  const filePath = path22.join(root, "CLAUDE.md");
3776
- if (!await fs16.pathExists(filePath)) return false;
3777
- const prev = await fs16.readFile(filePath, "utf8");
3767
+ if (!await fs17.pathExists(filePath)) return false;
3768
+ const prev = await fs17.readFile(filePath, "utf8");
3778
3769
  if (!prev.includes(BLOCK_BEGIN)) return false;
3779
3770
  const next = stripHausBlock(prev);
3780
3771
  if (next.length === 0) {
3781
- await fs16.remove(filePath);
3772
+ await fs17.remove(filePath);
3782
3773
  log("Removed CLAUDE.md (only contained haus import block).");
3783
3774
  } else {
3784
- await fs16.writeFile(filePath, next, "utf8");
3775
+ await fs17.writeFile(filePath, next, "utf8");
3785
3776
  log("Removed haus import block from CLAUDE.md (user content preserved).");
3786
3777
  }
3787
3778
  return true;
3788
3779
  }
3789
3780
  async function pruneDirIfEmpty(dir) {
3790
- if (!await fs16.pathExists(dir)) return;
3791
- const entries = await fs16.readdir(dir);
3792
- if (entries.length === 0) await fs16.remove(dir);
3781
+ if (!await fs17.pathExists(dir)) return;
3782
+ const entries = await fs17.readdir(dir);
3783
+ if (entries.length === 0) await fs17.remove(dir);
3793
3784
  }
3794
3785
  async function runUndo(options) {
3795
3786
  const root = process.cwd();
@@ -3816,8 +3807,8 @@ User-owned .claude/ files will be preserved.`
3816
3807
  }
3817
3808
  }
3818
3809
  for (const abs of managed) {
3819
- if (!await fs16.pathExists(abs)) continue;
3820
- await fs16.remove(abs);
3810
+ if (!await fs17.pathExists(abs)) continue;
3811
+ await fs17.remove(abs);
3821
3812
  log(`Removed ${path22.relative(root, abs)}`);
3822
3813
  }
3823
3814
  if (stripSettings) await stripProjectSettings(root);
@@ -3830,7 +3821,7 @@ User-owned .claude/ files will be preserved.`
3830
3821
  // src/install/uninstall.ts
3831
3822
  import crypto3 from "crypto";
3832
3823
  import path23 from "path";
3833
- import fs17 from "fs-extra";
3824
+ import fs18 from "fs-extra";
3834
3825
  async function runUninstall(options = {}) {
3835
3826
  const { force = false } = options;
3836
3827
  const manifest = await readManifest();
@@ -3840,7 +3831,7 @@ async function runUninstall(options = {}) {
3840
3831
  return result;
3841
3832
  }
3842
3833
  for (const entry of manifest.files) {
3843
- const exists = fs17.pathExistsSync(entry.destPath);
3834
+ const exists = fs18.pathExistsSync(entry.destPath);
3844
3835
  if (!exists) continue;
3845
3836
  const content2 = await readText(entry.destPath);
3846
3837
  if (content2 === void 0) continue;
@@ -3858,8 +3849,8 @@ async function runUninstall(options = {}) {
3858
3849
  result.skipped.push(entry.destPath);
3859
3850
  continue;
3860
3851
  }
3861
- await fs17.remove(entry.destPath);
3862
- await pruneEmptyDir2(path23.dirname(entry.destPath));
3852
+ await fs18.remove(entry.destPath);
3853
+ await pruneEmptyDir(path23.dirname(entry.destPath));
3863
3854
  result.deleted.push(entry.destPath);
3864
3855
  }
3865
3856
  const settings = await readSettings();
@@ -3868,12 +3859,12 @@ async function runUninstall(options = {}) {
3868
3859
  result.hooksStripped = true;
3869
3860
  const hausDir = path23.join(globalClaudeDir(), "haus");
3870
3861
  const manifestPath2 = hausManifestPath();
3871
- if (fs17.pathExistsSync(manifestPath2)) {
3872
- await fs17.remove(manifestPath2);
3862
+ if (fs18.pathExistsSync(manifestPath2)) {
3863
+ await fs18.remove(manifestPath2);
3873
3864
  }
3874
- if (fs17.pathExistsSync(hausDir)) {
3875
- const remaining = await fs17.readdir(hausDir);
3876
- if (remaining.length === 0) await fs17.remove(hausDir);
3865
+ if (fs18.pathExistsSync(hausDir)) {
3866
+ const remaining = await fs18.readdir(hausDir);
3867
+ if (remaining.length === 0) await fs18.remove(hausDir);
3877
3868
  }
3878
3869
  return result;
3879
3870
  }
@@ -3890,13 +3881,6 @@ function printUninstallResult(result) {
3890
3881
  log("Haus hook entries removed from ~/.claude/settings.json");
3891
3882
  }
3892
3883
  }
3893
- async function pruneEmptyDir2(dir) {
3894
- try {
3895
- const entries = await fs17.readdir(dir);
3896
- if (entries.length === 0) await fs17.remove(dir);
3897
- } catch {
3898
- }
3899
- }
3900
3884
 
3901
3885
  // src/commands/uninstall.ts
3902
3886
  async function runUninstallCommand(options) {
@@ -4110,7 +4094,7 @@ async function detectGlobalInstallDrift() {
4110
4094
  }
4111
4095
 
4112
4096
  // src/commands/validate-catalog.ts
4113
- import fs18 from "fs";
4097
+ import fs19 from "fs";
4114
4098
  import path26 from "path";
4115
4099
 
4116
4100
  // src/catalog/forbidden-content.ts
@@ -4129,13 +4113,14 @@ function extractUseWhenSection(text) {
4129
4113
  function isYamlBlockScalarHeader(rest) {
4130
4114
  return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
4131
4115
  }
4132
- function extractFrontmatterDescription(text) {
4116
+ function extractFrontmatterValue(text, key) {
4133
4117
  const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
4134
4118
  if (!m) return "";
4135
4119
  const lines = m[1].split(/\r?\n/);
4136
- const idx = lines.findIndex((l) => /^description:[ \t]*/.test(l));
4120
+ const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
4121
+ const idx = lines.findIndex((l) => keyRe.test(l));
4137
4122
  if (idx < 0) return "";
4138
- const rest = lines[idx].replace(/^description:[ \t]*/, "").trim();
4123
+ const rest = lines[idx].replace(keyRe, "").trim();
4139
4124
  if (!rest) return "";
4140
4125
  if (!isYamlBlockScalarHeader(rest)) {
4141
4126
  return rest.replace(/^["']|["']$/g, "").trim();
@@ -4150,6 +4135,9 @@ function extractFrontmatterDescription(text) {
4150
4135
  }
4151
4136
  return body.join(" ").replace(/\s+/g, " ").trim();
4152
4137
  }
4138
+ function extractFrontmatterDescription(text) {
4139
+ return extractFrontmatterValue(text, "description");
4140
+ }
4153
4141
  function auditForbiddenTagsInText(text, label) {
4154
4142
  const body = `${extractFrontmatterDescription(text)}
4155
4143
  ${extractUseWhenSection(text)}`;
@@ -4226,6 +4214,15 @@ function auditManifestStructure(items) {
4226
4214
  }
4227
4215
  return failures;
4228
4216
  }
4217
+ function checkRequiredFrontmatter(text, label) {
4218
+ const failures = [];
4219
+ for (const key of REQUIRED_SKILL_FRONTMATTER) {
4220
+ if (!extractFrontmatterValue(text, key)) {
4221
+ failures.push(`${label}: missing non-empty frontmatter '${key}:'`);
4222
+ }
4223
+ }
4224
+ return failures;
4225
+ }
4229
4226
  function auditShippedFiles(manifestDir, items) {
4230
4227
  const failures = [];
4231
4228
  for (const item of items) {
@@ -4233,26 +4230,21 @@ function auditShippedFiles(manifestDir, items) {
4233
4230
  const absPath = path26.join(manifestDir, item.path);
4234
4231
  if (item.type === "skill") {
4235
4232
  const skillMd = path26.join(absPath, "SKILL.md");
4236
- if (!fs18.existsSync(skillMd)) {
4233
+ if (!fs19.existsSync(skillMd)) {
4237
4234
  failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4238
4235
  continue;
4239
4236
  }
4240
- const text = fs18.readFileSync(skillMd, "utf8");
4241
- const description = extractFrontmatterDescription(text);
4242
- for (const key of REQUIRED_SKILL_FRONTMATTER) {
4243
- if (key === "description" && !description) {
4244
- failures.push(`${item.id}: SKILL.md missing non-empty frontmatter 'description:'`);
4245
- }
4246
- }
4237
+ const text = fs19.readFileSync(skillMd, "utf8");
4238
+ failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4247
4239
  failures.push(
4248
4240
  ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4249
4241
  );
4250
4242
  } else if (item.type === "agent") {
4251
- if (!fs18.existsSync(absPath)) {
4243
+ if (!fs19.existsSync(absPath)) {
4252
4244
  failures.push(`${item.id}: missing agent file ${item.path}`);
4253
4245
  continue;
4254
4246
  }
4255
- const text = fs18.readFileSync(absPath, "utf8");
4247
+ const text = fs19.readFileSync(absPath, "utf8");
4256
4248
  if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
4257
4249
  for (const section of REQUIRED_AGENT_SECTIONS) {
4258
4250
  if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
@@ -4266,16 +4258,19 @@ function auditShippedFiles(manifestDir, items) {
4266
4258
  ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, absPath)}`)
4267
4259
  );
4268
4260
  } else if (item.type === "template") {
4269
- if (!fs18.existsSync(absPath)) {
4261
+ if (!fs19.existsSync(absPath)) {
4270
4262
  failures.push(`${item.id}: missing template file ${item.path}`);
4271
4263
  continue;
4272
4264
  }
4273
4265
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4274
4266
  } else if (item.type === "command") {
4275
- if (!fs18.existsSync(absPath)) {
4267
+ if (!fs19.existsSync(absPath)) {
4276
4268
  failures.push(`${item.id}: missing command file ${item.path}`);
4277
4269
  continue;
4278
4270
  }
4271
+ const text = fs19.readFileSync(absPath, "utf8");
4272
+ const rel = path26.relative(manifestDir, absPath);
4273
+ failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4279
4274
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4280
4275
  }
4281
4276
  }
@@ -4283,7 +4278,7 @@ function auditShippedFiles(manifestDir, items) {
4283
4278
  }
4284
4279
  function auditTemplateContent(manifestDir, absPath, itemId) {
4285
4280
  const rel = path26.relative(manifestDir, absPath);
4286
- const text = fs18.readFileSync(absPath, "utf8");
4281
+ const text = fs19.readFileSync(absPath, "utf8");
4287
4282
  const failures = [];
4288
4283
  const lines = text.split(/\r?\n/);
4289
4284
  for (let i = 0; i < lines.length; i++) {
@@ -4306,9 +4301,9 @@ function auditMarkdownContent(manifestDir) {
4306
4301
  const dirs = ["skills", "agents", "templates", "commands"];
4307
4302
  for (const dir of dirs) {
4308
4303
  const abs = path26.join(manifestDir, dir);
4309
- if (!fs18.existsSync(abs)) continue;
4304
+ if (!fs19.existsSync(abs)) continue;
4310
4305
  walkMd(abs, (file) => {
4311
- const text = fs18.readFileSync(file, "utf8");
4306
+ const text = fs19.readFileSync(file, "utf8");
4312
4307
  const rel = path26.relative(manifestDir, file);
4313
4308
  const lines = text.split(/\r?\n/);
4314
4309
  for (let i = 0; i < lines.length; i++) {
@@ -4328,7 +4323,7 @@ function auditMarkdownContent(manifestDir) {
4328
4323
  return failures;
4329
4324
  }
4330
4325
  function walkMd(dir, fn) {
4331
- for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
4326
+ for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
4332
4327
  const full = path26.join(dir, entry.name);
4333
4328
  if (entry.isDirectory()) walkMd(full, fn);
4334
4329
  else if (entry.name.endsWith(".md")) fn(full);
@@ -4731,7 +4726,7 @@ import path32 from "path";
4731
4726
 
4732
4727
  // src/claude/write-workspace-claude-md.ts
4733
4728
  import path31 from "path";
4734
- import fs19 from "fs-extra";
4729
+ import fs20 from "fs-extra";
4735
4730
  function buildWorkspaceImportBlock(client, members) {
4736
4731
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
4737
4732
  const body = [
@@ -4750,7 +4745,7 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4750
4745
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
4751
4746
  const dryRun = opts.dryRun ?? false;
4752
4747
  const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
4753
- const prev = await fs19.pathExists(filePath) ? await fs19.readFile(filePath, "utf8") : "";
4748
+ const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
4754
4749
  const next = opts.collision ? `${block}
4755
4750
  ` : injectHausBlock(prev, block);
4756
4751
  const printable = displayPath(workspaceRoot, filePath);