@haus-tech/haus-workflow 0.18.1 → 0.19.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 (61) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +320 -288
  4. package/library/catalog/manifest.json +1 -1
  5. package/library/global/commands/haus-clone.md +32 -0
  6. package/library/global/skills/haus-workflow/SKILL.md +18 -9
  7. package/package.json +4 -8
  8. package/tests/README.md +0 -54
  9. package/tests/fixtures/catalog/agents/code-reviewer.md +0 -15
  10. package/tests/fixtures/catalog/agents/docs-researcher.md +0 -15
  11. package/tests/fixtures/catalog/agents/planner.md +0 -15
  12. package/tests/fixtures/catalog/agents/security-reviewer.md +0 -15
  13. package/tests/fixtures/catalog/agents/test-reviewer.md +0 -15
  14. package/tests/fixtures/catalog/manifest.json +0 -1065
  15. package/tests/fixtures/catalog/policy-gates-manifest.json +0 -120
  16. package/tests/fixtures/catalog/skills/auth-oidc-azure-bankid-patterns/SKILL.md +0 -14
  17. package/tests/fixtures/catalog/skills/bullmq-patterns/SKILL.md +0 -14
  18. package/tests/fixtures/catalog/skills/database-patterns/SKILL.md +0 -14
  19. package/tests/fixtures/catalog/skills/dotnet-patterns/SKILL.md +0 -14
  20. package/tests/fixtures/catalog/skills/dotnet-service-patterns/SKILL.md +0 -14
  21. package/tests/fixtures/catalog/skills/eslint-setup/SKILL.md +0 -14
  22. package/tests/fixtures/catalog/skills/expo-react-native-patterns/SKILL.md +0 -14
  23. package/tests/fixtures/catalog/skills/global-engineering-rules/SKILL.md +0 -14
  24. package/tests/fixtures/catalog/skills/i18next-patterns/SKILL.md +0 -14
  25. package/tests/fixtures/catalog/skills/jest-patterns/SKILL.md +0 -14
  26. package/tests/fixtures/catalog/skills/laravel-nova-patterns/SKILL.md +0 -14
  27. package/tests/fixtures/catalog/skills/laravel-patterns/SKILL.md +0 -14
  28. package/tests/fixtures/catalog/skills/nestjs-graphql-patterns/SKILL.md +0 -14
  29. package/tests/fixtures/catalog/skills/nextauth-patterns/SKILL.md +0 -14
  30. package/tests/fixtures/catalog/skills/nextjs-patterns/SKILL.md +0 -14
  31. package/tests/fixtures/catalog/skills/nx21-monorepo-patterns/SKILL.md +0 -14
  32. package/tests/fixtures/catalog/skills/package-manager-yarn4-pnpm89/SKILL.md +0 -14
  33. package/tests/fixtures/catalog/skills/phpunit-patterns/SKILL.md +0 -14
  34. package/tests/fixtures/catalog/skills/playwright-patterns/SKILL.md +0 -14
  35. package/tests/fixtures/catalog/skills/prettier-setup/SKILL.md +0 -14
  36. package/tests/fixtures/catalog/skills/prisma-patterns/SKILL.md +0 -14
  37. package/tests/fixtures/catalog/skills/production-readiness-review/SKILL.md +0 -14
  38. package/tests/fixtures/catalog/skills/qliro-patterns/SKILL.md +0 -14
  39. package/tests/fixtures/catalog/skills/radix-shadcn-patterns/SKILL.md +0 -14
  40. package/tests/fixtures/catalog/skills/react-router-v7-patterns/SKILL.md +0 -14
  41. package/tests/fixtures/catalog/skills/react19-patterns/SKILL.md +0 -14
  42. package/tests/fixtures/catalog/skills/sanity-patterns/SKILL.md +0 -14
  43. package/tests/fixtures/catalog/skills/security-review/SKILL.md +0 -14
  44. package/tests/fixtures/catalog/skills/sentry-patterns/SKILL.md +0 -14
  45. package/tests/fixtures/catalog/skills/storybook-patterns/SKILL.md +0 -14
  46. package/tests/fixtures/catalog/skills/strapi-patterns/SKILL.md +0 -14
  47. package/tests/fixtures/catalog/skills/stripe-patterns/SKILL.md +0 -14
  48. package/tests/fixtures/catalog/skills/supabase-patterns/SKILL.md +0 -14
  49. package/tests/fixtures/catalog/skills/tailwind-scss-patterns/SKILL.md +0 -14
  50. package/tests/fixtures/catalog/skills/tanstack-query-router-patterns/SKILL.md +0 -14
  51. package/tests/fixtures/catalog/skills/testing-library-patterns/SKILL.md +0 -14
  52. package/tests/fixtures/catalog/skills/turbo-monorepo-patterns/SKILL.md +0 -14
  53. package/tests/fixtures/catalog/skills/typescript5-patterns/SKILL.md +0 -14
  54. package/tests/fixtures/catalog/skills/vendure-app-patterns/SKILL.md +0 -14
  55. package/tests/fixtures/catalog/skills/vendure-plugin-patterns/SKILL.md +0 -14
  56. package/tests/fixtures/catalog/skills/vite8-patterns/SKILL.md +0 -14
  57. package/tests/fixtures/catalog/skills/vitest-patterns/SKILL.md +0 -14
  58. package/tests/fixtures/catalog/skills/vue-patterns/SKILL.md +0 -14
  59. package/tests/fixtures/catalog/skills/wordpress-acf-elementor-jetengine-patterns/SKILL.md +0 -14
  60. package/tests/fixtures/catalog/skills/wordpress-bedrock-patterns/SKILL.md +0 -14
  61. package/tests/fixtures/catalog/skills/wordpress-patterns/SKILL.md +0 -14
package/dist/cli.js CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
- import path34 from "path";
5
+ import path35 from "path";
6
6
  import { Command } from "commander";
7
7
 
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");
@@ -547,8 +554,8 @@ function buildDenyRules() {
547
554
  for (const command of DANGEROUS_COMMANDS) {
548
555
  rules.push(`Bash(${command}:*)`);
549
556
  }
550
- for (const path35 of SENSITIVE_PATHS) {
551
- const pattern = SENSITIVE_DIRS.has(path35) ? `${path35}/**` : path35;
557
+ for (const path36 of SENSITIVE_PATHS) {
558
+ const pattern = SENSITIVE_DIRS.has(path36) ? `${path36}/**` : path36;
552
559
  for (const tool of FILE_TOOLS) {
553
560
  rules.push(`${tool}(${pattern})`);
554
561
  }
@@ -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
  }
@@ -1653,8 +1643,67 @@ async function runCatalogAudit() {
1653
1643
  log("Catalog audit passed.");
1654
1644
  }
1655
1645
 
1656
- // src/commands/config.ts
1646
+ // src/commands/clone.ts
1647
+ import { existsSync as existsSync2 } from "fs";
1657
1648
  import path14 from "path";
1649
+
1650
+ // src/utils/exec.ts
1651
+ import { execa } from "execa";
1652
+ async function runCommand(command, args = [], options = {}) {
1653
+ try {
1654
+ const result = await execa(command, args, {
1655
+ reject: false,
1656
+ // non-zero exits are returned, not thrown
1657
+ ...options
1658
+ });
1659
+ return {
1660
+ command,
1661
+ args,
1662
+ stdout: String(result.stdout ?? ""),
1663
+ stderr: String(result.stderr ?? ""),
1664
+ exitCode: result.exitCode ?? 0
1665
+ };
1666
+ } catch (error2) {
1667
+ const message = error2 instanceof Error ? error2.message : String(error2);
1668
+ throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
1669
+ }
1670
+ }
1671
+ async function runGit(args, options = {}) {
1672
+ return runCommand("git", args, options);
1673
+ }
1674
+
1675
+ // src/commands/clone.ts
1676
+ function repoNameFromUrl(url) {
1677
+ const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
1678
+ const tail = trimmed.split(/[/:]/).pop() ?? "";
1679
+ return tail || "repo";
1680
+ }
1681
+ async function runClone(url, opts = {}) {
1682
+ if (!url || !url.trim()) {
1683
+ error("A git URL is required: `haus clone <url> [dir]`.");
1684
+ process.exitCode = 1;
1685
+ return;
1686
+ }
1687
+ const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
1688
+ if (existsSync2(target)) {
1689
+ log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
1690
+ return;
1691
+ }
1692
+ if (opts.dryRun) {
1693
+ log(`would clone ${url} \u2192 ${target}`);
1694
+ return;
1695
+ }
1696
+ const res = await runGit(["clone", url, target]);
1697
+ if (res.exitCode !== 0) {
1698
+ error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
1699
+ process.exitCode = 1;
1700
+ return;
1701
+ }
1702
+ log(`\u2713 cloned ${url} \u2192 ${target}`);
1703
+ }
1704
+
1705
+ // src/commands/config.ts
1706
+ import path15 from "path";
1658
1707
  var CONFIG_PATH2 = ".haus-workflow/config.json";
1659
1708
  var HOOK_ALIASES = {
1660
1709
  "hook.context": "context"
@@ -1667,7 +1716,7 @@ async function runConfig(key, action) {
1667
1716
  );
1668
1717
  }
1669
1718
  const root = process.cwd();
1670
- const configPath = path14.join(root, CONFIG_PATH2);
1719
+ const configPath = path15.join(root, CONFIG_PATH2);
1671
1720
  const existing = await readJson(configPath);
1672
1721
  const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
1673
1722
  cfg.hooks ??= {};
@@ -2039,7 +2088,7 @@ function selectRules(recommended, task, taskIntents) {
2039
2088
 
2040
2089
  // src/scanner/scan-project.ts
2041
2090
  import { readFile as readFile2 } from "fs/promises";
2042
- import path18 from "path";
2091
+ import path19 from "path";
2043
2092
 
2044
2093
  // src/utils/audit-checks.ts
2045
2094
  function isRecord(v) {
@@ -2066,8 +2115,8 @@ function compareVersions(a, b) {
2066
2115
  }
2067
2116
 
2068
2117
  // src/scanner/detect-package-manager.ts
2069
- import path15 from "path";
2070
- import fs12 from "fs-extra";
2118
+ import path16 from "path";
2119
+ import fs13 from "fs-extra";
2071
2120
  function detectPackageManager(root, packageManagerField) {
2072
2121
  const field = String(packageManagerField ?? "").trim();
2073
2122
  if (field.startsWith("yarn@")) {
@@ -2085,9 +2134,9 @@ function detectPackageManager(root, packageManagerField) {
2085
2134
  if (satisfiesVersion(version, ">=9")) return "npm";
2086
2135
  return "unknown";
2087
2136
  }
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";
2137
+ if (fs13.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
2138
+ if (fs13.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
2139
+ if (fs13.existsSync(path16.join(root, "package-lock.json"))) return "npm";
2091
2140
  return "unknown";
2092
2141
  }
2093
2142
 
@@ -2260,7 +2309,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2260
2309
  }
2261
2310
 
2262
2311
  // src/scanner/detection.ts
2263
- import path16 from "path";
2312
+ import path17 from "path";
2264
2313
  var UNSUPPORTED_MARKERS = {
2265
2314
  "requirements.txt": "python",
2266
2315
  "pyproject.toml": "python",
@@ -2314,14 +2363,14 @@ function finalizeRoles(registryRoles, deps, files) {
2314
2363
  function collectUnsupportedSignals(files) {
2315
2364
  return [
2316
2365
  ...new Set(
2317
- files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
2366
+ files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
2318
2367
  )
2319
2368
  ].sort();
2320
2369
  }
2321
2370
 
2322
2371
  // src/scanner/render.ts
2323
2372
  import { readFile } from "fs/promises";
2324
- import path17 from "path";
2373
+ import path18 from "path";
2325
2374
 
2326
2375
  // src/scanner/role-labels.ts
2327
2376
  var ROLE_LABELS = {
@@ -2383,7 +2432,7 @@ async function buildContentBlob(root, files) {
2383
2432
  const slice = candidates.slice(0, 300);
2384
2433
  const parts = await mapWithConcurrency(slice, async (rel) => {
2385
2434
  try {
2386
- return await readFile(path17.join(root, rel), "utf8");
2435
+ return await readFile(path18.join(root, rel), "utf8");
2387
2436
  } catch {
2388
2437
  return "";
2389
2438
  }
@@ -2481,8 +2530,8 @@ var SAFE_FILES = [
2481
2530
  "Gemfile"
2482
2531
  ];
2483
2532
  async function scanProject(root, mode = "fast") {
2484
- const pkg = await readJson(path18.join(root, "package.json"));
2485
- const composer = await readJson(path18.join(root, "composer.json"));
2533
+ const pkg = await readJson(path19.join(root, "package.json"));
2534
+ const composer = await readJson(path19.join(root, "composer.json"));
2486
2535
  const files = await listFiles(root, SAFE_FILES);
2487
2536
  const safeFiles = files.filter((f) => !blocked(f));
2488
2537
  const deps = dependencySet(pkg, composer);
@@ -2516,7 +2565,7 @@ async function scanProject(root, mode = "fast") {
2516
2565
  mode,
2517
2566
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2518
2567
  root,
2519
- repoName: String(pkg?.name ?? path18.basename(root)),
2568
+ repoName: String(pkg?.name ?? path19.basename(root)),
2520
2569
  packageManager,
2521
2570
  repoRoles: roles,
2522
2571
  detectedStacks: stacks,
@@ -2534,7 +2583,7 @@ async function scanProject(root, mode = "fast") {
2534
2583
  const scanHashes = Object.fromEntries(
2535
2584
  await mapWithConcurrency(
2536
2585
  safeFiles,
2537
- async (f) => [f, hashText(await readFile2(path18.join(root, f), "utf8"))]
2586
+ async (f) => [f, hashText(await readFile2(path19.join(root, f), "utf8"))]
2538
2587
  )
2539
2588
  );
2540
2589
  const repoSummary = renderSummary(context);
@@ -2619,8 +2668,8 @@ async function runContext(options) {
2619
2668
  }
2620
2669
 
2621
2670
  // src/commands/doctor.ts
2622
- import path19 from "path";
2623
- import fs13 from "fs-extra";
2671
+ import path20 from "path";
2672
+ import fs14 from "fs-extra";
2624
2673
 
2625
2674
  // src/update/npm-version.ts
2626
2675
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
@@ -2700,7 +2749,7 @@ async function runDoctor(options) {
2700
2749
  const enabled = await isHookEnabled(root, key);
2701
2750
  ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
2702
2751
  }
2703
- const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
2752
+ const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
2704
2753
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
2705
2754
  if (!rootClaudeMdContent) {
2706
2755
  flag(
@@ -2728,7 +2777,7 @@ async function runDoctor(options) {
2728
2777
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
2729
2778
  const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
2730
2779
  for (const target of importTargets) {
2731
- if (!await fs13.pathExists(hausPath(root, target))) {
2780
+ if (!await fs14.pathExists(hausPath(root, target))) {
2732
2781
  flag(
2733
2782
  `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
2734
2783
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
@@ -2739,7 +2788,7 @@ async function runDoctor(options) {
2739
2788
  }
2740
2789
  }
2741
2790
  const workflowPath = hausPath(root, "WORKFLOW.md");
2742
- const workflowExists = await fs13.pathExists(workflowPath);
2791
+ const workflowExists = await fs14.pathExists(workflowPath);
2743
2792
  if (!workflowExists) {
2744
2793
  flag(
2745
2794
  "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
@@ -2753,15 +2802,15 @@ async function runDoctor(options) {
2753
2802
  ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
2754
2803
  } else {
2755
2804
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
2756
- const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2757
- const bundledPath = path19.join(
2805
+ const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2806
+ const bundledPath = path20.join(
2758
2807
  packageRoot(),
2759
2808
  "library",
2760
2809
  "global",
2761
2810
  "templates",
2762
2811
  "agentic-workflow-standard.md"
2763
2812
  );
2764
- const templatePath = await fs13.pathExists(cachePath) ? cachePath : bundledPath;
2813
+ const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
2765
2814
  const templateContent = await readText(templatePath);
2766
2815
  if (storedHashMatch && templateContent) {
2767
2816
  const currentHash = hashText(normaliseLF(templateContent));
@@ -2780,7 +2829,7 @@ async function runDoctor(options) {
2780
2829
  }
2781
2830
  }
2782
2831
  const workflowConfigPath = hausPath(root, "workflow-config.md");
2783
- const workflowConfigExists = await fs13.pathExists(workflowConfigPath);
2832
+ const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
2784
2833
  if (!workflowConfigExists) {
2785
2834
  flag(
2786
2835
  "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
@@ -2788,7 +2837,7 @@ async function runDoctor(options) {
2788
2837
  "haus apply --write"
2789
2838
  );
2790
2839
  } else {
2791
- const cfg = await fs13.readFile(workflowConfigPath, "utf8");
2840
+ const cfg = await fs14.readFile(workflowConfigPath, "utf8");
2792
2841
  const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
2793
2842
  if (unfilled > 0) {
2794
2843
  flag(
@@ -2819,7 +2868,7 @@ async function runDoctor(options) {
2819
2868
  ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
2820
2869
  }
2821
2870
  }
2822
- const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
2871
+ const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
2823
2872
  const currentVersion = pkgJson?.version ?? "0.0.0";
2824
2873
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
2825
2874
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -2962,8 +3011,8 @@ async function runGuard(kind, _options) {
2962
3011
  }
2963
3012
 
2964
3013
  // src/commands/init.ts
2965
- import path20 from "path";
2966
- import fs14 from "fs-extra";
3014
+ import path21 from "path";
3015
+ import fs15 from "fs-extra";
2967
3016
 
2968
3017
  // src/utils/prompts.ts
2969
3018
  import { stdin as input, stdout as output } from "process";
@@ -2983,31 +3032,6 @@ async function confirm(question) {
2983
3032
  return answer === "y" || answer === "yes";
2984
3033
  }
2985
3034
 
2986
- // src/utils/exec.ts
2987
- import { execa } from "execa";
2988
- async function runCommand(command, args = [], options = {}) {
2989
- try {
2990
- const result = await execa(command, args, {
2991
- reject: false,
2992
- // non-zero exits are returned, not thrown
2993
- ...options
2994
- });
2995
- return {
2996
- command,
2997
- args,
2998
- stdout: String(result.stdout ?? ""),
2999
- stderr: String(result.stderr ?? ""),
3000
- exitCode: result.exitCode ?? 0
3001
- };
3002
- } catch (error2) {
3003
- const message = error2 instanceof Error ? error2.message : String(error2);
3004
- throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
3005
- }
3006
- }
3007
- async function runGit(args, options = {}) {
3008
- return runCommand("git", args, options);
3009
- }
3010
-
3011
3035
  // src/recommender/git-signal.ts
3012
3036
  async function readChangedFiles(root) {
3013
3037
  if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
@@ -3186,7 +3210,8 @@ async function recommend(root, context) {
3186
3210
  (t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
3187
3211
  );
3188
3212
  if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
3189
- const changedMatch = changedFiles.find((f) => f.includes(item.id.split(".").pop() ?? ""));
3213
+ const idSegment = item.id.split(".").pop() ?? "";
3214
+ const changedMatch = idSegment ? changedFiles.find((f) => f.includes(idSegment)) : void 0;
3190
3215
  if (changedMatch)
3191
3216
  push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
3192
3217
  const requiresAny = item.requiresAny ?? [];
@@ -3349,8 +3374,8 @@ async function runSetupProject(options) {
3349
3374
  // src/commands/init.ts
3350
3375
  async function runInit(options) {
3351
3376
  const root = process.cwd();
3352
- const hausDir = path20.join(root, ".haus-workflow");
3353
- const alreadyInit = await fs14.pathExists(hausDir);
3377
+ const hausDir = path21.join(root, ".haus-workflow");
3378
+ const alreadyInit = await fs15.pathExists(hausDir);
3354
3379
  if (alreadyInit) {
3355
3380
  log("Haus AI already initialized in this project.");
3356
3381
  log("Run `haus setup-project` to reconfigure.");
@@ -3362,8 +3387,8 @@ async function runInit(options) {
3362
3387
 
3363
3388
  // src/install/apply.ts
3364
3389
  import crypto2 from "crypto";
3365
- import path21 from "path";
3366
- import fs15 from "fs-extra";
3390
+ import path22 from "path";
3391
+ import fs16 from "fs-extra";
3367
3392
 
3368
3393
  // src/install/header.ts
3369
3394
  var MD_PREFIX = "<!-- HAUS-MANAGED";
@@ -3451,40 +3476,40 @@ function hashContent(content2) {
3451
3476
  }
3452
3477
  function sourceVersion() {
3453
3478
  try {
3454
- const pkgPath = path21.join(packageRoot(), "package.json");
3455
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
3479
+ const pkgPath = path22.join(packageRoot(), "package.json");
3480
+ const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
3456
3481
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
3457
3482
  } catch {
3458
3483
  return "haus@0.0.0";
3459
3484
  }
3460
3485
  }
3461
3486
  function globalSrcDir() {
3462
- return path21.join(packageRoot(), "library", "global");
3487
+ return path22.join(packageRoot(), "library", "global");
3463
3488
  }
3464
3489
  function collectSourceFiles(srcDir, claudeDir) {
3465
3490
  const entries = [];
3466
- const skillsDir = path21.join(srcDir, "skills");
3467
- if (fs15.pathExistsSync(skillsDir)) {
3468
- for (const skillName of fs15.readdirSync(skillsDir)) {
3469
- const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
3470
- if (fs15.pathExistsSync(skillFile)) {
3491
+ const skillsDir = path22.join(srcDir, "skills");
3492
+ if (fs16.pathExistsSync(skillsDir)) {
3493
+ for (const skillName of fs16.readdirSync(skillsDir)) {
3494
+ const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
3495
+ if (fs16.pathExistsSync(skillFile)) {
3471
3496
  entries.push({
3472
3497
  stableId: `skill.${skillName}`,
3473
- srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
3474
- destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
3498
+ srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
3499
+ destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
3475
3500
  });
3476
3501
  }
3477
3502
  }
3478
3503
  }
3479
- const commandsDir = path21.join(srcDir, "commands");
3480
- if (fs15.pathExistsSync(commandsDir)) {
3481
- for (const fileName of fs15.readdirSync(commandsDir)) {
3504
+ const commandsDir = path22.join(srcDir, "commands");
3505
+ if (fs16.pathExistsSync(commandsDir)) {
3506
+ for (const fileName of fs16.readdirSync(commandsDir)) {
3482
3507
  if (!fileName.endsWith(".md")) continue;
3483
3508
  const commandName = fileName.slice(0, -".md".length);
3484
3509
  entries.push({
3485
3510
  stableId: `command.${commandName}`,
3486
- srcRelPath: path21.join("library", "global", "commands", fileName),
3487
- destPath: path21.join(claudeDir, "commands", fileName)
3511
+ srcRelPath: path22.join("library", "global", "commands", fileName),
3512
+ destPath: path22.join(claudeDir, "commands", fileName)
3488
3513
  });
3489
3514
  }
3490
3515
  }
@@ -3508,7 +3533,7 @@ async function applyInstall(options = {}) {
3508
3533
  };
3509
3534
  const manifestFiles = [];
3510
3535
  for (const entry of sourceFiles) {
3511
- const srcPath = path21.join(packageRoot(), entry.srcRelPath);
3536
+ const srcPath = path22.join(packageRoot(), entry.srcRelPath);
3512
3537
  const rawContent = await readText(srcPath);
3513
3538
  if (rawContent === void 0) {
3514
3539
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -3528,7 +3553,7 @@ async function applyInstall(options = {}) {
3528
3553
  }
3529
3554
  continue;
3530
3555
  }
3531
- const destExists = fs15.pathExistsSync(entry.destPath);
3556
+ const destExists = fs16.pathExistsSync(entry.destPath);
3532
3557
  if (destExists) {
3533
3558
  const currentContent = await readText(entry.destPath);
3534
3559
  if (currentContent !== void 0) {
@@ -3564,7 +3589,7 @@ async function applyInstall(options = {}) {
3564
3589
  schemaVersion: SCHEMA_VERSION2
3565
3590
  });
3566
3591
  }
3567
- const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
3592
+ const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
3568
3593
  const fragments = await loadHooksFragment(fragmentPath);
3569
3594
  const settings = await readSettings();
3570
3595
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -3575,13 +3600,13 @@ async function applyInstall(options = {}) {
3575
3600
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
3576
3601
  for (const entry of existingManifest.files) {
3577
3602
  if (currentDestPaths.has(entry.destPath)) continue;
3578
- if (!fs15.pathExistsSync(entry.destPath)) continue;
3603
+ if (!fs16.pathExistsSync(entry.destPath)) continue;
3579
3604
  const content2 = await readText(entry.destPath);
3580
3605
  if (!content2) continue;
3581
3606
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
3582
3607
  const currentHash = hashContent(content2);
3583
3608
  if (hasHeader && currentHash === entry.hash) {
3584
- if (!dryRun) await fs15.remove(entry.destPath);
3609
+ if (!dryRun) await fs16.remove(entry.destPath);
3585
3610
  result.deleted.push(entry.destPath);
3586
3611
  } else {
3587
3612
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -3708,8 +3733,8 @@ async function runScan(options) {
3708
3733
  }
3709
3734
 
3710
3735
  // src/commands/undo.ts
3711
- import path22 from "path";
3712
- import fs16 from "fs-extra";
3736
+ import path23 from "path";
3737
+ import fs17 from "fs-extra";
3713
3738
 
3714
3739
  // src/claude/managed-paths.ts
3715
3740
  var PROJECT_MANAGED_CLAUDE_REL = [
@@ -3735,61 +3760,61 @@ async function collectManagedPaths(root) {
3735
3760
  const lock = await readJson(hausPath(root, "haus.lock.json"));
3736
3761
  for (const row of lock ?? []) {
3737
3762
  for (const rel of row.paths ?? []) {
3738
- paths.add(path22.resolve(root, rel));
3763
+ paths.add(path23.resolve(root, rel));
3739
3764
  }
3740
3765
  }
3741
3766
  const existing = [];
3742
3767
  for (const abs of paths) {
3743
- if (await fs16.pathExists(abs)) existing.push(abs);
3768
+ if (await fs17.pathExists(abs)) existing.push(abs);
3744
3769
  }
3745
3770
  return existing;
3746
3771
  }
3747
3772
  async function settingsHasHausContent(root) {
3748
3773
  const settingsPath = claudePath(root, "settings.json");
3749
- if (!await fs16.pathExists(settingsPath)) return false;
3774
+ if (!await fs17.pathExists(settingsPath)) return false;
3750
3775
  const settings = await readProjectSettings(root);
3751
3776
  return settings._haus != null;
3752
3777
  }
3753
3778
  async function claudeMdHasHausBlock(root) {
3754
- const filePath = path22.join(root, "CLAUDE.md");
3755
- if (!await fs16.pathExists(filePath)) return false;
3756
- const text = await fs16.readFile(filePath, "utf8");
3779
+ const filePath = path23.join(root, "CLAUDE.md");
3780
+ if (!await fs17.pathExists(filePath)) return false;
3781
+ const text = await fs17.readFile(filePath, "utf8");
3757
3782
  return text.includes(BLOCK_BEGIN);
3758
3783
  }
3759
3784
  async function stripProjectSettings(root) {
3760
3785
  const settingsPath = claudePath(root, "settings.json");
3761
- if (!await fs16.pathExists(settingsPath)) return false;
3786
+ if (!await fs17.pathExists(settingsPath)) return false;
3762
3787
  let settings = await readProjectSettings(root);
3763
3788
  settings = stripHausAllow(stripHausDeny(stripHausHooks(settings)));
3764
3789
  const hasContent = Object.keys(settings).length > 0;
3765
3790
  if (hasContent) {
3766
3791
  await writeProjectSettings(root, settings);
3767
- log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
3792
+ log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
3768
3793
  return true;
3769
3794
  }
3770
- await fs16.remove(settingsPath);
3771
- log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
3795
+ await fs17.remove(settingsPath);
3796
+ log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
3772
3797
  return true;
3773
3798
  }
3774
3799
  async function stripRootClaudeMd(root) {
3775
- const filePath = path22.join(root, "CLAUDE.md");
3776
- if (!await fs16.pathExists(filePath)) return false;
3777
- const prev = await fs16.readFile(filePath, "utf8");
3800
+ const filePath = path23.join(root, "CLAUDE.md");
3801
+ if (!await fs17.pathExists(filePath)) return false;
3802
+ const prev = await fs17.readFile(filePath, "utf8");
3778
3803
  if (!prev.includes(BLOCK_BEGIN)) return false;
3779
3804
  const next = stripHausBlock(prev);
3780
3805
  if (next.length === 0) {
3781
- await fs16.remove(filePath);
3806
+ await fs17.remove(filePath);
3782
3807
  log("Removed CLAUDE.md (only contained haus import block).");
3783
3808
  } else {
3784
- await fs16.writeFile(filePath, next, "utf8");
3809
+ await fs17.writeFile(filePath, next, "utf8");
3785
3810
  log("Removed haus import block from CLAUDE.md (user content preserved).");
3786
3811
  }
3787
3812
  return true;
3788
3813
  }
3789
3814
  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);
3815
+ if (!await fs17.pathExists(dir)) return;
3816
+ const entries = await fs17.readdir(dir);
3817
+ if (entries.length === 0) await fs17.remove(dir);
3793
3818
  }
3794
3819
  async function runUndo(options) {
3795
3820
  const root = process.cwd();
@@ -3800,7 +3825,7 @@ async function runUndo(options) {
3800
3825
  log("Nothing to remove: no haus-managed files found in this directory.");
3801
3826
  return;
3802
3827
  }
3803
- const relTargets = managed.map((p) => path22.relative(root, p));
3828
+ const relTargets = managed.map((p) => path23.relative(root, p));
3804
3829
  const summaryParts = [...relTargets];
3805
3830
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
3806
3831
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -3816,9 +3841,9 @@ User-owned .claude/ files will be preserved.`
3816
3841
  }
3817
3842
  }
3818
3843
  for (const abs of managed) {
3819
- if (!await fs16.pathExists(abs)) continue;
3820
- await fs16.remove(abs);
3821
- log(`Removed ${path22.relative(root, abs)}`);
3844
+ if (!await fs17.pathExists(abs)) continue;
3845
+ await fs17.remove(abs);
3846
+ log(`Removed ${path23.relative(root, abs)}`);
3822
3847
  }
3823
3848
  if (stripSettings) await stripProjectSettings(root);
3824
3849
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -3829,8 +3854,8 @@ User-owned .claude/ files will be preserved.`
3829
3854
 
3830
3855
  // src/install/uninstall.ts
3831
3856
  import crypto3 from "crypto";
3832
- import path23 from "path";
3833
- import fs17 from "fs-extra";
3857
+ import path24 from "path";
3858
+ import fs18 from "fs-extra";
3834
3859
  async function runUninstall(options = {}) {
3835
3860
  const { force = false } = options;
3836
3861
  const manifest = await readManifest();
@@ -3840,7 +3865,7 @@ async function runUninstall(options = {}) {
3840
3865
  return result;
3841
3866
  }
3842
3867
  for (const entry of manifest.files) {
3843
- const exists = fs17.pathExistsSync(entry.destPath);
3868
+ const exists = fs18.pathExistsSync(entry.destPath);
3844
3869
  if (!exists) continue;
3845
3870
  const content2 = await readText(entry.destPath);
3846
3871
  if (content2 === void 0) continue;
@@ -3858,22 +3883,22 @@ async function runUninstall(options = {}) {
3858
3883
  result.skipped.push(entry.destPath);
3859
3884
  continue;
3860
3885
  }
3861
- await fs17.remove(entry.destPath);
3862
- await pruneEmptyDir2(path23.dirname(entry.destPath));
3886
+ await fs18.remove(entry.destPath);
3887
+ await pruneEmptyDir(path24.dirname(entry.destPath));
3863
3888
  result.deleted.push(entry.destPath);
3864
3889
  }
3865
3890
  const settings = await readSettings();
3866
3891
  const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
3867
3892
  await writeSettings(stripped);
3868
3893
  result.hooksStripped = true;
3869
- const hausDir = path23.join(globalClaudeDir(), "haus");
3894
+ const hausDir = path24.join(globalClaudeDir(), "haus");
3870
3895
  const manifestPath2 = hausManifestPath();
3871
- if (fs17.pathExistsSync(manifestPath2)) {
3872
- await fs17.remove(manifestPath2);
3896
+ if (fs18.pathExistsSync(manifestPath2)) {
3897
+ await fs18.remove(manifestPath2);
3873
3898
  }
3874
- if (fs17.pathExistsSync(hausDir)) {
3875
- const remaining = await fs17.readdir(hausDir);
3876
- if (remaining.length === 0) await fs17.remove(hausDir);
3899
+ if (fs18.pathExistsSync(hausDir)) {
3900
+ const remaining = await fs18.readdir(hausDir);
3901
+ if (remaining.length === 0) await fs18.remove(hausDir);
3877
3902
  }
3878
3903
  return result;
3879
3904
  }
@@ -3890,13 +3915,6 @@ function printUninstallResult(result) {
3890
3915
  log("Haus hook entries removed from ~/.claude/settings.json");
3891
3916
  }
3892
3917
  }
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
3918
 
3901
3919
  // src/commands/uninstall.ts
3902
3920
  async function runUninstallCommand(options) {
@@ -3911,7 +3929,7 @@ async function runUninstallCommand(options) {
3911
3929
  }
3912
3930
 
3913
3931
  // src/commands/update.ts
3914
- import path25 from "path";
3932
+ import path26 from "path";
3915
3933
 
3916
3934
  // src/update/diff-generated-files.ts
3917
3935
  function diffGeneratedFiles() {
@@ -3938,7 +3956,7 @@ function summarizeLockDiff(before, after) {
3938
3956
 
3939
3957
  // src/update/lockfile.ts
3940
3958
  import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
3941
- import path24 from "path";
3959
+ import path25 from "path";
3942
3960
  async function checkLock(root) {
3943
3961
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
3944
3962
  const hasValidVersions = lock.every(
@@ -3969,7 +3987,7 @@ async function applyLock(root) {
3969
3987
  try {
3970
3988
  const backupDir = hausPath(root, "backups");
3971
3989
  await mkdir(backupDir, { recursive: true });
3972
- await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
3990
+ await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
3973
3991
  } catch {
3974
3992
  }
3975
3993
  const enriched = await Promise.all(
@@ -3991,7 +4009,7 @@ function diffLock(before, after) {
3991
4009
  }
3992
4010
  async function hasLocalOverrides(root) {
3993
4011
  try {
3994
- await readFile3(path24.join(root, ".claude", "settings.json"), "utf8");
4012
+ await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
3995
4013
  return true;
3996
4014
  } catch {
3997
4015
  return false;
@@ -4003,7 +4021,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4003
4021
  async function runUpdate(options) {
4004
4022
  const root = process.cwd();
4005
4023
  if (options.check) {
4006
- const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
4024
+ const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
4007
4025
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4008
4026
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4009
4027
  checkLock(root),
@@ -4033,7 +4051,7 @@ async function runUpdate(options) {
4033
4051
  if (status.driftCount > 0) process.exitCode = 1;
4034
4052
  return;
4035
4053
  }
4036
- const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
4054
+ const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
4037
4055
  const currentVersion = pkgJson?.version ?? "0.0.0";
4038
4056
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4039
4057
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4110,8 +4128,8 @@ async function detectGlobalInstallDrift() {
4110
4128
  }
4111
4129
 
4112
4130
  // src/commands/validate-catalog.ts
4113
- import fs18 from "fs";
4114
- import path26 from "path";
4131
+ import fs19 from "fs";
4132
+ import path27 from "path";
4115
4133
 
4116
4134
  // src/catalog/forbidden-content.ts
4117
4135
  var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
@@ -4129,13 +4147,14 @@ function extractUseWhenSection(text) {
4129
4147
  function isYamlBlockScalarHeader(rest) {
4130
4148
  return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
4131
4149
  }
4132
- function extractFrontmatterDescription(text) {
4150
+ function extractFrontmatterValue(text, key) {
4133
4151
  const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
4134
4152
  if (!m) return "";
4135
4153
  const lines = m[1].split(/\r?\n/);
4136
- const idx = lines.findIndex((l) => /^description:[ \t]*/.test(l));
4154
+ const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
4155
+ const idx = lines.findIndex((l) => keyRe.test(l));
4137
4156
  if (idx < 0) return "";
4138
- const rest = lines[idx].replace(/^description:[ \t]*/, "").trim();
4157
+ const rest = lines[idx].replace(keyRe, "").trim();
4139
4158
  if (!rest) return "";
4140
4159
  if (!isYamlBlockScalarHeader(rest)) {
4141
4160
  return rest.replace(/^["']|["']$/g, "").trim();
@@ -4150,6 +4169,9 @@ function extractFrontmatterDescription(text) {
4150
4169
  }
4151
4170
  return body.join(" ").replace(/\s+/g, " ").trim();
4152
4171
  }
4172
+ function extractFrontmatterDescription(text) {
4173
+ return extractFrontmatterValue(text, "description");
4174
+ }
4153
4175
  function auditForbiddenTagsInText(text, label) {
4154
4176
  const body = `${extractFrontmatterDescription(text)}
4155
4177
  ${extractUseWhenSection(text)}`;
@@ -4226,33 +4248,37 @@ function auditManifestStructure(items) {
4226
4248
  }
4227
4249
  return failures;
4228
4250
  }
4251
+ function checkRequiredFrontmatter(text, label) {
4252
+ const failures = [];
4253
+ for (const key of REQUIRED_SKILL_FRONTMATTER) {
4254
+ if (!extractFrontmatterValue(text, key)) {
4255
+ failures.push(`${label}: missing non-empty frontmatter '${key}:'`);
4256
+ }
4257
+ }
4258
+ return failures;
4259
+ }
4229
4260
  function auditShippedFiles(manifestDir, items) {
4230
4261
  const failures = [];
4231
4262
  for (const item of items) {
4232
4263
  if (!item.path) continue;
4233
- const absPath = path26.join(manifestDir, item.path);
4264
+ const absPath = path27.join(manifestDir, item.path);
4234
4265
  if (item.type === "skill") {
4235
- const skillMd = path26.join(absPath, "SKILL.md");
4236
- if (!fs18.existsSync(skillMd)) {
4237
- failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4266
+ const skillMd = path27.join(absPath, "SKILL.md");
4267
+ if (!fs19.existsSync(skillMd)) {
4268
+ failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
4238
4269
  continue;
4239
4270
  }
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
- }
4271
+ const text = fs19.readFileSync(skillMd, "utf8");
4272
+ failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4247
4273
  failures.push(
4248
- ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4274
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
4249
4275
  );
4250
4276
  } else if (item.type === "agent") {
4251
- if (!fs18.existsSync(absPath)) {
4277
+ if (!fs19.existsSync(absPath)) {
4252
4278
  failures.push(`${item.id}: missing agent file ${item.path}`);
4253
4279
  continue;
4254
4280
  }
4255
- const text = fs18.readFileSync(absPath, "utf8");
4281
+ const text = fs19.readFileSync(absPath, "utf8");
4256
4282
  if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
4257
4283
  for (const section of REQUIRED_AGENT_SECTIONS) {
4258
4284
  if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
@@ -4263,27 +4289,30 @@ function auditShippedFiles(manifestDir, items) {
4263
4289
  failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
4264
4290
  }
4265
4291
  failures.push(
4266
- ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, absPath)}`)
4292
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, absPath)}`)
4267
4293
  );
4268
4294
  } else if (item.type === "template") {
4269
- if (!fs18.existsSync(absPath)) {
4295
+ if (!fs19.existsSync(absPath)) {
4270
4296
  failures.push(`${item.id}: missing template file ${item.path}`);
4271
4297
  continue;
4272
4298
  }
4273
4299
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4274
4300
  } else if (item.type === "command") {
4275
- if (!fs18.existsSync(absPath)) {
4301
+ if (!fs19.existsSync(absPath)) {
4276
4302
  failures.push(`${item.id}: missing command file ${item.path}`);
4277
4303
  continue;
4278
4304
  }
4305
+ const text = fs19.readFileSync(absPath, "utf8");
4306
+ const rel = path27.relative(manifestDir, absPath);
4307
+ failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4279
4308
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4280
4309
  }
4281
4310
  }
4282
4311
  return failures;
4283
4312
  }
4284
4313
  function auditTemplateContent(manifestDir, absPath, itemId) {
4285
- const rel = path26.relative(manifestDir, absPath);
4286
- const text = fs18.readFileSync(absPath, "utf8");
4314
+ const rel = path27.relative(manifestDir, absPath);
4315
+ const text = fs19.readFileSync(absPath, "utf8");
4287
4316
  const failures = [];
4288
4317
  const lines = text.split(/\r?\n/);
4289
4318
  for (let i = 0; i < lines.length; i++) {
@@ -4305,11 +4334,11 @@ function auditMarkdownContent(manifestDir) {
4305
4334
  const failures = [];
4306
4335
  const dirs = ["skills", "agents", "templates", "commands"];
4307
4336
  for (const dir of dirs) {
4308
- const abs = path26.join(manifestDir, dir);
4309
- if (!fs18.existsSync(abs)) continue;
4337
+ const abs = path27.join(manifestDir, dir);
4338
+ if (!fs19.existsSync(abs)) continue;
4310
4339
  walkMd(abs, (file) => {
4311
- const text = fs18.readFileSync(file, "utf8");
4312
- const rel = path26.relative(manifestDir, file);
4340
+ const text = fs19.readFileSync(file, "utf8");
4341
+ const rel = path27.relative(manifestDir, file);
4313
4342
  const lines = text.split(/\r?\n/);
4314
4343
  for (let i = 0; i < lines.length; i++) {
4315
4344
  const line2 = lines[i] ?? "";
@@ -4328,8 +4357,8 @@ function auditMarkdownContent(manifestDir) {
4328
4357
  return failures;
4329
4358
  }
4330
4359
  function walkMd(dir, fn) {
4331
- for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
4332
- const full = path26.join(dir, entry.name);
4360
+ for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
4361
+ const full = path27.join(dir, entry.name);
4333
4362
  if (entry.isDirectory()) walkMd(full, fn);
4334
4363
  else if (entry.name.endsWith(".md")) fn(full);
4335
4364
  }
@@ -4340,8 +4369,8 @@ async function runValidateCatalog(manifestPath2) {
4340
4369
  process.exitCode = 1;
4341
4370
  return;
4342
4371
  }
4343
- const abs = path26.resolve(process.cwd(), manifestPath2);
4344
- const manifestDir = path26.dirname(abs);
4372
+ const abs = path27.resolve(process.cwd(), manifestPath2);
4373
+ const manifestDir = path27.dirname(abs);
4345
4374
  const data = await readJson(abs);
4346
4375
  if (!data?.items) {
4347
4376
  error(`Could not read catalog manifest at ${abs}`);
@@ -4370,8 +4399,8 @@ async function runValidateCatalog(manifestPath2) {
4370
4399
  }
4371
4400
 
4372
4401
  // src/commands/workspace.ts
4373
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
4374
- import path33 from "path";
4402
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4403
+ import path34 from "path";
4375
4404
 
4376
4405
  // src/commands/workspace/aggregate.ts
4377
4406
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4425,7 +4454,7 @@ ${summaries.map(
4425
4454
  }
4426
4455
 
4427
4456
  // src/commands/workspace/config.ts
4428
- import path27 from "path";
4457
+ import path28 from "path";
4429
4458
  import YAML from "yaml";
4430
4459
  var WORKSPACE_FILE = "haus.workspace.yaml";
4431
4460
  function parseWorkspaceConfig(text) {
@@ -4448,11 +4477,11 @@ function parseWorkspaceConfig(text) {
4448
4477
  };
4449
4478
  }
4450
4479
  async function readWorkspaceConfig(workspaceRoot) {
4451
- return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
4480
+ return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
4452
4481
  }
4453
4482
 
4454
4483
  // src/commands/workspace/discover.ts
4455
- import path28 from "path";
4484
+ import path29 from "path";
4456
4485
  import fg3 from "fast-glob";
4457
4486
  import YAML2 from "yaml";
4458
4487
  var DEFAULT_MAX_DEPTH = 3;
@@ -4480,8 +4509,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4480
4509
  const gitDirs = /* @__PURE__ */ new Set();
4481
4510
  const manifestDirs = /* @__PURE__ */ new Set();
4482
4511
  for (const match of matches) {
4483
- const base = path28.posix.basename(match);
4484
- const dir = path28.posix.dirname(match);
4512
+ const base = path29.posix.basename(match);
4513
+ const dir = path29.posix.dirname(match);
4485
4514
  const owner = dir === "." ? "." : dir;
4486
4515
  if (base === ".git") gitDirs.add(owner);
4487
4516
  else manifestDirs.add(owner);
@@ -4497,9 +4526,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4497
4526
  }
4498
4527
  repoRoots.sort((a, b) => a.localeCompare(b));
4499
4528
  return mapWithConcurrency(repoRoots, async (relDir) => {
4500
- const absDir = path28.resolve(workspaceRoot, relDir);
4501
- const pkg = await readJson(path28.join(absDir, "package.json"));
4502
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
4529
+ const absDir = path29.resolve(workspaceRoot, relDir);
4530
+ const pkg = await readJson(path29.join(absDir, "package.json"));
4531
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
4503
4532
  let role = "auto";
4504
4533
  try {
4505
4534
  const scan = await scanProject(absDir, "fast");
@@ -4542,7 +4571,7 @@ function renderWorkspaceYaml(config2) {
4542
4571
  });
4543
4572
  }
4544
4573
  async function runDiscover(workspaceRoot, opts = {}) {
4545
- const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
4574
+ const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
4546
4575
  const existingText = await readText(yamlPath);
4547
4576
  const existing = parseWorkspaceConfig(existingText);
4548
4577
  if (existingText && !existing) {
@@ -4575,19 +4604,19 @@ async function runDiscover(workspaceRoot, opts = {}) {
4575
4604
  }
4576
4605
 
4577
4606
  // src/commands/workspace/doctor.ts
4578
- import { existsSync as existsSync2 } from "fs";
4579
- import path30 from "path";
4607
+ import { existsSync as existsSync3 } from "fs";
4608
+ import path31 from "path";
4580
4609
 
4581
4610
  // src/commands/workspace/manifest.ts
4582
4611
  import { readFileSync as readFileSync3 } from "fs";
4583
- import path29 from "path";
4612
+ import path30 from "path";
4584
4613
  var MANIFEST_FILE = "workspace.manifest.json";
4585
4614
  function manifestPath(workspaceRoot) {
4586
4615
  return hausPath(workspaceRoot, MANIFEST_FILE);
4587
4616
  }
4588
4617
  function hausVersion() {
4589
4618
  try {
4590
- const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
4619
+ const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
4591
4620
  return pkg.version ?? "0.0.0";
4592
4621
  } catch {
4593
4622
  return "0.0.0";
@@ -4653,7 +4682,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4653
4682
  }
4654
4683
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
4655
4684
  for (const repo of config2.repos) {
4656
- const repoRoot = path30.resolve(workspaceRoot, repo.path);
4685
+ const repoRoot = path31.resolve(workspaceRoot, repo.path);
4657
4686
  const entry = manifestByName.get(repo.name);
4658
4687
  if (!entry) {
4659
4688
  flag({
@@ -4678,7 +4707,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4678
4707
  detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
4679
4708
  });
4680
4709
  }
4681
- if (!existsSync2(claudePath(repoRoot))) {
4710
+ if (!existsSync3(claudePath(repoRoot))) {
4682
4711
  flag({
4683
4712
  repo: repo.name,
4684
4713
  kind: "missing-claude",
@@ -4686,7 +4715,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4686
4715
  });
4687
4716
  }
4688
4717
  const lock = await checkLock(repoRoot);
4689
- if (!existsSync2(hausPath(repoRoot, "haus.lock.json"))) {
4718
+ if (!existsSync3(hausPath(repoRoot, "haus.lock.json"))) {
4690
4719
  flag({
4691
4720
  repo: repo.name,
4692
4721
  kind: "missing-lock",
@@ -4726,12 +4755,12 @@ function emit(args) {
4726
4755
  }
4727
4756
 
4728
4757
  // src/commands/workspace/setup.ts
4729
- import { existsSync as existsSync3, statSync } from "fs";
4730
- import path32 from "path";
4758
+ import { existsSync as existsSync4, statSync } from "fs";
4759
+ import path33 from "path";
4731
4760
 
4732
4761
  // src/claude/write-workspace-claude-md.ts
4733
- import path31 from "path";
4734
- import fs19 from "fs-extra";
4762
+ import path32 from "path";
4763
+ import fs20 from "fs-extra";
4735
4764
  function buildWorkspaceImportBlock(client, members) {
4736
4765
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
4737
4766
  const body = [
@@ -4749,8 +4778,8 @@ ${BLOCK_END}`;
4749
4778
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4750
4779
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
4751
4780
  const dryRun = opts.dryRun ?? false;
4752
- 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") : "";
4781
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
4782
+ const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
4754
4783
  const next = opts.collision ? `${block}
4755
4784
  ` : injectHausBlock(prev, block);
4756
4785
  const printable = displayPath(workspaceRoot, filePath);
@@ -4775,21 +4804,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4775
4804
 
4776
4805
  // src/commands/workspace/setup.ts
4777
4806
  function resolveWorkspaceRoot(start = process.cwd()) {
4778
- let dir = path32.resolve(start);
4807
+ let dir = path33.resolve(start);
4779
4808
  for (; ; ) {
4780
- if (existsSync3(path32.join(dir, WORKSPACE_FILE))) return dir;
4781
- const parent = path32.dirname(dir);
4782
- if (parent === dir) return path32.resolve(start);
4809
+ if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
4810
+ const parent = path33.dirname(dir);
4811
+ if (parent === dir) return path33.resolve(start);
4783
4812
  dir = parent;
4784
4813
  }
4785
4814
  }
4786
4815
  function isRootRepo(workspaceRoot, repoPath) {
4787
- return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
4816
+ return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
4788
4817
  }
4789
4818
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
4790
4819
  const mode = options.mode ?? "fast";
4791
4820
  const apply = options.write ?? false;
4792
- const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
4821
+ const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
4793
4822
  if (!configText) {
4794
4823
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4795
4824
  process.exitCode = 1;
@@ -4806,11 +4835,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4806
4835
  const statuses = [];
4807
4836
  const aggregateInputs = [];
4808
4837
  for (const repo of repos) {
4809
- const repoRoot = path32.resolve(workspaceRoot, repo.path);
4838
+ const repoRoot = path33.resolve(workspaceRoot, repo.path);
4810
4839
  log(`
4811
4840
  \u2192 ${repo.name} (${repo.path})`);
4812
4841
  try {
4813
- if (!existsSync3(repoRoot) || !statSync(repoRoot).isDirectory()) {
4842
+ if (!existsSync4(repoRoot) || !statSync(repoRoot).isDirectory()) {
4814
4843
  throw new Error(`Repo path is not a directory: ${repo.path}`);
4815
4844
  }
4816
4845
  const res = await runSetupCore(repoRoot, {
@@ -4864,7 +4893,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4864
4893
  if (apply && !options.dryRun) {
4865
4894
  const statusByName = new Map(statuses.map((s) => [s.name, s]));
4866
4895
  const prior = await readManifest2(workspaceRoot);
4867
- if (!prior && existsSync3(manifestPath(workspaceRoot))) {
4896
+ if (!prior && existsSync4(manifestPath(workspaceRoot))) {
4868
4897
  warn(
4869
4898
  "Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
4870
4899
  );
@@ -4875,7 +4904,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4875
4904
  const status = statusByName.get(repo.name);
4876
4905
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
4877
4906
  if (status?.status === "ok") {
4878
- const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
4907
+ const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
4879
4908
  manifestRepos.push({
4880
4909
  name: repo.name,
4881
4910
  path: repo.path,
@@ -4955,7 +4984,7 @@ relationships: []
4955
4984
  log("Workspace initialized.");
4956
4985
  }
4957
4986
  async function scanWorkspace(workspaceRoot, opts) {
4958
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
4987
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
4959
4988
  if (!configText) {
4960
4989
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4961
4990
  process.exitCode = 1;
@@ -4976,8 +5005,8 @@ async function scanWorkspace(workspaceRoot, opts) {
4976
5005
  }
4977
5006
  const inputs = [];
4978
5007
  for (const repo of config2.repos) {
4979
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
4980
- if (!existsSync4(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5008
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5009
+ if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
4981
5010
  throw new Error(`Repo path is not a directory: ${repo.path}`);
4982
5011
  }
4983
5012
  const result = await scanProject(repoRoot, "fast");
@@ -5027,7 +5056,7 @@ async function runWorkspace(action, options = {}) {
5027
5056
  // src/cli.ts
5028
5057
  function cliVersion() {
5029
5058
  try {
5030
- const pkgPath = path34.join(packageRoot(), "package.json");
5059
+ const pkgPath = path35.join(packageRoot(), "package.json");
5031
5060
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5032
5061
  return pkg.version ?? "0.0.0";
5033
5062
  } catch {
@@ -5037,7 +5066,7 @@ function cliVersion() {
5037
5066
  var program = new Command();
5038
5067
  function validateRuntimeNodeVersion() {
5039
5068
  try {
5040
- const pkgPath = path34.join(packageRoot(), "package.json");
5069
+ const pkgPath = path35.join(packageRoot(), "package.json");
5041
5070
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5042
5071
  const requiredRange = pkg.engines?.node;
5043
5072
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -5066,6 +5095,9 @@ program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo)
5066
5095
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
5067
5096
  program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
5068
5097
  program.command("init").option("--json").action(runInit);
5098
+ program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
5099
+ (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
5100
+ );
5069
5101
  program.command("refresh").action(runRefresh);
5070
5102
  program.command("catalog-audit").action(runCatalogAudit);
5071
5103
  program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);