@haus-tech/haus-workflow 0.22.1 → 0.23.1

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.
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 path35 from "path";
5
+ import path36 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path13 from "path";
9
+ import path14 from "path";
10
10
  import checkbox from "@inquirer/checkbox";
11
- import fs12 from "fs-extra";
11
+ import fs13 from "fs-extra";
12
12
 
13
13
  // src/catalog/remote-catalog.ts
14
14
  import os from "os";
@@ -86,6 +86,8 @@ var error = (msg, ...args) => {
86
86
 
87
87
  // src/catalog/constants.ts
88
88
  var CATALOG_REPO_URL = "https://raw.githubusercontent.com/wearehaustech/haus-workflow-catalog";
89
+ var CATALOG_GITHUB_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog";
90
+ var SUPERPOWERS_SHARED_CATALOG_REL = "skills/superpowers/shared";
89
91
  var CATALOG_REF = process.env.HAUS_CATALOG_REF;
90
92
  var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
91
93
 
@@ -405,6 +407,7 @@ function getCacheDir() {
405
407
  return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path2.join(os.homedir(), CATALOG_CACHE_SUBDIR);
406
408
  }
407
409
  var cachedCatalogRef;
410
+ var cachedBlobPaths;
408
411
  function getResolvedCatalogRef() {
409
412
  return cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? "main";
410
413
  }
@@ -436,6 +439,15 @@ async function fetchText(url) {
436
439
  return null;
437
440
  }
438
441
  }
442
+ async function fetchBytes(url) {
443
+ try {
444
+ const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
445
+ if (!res.ok) return null;
446
+ return Buffer.from(await res.arrayBuffer());
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
439
451
  async function fetchRemoteManifest() {
440
452
  const base = await remoteBase();
441
453
  const text = await fetchText(`${base}/manifest.json`);
@@ -480,29 +492,246 @@ function isSafeCatalogPath(itemPath) {
480
492
  const normalized = path2.normalize(itemPath);
481
493
  return !normalized.startsWith("..") && !normalized.includes("/..");
482
494
  }
495
+ function isSafeRelativeFilePath(rel) {
496
+ if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.includes("//")) return false;
497
+ if (path2.isAbsolute(rel)) return false;
498
+ const normalized = path2.posix.normalize(rel.replace(/\\/g, "/"));
499
+ return normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../");
500
+ }
501
+ function githubApiHeaders() {
502
+ const headers = { Accept: "application/vnd.github+json" };
503
+ const auth = process.env["HAUS_GITHUB_TOKEN"] ?? process.env["GITHUB_TOKEN"];
504
+ if (auth) headers["Authorization"] = `Bearer ${auth}`;
505
+ return headers;
506
+ }
507
+ function sanitizeRelativeFilePaths(files, label) {
508
+ const safe = [];
509
+ for (const rel of files) {
510
+ if (!isSafeRelativeFilePath(rel)) {
511
+ warn(`Rejected unsafe path in ${label}: ${rel}`);
512
+ return null;
513
+ }
514
+ safe.push(rel);
515
+ }
516
+ return safe;
517
+ }
483
518
  function safeJoin(base, itemPath) {
484
519
  if (!isSafeCatalogPath(itemPath)) return null;
485
520
  const resolved = path2.resolve(base, itemPath);
486
521
  return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
487
522
  }
488
523
  var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
489
- function isExternalReference(ref) {
490
- return /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
491
- }
492
- async function downloadSkillReferences(item, destDir, base) {
493
- for (const ref of item.references ?? []) {
494
- if (isExternalReference(ref)) continue;
495
- const refDest = safeJoin(destDir, ref);
496
- if (!refDest) {
497
- warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
498
- continue;
524
+ function isMarkdownPath(rel) {
525
+ return rel.toLowerCase().endsWith(".md");
526
+ }
527
+ async function listFilesRecursive(dir, base = dir) {
528
+ const out = [];
529
+ if (!await fs2.pathExists(dir)) return out;
530
+ for (const entry of await fs2.readdir(dir, { withFileTypes: true })) {
531
+ const full = path2.join(dir, entry.name);
532
+ if (entry.isDirectory()) {
533
+ out.push(...await listFilesRecursive(full, base));
534
+ } else if (entry.isFile()) {
535
+ out.push(path2.relative(base, full).replace(/\\/g, "/"));
499
536
  }
500
- const text = await fetchText(`${base}/${item.path}/${ref}`);
501
- if (text === null) {
502
- warn(`Failed to fetch reference "${ref}" for ${item.id}`);
503
- continue;
537
+ }
538
+ return out.sort();
539
+ }
540
+ async function listMockPrefixFiles(base, prefix) {
541
+ const text = await fetchText(`${base}/__haus_tree__/${encodeURIComponent(prefix)}`);
542
+ if (text === null) return null;
543
+ try {
544
+ const parsed = JSON.parse(text);
545
+ if (!Array.isArray(parsed) || !parsed.every((e) => typeof e === "string")) return null;
546
+ return parsed;
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ async function fetchGitHubRecursiveBlobPaths(ref) {
552
+ try {
553
+ const headers = githubApiHeaders();
554
+ const commitRes = await fetch(`${CATALOG_GITHUB_API_URL}/commits/${encodeURIComponent(ref)}`, {
555
+ signal: AbortSignal.timeout(15e3),
556
+ headers
557
+ });
558
+ if (!commitRes.ok) return null;
559
+ const commit = await commitRes.json();
560
+ const treeSha = commit.commit.tree.sha;
561
+ const treeRes = await fetch(`${CATALOG_GITHUB_API_URL}/git/trees/${treeSha}?recursive=1`, {
562
+ signal: AbortSignal.timeout(3e4),
563
+ headers
564
+ });
565
+ if (!treeRes.ok) return null;
566
+ const tree = await treeRes.json();
567
+ if (tree.truncated) {
568
+ warn("Catalog GitHub tree listing was truncated \u2014 refusing partial cache sync");
569
+ return null;
570
+ }
571
+ return tree.tree.filter((e) => e.type === "blob").map((e) => e.path);
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ async function fetchCatalogBlobPaths(_base) {
577
+ if (cachedBlobPaths) return cachedBlobPaths;
578
+ if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
579
+ const ref = getResolvedCatalogRef();
580
+ const paths = await fetchGitHubRecursiveBlobPaths(ref);
581
+ if (paths) cachedBlobPaths = paths;
582
+ return paths;
583
+ }
584
+ async function listFilesUnderCatalogPrefix(prefix, base) {
585
+ const normalized = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
586
+ const prefixSlash = `${normalized}/`;
587
+ let relFiles;
588
+ if (process.env["HAUS_CATALOG_REMOTE_BASE"]) {
589
+ relFiles = await listMockPrefixFiles(base, normalized);
590
+ } else {
591
+ const blobs = await fetchCatalogBlobPaths(base);
592
+ if (!blobs) return null;
593
+ relFiles = blobs.filter((p) => p.startsWith(prefixSlash)).map((p) => p.slice(prefixSlash.length)).sort();
594
+ }
595
+ if (!relFiles) return null;
596
+ return sanitizeRelativeFilePaths(relFiles, normalized);
597
+ }
598
+ async function fetchPrefixFiles(catalogPrefix, relFiles, base, label) {
599
+ const fetched = [];
600
+ for (const rel of relFiles) {
601
+ const url = `${base}/${catalogPrefix}/${rel}`;
602
+ if (isMarkdownPath(rel)) {
603
+ const text = await fetchText(url);
604
+ if (text === null) {
605
+ warn(`Failed to fetch ${rel} for ${label}`);
606
+ return null;
607
+ }
608
+ fetched.push({ rel, kind: "text", body: text });
609
+ } else {
610
+ const bytes = await fetchBytes(url);
611
+ if (bytes === null) {
612
+ warn(`Failed to fetch ${rel} for ${label}`);
613
+ return null;
614
+ }
615
+ fetched.push({ rel, kind: "binary", body: bytes });
616
+ }
617
+ }
618
+ return fetched;
619
+ }
620
+ function validateMarkdownFiles(item, fetched) {
621
+ for (const file of fetched) {
622
+ if (file.kind !== "text" || !isMarkdownPath(file.rel)) continue;
623
+ const verdict = validateCatalogItem(item, file.body);
624
+ if (!verdict.ok) {
625
+ warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
626
+ return false;
504
627
  }
505
- await writeTextIfChanged(refDest, text);
628
+ }
629
+ return true;
630
+ }
631
+ async function directoryMatchesFetched(destDir, fetched) {
632
+ if (!await fs2.pathExists(destDir)) return false;
633
+ const existing = await listFilesRecursive(destDir);
634
+ const relSet = new Set(fetched.map((f) => f.rel));
635
+ if (existing.length !== fetched.length) return false;
636
+ for (const rel of existing) {
637
+ if (!relSet.has(rel)) return false;
638
+ }
639
+ for (const file of fetched) {
640
+ const dest = path2.join(destDir, file.rel);
641
+ if (!await fs2.pathExists(dest)) return false;
642
+ if (file.kind === "text") {
643
+ const local = await fs2.readFile(dest, "utf8");
644
+ if (local !== file.body) return false;
645
+ } else {
646
+ const local = await fs2.readFile(dest);
647
+ if (!local.equals(file.body)) return false;
648
+ }
649
+ }
650
+ return true;
651
+ }
652
+ async function writeFetchedDirectory(destDir, fetched) {
653
+ if (await fs2.pathExists(destDir)) {
654
+ await fs2.remove(destDir);
655
+ }
656
+ await fs2.ensureDir(destDir);
657
+ for (const file of fetched) {
658
+ const dest = path2.join(destDir, file.rel);
659
+ await fs2.ensureDir(path2.dirname(dest));
660
+ if (file.kind === "text") {
661
+ await fs2.writeFile(dest, file.body, "utf8");
662
+ } else {
663
+ await fs2.writeFile(dest, file.body);
664
+ }
665
+ }
666
+ }
667
+ async function syncDirectoryFromPrefix(item, catalogPrefix, destDir, base, opts) {
668
+ const relFiles = await listFilesUnderCatalogPrefix(catalogPrefix, base);
669
+ if (!relFiles) {
670
+ warn(`Failed to list files for ${item.id}`);
671
+ return "failed";
672
+ }
673
+ if (!relFiles.includes("SKILL.md") && catalogPrefix !== SUPERPOWERS_SHARED_CATALOG_REL) {
674
+ warn(`Failed to fetch content for ${item.id}: missing SKILL.md`);
675
+ return "failed";
676
+ }
677
+ if (relFiles.length === 0) {
678
+ return "unchanged";
679
+ }
680
+ const fetched = await fetchPrefixFiles(catalogPrefix, relFiles, base, item.id);
681
+ if (!fetched) return "failed";
682
+ if (opts.validateMarkdown && "type" in item) {
683
+ if (!validateMarkdownFiles(item, fetched)) return "failed";
684
+ } else if (opts.validateMarkdown) {
685
+ for (const file of fetched) {
686
+ if (file.kind !== "text" || !isMarkdownPath(file.rel)) continue;
687
+ const verdict = validateCatalogItem(
688
+ { id: item.id, type: "skill", path: catalogPrefix },
689
+ file.body
690
+ );
691
+ if (!verdict.ok) {
692
+ warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
693
+ return "failed";
694
+ }
695
+ }
696
+ }
697
+ const existed = await fs2.pathExists(destDir);
698
+ if (await directoryMatchesFetched(destDir, fetched)) {
699
+ return "unchanged";
700
+ }
701
+ await writeFetchedDirectory(destDir, fetched);
702
+ return existed ? "updated" : "created";
703
+ }
704
+ async function syncSkillDirectory(item, base) {
705
+ const destDir = safeJoin(getCacheDir(), item.path);
706
+ if (!destDir) {
707
+ warn(`Skipping ${item.id}: path traversal detected`);
708
+ return "failed";
709
+ }
710
+ try {
711
+ return await syncDirectoryFromPrefix(item, item.path, destDir, base, {
712
+ validateMarkdown: true
713
+ });
714
+ } catch (err) {
715
+ warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
716
+ return "failed";
717
+ }
718
+ }
719
+ async function syncSuperpowersShared(base) {
720
+ const relFiles = await listFilesUnderCatalogPrefix(SUPERPOWERS_SHARED_CATALOG_REL, base);
721
+ if (!relFiles || relFiles.length === 0) return "skipped";
722
+ const destDir = safeJoin(getCacheDir(), SUPERPOWERS_SHARED_CATALOG_REL);
723
+ if (!destDir) return "failed";
724
+ try {
725
+ return await syncDirectoryFromPrefix(
726
+ { id: "haus.superpowers-shared", path: SUPERPOWERS_SHARED_CATALOG_REL },
727
+ SUPERPOWERS_SHARED_CATALOG_REL,
728
+ destDir,
729
+ base,
730
+ { validateMarkdown: true }
731
+ );
732
+ } catch (err) {
733
+ warn(`Failed to cache superpowers shared: ${err instanceof Error ? err.message : String(err)}`);
734
+ return "failed";
506
735
  }
507
736
  }
508
737
  async function syncOneItem(item, base) {
@@ -518,30 +747,7 @@ async function syncOneItem(item, base) {
518
747
  return "failed";
519
748
  }
520
749
  if (item.type === "skill") {
521
- const destDir = safeJoin(getCacheDir(), item.path);
522
- if (!destDir) {
523
- warn(`Skipping ${item.id}: path traversal detected`);
524
- return "failed";
525
- }
526
- const dest2 = path2.join(destDir, "SKILL.md");
527
- const text2 = await fetchText(`${base}/${item.path}/SKILL.md`);
528
- if (!text2) {
529
- warn(`Failed to fetch content for ${item.id}`);
530
- return "failed";
531
- }
532
- const verdict2 = validateCatalogItem(item, text2);
533
- if (!verdict2.ok) {
534
- warn(`Rejected ${item.id} at ingest: ${verdict2.reason}`);
535
- return "failed";
536
- }
537
- try {
538
- const outcome = await writeTextIfChanged(dest2, text2);
539
- await downloadSkillReferences(item, destDir, base);
540
- return outcome;
541
- } catch (err) {
542
- warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
543
- return "failed";
544
- }
750
+ return syncSkillDirectory(item, base);
545
751
  }
546
752
  const dest = safeJoin(getCacheDir(), item.path);
547
753
  if (!dest) {
@@ -566,6 +772,7 @@ async function syncOneItem(item, base) {
566
772
  }
567
773
  }
568
774
  async function syncRemoteCatalog() {
775
+ cachedBlobPaths = void 0;
569
776
  const manifest = await fetchRemoteManifest();
570
777
  if (!manifest) {
571
778
  warn("Remote catalog fetch failed \u2014 using bundled catalog");
@@ -593,6 +800,10 @@ async function syncRemoteCatalog() {
593
800
  const failed = [];
594
801
  const base = await remoteBase();
595
802
  const outcomes = await mapWithConcurrency(items, (item) => syncOneItem(item, base), 8);
803
+ const sharedOutcome = await syncSuperpowersShared(base);
804
+ if (sharedOutcome === "failed") {
805
+ warn("Failed to cache superpowers shared support files");
806
+ }
596
807
  for (let i = 0; i < items.length; i++) {
597
808
  const item = items[i];
598
809
  const outcome = outcomes[i];
@@ -609,7 +820,7 @@ async function fetchLatestCatalogTag() {
609
820
  try {
610
821
  const res = await fetch(CATALOG_TAGS_API_URL, {
611
822
  signal: AbortSignal.timeout(5e3),
612
- headers: { Accept: "application/vnd.github+json" }
823
+ headers: githubApiHeaders()
613
824
  });
614
825
  if (!res.ok) return null;
615
826
  const tags = await res.json();
@@ -718,9 +929,10 @@ function mergeHooks(settings, fragments) {
718
929
  updated._haus = {
719
930
  hooks: [...existing, ...addedIds],
720
931
  hookCommands: [...existingCommands, ...addedCommands],
721
- // Preserve deny/allow tracking so hook, deny, and allow merges are order-independent.
932
+ // Preserve deny/allow/ask tracking so hook, deny, allow, and ask merges are order-independent.
722
933
  ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
723
- ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
934
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
935
+ ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
724
936
  };
725
937
  return { settings: updated, addedIds };
726
938
  }
@@ -743,7 +955,8 @@ function mergeDenyRules(settings, rules) {
743
955
  hooks: settings._haus?.hooks ?? [],
744
956
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
745
957
  denyRules: [...trackedDeny, ...addedRules],
746
- ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
958
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
959
+ ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
747
960
  };
748
961
  return { settings: updated, addedRules };
749
962
  }
@@ -766,7 +979,8 @@ function mergeAllowRules(settings, rules) {
766
979
  hooks: settings._haus?.hooks ?? [],
767
980
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
768
981
  ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
769
- allowRules: [...trackedAllow, ...addedRules]
982
+ allowRules: [...trackedAllow, ...addedRules],
983
+ ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
770
984
  };
771
985
  return { settings: updated, addedRules };
772
986
  }
@@ -783,7 +997,7 @@ function stripHausAllow(settings) {
783
997
  else delete updated.permissions;
784
998
  const haus = { ...prevHaus };
785
999
  delete haus.allowRules;
786
- const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0;
1000
+ const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
787
1001
  if (stillTracking) updated._haus = haus;
788
1002
  else delete updated._haus;
789
1003
  return updated;
@@ -801,7 +1015,7 @@ function stripHausDeny(settings) {
801
1015
  else delete updated.permissions;
802
1016
  const haus = { ...prevHaus };
803
1017
  delete haus.denyRules;
804
- const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
1018
+ const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0 || (haus.askRules?.length ?? 0) > 0;
805
1019
  if (stillTracking) updated._haus = haus;
806
1020
  else delete updated._haus;
807
1021
  return updated;
@@ -823,6 +1037,48 @@ function stripHausHooks(settings) {
823
1037
  void _;
824
1038
  return rest;
825
1039
  }
1040
+ function mergeAskRules(settings, rules) {
1041
+ const existingAsk = settings.permissions?.ask ?? [];
1042
+ const seen = new Set(existingAsk);
1043
+ const trackedAsk = settings._haus?.askRules ?? [];
1044
+ const addedRules = [];
1045
+ for (const rule of rules) {
1046
+ if (seen.has(rule)) continue;
1047
+ seen.add(rule);
1048
+ addedRules.push(rule);
1049
+ }
1050
+ const updated = { ...settings };
1051
+ updated.permissions = {
1052
+ ...settings.permissions ?? {},
1053
+ ask: [...existingAsk, ...addedRules]
1054
+ };
1055
+ updated._haus = {
1056
+ hooks: settings._haus?.hooks ?? [],
1057
+ ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
1058
+ ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
1059
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
1060
+ askRules: [...trackedAsk, ...addedRules]
1061
+ };
1062
+ return { settings: updated, addedRules };
1063
+ }
1064
+ function stripHausAsk(settings) {
1065
+ const prevHaus = settings._haus;
1066
+ if (!prevHaus?.askRules || prevHaus.askRules.length === 0) return settings;
1067
+ const ownedSet = new Set(prevHaus.askRules);
1068
+ const updated = { ...settings };
1069
+ const remainingAsk = (settings.permissions?.ask ?? []).filter((rule) => !ownedSet.has(rule));
1070
+ const permissions = { ...settings.permissions ?? {} };
1071
+ if (remainingAsk.length > 0) permissions.ask = remainingAsk;
1072
+ else delete permissions.ask;
1073
+ if (Object.keys(permissions).length > 0) updated.permissions = permissions;
1074
+ else delete updated.permissions;
1075
+ const haus = { ...prevHaus };
1076
+ delete haus.askRules;
1077
+ const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
1078
+ if (stillTracking) updated._haus = haus;
1079
+ else delete updated._haus;
1080
+ return updated;
1081
+ }
826
1082
  async function loadHooksFragment(fragmentPath) {
827
1083
  let raw;
828
1084
  try {
@@ -835,44 +1091,48 @@ async function loadHooksFragment(fragmentPath) {
835
1091
  }
836
1092
 
837
1093
  // src/security/dangerous-commands.ts
838
- var DANGEROUS_COMMANDS = [
839
- "rm -rf",
1094
+ var DENY_COMMANDS = [
840
1095
  "sudo",
841
1096
  "chmod -R 777",
842
- "chown -R",
843
1097
  "git push --force",
844
- "git reset --hard",
845
- "docker system prune",
846
1098
  "drop database",
847
1099
  "truncate table",
848
- "php artisan migrate --force",
849
1100
  "npm publish",
850
1101
  "yarn npm publish",
851
1102
  "pnpm publish"
852
1103
  ];
1104
+ var ASK_COMMANDS = [
1105
+ "rm -rf",
1106
+ "chown -R",
1107
+ "git reset --hard",
1108
+ "docker system prune",
1109
+ "php artisan migrate --force"
1110
+ ];
853
1111
 
854
1112
  // src/security/sensitive-paths.ts
855
- var SENSITIVE_PATHS = [
856
- ".env",
857
- ".env.*",
1113
+ var DENY_PATHS = [
858
1114
  "*.pem",
859
1115
  "*.key",
860
1116
  "*.p12",
861
1117
  "*.pfx",
862
1118
  "id_rsa",
863
1119
  "id_ed25519",
864
- "*.sql",
865
- "*.dump",
866
- "*.backup",
867
- "*.bak",
868
- "storage/logs",
869
- "wp-content/uploads",
870
- "uploads",
871
1120
  "customer-data",
872
- "exports",
873
1121
  "secrets",
874
1122
  "certs"
875
1123
  ];
1124
+ var DENY_DIRS = /* @__PURE__ */ new Set(["customer-data", "secrets", "certs"]);
1125
+ var ASK_PATHS = [
1126
+ { pattern: ".env", tools: ["Edit", "Write"] },
1127
+ { pattern: ".env.*", tools: ["Edit", "Write"] },
1128
+ { pattern: "storage/logs/**", tools: ["Edit", "Write"] },
1129
+ { pattern: "exports/**", tools: ["Edit", "Write"] },
1130
+ { pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
1131
+ { pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
1132
+ { pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
1133
+ { pattern: "wp-content/uploads/**", tools: ["Read", "Edit", "Write"] },
1134
+ { pattern: "uploads/**", tools: ["Read", "Edit", "Write"] }
1135
+ ];
876
1136
  var SENSITIVE_PATH_REGEXES = [
877
1137
  /^\.env(\.|$)/,
878
1138
  /(^|\/)\.env(\.|$)/,
@@ -900,24 +1160,29 @@ var SENSITIVE_ITEM_KEYWORDS = [
900
1160
  ".key"
901
1161
  ];
902
1162
 
1163
+ // src/security/ask-rules.ts
1164
+ function buildAskRules() {
1165
+ const rules = [];
1166
+ for (const command of ASK_COMMANDS) {
1167
+ rules.push(`Bash(${command}:*)`);
1168
+ }
1169
+ for (const { pattern, tools } of ASK_PATHS) {
1170
+ for (const tool of tools) {
1171
+ rules.push(`${tool}(${pattern})`);
1172
+ }
1173
+ }
1174
+ return [...new Set(rules)];
1175
+ }
1176
+
903
1177
  // src/security/deny-rules.ts
904
- var SENSITIVE_DIRS = /* @__PURE__ */ new Set([
905
- "storage/logs",
906
- "wp-content/uploads",
907
- "uploads",
908
- "customer-data",
909
- "exports",
910
- "secrets",
911
- "certs"
912
- ]);
913
1178
  var FILE_TOOLS = ["Read", "Edit", "Write"];
914
1179
  function buildDenyRules() {
915
1180
  const rules = [];
916
- for (const command of DANGEROUS_COMMANDS) {
1181
+ for (const command of DENY_COMMANDS) {
917
1182
  rules.push(`Bash(${command}:*)`);
918
1183
  }
919
- for (const path36 of SENSITIVE_PATHS) {
920
- const pattern = SENSITIVE_DIRS.has(path36) ? `${path36}/**` : path36;
1184
+ for (const path37 of DENY_PATHS) {
1185
+ const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
921
1186
  for (const tool of FILE_TOOLS) {
922
1187
  rules.push(`${tool}(${pattern})`);
923
1188
  }
@@ -1005,7 +1270,8 @@ async function mergeProjectSettings(root) {
1005
1270
  const base = await readProjectSettings(root);
1006
1271
  const { settings: withHooks } = mergeHooks(base, PROJECT_HOOK_FRAGMENTS);
1007
1272
  const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
1008
- const { settings: merged } = mergeAllowRules(withDeny, buildAllowRules());
1273
+ const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
1274
+ const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
1009
1275
  return merged;
1010
1276
  }
1011
1277
  async function applyProjectSettingsMerge(root) {
@@ -1015,8 +1281,8 @@ async function applyProjectSettingsMerge(root) {
1015
1281
  }
1016
1282
 
1017
1283
  // src/claude/write-claude-files.ts
1018
- import path12 from "path";
1019
- import fs11 from "fs-extra";
1284
+ import path13 from "path";
1285
+ import fs12 from "fs-extra";
1020
1286
 
1021
1287
  // src/catalog/load-catalog.ts
1022
1288
  import path6 from "path";
@@ -1183,8 +1449,59 @@ async function writeManagedJson(root, filePath, value, dryRun) {
1183
1449
  await writeManagedText(root, filePath, nextText, dryRun);
1184
1450
  }
1185
1451
 
1186
- // src/claude/verify-hooks-contract.ts
1452
+ // src/claude/superpowers-install.ts
1453
+ import path9 from "path";
1454
+ import fg3 from "fast-glob";
1187
1455
  import fs6 from "fs-extra";
1456
+ var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
1457
+ var PATH_REWRITES = [
1458
+ ["skills/shared/", ".claude/skills/shared/"]
1459
+ ];
1460
+ function rewriteSuperpowersMarkdown(text) {
1461
+ let out = text;
1462
+ for (const [from, to] of PATH_REWRITES) {
1463
+ out = out.split(from).join(to);
1464
+ }
1465
+ return out;
1466
+ }
1467
+ async function rewriteMarkdownTree(dir) {
1468
+ const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
1469
+ for (const rel of files) {
1470
+ const abs = path9.join(dir, rel);
1471
+ const text = await fs6.readFile(abs, "utf8");
1472
+ const rewritten = rewriteSuperpowersMarkdown(text);
1473
+ if (rewritten !== text) {
1474
+ await fs6.writeFile(abs, rewritten, "utf8");
1475
+ }
1476
+ }
1477
+ }
1478
+ async function installCatalogSkill(sourcePath, destination, opts) {
1479
+ if (opts.dryRun) return;
1480
+ await fs6.ensureDir(path9.dirname(destination));
1481
+ if (await fs6.pathExists(destination)) {
1482
+ await fs6.remove(destination);
1483
+ }
1484
+ await fs6.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1485
+ if (opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID) {
1486
+ await rewriteMarkdownTree(destination);
1487
+ }
1488
+ }
1489
+ async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
1490
+ const source = path9.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
1491
+ if (!await fs6.pathExists(source)) return null;
1492
+ const destination = claudePath(projectRoot, "skills", "shared");
1493
+ if (dryRun) return path9.relative(projectRoot, destination);
1494
+ await fs6.ensureDir(path9.dirname(destination));
1495
+ if (await fs6.pathExists(destination)) {
1496
+ await fs6.remove(destination);
1497
+ }
1498
+ await fs6.copy(source, destination, { overwrite: true, errorOnExist: false });
1499
+ await rewriteMarkdownTree(destination);
1500
+ return path9.relative(projectRoot, destination);
1501
+ }
1502
+
1503
+ // src/claude/verify-hooks-contract.ts
1504
+ import fs7 from "fs-extra";
1188
1505
 
1189
1506
  // src/claude/load-hooks.ts
1190
1507
  var CANONICAL_HOOKS = {
@@ -1212,7 +1529,7 @@ var STABLE_HOOK_IDS = {
1212
1529
  "haus guard bash --from-hook": "haus.guard-bash"
1213
1530
  };
1214
1531
  async function loadClaudeHooksSettings() {
1215
- return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
1532
+ return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
1216
1533
  }
1217
1534
  function flattenRecommendedHooks(settings) {
1218
1535
  const out = [];
@@ -1278,7 +1595,7 @@ async function assertPostApplySettingsHausContract(root) {
1278
1595
  }
1279
1596
  async function verifyProjectSettingsHooksContract(root) {
1280
1597
  const settingsPath = claudePath(root, "settings.json");
1281
- if (!await fs6.pathExists(settingsPath)) {
1598
+ if (!await fs7.pathExists(settingsPath)) {
1282
1599
  return {
1283
1600
  ok: true,
1284
1601
  skipped: true,
@@ -1305,8 +1622,8 @@ async function verifyProjectSettingsHooksContract(root) {
1305
1622
  }
1306
1623
 
1307
1624
  // src/claude/write-root-claude-md.ts
1308
- import path9 from "path";
1309
- import fs7 from "fs-extra";
1625
+ import path10 from "path";
1626
+ import fs8 from "fs-extra";
1310
1627
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
1311
1628
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
1312
1629
  var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
@@ -1346,21 +1663,21 @@ ${block}
1346
1663
  `;
1347
1664
  }
1348
1665
  async function writeRootClaudeMd(root, dryRun) {
1349
- const filePath = path9.join(root, "CLAUDE.md");
1666
+ const filePath = path10.join(root, "CLAUDE.md");
1350
1667
  const block = buildImportBlock();
1351
- const prev = await fs7.pathExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
1668
+ const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
1352
1669
  const next = injectHausBlock(prev, block);
1353
1670
  await writeManagedText(root, filePath, next, dryRun);
1354
1671
  return filePath;
1355
1672
  }
1356
1673
 
1357
1674
  // src/claude/write-workflow-config.ts
1358
- import path11 from "path";
1359
- import fs9 from "fs-extra";
1675
+ import path12 from "path";
1676
+ import fs10 from "fs-extra";
1360
1677
 
1361
1678
  // src/claude/derive-workflow-config.ts
1362
- import path10 from "path";
1363
- import fs8 from "fs-extra";
1679
+ import path11 from "path";
1680
+ import fs9 from "fs-extra";
1364
1681
  function binCmd(pm, bin, args) {
1365
1682
  const tail = args ? ` ${args}` : "";
1366
1683
  if (pm === "yarn") return `yarn ${bin}${tail}`;
@@ -1369,7 +1686,7 @@ function binCmd(pm, bin, args) {
1369
1686
  }
1370
1687
  async function deriveWorkflowConfig(root, ctx) {
1371
1688
  const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
1372
- const pkg = await readJson(path10.join(root, "package.json"));
1689
+ const pkg = await readJson(path11.join(root, "package.json"));
1373
1690
  const scripts = pkg?.scripts ?? {};
1374
1691
  const deps = new Set(ctx.dependencies);
1375
1692
  const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
@@ -1379,7 +1696,7 @@ async function deriveWorkflowConfig(root, ctx) {
1379
1696
  return null;
1380
1697
  };
1381
1698
  const hasDep = (name) => deps.has(name);
1382
- const exists = (rel) => fs8.pathExistsSync(path10.join(root, rel));
1699
+ const exists = (rel) => fs9.pathExistsSync(path11.join(root, rel));
1383
1700
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
1384
1701
  const hasCypress = hasDep("cypress");
1385
1702
  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;
@@ -1448,7 +1765,7 @@ var FALLBACK_CONTEXT = {
1448
1765
  async function writeWorkflowConfig(root, dryRun, opts = {}) {
1449
1766
  const destPath = hausPath(root, "workflow-config.md");
1450
1767
  const printable = displayPath(root, destPath);
1451
- const exists = await fs9.pathExists(destPath);
1768
+ const exists = await fs10.pathExists(destPath);
1452
1769
  if (exists && !opts.refill) {
1453
1770
  if (dryRun) log(printable + ": exists (project-owned, skipping)");
1454
1771
  return null;
@@ -1456,11 +1773,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1456
1773
  const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
1457
1774
  ...FALLBACK_CONTEXT,
1458
1775
  root,
1459
- repoName: path11.basename(root)
1776
+ repoName: path12.basename(root)
1460
1777
  };
1461
1778
  const values = await deriveWorkflowConfig(root, ctx);
1462
1779
  if (exists) {
1463
- const current = await fs9.readFile(destPath, "utf8");
1780
+ const current = await fs10.readFile(destPath, "utf8");
1464
1781
  const refilled = refillContent(current, values);
1465
1782
  if (refilled === current) {
1466
1783
  if (dryRun) log(printable + ": no blank fields to refill");
@@ -1482,7 +1799,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1482
1799
  }
1483
1800
 
1484
1801
  // src/claude/write-workflow.ts
1485
- import fs10 from "fs-extra";
1802
+ import fs11 from "fs-extra";
1486
1803
  var STABLE_ID = "template.workflow";
1487
1804
  function makeWorkflowHeader(pkgVersion, contentHash) {
1488
1805
  return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
@@ -1501,8 +1818,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
1501
1818
  ${templateContent}`;
1502
1819
  const destPath = hausPath(root, "WORKFLOW.md");
1503
1820
  const printable = displayPath(root, destPath);
1504
- if (await fs10.pathExists(destPath)) {
1505
- const existing = await fs10.readFile(destPath, "utf8");
1821
+ if (await fs11.pathExists(destPath)) {
1822
+ const existing = await fs11.readFile(destPath, "utf8");
1506
1823
  const firstLine = existing.split("\n")[0] ?? "";
1507
1824
  const parsed = parseHausManagedHeader(firstLine);
1508
1825
  if (!parsed) {
@@ -1530,7 +1847,7 @@ ${templateContent}`;
1530
1847
  }
1531
1848
  }
1532
1849
  if (dryRun) {
1533
- const prev = await fs10.pathExists(destPath) ? await fs10.readFile(destPath, "utf8") : "";
1850
+ const prev = await fs11.pathExists(destPath) ? await fs11.readFile(destPath, "utf8") : "";
1534
1851
  if (!prev) {
1535
1852
  log(createUnifiedDiff(printable, "", next));
1536
1853
  } else {
@@ -1564,13 +1881,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1564
1881
  estimatedTokenReductionPct: 0
1565
1882
  };
1566
1883
  const pkgRoot = packageRoot();
1567
- const hausVersion2 = (await readJson(path12.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1884
+ const hausVersion2 = (await readJson(path13.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1568
1885
  const coreFiles = [
1569
1886
  claudePath(root, "settings.json"),
1570
1887
  claudePath(root, "rules", "haus.md"),
1571
1888
  claudePath(root, "rules", "security.md"),
1572
- claudePath(root, "commands", "haus-doctor.md"),
1573
- claudePath(root, "commands", "haus-review.md")
1889
+ claudePath(root, "commands", "haus-doctor.md")
1574
1890
  ];
1575
1891
  const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
1576
1892
  const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force);
@@ -1596,7 +1912,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1596
1912
  await assertPostApplySettingsHausContract(root);
1597
1913
  }
1598
1914
  const configPath = hausPath(root, "config.json");
1599
- if (!await fs11.pathExists(configPath)) {
1915
+ if (!await fs12.pathExists(configPath)) {
1600
1916
  await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
1601
1917
  }
1602
1918
  await writeManagedText(
@@ -1605,12 +1921,20 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1605
1921
  "Run `haus doctor`.",
1606
1922
  dryRun
1607
1923
  );
1608
- await writeManagedText(
1609
- root,
1610
- claudePath(root, "commands", "haus-review.md"),
1611
- 'Run `haus context --task "code review"` then review diff.',
1612
- dryRun
1613
- );
1924
+ const legacyReviewPath = claudePath(root, "commands", "haus-review.md");
1925
+ if (await fs12.pathExists(legacyReviewPath)) {
1926
+ const content2 = await fs12.readFile(legacyReviewPath, "utf8");
1927
+ const stub = 'Run `haus context --task "code review"` then review diff.';
1928
+ if (content2 === stub || content2 === `${stub}
1929
+ ` || content2 === `${stub}\r
1930
+ `) {
1931
+ if (dryRun) {
1932
+ log(`[dry-run] would remove stale ${displayPath(root, legacyReviewPath)}`);
1933
+ } else {
1934
+ await fs12.remove(legacyReviewPath);
1935
+ }
1936
+ }
1937
+ }
1614
1938
  await writeManagedText(
1615
1939
  root,
1616
1940
  claudePath(root, "rules", "haus.md"),
@@ -1629,6 +1953,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1629
1953
  const installedIds = /* @__PURE__ */ new Set();
1630
1954
  const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
1631
1955
  let curatedReviewStatusSkips = 0;
1956
+ let superpowersSharedInstalled = false;
1632
1957
  for (const item of catalogItems) {
1633
1958
  const manifestItem = manifestById.get(item.id);
1634
1959
  if (!manifestItem?.path) continue;
@@ -1655,20 +1980,34 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1655
1980
  );
1656
1981
  continue;
1657
1982
  }
1658
- const destination = claudePath(root, target, path12.basename(sourcePath));
1659
- if (await fs11.pathExists(sourcePath)) {
1983
+ const destination = claudePath(root, target, path13.basename(sourcePath));
1984
+ if (await fs12.pathExists(sourcePath)) {
1660
1985
  if (dryRun) {
1661
- const exists = await fs11.pathExists(destination);
1986
+ const exists = await fs12.pathExists(destination);
1662
1987
  log(
1663
1988
  `${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
1664
1989
  );
1990
+ } else if (item.type === "skill") {
1991
+ await installCatalogSkill(sourcePath, destination, {
1992
+ originSourceId: manifestItem.originSourceId,
1993
+ dryRun: false
1994
+ });
1665
1995
  } else {
1666
- await fs11.ensureDir(path12.dirname(destination));
1667
- await fs11.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1996
+ await fs12.ensureDir(path13.dirname(destination));
1997
+ await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1668
1998
  }
1669
1999
  files.push(destination);
2000
+ const relPaths = [path13.relative(root, destination)];
2001
+ if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
2002
+ const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
2003
+ if (sharedRel) {
2004
+ superpowersSharedInstalled = true;
2005
+ relPaths.push(sharedRel);
2006
+ files.push(path13.join(root, sharedRel));
2007
+ }
2008
+ }
1670
2009
  const current = installedPathsByItem.get(item.id) ?? [];
1671
- installedPathsByItem.set(item.id, [...current, path12.relative(root, destination)]);
2010
+ installedPathsByItem.set(item.id, [...current, ...relPaths]);
1672
2011
  installedIds.add(item.id);
1673
2012
  } else {
1674
2013
  warn(
@@ -1738,7 +2077,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1738
2077
  if (relPaths.length === 0) continue;
1739
2078
  const existing = [];
1740
2079
  for (const rel of relPaths) {
1741
- if (await fs11.pathExists(path12.join(root, rel))) existing.push(rel);
2080
+ if (await fs12.pathExists(path13.join(root, rel))) existing.push(rel);
1742
2081
  }
1743
2082
  if (existing.length === 0) continue;
1744
2083
  if (entry.hash === void 0) {
@@ -1755,13 +2094,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1755
2094
  continue;
1756
2095
  }
1757
2096
  for (const rel of existing) {
1758
- const abs = path12.join(root, rel);
2097
+ const abs = path13.join(root, rel);
1759
2098
  if (dryRun) {
1760
2099
  log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
1761
2100
  continue;
1762
2101
  }
1763
- await fs11.remove(abs);
1764
- await pruneEmptyDir(path12.dirname(abs));
2102
+ await fs12.remove(abs);
2103
+ await pruneEmptyDir(path13.dirname(abs));
1765
2104
  log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
1766
2105
  }
1767
2106
  }
@@ -1769,7 +2108,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1769
2108
 
1770
2109
  // src/commands/apply.ts
1771
2110
  async function cacheHasItems() {
1772
- const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
2111
+ const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
1773
2112
  return Array.isArray(data?.items) && data.items.length > 0;
1774
2113
  }
1775
2114
  async function runApply(options) {
@@ -1835,8 +2174,8 @@ async function runApply(options) {
1835
2174
  }
1836
2175
  }
1837
2176
  async function isHausProject(root) {
1838
- if (await fs12.pathExists(hausPath(root, "recommendation.json"))) return true;
1839
- if (await fs12.pathExists(claudePath(root, "settings.json"))) {
2177
+ if (await fs13.pathExists(hausPath(root, "recommendation.json"))) return true;
2178
+ if (await fs13.pathExists(claudePath(root, "settings.json"))) {
1840
2179
  const settings = await readProjectSettings(root);
1841
2180
  if (settings._haus != null) return true;
1842
2181
  }
@@ -1874,7 +2213,7 @@ async function runCatalogAudit() {
1874
2213
 
1875
2214
  // src/commands/clone.ts
1876
2215
  import { existsSync as existsSync2 } from "fs";
1877
- import path14 from "path";
2216
+ import path15 from "path";
1878
2217
 
1879
2218
  // src/utils/exec.ts
1880
2219
  import { execa } from "execa";
@@ -1902,6 +2241,20 @@ async function runGit(args, options = {}) {
1902
2241
  }
1903
2242
 
1904
2243
  // src/commands/clone.ts
2244
+ var GIT_LOCATION_VARS = [
2245
+ "GIT_DIR",
2246
+ "GIT_WORK_TREE",
2247
+ "GIT_INDEX_FILE",
2248
+ "GIT_COMMON_DIR",
2249
+ "GIT_OBJECT_DIRECTORY",
2250
+ "GIT_NAMESPACE",
2251
+ "GIT_PREFIX"
2252
+ ];
2253
+ function cloneEnv() {
2254
+ const env = { ...process.env };
2255
+ for (const key of GIT_LOCATION_VARS) delete env[key];
2256
+ return env;
2257
+ }
1905
2258
  function repoNameFromUrl(url) {
1906
2259
  const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
1907
2260
  const tail = trimmed.split(/[/:]/).pop() ?? "";
@@ -1913,16 +2266,16 @@ async function runClone(url, opts = {}) {
1913
2266
  process.exitCode = 1;
1914
2267
  return;
1915
2268
  }
1916
- const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
2269
+ const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
1917
2270
  if (existsSync2(target)) {
1918
- log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
2271
+ log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
1919
2272
  return;
1920
2273
  }
1921
2274
  if (opts.dryRun) {
1922
2275
  log(`would clone ${url} \u2192 ${target}`);
1923
2276
  return;
1924
2277
  }
1925
- const res = await runGit(["clone", url, target]);
2278
+ const res = await runGit(["clone", url, target], { env: cloneEnv(), extendEnv: false });
1926
2279
  if (res.exitCode !== 0) {
1927
2280
  error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
1928
2281
  process.exitCode = 1;
@@ -1932,7 +2285,7 @@ async function runClone(url, opts = {}) {
1932
2285
  }
1933
2286
 
1934
2287
  // src/commands/config.ts
1935
- import path15 from "path";
2288
+ import path16 from "path";
1936
2289
  var CONFIG_PATH2 = ".haus-workflow/config.json";
1937
2290
  var HOOK_ALIASES = {
1938
2291
  "hook.context": "context"
@@ -1945,7 +2298,7 @@ async function runConfig(key, action) {
1945
2298
  );
1946
2299
  }
1947
2300
  const root = process.cwd();
1948
- const configPath = path15.join(root, CONFIG_PATH2);
2301
+ const configPath = path16.join(root, CONFIG_PATH2);
1949
2302
  const existing = await readJson(configPath);
1950
2303
  const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
1951
2304
  cfg.hooks ??= {};
@@ -2325,7 +2678,7 @@ function selectRules(recommended, task, taskIntents) {
2325
2678
 
2326
2679
  // src/scanner/scan-project.ts
2327
2680
  import { readFile as readFile2 } from "fs/promises";
2328
- import path19 from "path";
2681
+ import path20 from "path";
2329
2682
 
2330
2683
  // src/utils/audit-checks.ts
2331
2684
  function isRecord(v) {
@@ -2352,8 +2705,8 @@ function compareVersions(a, b) {
2352
2705
  }
2353
2706
 
2354
2707
  // src/scanner/detect-package-manager.ts
2355
- import path16 from "path";
2356
- import fs13 from "fs-extra";
2708
+ import path17 from "path";
2709
+ import fs14 from "fs-extra";
2357
2710
  function detectPackageManager(root, packageManagerField) {
2358
2711
  const field = String(packageManagerField ?? "").trim();
2359
2712
  if (field.startsWith("yarn@")) {
@@ -2371,9 +2724,9 @@ function detectPackageManager(root, packageManagerField) {
2371
2724
  if (satisfiesVersion(version, ">=9")) return "npm";
2372
2725
  return "unknown";
2373
2726
  }
2374
- if (fs13.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
2375
- if (fs13.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
2376
- if (fs13.existsSync(path16.join(root, "package-lock.json"))) return "npm";
2727
+ if (fs14.existsSync(path17.join(root, "yarn.lock"))) return "yarn";
2728
+ if (fs14.existsSync(path17.join(root, "pnpm-lock.yaml"))) return "pnpm";
2729
+ if (fs14.existsSync(path17.join(root, "package-lock.json"))) return "npm";
2377
2730
  return "unknown";
2378
2731
  }
2379
2732
 
@@ -2570,7 +2923,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2570
2923
  }
2571
2924
 
2572
2925
  // src/scanner/detection.ts
2573
- import path17 from "path";
2926
+ import path18 from "path";
2574
2927
  var UNSUPPORTED_MARKERS = {
2575
2928
  "requirements.txt": "python",
2576
2929
  "pyproject.toml": "python",
@@ -2624,14 +2977,14 @@ function finalizeRoles(registryRoles, deps, files) {
2624
2977
  function collectUnsupportedSignals(files) {
2625
2978
  return [
2626
2979
  ...new Set(
2627
- files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
2980
+ files.map((f) => UNSUPPORTED_MARKERS[path18.basename(f)]).filter((s) => Boolean(s))
2628
2981
  )
2629
2982
  ].sort();
2630
2983
  }
2631
2984
 
2632
2985
  // src/scanner/render.ts
2633
2986
  import { readFile } from "fs/promises";
2634
- import path18 from "path";
2987
+ import path19 from "path";
2635
2988
 
2636
2989
  // src/scanner/role-labels.ts
2637
2990
  var ROLE_LABELS = {
@@ -2693,7 +3046,7 @@ async function buildContentBlob(root, files) {
2693
3046
  const slice = candidates.slice(0, 300);
2694
3047
  const parts = await mapWithConcurrency(slice, async (rel) => {
2695
3048
  try {
2696
- return await readFile(path18.join(root, rel), "utf8");
3049
+ return await readFile(path19.join(root, rel), "utf8");
2697
3050
  } catch {
2698
3051
  return "";
2699
3052
  }
@@ -2791,8 +3144,8 @@ var SAFE_FILES = [
2791
3144
  "Gemfile"
2792
3145
  ];
2793
3146
  async function scanProject(root, mode = "fast") {
2794
- const pkg = await readJson(path19.join(root, "package.json"));
2795
- const composer = await readJson(path19.join(root, "composer.json"));
3147
+ const pkg = await readJson(path20.join(root, "package.json"));
3148
+ const composer = await readJson(path20.join(root, "composer.json"));
2796
3149
  const files = await listFiles(root, SAFE_FILES);
2797
3150
  const safeFiles = files.filter((f) => !blocked(f));
2798
3151
  const deps = dependencySet(pkg, composer);
@@ -2826,7 +3179,7 @@ async function scanProject(root, mode = "fast") {
2826
3179
  mode,
2827
3180
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2828
3181
  root,
2829
- repoName: String(pkg?.name ?? path19.basename(root)),
3182
+ repoName: String(pkg?.name ?? path20.basename(root)),
2830
3183
  packageManager,
2831
3184
  repoRoles: roles,
2832
3185
  detectedStacks: stacks,
@@ -2844,7 +3197,7 @@ async function scanProject(root, mode = "fast") {
2844
3197
  const scanHashes = Object.fromEntries(
2845
3198
  await mapWithConcurrency(
2846
3199
  safeFiles,
2847
- async (f) => [f, hashText(await readFile2(path19.join(root, f), "utf8"))]
3200
+ async (f) => [f, hashText(await readFile2(path20.join(root, f), "utf8"))]
2848
3201
  )
2849
3202
  );
2850
3203
  const repoSummary = renderSummary(context);
@@ -2929,8 +3282,8 @@ async function runContext(options) {
2929
3282
  }
2930
3283
 
2931
3284
  // src/commands/doctor.ts
2932
- import path20 from "path";
2933
- import fs14 from "fs-extra";
3285
+ import path21 from "path";
3286
+ import fs15 from "fs-extra";
2934
3287
 
2935
3288
  // src/update/npm-version.ts
2936
3289
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
@@ -3010,7 +3363,7 @@ async function runDoctor(options) {
3010
3363
  const enabled = await isHookEnabled(root, key);
3011
3364
  ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
3012
3365
  }
3013
- const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
3366
+ const rootClaudeMdPath = path21.join(root, "CLAUDE.md");
3014
3367
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
3015
3368
  if (!rootClaudeMdContent) {
3016
3369
  flag(
@@ -3038,7 +3391,7 @@ async function runDoctor(options) {
3038
3391
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
3039
3392
  const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
3040
3393
  for (const target of importTargets) {
3041
- if (!await fs14.pathExists(hausPath(root, target))) {
3394
+ if (!await fs15.pathExists(hausPath(root, target))) {
3042
3395
  flag(
3043
3396
  `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
3044
3397
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
@@ -3049,7 +3402,7 @@ async function runDoctor(options) {
3049
3402
  }
3050
3403
  }
3051
3404
  const workflowPath = hausPath(root, "WORKFLOW.md");
3052
- const workflowExists = await fs14.pathExists(workflowPath);
3405
+ const workflowExists = await fs15.pathExists(workflowPath);
3053
3406
  if (!workflowExists) {
3054
3407
  flag(
3055
3408
  "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
@@ -3072,15 +3425,15 @@ async function runDoctor(options) {
3072
3425
  "haus apply --write --force"
3073
3426
  );
3074
3427
  } else {
3075
- const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
3076
- const bundledPath = path20.join(
3428
+ const cachePath = path21.join(getCacheDir(), "templates/agentic-workflow-standard.md");
3429
+ const bundledPath = path21.join(
3077
3430
  packageRoot(),
3078
3431
  "library",
3079
3432
  "global",
3080
3433
  "templates",
3081
3434
  "agentic-workflow-standard.md"
3082
3435
  );
3083
- const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
3436
+ const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
3084
3437
  const templateContent = await readText(templatePath);
3085
3438
  if (storedHashMatch && templateContent) {
3086
3439
  const currentHash = hashText(normaliseLF(templateContent));
@@ -3100,7 +3453,7 @@ async function runDoctor(options) {
3100
3453
  }
3101
3454
  }
3102
3455
  const workflowConfigPath = hausPath(root, "workflow-config.md");
3103
- const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
3456
+ const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
3104
3457
  if (!workflowConfigExists) {
3105
3458
  flag(
3106
3459
  "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
@@ -3108,7 +3461,7 @@ async function runDoctor(options) {
3108
3461
  "haus apply --write"
3109
3462
  );
3110
3463
  } else {
3111
- const cfg = await fs14.readFile(workflowConfigPath, "utf8");
3464
+ const cfg = await fs15.readFile(workflowConfigPath, "utf8");
3112
3465
  const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
3113
3466
  if (unfilled > 0) {
3114
3467
  flag(
@@ -3139,7 +3492,7 @@ async function runDoctor(options) {
3139
3492
  ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
3140
3493
  }
3141
3494
  }
3142
- const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
3495
+ const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
3143
3496
  const currentVersion = pkgJson?.version ?? "0.0.0";
3144
3497
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3145
3498
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3225,15 +3578,16 @@ import { readFileSync as readFileSync2 } from "fs";
3225
3578
 
3226
3579
  // src/security/guard-bash.ts
3227
3580
  function guardBash(command) {
3228
- const matched = DANGEROUS_COMMANDS.find((token) => command.includes(token));
3581
+ const matched = DENY_COMMANDS.find((token) => command.includes(token));
3229
3582
  if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
3230
3583
  return void 0;
3231
3584
  }
3232
3585
 
3233
3586
  // src/security/guard-file-access.ts
3234
3587
  function guardFileAccess(candidate) {
3235
- const matched = SENSITIVE_PATHS.find((token) => candidate.includes(token.replace("*", "")));
3236
- if (matched) return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
3588
+ const matched = DENY_PATHS.find((token) => candidate.includes(token.replace(/\*/g, "")));
3589
+ if (matched)
3590
+ return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
3237
3591
  return void 0;
3238
3592
  }
3239
3593
 
@@ -3282,8 +3636,8 @@ async function runGuard(kind, _options) {
3282
3636
  }
3283
3637
 
3284
3638
  // src/commands/init.ts
3285
- import path21 from "path";
3286
- import fs15 from "fs-extra";
3639
+ import path22 from "path";
3640
+ import fs16 from "fs-extra";
3287
3641
 
3288
3642
  // src/utils/prompts.ts
3289
3643
  import { stdin as input, stdout as output } from "process";
@@ -3644,8 +3998,8 @@ async function runSetupProject(options) {
3644
3998
  // src/commands/init.ts
3645
3999
  async function runInit(options) {
3646
4000
  const root = process.cwd();
3647
- const hausDir = path21.join(root, ".haus-workflow");
3648
- const alreadyInit = await fs15.pathExists(hausDir);
4001
+ const hausDir = path22.join(root, ".haus-workflow");
4002
+ const alreadyInit = await fs16.pathExists(hausDir);
3649
4003
  if (alreadyInit) {
3650
4004
  log("Haus AI already initialized in this project.");
3651
4005
  log("Run `haus setup-project` to reconfigure.");
@@ -3657,8 +4011,8 @@ async function runInit(options) {
3657
4011
 
3658
4012
  // src/install/apply.ts
3659
4013
  import crypto2 from "crypto";
3660
- import path22 from "path";
3661
- import fs16 from "fs-extra";
4014
+ import path23 from "path";
4015
+ import fs17 from "fs-extra";
3662
4016
 
3663
4017
  // src/install/header.ts
3664
4018
  var MD_PREFIX = "<!-- HAUS-MANAGED";
@@ -3746,40 +4100,40 @@ function hashContent(content2) {
3746
4100
  }
3747
4101
  function sourceVersion() {
3748
4102
  try {
3749
- const pkgPath = path22.join(packageRoot(), "package.json");
3750
- const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
4103
+ const pkgPath = path23.join(packageRoot(), "package.json");
4104
+ const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
3751
4105
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
3752
4106
  } catch {
3753
4107
  return "haus@0.0.0";
3754
4108
  }
3755
4109
  }
3756
4110
  function globalSrcDir() {
3757
- return path22.join(packageRoot(), "library", "global");
4111
+ return path23.join(packageRoot(), "library", "global");
3758
4112
  }
3759
4113
  function collectSourceFiles(srcDir, claudeDir) {
3760
4114
  const entries = [];
3761
- const skillsDir = path22.join(srcDir, "skills");
3762
- if (fs16.pathExistsSync(skillsDir)) {
3763
- for (const skillName of fs16.readdirSync(skillsDir)) {
3764
- const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
3765
- if (fs16.pathExistsSync(skillFile)) {
4115
+ const skillsDir = path23.join(srcDir, "skills");
4116
+ if (fs17.pathExistsSync(skillsDir)) {
4117
+ for (const skillName of fs17.readdirSync(skillsDir)) {
4118
+ const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
4119
+ if (fs17.pathExistsSync(skillFile)) {
3766
4120
  entries.push({
3767
4121
  stableId: `skill.${skillName}`,
3768
- srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
3769
- destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
4122
+ srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
4123
+ destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
3770
4124
  });
3771
4125
  }
3772
4126
  }
3773
4127
  }
3774
- const commandsDir = path22.join(srcDir, "commands");
3775
- if (fs16.pathExistsSync(commandsDir)) {
3776
- for (const fileName of fs16.readdirSync(commandsDir)) {
4128
+ const commandsDir = path23.join(srcDir, "commands");
4129
+ if (fs17.pathExistsSync(commandsDir)) {
4130
+ for (const fileName of fs17.readdirSync(commandsDir)) {
3777
4131
  if (!fileName.endsWith(".md")) continue;
3778
4132
  const commandName = fileName.slice(0, -".md".length);
3779
4133
  entries.push({
3780
4134
  stableId: `command.${commandName}`,
3781
- srcRelPath: path22.join("library", "global", "commands", fileName),
3782
- destPath: path22.join(claudeDir, "commands", fileName)
4135
+ srcRelPath: path23.join("library", "global", "commands", fileName),
4136
+ destPath: path23.join(claudeDir, "commands", fileName)
3783
4137
  });
3784
4138
  }
3785
4139
  }
@@ -3803,7 +4157,7 @@ async function applyInstall(options = {}) {
3803
4157
  };
3804
4158
  const manifestFiles = [];
3805
4159
  for (const entry of sourceFiles) {
3806
- const srcPath = path22.join(packageRoot(), entry.srcRelPath);
4160
+ const srcPath = path23.join(packageRoot(), entry.srcRelPath);
3807
4161
  const rawContent = await readText(srcPath);
3808
4162
  if (rawContent === void 0) {
3809
4163
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -3823,7 +4177,7 @@ async function applyInstall(options = {}) {
3823
4177
  }
3824
4178
  continue;
3825
4179
  }
3826
- const destExists = fs16.pathExistsSync(entry.destPath);
4180
+ const destExists = fs17.pathExistsSync(entry.destPath);
3827
4181
  if (destExists) {
3828
4182
  const currentContent = await readText(entry.destPath);
3829
4183
  if (currentContent !== void 0) {
@@ -3859,24 +4213,25 @@ async function applyInstall(options = {}) {
3859
4213
  schemaVersion: SCHEMA_VERSION2
3860
4214
  });
3861
4215
  }
3862
- const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
4216
+ const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
3863
4217
  const fragments = await loadHooksFragment(fragmentPath);
3864
4218
  const settings = await readSettings();
3865
4219
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
3866
4220
  const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
3867
- const { settings: mergedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
4221
+ const { settings: allowedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
4222
+ const { settings: mergedSettings } = mergeAskRules(allowedSettings, buildAskRules());
3868
4223
  result.hookIds = addedIds;
3869
4224
  if (!check && existingManifest) {
3870
4225
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
3871
4226
  for (const entry of existingManifest.files) {
3872
4227
  if (currentDestPaths.has(entry.destPath)) continue;
3873
- if (!fs16.pathExistsSync(entry.destPath)) continue;
4228
+ if (!fs17.pathExistsSync(entry.destPath)) continue;
3874
4229
  const content2 = await readText(entry.destPath);
3875
4230
  if (!content2) continue;
3876
4231
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
3877
4232
  const currentHash = hashContent(content2);
3878
4233
  if (hasHeader && currentHash === entry.hash) {
3879
- if (!dryRun) await fs16.remove(entry.destPath);
4234
+ if (!dryRun) await fs17.remove(entry.destPath);
3880
4235
  result.deleted.push(entry.destPath);
3881
4236
  } else {
3882
4237
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -4003,15 +4358,14 @@ async function runScan(options) {
4003
4358
  }
4004
4359
 
4005
4360
  // src/commands/undo.ts
4006
- import path23 from "path";
4007
- import fs17 from "fs-extra";
4361
+ import path24 from "path";
4362
+ import fs18 from "fs-extra";
4008
4363
 
4009
4364
  // src/claude/managed-paths.ts
4010
4365
  var PROJECT_MANAGED_CLAUDE_REL = [
4011
4366
  "rules/haus.md",
4012
4367
  "rules/security.md",
4013
- "commands/haus-doctor.md",
4014
- "commands/haus-review.md"
4368
+ "commands/haus-doctor.md"
4015
4369
  ];
4016
4370
  var PROJECT_MANAGED_HAUS_REL = [
4017
4371
  "selected-context.json",
@@ -4030,61 +4384,61 @@ async function collectManagedPaths(root) {
4030
4384
  const lock = await readJson(hausPath(root, "haus.lock.json"));
4031
4385
  for (const row of lock ?? []) {
4032
4386
  for (const rel of row.paths ?? []) {
4033
- paths.add(path23.resolve(root, rel));
4387
+ paths.add(path24.resolve(root, rel));
4034
4388
  }
4035
4389
  }
4036
4390
  const existing = [];
4037
4391
  for (const abs of paths) {
4038
- if (await fs17.pathExists(abs)) existing.push(abs);
4392
+ if (await fs18.pathExists(abs)) existing.push(abs);
4039
4393
  }
4040
4394
  return existing;
4041
4395
  }
4042
4396
  async function settingsHasHausContent(root) {
4043
4397
  const settingsPath = claudePath(root, "settings.json");
4044
- if (!await fs17.pathExists(settingsPath)) return false;
4398
+ if (!await fs18.pathExists(settingsPath)) return false;
4045
4399
  const settings = await readProjectSettings(root);
4046
4400
  return settings._haus != null;
4047
4401
  }
4048
4402
  async function claudeMdHasHausBlock(root) {
4049
- const filePath = path23.join(root, "CLAUDE.md");
4050
- if (!await fs17.pathExists(filePath)) return false;
4051
- const text = await fs17.readFile(filePath, "utf8");
4403
+ const filePath = path24.join(root, "CLAUDE.md");
4404
+ if (!await fs18.pathExists(filePath)) return false;
4405
+ const text = await fs18.readFile(filePath, "utf8");
4052
4406
  return text.includes(BLOCK_BEGIN);
4053
4407
  }
4054
4408
  async function stripProjectSettings(root) {
4055
4409
  const settingsPath = claudePath(root, "settings.json");
4056
- if (!await fs17.pathExists(settingsPath)) return false;
4410
+ if (!await fs18.pathExists(settingsPath)) return false;
4057
4411
  let settings = await readProjectSettings(root);
4058
- settings = stripHausAllow(stripHausDeny(stripHausHooks(settings)));
4412
+ settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4059
4413
  const hasContent = Object.keys(settings).length > 0;
4060
4414
  if (hasContent) {
4061
4415
  await writeProjectSettings(root, settings);
4062
- log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
4416
+ log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
4063
4417
  return true;
4064
4418
  }
4065
- await fs17.remove(settingsPath);
4066
- log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
4419
+ await fs18.remove(settingsPath);
4420
+ log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
4067
4421
  return true;
4068
4422
  }
4069
4423
  async function stripRootClaudeMd(root) {
4070
- const filePath = path23.join(root, "CLAUDE.md");
4071
- if (!await fs17.pathExists(filePath)) return false;
4072
- const prev = await fs17.readFile(filePath, "utf8");
4424
+ const filePath = path24.join(root, "CLAUDE.md");
4425
+ if (!await fs18.pathExists(filePath)) return false;
4426
+ const prev = await fs18.readFile(filePath, "utf8");
4073
4427
  if (!prev.includes(BLOCK_BEGIN)) return false;
4074
4428
  const next = stripHausBlock(prev);
4075
4429
  if (next.length === 0) {
4076
- await fs17.remove(filePath);
4430
+ await fs18.remove(filePath);
4077
4431
  log("Removed CLAUDE.md (only contained haus import block).");
4078
4432
  } else {
4079
- await fs17.writeFile(filePath, next, "utf8");
4433
+ await fs18.writeFile(filePath, next, "utf8");
4080
4434
  log("Removed haus import block from CLAUDE.md (user content preserved).");
4081
4435
  }
4082
4436
  return true;
4083
4437
  }
4084
4438
  async function pruneDirIfEmpty(dir) {
4085
- if (!await fs17.pathExists(dir)) return;
4086
- const entries = await fs17.readdir(dir);
4087
- if (entries.length === 0) await fs17.remove(dir);
4439
+ if (!await fs18.pathExists(dir)) return;
4440
+ const entries = await fs18.readdir(dir);
4441
+ if (entries.length === 0) await fs18.remove(dir);
4088
4442
  }
4089
4443
  async function runUndo(options) {
4090
4444
  const root = process.cwd();
@@ -4095,7 +4449,7 @@ async function runUndo(options) {
4095
4449
  log("Nothing to remove: no haus-managed files found in this directory.");
4096
4450
  return;
4097
4451
  }
4098
- const relTargets = managed.map((p) => path23.relative(root, p));
4452
+ const relTargets = managed.map((p) => path24.relative(root, p));
4099
4453
  const summaryParts = [...relTargets];
4100
4454
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
4101
4455
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -4111,9 +4465,9 @@ User-owned .claude/ files will be preserved.`
4111
4465
  }
4112
4466
  }
4113
4467
  for (const abs of managed) {
4114
- if (!await fs17.pathExists(abs)) continue;
4115
- await fs17.remove(abs);
4116
- log(`Removed ${path23.relative(root, abs)}`);
4468
+ if (!await fs18.pathExists(abs)) continue;
4469
+ await fs18.remove(abs);
4470
+ log(`Removed ${path24.relative(root, abs)}`);
4117
4471
  }
4118
4472
  if (stripSettings) await stripProjectSettings(root);
4119
4473
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -4124,8 +4478,8 @@ User-owned .claude/ files will be preserved.`
4124
4478
 
4125
4479
  // src/install/uninstall.ts
4126
4480
  import crypto3 from "crypto";
4127
- import path24 from "path";
4128
- import fs18 from "fs-extra";
4481
+ import path25 from "path";
4482
+ import fs19 from "fs-extra";
4129
4483
  async function runUninstall(options = {}) {
4130
4484
  const { force = false } = options;
4131
4485
  const manifest = await readManifest();
@@ -4135,7 +4489,7 @@ async function runUninstall(options = {}) {
4135
4489
  return result;
4136
4490
  }
4137
4491
  for (const entry of manifest.files) {
4138
- const exists = fs18.pathExistsSync(entry.destPath);
4492
+ const exists = fs19.pathExistsSync(entry.destPath);
4139
4493
  if (!exists) continue;
4140
4494
  const content2 = await readText(entry.destPath);
4141
4495
  if (content2 === void 0) continue;
@@ -4153,22 +4507,22 @@ async function runUninstall(options = {}) {
4153
4507
  result.skipped.push(entry.destPath);
4154
4508
  continue;
4155
4509
  }
4156
- await fs18.remove(entry.destPath);
4157
- await pruneEmptyDir(path24.dirname(entry.destPath));
4510
+ await fs19.remove(entry.destPath);
4511
+ await pruneEmptyDir(path25.dirname(entry.destPath));
4158
4512
  result.deleted.push(entry.destPath);
4159
4513
  }
4160
4514
  const settings = await readSettings();
4161
- const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
4515
+ const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4162
4516
  await writeSettings(stripped);
4163
4517
  result.hooksStripped = true;
4164
- const hausDir = path24.join(globalClaudeDir(), "haus");
4518
+ const hausDir = path25.join(globalClaudeDir(), "haus");
4165
4519
  const manifestPath2 = hausManifestPath();
4166
- if (fs18.pathExistsSync(manifestPath2)) {
4167
- await fs18.remove(manifestPath2);
4520
+ if (fs19.pathExistsSync(manifestPath2)) {
4521
+ await fs19.remove(manifestPath2);
4168
4522
  }
4169
- if (fs18.pathExistsSync(hausDir)) {
4170
- const remaining = await fs18.readdir(hausDir);
4171
- if (remaining.length === 0) await fs18.remove(hausDir);
4523
+ if (fs19.pathExistsSync(hausDir)) {
4524
+ const remaining = await fs19.readdir(hausDir);
4525
+ if (remaining.length === 0) await fs19.remove(hausDir);
4172
4526
  }
4173
4527
  return result;
4174
4528
  }
@@ -4199,7 +4553,7 @@ async function runUninstallCommand(options) {
4199
4553
  }
4200
4554
 
4201
4555
  // src/commands/update.ts
4202
- import path26 from "path";
4556
+ import path27 from "path";
4203
4557
 
4204
4558
  // src/update/diff-generated-files.ts
4205
4559
  function diffGeneratedFiles() {
@@ -4226,7 +4580,7 @@ function summarizeLockDiff(before, after) {
4226
4580
 
4227
4581
  // src/update/lockfile.ts
4228
4582
  import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
4229
- import path25 from "path";
4583
+ import path26 from "path";
4230
4584
  async function checkLock(root) {
4231
4585
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4232
4586
  const hasValidVersions = lock.every(
@@ -4257,7 +4611,7 @@ async function applyLock(root) {
4257
4611
  try {
4258
4612
  const backupDir = hausPath(root, "backups");
4259
4613
  await mkdir(backupDir, { recursive: true });
4260
- await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
4614
+ await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
4261
4615
  } catch {
4262
4616
  }
4263
4617
  const enriched = await Promise.all(
@@ -4279,7 +4633,7 @@ function diffLock(before, after) {
4279
4633
  }
4280
4634
  async function hasLocalOverrides(root) {
4281
4635
  try {
4282
- await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
4636
+ await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
4283
4637
  return true;
4284
4638
  } catch {
4285
4639
  return false;
@@ -4291,7 +4645,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4291
4645
  async function runUpdate(options) {
4292
4646
  const root = process.cwd();
4293
4647
  if (options.check) {
4294
- const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
4648
+ const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
4295
4649
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4296
4650
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4297
4651
  checkLock(root),
@@ -4321,7 +4675,7 @@ async function runUpdate(options) {
4321
4675
  if (status.driftCount > 0) process.exitCode = 1;
4322
4676
  return;
4323
4677
  }
4324
- const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
4678
+ const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
4325
4679
  const currentVersion = pkgJson?.version ?? "0.0.0";
4326
4680
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4327
4681
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4398,8 +4752,8 @@ async function detectGlobalInstallDrift() {
4398
4752
  }
4399
4753
 
4400
4754
  // src/commands/validate-catalog.ts
4401
- import fs19 from "fs";
4402
- import path27 from "path";
4755
+ import fs20 from "fs";
4756
+ import path28 from "path";
4403
4757
  function auditForbiddenStacks(items) {
4404
4758
  const failures = [];
4405
4759
  for (const item of items) {
@@ -4476,40 +4830,40 @@ function auditShippedFiles(manifestDir, items) {
4476
4830
  const failures = [];
4477
4831
  for (const item of items) {
4478
4832
  if (!item.path) continue;
4479
- const absPath = path27.join(manifestDir, item.path);
4833
+ const absPath = path28.join(manifestDir, item.path);
4480
4834
  if (item.type === "skill") {
4481
- const skillMd = path27.join(absPath, "SKILL.md");
4482
- if (!fs19.existsSync(skillMd)) {
4483
- failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
4835
+ const skillMd = path28.join(absPath, "SKILL.md");
4836
+ if (!fs20.existsSync(skillMd)) {
4837
+ failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
4484
4838
  continue;
4485
4839
  }
4486
- const text = fs19.readFileSync(skillMd, "utf8");
4840
+ const text = fs20.readFileSync(skillMd, "utf8");
4487
4841
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4488
4842
  failures.push(
4489
- ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
4843
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
4490
4844
  );
4491
4845
  } else if (item.type === "agent") {
4492
- if (!fs19.existsSync(absPath)) {
4846
+ if (!fs20.existsSync(absPath)) {
4493
4847
  failures.push(`${item.id}: missing agent file ${item.path}`);
4494
4848
  continue;
4495
4849
  }
4496
- const text = fs19.readFileSync(absPath, "utf8");
4497
- const rel = path27.relative(manifestDir, absPath);
4850
+ const text = fs20.readFileSync(absPath, "utf8");
4851
+ const rel = path28.relative(manifestDir, absPath);
4498
4852
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4499
4853
  failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
4500
4854
  } else if (item.type === "template") {
4501
- if (!fs19.existsSync(absPath)) {
4855
+ if (!fs20.existsSync(absPath)) {
4502
4856
  failures.push(`${item.id}: missing template file ${item.path}`);
4503
4857
  continue;
4504
4858
  }
4505
4859
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4506
4860
  } else if (item.type === "command") {
4507
- if (!fs19.existsSync(absPath)) {
4861
+ if (!fs20.existsSync(absPath)) {
4508
4862
  failures.push(`${item.id}: missing command file ${item.path}`);
4509
4863
  continue;
4510
4864
  }
4511
- const text = fs19.readFileSync(absPath, "utf8");
4512
- const rel = path27.relative(manifestDir, absPath);
4865
+ const text = fs20.readFileSync(absPath, "utf8");
4866
+ const rel = path28.relative(manifestDir, absPath);
4513
4867
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4514
4868
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4515
4869
  }
@@ -4517,8 +4871,8 @@ function auditShippedFiles(manifestDir, items) {
4517
4871
  return failures;
4518
4872
  }
4519
4873
  function auditTemplateContent(manifestDir, absPath, itemId) {
4520
- const rel = path27.relative(manifestDir, absPath);
4521
- const text = fs19.readFileSync(absPath, "utf8");
4874
+ const rel = path28.relative(manifestDir, absPath);
4875
+ const text = fs20.readFileSync(absPath, "utf8");
4522
4876
  const failures = [];
4523
4877
  const lines = text.split(/\r?\n/);
4524
4878
  for (let i = 0; i < lines.length; i++) {
@@ -4540,11 +4894,11 @@ function auditMarkdownContent(manifestDir) {
4540
4894
  const failures = [];
4541
4895
  const dirs = ["skills", "agents", "templates", "commands"];
4542
4896
  for (const dir of dirs) {
4543
- const abs = path27.join(manifestDir, dir);
4544
- if (!fs19.existsSync(abs)) continue;
4897
+ const abs = path28.join(manifestDir, dir);
4898
+ if (!fs20.existsSync(abs)) continue;
4545
4899
  walkMd(abs, (file) => {
4546
- const text = fs19.readFileSync(file, "utf8");
4547
- const rel = path27.relative(manifestDir, file);
4900
+ const text = fs20.readFileSync(file, "utf8");
4901
+ const rel = path28.relative(manifestDir, file);
4548
4902
  const lines = text.split(/\r?\n/);
4549
4903
  for (let i = 0; i < lines.length; i++) {
4550
4904
  const line2 = lines[i] ?? "";
@@ -4563,8 +4917,8 @@ function auditMarkdownContent(manifestDir) {
4563
4917
  return failures;
4564
4918
  }
4565
4919
  function walkMd(dir, fn) {
4566
- for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
4567
- const full = path27.join(dir, entry.name);
4920
+ for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4921
+ const full = path28.join(dir, entry.name);
4568
4922
  if (entry.isDirectory()) walkMd(full, fn);
4569
4923
  else if (entry.name.endsWith(".md")) fn(full);
4570
4924
  }
@@ -4575,8 +4929,8 @@ async function runValidateCatalog(manifestPath2) {
4575
4929
  process.exitCode = 1;
4576
4930
  return;
4577
4931
  }
4578
- const abs = path27.resolve(process.cwd(), manifestPath2);
4579
- const manifestDir = path27.dirname(abs);
4932
+ const abs = path28.resolve(process.cwd(), manifestPath2);
4933
+ const manifestDir = path28.dirname(abs);
4580
4934
  const data = await readJson(abs);
4581
4935
  if (!data?.items) {
4582
4936
  error(`Could not read catalog manifest at ${abs}`);
@@ -4606,7 +4960,7 @@ async function runValidateCatalog(manifestPath2) {
4606
4960
 
4607
4961
  // src/commands/workspace.ts
4608
4962
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4609
- import path34 from "path";
4963
+ import path35 from "path";
4610
4964
 
4611
4965
  // src/commands/workspace/aggregate.ts
4612
4966
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4660,7 +5014,7 @@ ${summaries.map(
4660
5014
  }
4661
5015
 
4662
5016
  // src/commands/workspace/config.ts
4663
- import path28 from "path";
5017
+ import path29 from "path";
4664
5018
  import YAML from "yaml";
4665
5019
  var WORKSPACE_FILE = "haus.workspace.yaml";
4666
5020
  function parseWorkspaceConfig(text) {
@@ -4683,12 +5037,12 @@ function parseWorkspaceConfig(text) {
4683
5037
  };
4684
5038
  }
4685
5039
  async function readWorkspaceConfig(workspaceRoot) {
4686
- return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
5040
+ return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4687
5041
  }
4688
5042
 
4689
5043
  // src/commands/workspace/discover.ts
4690
- import path29 from "path";
4691
- import fg3 from "fast-glob";
5044
+ import path30 from "path";
5045
+ import fg4 from "fast-glob";
4692
5046
  import YAML2 from "yaml";
4693
5047
  var DEFAULT_MAX_DEPTH = 3;
4694
5048
  var REPO_MARKERS = ["**/.git", "**/package.json", "**/composer.json"];
@@ -4704,7 +5058,7 @@ function isDescendant(child, ancestor) {
4704
5058
  return child === ancestor ? false : child.startsWith(`${ancestor}/`);
4705
5059
  }
4706
5060
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4707
- const matches = await fg3(REPO_MARKERS, {
5061
+ const matches = await fg4(REPO_MARKERS, {
4708
5062
  cwd: workspaceRoot,
4709
5063
  dot: true,
4710
5064
  onlyFiles: false,
@@ -4715,8 +5069,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4715
5069
  const gitDirs = /* @__PURE__ */ new Set();
4716
5070
  const manifestDirs = /* @__PURE__ */ new Set();
4717
5071
  for (const match of matches) {
4718
- const base = path29.posix.basename(match);
4719
- const dir = path29.posix.dirname(match);
5072
+ const base = path30.posix.basename(match);
5073
+ const dir = path30.posix.dirname(match);
4720
5074
  const owner = dir === "." ? "." : dir;
4721
5075
  if (base === ".git") gitDirs.add(owner);
4722
5076
  else manifestDirs.add(owner);
@@ -4732,9 +5086,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4732
5086
  }
4733
5087
  repoRoots.sort((a, b) => a.localeCompare(b));
4734
5088
  return mapWithConcurrency(repoRoots, async (relDir) => {
4735
- const absDir = path29.resolve(workspaceRoot, relDir);
4736
- const pkg = await readJson(path29.join(absDir, "package.json"));
4737
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
5089
+ const absDir = path30.resolve(workspaceRoot, relDir);
5090
+ const pkg = await readJson(path30.join(absDir, "package.json"));
5091
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
4738
5092
  let role = "auto";
4739
5093
  try {
4740
5094
  const scan = await scanProject(absDir, "fast");
@@ -4777,7 +5131,7 @@ function renderWorkspaceYaml(config2) {
4777
5131
  });
4778
5132
  }
4779
5133
  async function runDiscover(workspaceRoot, opts = {}) {
4780
- const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
5134
+ const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
4781
5135
  const existingText = await readText(yamlPath);
4782
5136
  const existing = parseWorkspaceConfig(existingText);
4783
5137
  if (existingText && !existing) {
@@ -4811,18 +5165,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
4811
5165
 
4812
5166
  // src/commands/workspace/doctor.ts
4813
5167
  import { existsSync as existsSync3 } from "fs";
4814
- import path31 from "path";
5168
+ import path32 from "path";
4815
5169
 
4816
5170
  // src/commands/workspace/manifest.ts
4817
5171
  import { readFileSync as readFileSync3 } from "fs";
4818
- import path30 from "path";
5172
+ import path31 from "path";
4819
5173
  var MANIFEST_FILE = "workspace.manifest.json";
4820
5174
  function manifestPath(workspaceRoot) {
4821
5175
  return hausPath(workspaceRoot, MANIFEST_FILE);
4822
5176
  }
4823
5177
  function hausVersion() {
4824
5178
  try {
4825
- const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
5179
+ const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
4826
5180
  return pkg.version ?? "0.0.0";
4827
5181
  } catch {
4828
5182
  return "0.0.0";
@@ -4888,7 +5242,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4888
5242
  }
4889
5243
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
4890
5244
  for (const repo of config2.repos) {
4891
- const repoRoot = path31.resolve(workspaceRoot, repo.path);
5245
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
4892
5246
  const entry = manifestByName.get(repo.name);
4893
5247
  if (!entry) {
4894
5248
  flag({
@@ -4962,11 +5316,11 @@ function emit(args) {
4962
5316
 
4963
5317
  // src/commands/workspace/setup.ts
4964
5318
  import { existsSync as existsSync4, statSync } from "fs";
4965
- import path33 from "path";
5319
+ import path34 from "path";
4966
5320
 
4967
5321
  // src/claude/write-workspace-claude-md.ts
4968
- import path32 from "path";
4969
- import fs20 from "fs-extra";
5322
+ import path33 from "path";
5323
+ import fs21 from "fs-extra";
4970
5324
  function buildWorkspaceImportBlock(client, members) {
4971
5325
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
4972
5326
  const body = [
@@ -4984,8 +5338,8 @@ ${BLOCK_END}`;
4984
5338
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4985
5339
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
4986
5340
  const dryRun = opts.dryRun ?? false;
4987
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
4988
- const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
5341
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
5342
+ const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
4989
5343
  const next = opts.collision ? `${block}
4990
5344
  ` : injectHausBlock(prev, block);
4991
5345
  const printable = displayPath(workspaceRoot, filePath);
@@ -5010,21 +5364,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5010
5364
 
5011
5365
  // src/commands/workspace/setup.ts
5012
5366
  function resolveWorkspaceRoot(start = process.cwd()) {
5013
- let dir = path33.resolve(start);
5367
+ let dir = path34.resolve(start);
5014
5368
  for (; ; ) {
5015
- if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
5016
- const parent = path33.dirname(dir);
5017
- if (parent === dir) return path33.resolve(start);
5369
+ if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
5370
+ const parent = path34.dirname(dir);
5371
+ if (parent === dir) return path34.resolve(start);
5018
5372
  dir = parent;
5019
5373
  }
5020
5374
  }
5021
5375
  function isRootRepo(workspaceRoot, repoPath) {
5022
- return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
5376
+ return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
5023
5377
  }
5024
5378
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5025
5379
  const mode = options.mode ?? "fast";
5026
5380
  const apply = options.write ?? false;
5027
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5381
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5028
5382
  if (!configText) {
5029
5383
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5030
5384
  process.exitCode = 1;
@@ -5041,7 +5395,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5041
5395
  const statuses = [];
5042
5396
  const aggregateInputs = [];
5043
5397
  for (const repo of repos) {
5044
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
5398
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5045
5399
  log(`
5046
5400
  \u2192 ${repo.name} (${repo.path})`);
5047
5401
  try {
@@ -5110,7 +5464,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5110
5464
  const status = statusByName.get(repo.name);
5111
5465
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5112
5466
  if (status?.status === "ok") {
5113
- const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
5467
+ const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
5114
5468
  manifestRepos.push({
5115
5469
  name: repo.name,
5116
5470
  path: repo.path,
@@ -5190,7 +5544,7 @@ relationships: []
5190
5544
  log("Workspace initialized.");
5191
5545
  }
5192
5546
  async function scanWorkspace(workspaceRoot, opts) {
5193
- const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5547
+ const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
5194
5548
  if (!configText) {
5195
5549
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5196
5550
  process.exitCode = 1;
@@ -5211,7 +5565,7 @@ async function scanWorkspace(workspaceRoot, opts) {
5211
5565
  }
5212
5566
  const inputs = [];
5213
5567
  for (const repo of config2.repos) {
5214
- const repoRoot = path34.resolve(workspaceRoot, repo.path);
5568
+ const repoRoot = path35.resolve(workspaceRoot, repo.path);
5215
5569
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5216
5570
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5217
5571
  }
@@ -5262,7 +5616,7 @@ async function runWorkspace(action, options = {}) {
5262
5616
  // src/cli.ts
5263
5617
  function cliVersion() {
5264
5618
  try {
5265
- const pkgPath = path35.join(packageRoot(), "package.json");
5619
+ const pkgPath = path36.join(packageRoot(), "package.json");
5266
5620
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5267
5621
  return pkg.version ?? "0.0.0";
5268
5622
  } catch {
@@ -5272,7 +5626,7 @@ function cliVersion() {
5272
5626
  var program = new Command();
5273
5627
  function validateRuntimeNodeVersion() {
5274
5628
  try {
5275
- const pkgPath = path35.join(packageRoot(), "package.json");
5629
+ const pkgPath = path36.join(packageRoot(), "package.json");
5276
5630
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5277
5631
  const requiredRange = pkg.engines?.node;
5278
5632
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {