@haus-tech/haus-workflow 0.22.0 → 0.23.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.
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
 
@@ -108,9 +110,7 @@ var validation_rules_default = {
108
110
  "defi",
109
111
  "trading"
110
112
  ],
111
- bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
112
113
  requiredSkillFrontmatter: ["description"],
113
- requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
114
114
  riskyInstallPatterns: [
115
115
  { source: "\\bnpx\\s+-y\\b", flags: "i" },
116
116
  { source: "\\bnpx\\s+--yes\\b", flags: "i" },
@@ -176,10 +176,7 @@ var validation_rules_default = {
176
176
  "oidc",
177
177
  "azure-ad",
178
178
  "bankid",
179
- "myid",
180
- "cgi",
181
179
  "crypto",
182
- "collection2",
183
180
  "postgresql",
184
181
  "mariadb",
185
182
  "mssql",
@@ -190,7 +187,6 @@ var validation_rules_default = {
190
187
  "testing-library",
191
188
  "phpunit",
192
189
  "storybook",
193
- "wisest",
194
190
  "vitest",
195
191
  "jest",
196
192
  "redis",
@@ -219,7 +215,6 @@ var validation_rules_default = {
219
215
  "missing-eslint",
220
216
  "docker",
221
217
  "pm2",
222
- "deployer-php",
223
218
  "stripe",
224
219
  "qliro",
225
220
  "supabase",
@@ -240,9 +235,7 @@ var validation_rules_default = {
240
235
  // src/catalog/validation-rules.ts
241
236
  var toRegExp = (r) => new RegExp(r.source, r.flags);
242
237
  var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
243
- var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
244
238
  var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
245
- var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
246
239
  var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
247
240
  var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
248
241
  var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
@@ -334,12 +327,6 @@ function validateCatalogItem(item, content2) {
334
327
  return { ok: false, reason: `${label}: disallowed npx at line ${i + 1}` };
335
328
  }
336
329
  }
337
- const lower = content2.toLowerCase();
338
- for (const phrase of BANNED_AGENT_PHRASES) {
339
- if (lower.includes(phrase)) {
340
- return { ok: false, reason: `${label}: banned phrase "${phrase}"` };
341
- }
342
- }
343
330
  const tagFailures = auditForbiddenTagsInText(content2, label);
344
331
  if (tagFailures.length > 0) {
345
332
  return { ok: false, reason: tagFailures[0] };
@@ -420,6 +407,7 @@ function getCacheDir() {
420
407
  return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path2.join(os.homedir(), CATALOG_CACHE_SUBDIR);
421
408
  }
422
409
  var cachedCatalogRef;
410
+ var cachedBlobPaths;
423
411
  function getResolvedCatalogRef() {
424
412
  return cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? "main";
425
413
  }
@@ -451,6 +439,15 @@ async function fetchText(url) {
451
439
  return null;
452
440
  }
453
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
+ }
454
451
  async function fetchRemoteManifest() {
455
452
  const base = await remoteBase();
456
453
  const text = await fetchText(`${base}/manifest.json`);
@@ -495,29 +492,246 @@ function isSafeCatalogPath(itemPath) {
495
492
  const normalized = path2.normalize(itemPath);
496
493
  return !normalized.startsWith("..") && !normalized.includes("/..");
497
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
+ }
498
518
  function safeJoin(base, itemPath) {
499
519
  if (!isSafeCatalogPath(itemPath)) return null;
500
520
  const resolved = path2.resolve(base, itemPath);
501
521
  return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
502
522
  }
503
523
  var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
504
- function isExternalReference(ref) {
505
- return /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
506
- }
507
- async function downloadSkillReferences(item, destDir, base) {
508
- for (const ref of item.references ?? []) {
509
- if (isExternalReference(ref)) continue;
510
- const refDest = safeJoin(destDir, ref);
511
- if (!refDest) {
512
- warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
513
- 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, "/"));
514
536
  }
515
- const text = await fetchText(`${base}/${item.path}/${ref}`);
516
- if (text === null) {
517
- warn(`Failed to fetch reference "${ref}" for ${item.id}`);
518
- 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;
627
+ }
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
+ }
519
695
  }
520
- await writeTextIfChanged(refDest, text);
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";
521
735
  }
522
736
  }
523
737
  async function syncOneItem(item, base) {
@@ -533,30 +747,7 @@ async function syncOneItem(item, base) {
533
747
  return "failed";
534
748
  }
535
749
  if (item.type === "skill") {
536
- const destDir = safeJoin(getCacheDir(), item.path);
537
- if (!destDir) {
538
- warn(`Skipping ${item.id}: path traversal detected`);
539
- return "failed";
540
- }
541
- const dest2 = path2.join(destDir, "SKILL.md");
542
- const text2 = await fetchText(`${base}/${item.path}/SKILL.md`);
543
- if (!text2) {
544
- warn(`Failed to fetch content for ${item.id}`);
545
- return "failed";
546
- }
547
- const verdict2 = validateCatalogItem(item, text2);
548
- if (!verdict2.ok) {
549
- warn(`Rejected ${item.id} at ingest: ${verdict2.reason}`);
550
- return "failed";
551
- }
552
- try {
553
- const outcome = await writeTextIfChanged(dest2, text2);
554
- await downloadSkillReferences(item, destDir, base);
555
- return outcome;
556
- } catch (err) {
557
- warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
558
- return "failed";
559
- }
750
+ return syncSkillDirectory(item, base);
560
751
  }
561
752
  const dest = safeJoin(getCacheDir(), item.path);
562
753
  if (!dest) {
@@ -581,6 +772,7 @@ async function syncOneItem(item, base) {
581
772
  }
582
773
  }
583
774
  async function syncRemoteCatalog() {
775
+ cachedBlobPaths = void 0;
584
776
  const manifest = await fetchRemoteManifest();
585
777
  if (!manifest) {
586
778
  warn("Remote catalog fetch failed \u2014 using bundled catalog");
@@ -608,6 +800,10 @@ async function syncRemoteCatalog() {
608
800
  const failed = [];
609
801
  const base = await remoteBase();
610
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
+ }
611
807
  for (let i = 0; i < items.length; i++) {
612
808
  const item = items[i];
613
809
  const outcome = outcomes[i];
@@ -624,7 +820,7 @@ async function fetchLatestCatalogTag() {
624
820
  try {
625
821
  const res = await fetch(CATALOG_TAGS_API_URL, {
626
822
  signal: AbortSignal.timeout(5e3),
627
- headers: { Accept: "application/vnd.github+json" }
823
+ headers: githubApiHeaders()
628
824
  });
629
825
  if (!res.ok) return null;
630
826
  const tags = await res.json();
@@ -733,9 +929,10 @@ function mergeHooks(settings, fragments) {
733
929
  updated._haus = {
734
930
  hooks: [...existing, ...addedIds],
735
931
  hookCommands: [...existingCommands, ...addedCommands],
736
- // 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.
737
933
  ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
738
- ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
934
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
935
+ ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
739
936
  };
740
937
  return { settings: updated, addedIds };
741
938
  }
@@ -758,7 +955,8 @@ function mergeDenyRules(settings, rules) {
758
955
  hooks: settings._haus?.hooks ?? [],
759
956
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
760
957
  denyRules: [...trackedDeny, ...addedRules],
761
- ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
958
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
959
+ ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
762
960
  };
763
961
  return { settings: updated, addedRules };
764
962
  }
@@ -781,7 +979,8 @@ function mergeAllowRules(settings, rules) {
781
979
  hooks: settings._haus?.hooks ?? [],
782
980
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
783
981
  ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
784
- allowRules: [...trackedAllow, ...addedRules]
982
+ allowRules: [...trackedAllow, ...addedRules],
983
+ ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
785
984
  };
786
985
  return { settings: updated, addedRules };
787
986
  }
@@ -798,7 +997,7 @@ function stripHausAllow(settings) {
798
997
  else delete updated.permissions;
799
998
  const haus = { ...prevHaus };
800
999
  delete haus.allowRules;
801
- 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;
802
1001
  if (stillTracking) updated._haus = haus;
803
1002
  else delete updated._haus;
804
1003
  return updated;
@@ -816,7 +1015,7 @@ function stripHausDeny(settings) {
816
1015
  else delete updated.permissions;
817
1016
  const haus = { ...prevHaus };
818
1017
  delete haus.denyRules;
819
- 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;
820
1019
  if (stillTracking) updated._haus = haus;
821
1020
  else delete updated._haus;
822
1021
  return updated;
@@ -838,6 +1037,48 @@ function stripHausHooks(settings) {
838
1037
  void _;
839
1038
  return rest;
840
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
+ }
841
1082
  async function loadHooksFragment(fragmentPath) {
842
1083
  let raw;
843
1084
  try {
@@ -850,44 +1091,48 @@ async function loadHooksFragment(fragmentPath) {
850
1091
  }
851
1092
 
852
1093
  // src/security/dangerous-commands.ts
853
- var DANGEROUS_COMMANDS = [
854
- "rm -rf",
1094
+ var DENY_COMMANDS = [
855
1095
  "sudo",
856
1096
  "chmod -R 777",
857
- "chown -R",
858
1097
  "git push --force",
859
- "git reset --hard",
860
- "docker system prune",
861
1098
  "drop database",
862
1099
  "truncate table",
863
- "php artisan migrate --force",
864
1100
  "npm publish",
865
1101
  "yarn npm publish",
866
1102
  "pnpm publish"
867
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
+ ];
868
1111
 
869
1112
  // src/security/sensitive-paths.ts
870
- var SENSITIVE_PATHS = [
871
- ".env",
872
- ".env.*",
1113
+ var DENY_PATHS = [
873
1114
  "*.pem",
874
1115
  "*.key",
875
1116
  "*.p12",
876
1117
  "*.pfx",
877
1118
  "id_rsa",
878
1119
  "id_ed25519",
879
- "*.sql",
880
- "*.dump",
881
- "*.backup",
882
- "*.bak",
883
- "storage/logs",
884
- "wp-content/uploads",
885
- "uploads",
886
1120
  "customer-data",
887
- "exports",
888
1121
  "secrets",
889
1122
  "certs"
890
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
+ ];
891
1136
  var SENSITIVE_PATH_REGEXES = [
892
1137
  /^\.env(\.|$)/,
893
1138
  /(^|\/)\.env(\.|$)/,
@@ -915,24 +1160,29 @@ var SENSITIVE_ITEM_KEYWORDS = [
915
1160
  ".key"
916
1161
  ];
917
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
+
918
1177
  // src/security/deny-rules.ts
919
- var SENSITIVE_DIRS = /* @__PURE__ */ new Set([
920
- "storage/logs",
921
- "wp-content/uploads",
922
- "uploads",
923
- "customer-data",
924
- "exports",
925
- "secrets",
926
- "certs"
927
- ]);
928
1178
  var FILE_TOOLS = ["Read", "Edit", "Write"];
929
1179
  function buildDenyRules() {
930
1180
  const rules = [];
931
- for (const command of DANGEROUS_COMMANDS) {
1181
+ for (const command of DENY_COMMANDS) {
932
1182
  rules.push(`Bash(${command}:*)`);
933
1183
  }
934
- for (const path36 of SENSITIVE_PATHS) {
935
- const pattern = SENSITIVE_DIRS.has(path36) ? `${path36}/**` : path36;
1184
+ for (const path37 of DENY_PATHS) {
1185
+ const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
936
1186
  for (const tool of FILE_TOOLS) {
937
1187
  rules.push(`${tool}(${pattern})`);
938
1188
  }
@@ -1020,7 +1270,8 @@ async function mergeProjectSettings(root) {
1020
1270
  const base = await readProjectSettings(root);
1021
1271
  const { settings: withHooks } = mergeHooks(base, PROJECT_HOOK_FRAGMENTS);
1022
1272
  const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
1023
- const { settings: merged } = mergeAllowRules(withDeny, buildAllowRules());
1273
+ const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
1274
+ const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
1024
1275
  return merged;
1025
1276
  }
1026
1277
  async function applyProjectSettingsMerge(root) {
@@ -1030,8 +1281,8 @@ async function applyProjectSettingsMerge(root) {
1030
1281
  }
1031
1282
 
1032
1283
  // src/claude/write-claude-files.ts
1033
- import path12 from "path";
1034
- import fs11 from "fs-extra";
1284
+ import path13 from "path";
1285
+ import fs12 from "fs-extra";
1035
1286
 
1036
1287
  // src/catalog/load-catalog.ts
1037
1288
  import path6 from "path";
@@ -1198,8 +1449,59 @@ async function writeManagedJson(root, filePath, value, dryRun) {
1198
1449
  await writeManagedText(root, filePath, nextText, dryRun);
1199
1450
  }
1200
1451
 
1201
- // src/claude/verify-hooks-contract.ts
1452
+ // src/claude/superpowers-install.ts
1453
+ import path9 from "path";
1454
+ import fg3 from "fast-glob";
1202
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";
1203
1505
 
1204
1506
  // src/claude/load-hooks.ts
1205
1507
  var CANONICAL_HOOKS = {
@@ -1227,7 +1529,7 @@ var STABLE_HOOK_IDS = {
1227
1529
  "haus guard bash --from-hook": "haus.guard-bash"
1228
1530
  };
1229
1531
  async function loadClaudeHooksSettings() {
1230
- return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
1532
+ return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
1231
1533
  }
1232
1534
  function flattenRecommendedHooks(settings) {
1233
1535
  const out = [];
@@ -1293,7 +1595,7 @@ async function assertPostApplySettingsHausContract(root) {
1293
1595
  }
1294
1596
  async function verifyProjectSettingsHooksContract(root) {
1295
1597
  const settingsPath = claudePath(root, "settings.json");
1296
- if (!await fs6.pathExists(settingsPath)) {
1598
+ if (!await fs7.pathExists(settingsPath)) {
1297
1599
  return {
1298
1600
  ok: true,
1299
1601
  skipped: true,
@@ -1320,8 +1622,8 @@ async function verifyProjectSettingsHooksContract(root) {
1320
1622
  }
1321
1623
 
1322
1624
  // src/claude/write-root-claude-md.ts
1323
- import path9 from "path";
1324
- import fs7 from "fs-extra";
1625
+ import path10 from "path";
1626
+ import fs8 from "fs-extra";
1325
1627
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
1326
1628
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
1327
1629
  var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
@@ -1361,21 +1663,21 @@ ${block}
1361
1663
  `;
1362
1664
  }
1363
1665
  async function writeRootClaudeMd(root, dryRun) {
1364
- const filePath = path9.join(root, "CLAUDE.md");
1666
+ const filePath = path10.join(root, "CLAUDE.md");
1365
1667
  const block = buildImportBlock();
1366
- const prev = await fs7.pathExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
1668
+ const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
1367
1669
  const next = injectHausBlock(prev, block);
1368
1670
  await writeManagedText(root, filePath, next, dryRun);
1369
1671
  return filePath;
1370
1672
  }
1371
1673
 
1372
1674
  // src/claude/write-workflow-config.ts
1373
- import path11 from "path";
1374
- import fs9 from "fs-extra";
1675
+ import path12 from "path";
1676
+ import fs10 from "fs-extra";
1375
1677
 
1376
1678
  // src/claude/derive-workflow-config.ts
1377
- import path10 from "path";
1378
- import fs8 from "fs-extra";
1679
+ import path11 from "path";
1680
+ import fs9 from "fs-extra";
1379
1681
  function binCmd(pm, bin, args) {
1380
1682
  const tail = args ? ` ${args}` : "";
1381
1683
  if (pm === "yarn") return `yarn ${bin}${tail}`;
@@ -1384,7 +1686,7 @@ function binCmd(pm, bin, args) {
1384
1686
  }
1385
1687
  async function deriveWorkflowConfig(root, ctx) {
1386
1688
  const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
1387
- const pkg = await readJson(path10.join(root, "package.json"));
1689
+ const pkg = await readJson(path11.join(root, "package.json"));
1388
1690
  const scripts = pkg?.scripts ?? {};
1389
1691
  const deps = new Set(ctx.dependencies);
1390
1692
  const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
@@ -1394,7 +1696,7 @@ async function deriveWorkflowConfig(root, ctx) {
1394
1696
  return null;
1395
1697
  };
1396
1698
  const hasDep = (name) => deps.has(name);
1397
- const exists = (rel) => fs8.pathExistsSync(path10.join(root, rel));
1699
+ const exists = (rel) => fs9.pathExistsSync(path11.join(root, rel));
1398
1700
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
1399
1701
  const hasCypress = hasDep("cypress");
1400
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;
@@ -1463,7 +1765,7 @@ var FALLBACK_CONTEXT = {
1463
1765
  async function writeWorkflowConfig(root, dryRun, opts = {}) {
1464
1766
  const destPath = hausPath(root, "workflow-config.md");
1465
1767
  const printable = displayPath(root, destPath);
1466
- const exists = await fs9.pathExists(destPath);
1768
+ const exists = await fs10.pathExists(destPath);
1467
1769
  if (exists && !opts.refill) {
1468
1770
  if (dryRun) log(printable + ": exists (project-owned, skipping)");
1469
1771
  return null;
@@ -1471,11 +1773,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1471
1773
  const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
1472
1774
  ...FALLBACK_CONTEXT,
1473
1775
  root,
1474
- repoName: path11.basename(root)
1776
+ repoName: path12.basename(root)
1475
1777
  };
1476
1778
  const values = await deriveWorkflowConfig(root, ctx);
1477
1779
  if (exists) {
1478
- const current = await fs9.readFile(destPath, "utf8");
1780
+ const current = await fs10.readFile(destPath, "utf8");
1479
1781
  const refilled = refillContent(current, values);
1480
1782
  if (refilled === current) {
1481
1783
  if (dryRun) log(printable + ": no blank fields to refill");
@@ -1497,7 +1799,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1497
1799
  }
1498
1800
 
1499
1801
  // src/claude/write-workflow.ts
1500
- import fs10 from "fs-extra";
1802
+ import fs11 from "fs-extra";
1501
1803
  var STABLE_ID = "template.workflow";
1502
1804
  function makeWorkflowHeader(pkgVersion, contentHash) {
1503
1805
  return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
@@ -1516,8 +1818,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
1516
1818
  ${templateContent}`;
1517
1819
  const destPath = hausPath(root, "WORKFLOW.md");
1518
1820
  const printable = displayPath(root, destPath);
1519
- if (await fs10.pathExists(destPath)) {
1520
- const existing = await fs10.readFile(destPath, "utf8");
1821
+ if (await fs11.pathExists(destPath)) {
1822
+ const existing = await fs11.readFile(destPath, "utf8");
1521
1823
  const firstLine = existing.split("\n")[0] ?? "";
1522
1824
  const parsed = parseHausManagedHeader(firstLine);
1523
1825
  if (!parsed) {
@@ -1545,7 +1847,7 @@ ${templateContent}`;
1545
1847
  }
1546
1848
  }
1547
1849
  if (dryRun) {
1548
- const prev = await fs10.pathExists(destPath) ? await fs10.readFile(destPath, "utf8") : "";
1850
+ const prev = await fs11.pathExists(destPath) ? await fs11.readFile(destPath, "utf8") : "";
1549
1851
  if (!prev) {
1550
1852
  log(createUnifiedDiff(printable, "", next));
1551
1853
  } else {
@@ -1579,7 +1881,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1579
1881
  estimatedTokenReductionPct: 0
1580
1882
  };
1581
1883
  const pkgRoot = packageRoot();
1582
- 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";
1583
1885
  const coreFiles = [
1584
1886
  claudePath(root, "settings.json"),
1585
1887
  claudePath(root, "rules", "haus.md"),
@@ -1611,7 +1913,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1611
1913
  await assertPostApplySettingsHausContract(root);
1612
1914
  }
1613
1915
  const configPath = hausPath(root, "config.json");
1614
- if (!await fs11.pathExists(configPath)) {
1916
+ if (!await fs12.pathExists(configPath)) {
1615
1917
  await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
1616
1918
  }
1617
1919
  await writeManagedText(
@@ -1644,6 +1946,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1644
1946
  const installedIds = /* @__PURE__ */ new Set();
1645
1947
  const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
1646
1948
  let curatedReviewStatusSkips = 0;
1949
+ let superpowersSharedInstalled = false;
1647
1950
  for (const item of catalogItems) {
1648
1951
  const manifestItem = manifestById.get(item.id);
1649
1952
  if (!manifestItem?.path) continue;
@@ -1670,20 +1973,34 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1670
1973
  );
1671
1974
  continue;
1672
1975
  }
1673
- const destination = claudePath(root, target, path12.basename(sourcePath));
1674
- if (await fs11.pathExists(sourcePath)) {
1976
+ const destination = claudePath(root, target, path13.basename(sourcePath));
1977
+ if (await fs12.pathExists(sourcePath)) {
1675
1978
  if (dryRun) {
1676
- const exists = await fs11.pathExists(destination);
1979
+ const exists = await fs12.pathExists(destination);
1677
1980
  log(
1678
1981
  `${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
1679
1982
  );
1983
+ } else if (item.type === "skill") {
1984
+ await installCatalogSkill(sourcePath, destination, {
1985
+ originSourceId: manifestItem.originSourceId,
1986
+ dryRun: false
1987
+ });
1680
1988
  } else {
1681
- await fs11.ensureDir(path12.dirname(destination));
1682
- await fs11.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1989
+ await fs12.ensureDir(path13.dirname(destination));
1990
+ await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1683
1991
  }
1684
1992
  files.push(destination);
1993
+ const relPaths = [path13.relative(root, destination)];
1994
+ if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
1995
+ const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
1996
+ if (sharedRel) {
1997
+ superpowersSharedInstalled = true;
1998
+ relPaths.push(sharedRel);
1999
+ files.push(path13.join(root, sharedRel));
2000
+ }
2001
+ }
1685
2002
  const current = installedPathsByItem.get(item.id) ?? [];
1686
- installedPathsByItem.set(item.id, [...current, path12.relative(root, destination)]);
2003
+ installedPathsByItem.set(item.id, [...current, ...relPaths]);
1687
2004
  installedIds.add(item.id);
1688
2005
  } else {
1689
2006
  warn(
@@ -1753,7 +2070,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1753
2070
  if (relPaths.length === 0) continue;
1754
2071
  const existing = [];
1755
2072
  for (const rel of relPaths) {
1756
- if (await fs11.pathExists(path12.join(root, rel))) existing.push(rel);
2073
+ if (await fs12.pathExists(path13.join(root, rel))) existing.push(rel);
1757
2074
  }
1758
2075
  if (existing.length === 0) continue;
1759
2076
  if (entry.hash === void 0) {
@@ -1770,13 +2087,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1770
2087
  continue;
1771
2088
  }
1772
2089
  for (const rel of existing) {
1773
- const abs = path12.join(root, rel);
2090
+ const abs = path13.join(root, rel);
1774
2091
  if (dryRun) {
1775
2092
  log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
1776
2093
  continue;
1777
2094
  }
1778
- await fs11.remove(abs);
1779
- await pruneEmptyDir(path12.dirname(abs));
2095
+ await fs12.remove(abs);
2096
+ await pruneEmptyDir(path13.dirname(abs));
1780
2097
  log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
1781
2098
  }
1782
2099
  }
@@ -1784,7 +2101,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
1784
2101
 
1785
2102
  // src/commands/apply.ts
1786
2103
  async function cacheHasItems() {
1787
- const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
2104
+ const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
1788
2105
  return Array.isArray(data?.items) && data.items.length > 0;
1789
2106
  }
1790
2107
  async function runApply(options) {
@@ -1850,8 +2167,8 @@ async function runApply(options) {
1850
2167
  }
1851
2168
  }
1852
2169
  async function isHausProject(root) {
1853
- if (await fs12.pathExists(hausPath(root, "recommendation.json"))) return true;
1854
- if (await fs12.pathExists(claudePath(root, "settings.json"))) {
2170
+ if (await fs13.pathExists(hausPath(root, "recommendation.json"))) return true;
2171
+ if (await fs13.pathExists(claudePath(root, "settings.json"))) {
1855
2172
  const settings = await readProjectSettings(root);
1856
2173
  if (settings._haus != null) return true;
1857
2174
  }
@@ -1889,7 +2206,7 @@ async function runCatalogAudit() {
1889
2206
 
1890
2207
  // src/commands/clone.ts
1891
2208
  import { existsSync as existsSync2 } from "fs";
1892
- import path14 from "path";
2209
+ import path15 from "path";
1893
2210
 
1894
2211
  // src/utils/exec.ts
1895
2212
  import { execa } from "execa";
@@ -1917,6 +2234,20 @@ async function runGit(args, options = {}) {
1917
2234
  }
1918
2235
 
1919
2236
  // src/commands/clone.ts
2237
+ var GIT_LOCATION_VARS = [
2238
+ "GIT_DIR",
2239
+ "GIT_WORK_TREE",
2240
+ "GIT_INDEX_FILE",
2241
+ "GIT_COMMON_DIR",
2242
+ "GIT_OBJECT_DIRECTORY",
2243
+ "GIT_NAMESPACE",
2244
+ "GIT_PREFIX"
2245
+ ];
2246
+ function cloneEnv() {
2247
+ const env = { ...process.env };
2248
+ for (const key of GIT_LOCATION_VARS) delete env[key];
2249
+ return env;
2250
+ }
1920
2251
  function repoNameFromUrl(url) {
1921
2252
  const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
1922
2253
  const tail = trimmed.split(/[/:]/).pop() ?? "";
@@ -1928,16 +2259,16 @@ async function runClone(url, opts = {}) {
1928
2259
  process.exitCode = 1;
1929
2260
  return;
1930
2261
  }
1931
- const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
2262
+ const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
1932
2263
  if (existsSync2(target)) {
1933
- log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
2264
+ log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
1934
2265
  return;
1935
2266
  }
1936
2267
  if (opts.dryRun) {
1937
2268
  log(`would clone ${url} \u2192 ${target}`);
1938
2269
  return;
1939
2270
  }
1940
- const res = await runGit(["clone", url, target]);
2271
+ const res = await runGit(["clone", url, target], { env: cloneEnv(), extendEnv: false });
1941
2272
  if (res.exitCode !== 0) {
1942
2273
  error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
1943
2274
  process.exitCode = 1;
@@ -1947,7 +2278,7 @@ async function runClone(url, opts = {}) {
1947
2278
  }
1948
2279
 
1949
2280
  // src/commands/config.ts
1950
- import path15 from "path";
2281
+ import path16 from "path";
1951
2282
  var CONFIG_PATH2 = ".haus-workflow/config.json";
1952
2283
  var HOOK_ALIASES = {
1953
2284
  "hook.context": "context"
@@ -1960,7 +2291,7 @@ async function runConfig(key, action) {
1960
2291
  );
1961
2292
  }
1962
2293
  const root = process.cwd();
1963
- const configPath = path15.join(root, CONFIG_PATH2);
2294
+ const configPath = path16.join(root, CONFIG_PATH2);
1964
2295
  const existing = await readJson(configPath);
1965
2296
  const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
1966
2297
  cfg.hooks ??= {};
@@ -2340,7 +2671,7 @@ function selectRules(recommended, task, taskIntents) {
2340
2671
 
2341
2672
  // src/scanner/scan-project.ts
2342
2673
  import { readFile as readFile2 } from "fs/promises";
2343
- import path19 from "path";
2674
+ import path20 from "path";
2344
2675
 
2345
2676
  // src/utils/audit-checks.ts
2346
2677
  function isRecord(v) {
@@ -2367,8 +2698,8 @@ function compareVersions(a, b) {
2367
2698
  }
2368
2699
 
2369
2700
  // src/scanner/detect-package-manager.ts
2370
- import path16 from "path";
2371
- import fs13 from "fs-extra";
2701
+ import path17 from "path";
2702
+ import fs14 from "fs-extra";
2372
2703
  function detectPackageManager(root, packageManagerField) {
2373
2704
  const field = String(packageManagerField ?? "").trim();
2374
2705
  if (field.startsWith("yarn@")) {
@@ -2386,9 +2717,9 @@ function detectPackageManager(root, packageManagerField) {
2386
2717
  if (satisfiesVersion(version, ">=9")) return "npm";
2387
2718
  return "unknown";
2388
2719
  }
2389
- if (fs13.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
2390
- if (fs13.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
2391
- if (fs13.existsSync(path16.join(root, "package-lock.json"))) return "npm";
2720
+ if (fs14.existsSync(path17.join(root, "yarn.lock"))) return "yarn";
2721
+ if (fs14.existsSync(path17.join(root, "pnpm-lock.yaml"))) return "pnpm";
2722
+ if (fs14.existsSync(path17.join(root, "package-lock.json"))) return "npm";
2392
2723
  return "unknown";
2393
2724
  }
2394
2725
 
@@ -2396,10 +2727,26 @@ function detectPackageManager(root, packageManagerField) {
2396
2727
  var dep = (value) => ({ kind: "dep", value });
2397
2728
  var depPrefix = (value) => ({ kind: "depPrefix", value });
2398
2729
  var depAbsent = (value) => ({ kind: "depAbsent", value });
2399
- var fileEndsWith = (value) => ({ kind: "file", value, mode: "endsWith" });
2400
- var fileIncludes = (value) => ({ kind: "file", value, mode: "includes" });
2401
- var fileEquals = (value) => ({ kind: "file", value, mode: "equals" });
2402
- var fileStartsWith = (value) => ({ kind: "file", value, mode: "startsWith" });
2730
+ var fileEndsWith = (value) => ({
2731
+ kind: "file",
2732
+ value,
2733
+ mode: "endsWith"
2734
+ });
2735
+ var fileIncludes = (value) => ({
2736
+ kind: "file",
2737
+ value,
2738
+ mode: "includes"
2739
+ });
2740
+ var fileEquals = (value) => ({
2741
+ kind: "file",
2742
+ value,
2743
+ mode: "equals"
2744
+ });
2745
+ var fileStartsWith = (value) => ({
2746
+ kind: "file",
2747
+ value,
2748
+ mode: "startsWith"
2749
+ });
2403
2750
  var content = (value) => ({ kind: "content", value });
2404
2751
  var STACK_BUCKETS = [
2405
2752
  "backend",
@@ -2466,7 +2813,10 @@ var STACK_RULES = [
2466
2813
  { stack: ["frontend", "vue"], any: [dep("vue")] },
2467
2814
  { stack: ["frontend", "vite8"], any: [dep("vite")] },
2468
2815
  { stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
2469
- { stack: ["frontend", "tailwindcss"], any: [dep("tailwindcss"), fileIncludes("tailwind.config.")] },
2816
+ {
2817
+ stack: ["frontend", "tailwindcss"],
2818
+ any: [dep("tailwindcss"), fileIncludes("tailwind.config.")]
2819
+ },
2470
2820
  {
2471
2821
  stack: ["frontend", "shadcn"],
2472
2822
  all: [fileEndsWith("components.json"), dep("class-variance-authority")]
@@ -2479,10 +2829,12 @@ var STACK_RULES = [
2479
2829
  { stack: ["frontend", "react-native"], any: [dep("react-native")] },
2480
2830
  { stack: ["tooling", "i18next"], any: [dep("i18next"), dep("react-i18next")] },
2481
2831
  { stack: ["tooling", "bullmq"], any: [dep("bullmq")] },
2482
- { stack: ["tooling", "docker"], any: [fileEquals("Dockerfile"), fileStartsWith("docker-compose")] },
2832
+ {
2833
+ stack: ["tooling", "docker"],
2834
+ any: [fileEquals("Dockerfile"), fileStartsWith("docker-compose")]
2835
+ },
2483
2836
  { stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
2484
2837
  { stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
2485
- { stack: ["tooling", "deployer-php"], any: [dep("deployer/deployer")] },
2486
2838
  { stack: ["tooling", "missing-prettier"], any: [depAbsent("prettier")] },
2487
2839
  { stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
2488
2840
  {
@@ -2499,7 +2851,10 @@ var STACK_RULES = [
2499
2851
  { stack: ["backend", "nestjs"], any: [content("NestFactory")] },
2500
2852
  { stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
2501
2853
  { stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
2502
- { stack: ["backend", "graphql"], any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")] },
2854
+ {
2855
+ stack: ["backend", "graphql"],
2856
+ any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")]
2857
+ },
2503
2858
  { stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
2504
2859
  { stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
2505
2860
  { stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
@@ -2561,7 +2916,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2561
2916
  }
2562
2917
 
2563
2918
  // src/scanner/detection.ts
2564
- import path17 from "path";
2919
+ import path18 from "path";
2565
2920
  var UNSUPPORTED_MARKERS = {
2566
2921
  "requirements.txt": "python",
2567
2922
  "pyproject.toml": "python",
@@ -2615,14 +2970,14 @@ function finalizeRoles(registryRoles, deps, files) {
2615
2970
  function collectUnsupportedSignals(files) {
2616
2971
  return [
2617
2972
  ...new Set(
2618
- files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
2973
+ files.map((f) => UNSUPPORTED_MARKERS[path18.basename(f)]).filter((s) => Boolean(s))
2619
2974
  )
2620
2975
  ].sort();
2621
2976
  }
2622
2977
 
2623
2978
  // src/scanner/render.ts
2624
2979
  import { readFile } from "fs/promises";
2625
- import path18 from "path";
2980
+ import path19 from "path";
2626
2981
 
2627
2982
  // src/scanner/role-labels.ts
2628
2983
  var ROLE_LABELS = {
@@ -2684,7 +3039,7 @@ async function buildContentBlob(root, files) {
2684
3039
  const slice = candidates.slice(0, 300);
2685
3040
  const parts = await mapWithConcurrency(slice, async (rel) => {
2686
3041
  try {
2687
- return await readFile(path18.join(root, rel), "utf8");
3042
+ return await readFile(path19.join(root, rel), "utf8");
2688
3043
  } catch {
2689
3044
  return "";
2690
3045
  }
@@ -2782,8 +3137,8 @@ var SAFE_FILES = [
2782
3137
  "Gemfile"
2783
3138
  ];
2784
3139
  async function scanProject(root, mode = "fast") {
2785
- const pkg = await readJson(path19.join(root, "package.json"));
2786
- const composer = await readJson(path19.join(root, "composer.json"));
3140
+ const pkg = await readJson(path20.join(root, "package.json"));
3141
+ const composer = await readJson(path20.join(root, "composer.json"));
2787
3142
  const files = await listFiles(root, SAFE_FILES);
2788
3143
  const safeFiles = files.filter((f) => !blocked(f));
2789
3144
  const deps = dependencySet(pkg, composer);
@@ -2817,7 +3172,7 @@ async function scanProject(root, mode = "fast") {
2817
3172
  mode,
2818
3173
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2819
3174
  root,
2820
- repoName: String(pkg?.name ?? path19.basename(root)),
3175
+ repoName: String(pkg?.name ?? path20.basename(root)),
2821
3176
  packageManager,
2822
3177
  repoRoles: roles,
2823
3178
  detectedStacks: stacks,
@@ -2835,7 +3190,7 @@ async function scanProject(root, mode = "fast") {
2835
3190
  const scanHashes = Object.fromEntries(
2836
3191
  await mapWithConcurrency(
2837
3192
  safeFiles,
2838
- async (f) => [f, hashText(await readFile2(path19.join(root, f), "utf8"))]
3193
+ async (f) => [f, hashText(await readFile2(path20.join(root, f), "utf8"))]
2839
3194
  )
2840
3195
  );
2841
3196
  const repoSummary = renderSummary(context);
@@ -2920,8 +3275,8 @@ async function runContext(options) {
2920
3275
  }
2921
3276
 
2922
3277
  // src/commands/doctor.ts
2923
- import path20 from "path";
2924
- import fs14 from "fs-extra";
3278
+ import path21 from "path";
3279
+ import fs15 from "fs-extra";
2925
3280
 
2926
3281
  // src/update/npm-version.ts
2927
3282
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
@@ -3001,7 +3356,7 @@ async function runDoctor(options) {
3001
3356
  const enabled = await isHookEnabled(root, key);
3002
3357
  ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
3003
3358
  }
3004
- const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
3359
+ const rootClaudeMdPath = path21.join(root, "CLAUDE.md");
3005
3360
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
3006
3361
  if (!rootClaudeMdContent) {
3007
3362
  flag(
@@ -3029,7 +3384,7 @@ async function runDoctor(options) {
3029
3384
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
3030
3385
  const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
3031
3386
  for (const target of importTargets) {
3032
- if (!await fs14.pathExists(hausPath(root, target))) {
3387
+ if (!await fs15.pathExists(hausPath(root, target))) {
3033
3388
  flag(
3034
3389
  `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
3035
3390
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
@@ -3040,7 +3395,7 @@ async function runDoctor(options) {
3040
3395
  }
3041
3396
  }
3042
3397
  const workflowPath = hausPath(root, "WORKFLOW.md");
3043
- const workflowExists = await fs14.pathExists(workflowPath);
3398
+ const workflowExists = await fs15.pathExists(workflowPath);
3044
3399
  if (!workflowExists) {
3045
3400
  flag(
3046
3401
  "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
@@ -3063,15 +3418,15 @@ async function runDoctor(options) {
3063
3418
  "haus apply --write --force"
3064
3419
  );
3065
3420
  } else {
3066
- const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
3067
- const bundledPath = path20.join(
3421
+ const cachePath = path21.join(getCacheDir(), "templates/agentic-workflow-standard.md");
3422
+ const bundledPath = path21.join(
3068
3423
  packageRoot(),
3069
3424
  "library",
3070
3425
  "global",
3071
3426
  "templates",
3072
3427
  "agentic-workflow-standard.md"
3073
3428
  );
3074
- const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
3429
+ const templatePath = await fs15.pathExists(cachePath) ? cachePath : bundledPath;
3075
3430
  const templateContent = await readText(templatePath);
3076
3431
  if (storedHashMatch && templateContent) {
3077
3432
  const currentHash = hashText(normaliseLF(templateContent));
@@ -3091,7 +3446,7 @@ async function runDoctor(options) {
3091
3446
  }
3092
3447
  }
3093
3448
  const workflowConfigPath = hausPath(root, "workflow-config.md");
3094
- const workflowConfigExists = await fs14.pathExists(workflowConfigPath);
3449
+ const workflowConfigExists = await fs15.pathExists(workflowConfigPath);
3095
3450
  if (!workflowConfigExists) {
3096
3451
  flag(
3097
3452
  "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
@@ -3099,7 +3454,7 @@ async function runDoctor(options) {
3099
3454
  "haus apply --write"
3100
3455
  );
3101
3456
  } else {
3102
- const cfg = await fs14.readFile(workflowConfigPath, "utf8");
3457
+ const cfg = await fs15.readFile(workflowConfigPath, "utf8");
3103
3458
  const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
3104
3459
  if (unfilled > 0) {
3105
3460
  flag(
@@ -3130,7 +3485,7 @@ async function runDoctor(options) {
3130
3485
  ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
3131
3486
  }
3132
3487
  }
3133
- const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
3488
+ const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
3134
3489
  const currentVersion = pkgJson?.version ?? "0.0.0";
3135
3490
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3136
3491
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3216,15 +3571,16 @@ import { readFileSync as readFileSync2 } from "fs";
3216
3571
 
3217
3572
  // src/security/guard-bash.ts
3218
3573
  function guardBash(command) {
3219
- const matched = DANGEROUS_COMMANDS.find((token) => command.includes(token));
3574
+ const matched = DENY_COMMANDS.find((token) => command.includes(token));
3220
3575
  if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
3221
3576
  return void 0;
3222
3577
  }
3223
3578
 
3224
3579
  // src/security/guard-file-access.ts
3225
3580
  function guardFileAccess(candidate) {
3226
- const matched = SENSITIVE_PATHS.find((token) => candidate.includes(token.replace("*", "")));
3227
- if (matched) return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
3581
+ const matched = DENY_PATHS.find((token) => candidate.includes(token.replace(/\*/g, "")));
3582
+ if (matched)
3583
+ return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
3228
3584
  return void 0;
3229
3585
  }
3230
3586
 
@@ -3273,8 +3629,8 @@ async function runGuard(kind, _options) {
3273
3629
  }
3274
3630
 
3275
3631
  // src/commands/init.ts
3276
- import path21 from "path";
3277
- import fs15 from "fs-extra";
3632
+ import path22 from "path";
3633
+ import fs16 from "fs-extra";
3278
3634
 
3279
3635
  // src/utils/prompts.ts
3280
3636
  import { stdin as input, stdout as output } from "process";
@@ -3635,8 +3991,8 @@ async function runSetupProject(options) {
3635
3991
  // src/commands/init.ts
3636
3992
  async function runInit(options) {
3637
3993
  const root = process.cwd();
3638
- const hausDir = path21.join(root, ".haus-workflow");
3639
- const alreadyInit = await fs15.pathExists(hausDir);
3994
+ const hausDir = path22.join(root, ".haus-workflow");
3995
+ const alreadyInit = await fs16.pathExists(hausDir);
3640
3996
  if (alreadyInit) {
3641
3997
  log("Haus AI already initialized in this project.");
3642
3998
  log("Run `haus setup-project` to reconfigure.");
@@ -3648,8 +4004,8 @@ async function runInit(options) {
3648
4004
 
3649
4005
  // src/install/apply.ts
3650
4006
  import crypto2 from "crypto";
3651
- import path22 from "path";
3652
- import fs16 from "fs-extra";
4007
+ import path23 from "path";
4008
+ import fs17 from "fs-extra";
3653
4009
 
3654
4010
  // src/install/header.ts
3655
4011
  var MD_PREFIX = "<!-- HAUS-MANAGED";
@@ -3737,40 +4093,40 @@ function hashContent(content2) {
3737
4093
  }
3738
4094
  function sourceVersion() {
3739
4095
  try {
3740
- const pkgPath = path22.join(packageRoot(), "package.json");
3741
- const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
4096
+ const pkgPath = path23.join(packageRoot(), "package.json");
4097
+ const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
3742
4098
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
3743
4099
  } catch {
3744
4100
  return "haus@0.0.0";
3745
4101
  }
3746
4102
  }
3747
4103
  function globalSrcDir() {
3748
- return path22.join(packageRoot(), "library", "global");
4104
+ return path23.join(packageRoot(), "library", "global");
3749
4105
  }
3750
4106
  function collectSourceFiles(srcDir, claudeDir) {
3751
4107
  const entries = [];
3752
- const skillsDir = path22.join(srcDir, "skills");
3753
- if (fs16.pathExistsSync(skillsDir)) {
3754
- for (const skillName of fs16.readdirSync(skillsDir)) {
3755
- const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
3756
- if (fs16.pathExistsSync(skillFile)) {
4108
+ const skillsDir = path23.join(srcDir, "skills");
4109
+ if (fs17.pathExistsSync(skillsDir)) {
4110
+ for (const skillName of fs17.readdirSync(skillsDir)) {
4111
+ const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
4112
+ if (fs17.pathExistsSync(skillFile)) {
3757
4113
  entries.push({
3758
4114
  stableId: `skill.${skillName}`,
3759
- srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
3760
- destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
4115
+ srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
4116
+ destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
3761
4117
  });
3762
4118
  }
3763
4119
  }
3764
4120
  }
3765
- const commandsDir = path22.join(srcDir, "commands");
3766
- if (fs16.pathExistsSync(commandsDir)) {
3767
- for (const fileName of fs16.readdirSync(commandsDir)) {
4121
+ const commandsDir = path23.join(srcDir, "commands");
4122
+ if (fs17.pathExistsSync(commandsDir)) {
4123
+ for (const fileName of fs17.readdirSync(commandsDir)) {
3768
4124
  if (!fileName.endsWith(".md")) continue;
3769
4125
  const commandName = fileName.slice(0, -".md".length);
3770
4126
  entries.push({
3771
4127
  stableId: `command.${commandName}`,
3772
- srcRelPath: path22.join("library", "global", "commands", fileName),
3773
- destPath: path22.join(claudeDir, "commands", fileName)
4128
+ srcRelPath: path23.join("library", "global", "commands", fileName),
4129
+ destPath: path23.join(claudeDir, "commands", fileName)
3774
4130
  });
3775
4131
  }
3776
4132
  }
@@ -3794,7 +4150,7 @@ async function applyInstall(options = {}) {
3794
4150
  };
3795
4151
  const manifestFiles = [];
3796
4152
  for (const entry of sourceFiles) {
3797
- const srcPath = path22.join(packageRoot(), entry.srcRelPath);
4153
+ const srcPath = path23.join(packageRoot(), entry.srcRelPath);
3798
4154
  const rawContent = await readText(srcPath);
3799
4155
  if (rawContent === void 0) {
3800
4156
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -3814,7 +4170,7 @@ async function applyInstall(options = {}) {
3814
4170
  }
3815
4171
  continue;
3816
4172
  }
3817
- const destExists = fs16.pathExistsSync(entry.destPath);
4173
+ const destExists = fs17.pathExistsSync(entry.destPath);
3818
4174
  if (destExists) {
3819
4175
  const currentContent = await readText(entry.destPath);
3820
4176
  if (currentContent !== void 0) {
@@ -3850,24 +4206,25 @@ async function applyInstall(options = {}) {
3850
4206
  schemaVersion: SCHEMA_VERSION2
3851
4207
  });
3852
4208
  }
3853
- const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
4209
+ const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
3854
4210
  const fragments = await loadHooksFragment(fragmentPath);
3855
4211
  const settings = await readSettings();
3856
4212
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
3857
4213
  const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
3858
- const { settings: mergedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
4214
+ const { settings: allowedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
4215
+ const { settings: mergedSettings } = mergeAskRules(allowedSettings, buildAskRules());
3859
4216
  result.hookIds = addedIds;
3860
4217
  if (!check && existingManifest) {
3861
4218
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
3862
4219
  for (const entry of existingManifest.files) {
3863
4220
  if (currentDestPaths.has(entry.destPath)) continue;
3864
- if (!fs16.pathExistsSync(entry.destPath)) continue;
4221
+ if (!fs17.pathExistsSync(entry.destPath)) continue;
3865
4222
  const content2 = await readText(entry.destPath);
3866
4223
  if (!content2) continue;
3867
4224
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
3868
4225
  const currentHash = hashContent(content2);
3869
4226
  if (hasHeader && currentHash === entry.hash) {
3870
- if (!dryRun) await fs16.remove(entry.destPath);
4227
+ if (!dryRun) await fs17.remove(entry.destPath);
3871
4228
  result.deleted.push(entry.destPath);
3872
4229
  } else {
3873
4230
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -3994,8 +4351,8 @@ async function runScan(options) {
3994
4351
  }
3995
4352
 
3996
4353
  // src/commands/undo.ts
3997
- import path23 from "path";
3998
- import fs17 from "fs-extra";
4354
+ import path24 from "path";
4355
+ import fs18 from "fs-extra";
3999
4356
 
4000
4357
  // src/claude/managed-paths.ts
4001
4358
  var PROJECT_MANAGED_CLAUDE_REL = [
@@ -4021,61 +4378,61 @@ async function collectManagedPaths(root) {
4021
4378
  const lock = await readJson(hausPath(root, "haus.lock.json"));
4022
4379
  for (const row of lock ?? []) {
4023
4380
  for (const rel of row.paths ?? []) {
4024
- paths.add(path23.resolve(root, rel));
4381
+ paths.add(path24.resolve(root, rel));
4025
4382
  }
4026
4383
  }
4027
4384
  const existing = [];
4028
4385
  for (const abs of paths) {
4029
- if (await fs17.pathExists(abs)) existing.push(abs);
4386
+ if (await fs18.pathExists(abs)) existing.push(abs);
4030
4387
  }
4031
4388
  return existing;
4032
4389
  }
4033
4390
  async function settingsHasHausContent(root) {
4034
4391
  const settingsPath = claudePath(root, "settings.json");
4035
- if (!await fs17.pathExists(settingsPath)) return false;
4392
+ if (!await fs18.pathExists(settingsPath)) return false;
4036
4393
  const settings = await readProjectSettings(root);
4037
4394
  return settings._haus != null;
4038
4395
  }
4039
4396
  async function claudeMdHasHausBlock(root) {
4040
- const filePath = path23.join(root, "CLAUDE.md");
4041
- if (!await fs17.pathExists(filePath)) return false;
4042
- const text = await fs17.readFile(filePath, "utf8");
4397
+ const filePath = path24.join(root, "CLAUDE.md");
4398
+ if (!await fs18.pathExists(filePath)) return false;
4399
+ const text = await fs18.readFile(filePath, "utf8");
4043
4400
  return text.includes(BLOCK_BEGIN);
4044
4401
  }
4045
4402
  async function stripProjectSettings(root) {
4046
4403
  const settingsPath = claudePath(root, "settings.json");
4047
- if (!await fs17.pathExists(settingsPath)) return false;
4404
+ if (!await fs18.pathExists(settingsPath)) return false;
4048
4405
  let settings = await readProjectSettings(root);
4049
- settings = stripHausAllow(stripHausDeny(stripHausHooks(settings)));
4406
+ settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4050
4407
  const hasContent = Object.keys(settings).length > 0;
4051
4408
  if (hasContent) {
4052
4409
  await writeProjectSettings(root, settings);
4053
- log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
4410
+ log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
4054
4411
  return true;
4055
4412
  }
4056
- await fs17.remove(settingsPath);
4057
- log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
4413
+ await fs18.remove(settingsPath);
4414
+ log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
4058
4415
  return true;
4059
4416
  }
4060
4417
  async function stripRootClaudeMd(root) {
4061
- const filePath = path23.join(root, "CLAUDE.md");
4062
- if (!await fs17.pathExists(filePath)) return false;
4063
- const prev = await fs17.readFile(filePath, "utf8");
4418
+ const filePath = path24.join(root, "CLAUDE.md");
4419
+ if (!await fs18.pathExists(filePath)) return false;
4420
+ const prev = await fs18.readFile(filePath, "utf8");
4064
4421
  if (!prev.includes(BLOCK_BEGIN)) return false;
4065
4422
  const next = stripHausBlock(prev);
4066
4423
  if (next.length === 0) {
4067
- await fs17.remove(filePath);
4424
+ await fs18.remove(filePath);
4068
4425
  log("Removed CLAUDE.md (only contained haus import block).");
4069
4426
  } else {
4070
- await fs17.writeFile(filePath, next, "utf8");
4427
+ await fs18.writeFile(filePath, next, "utf8");
4071
4428
  log("Removed haus import block from CLAUDE.md (user content preserved).");
4072
4429
  }
4073
4430
  return true;
4074
4431
  }
4075
4432
  async function pruneDirIfEmpty(dir) {
4076
- if (!await fs17.pathExists(dir)) return;
4077
- const entries = await fs17.readdir(dir);
4078
- if (entries.length === 0) await fs17.remove(dir);
4433
+ if (!await fs18.pathExists(dir)) return;
4434
+ const entries = await fs18.readdir(dir);
4435
+ if (entries.length === 0) await fs18.remove(dir);
4079
4436
  }
4080
4437
  async function runUndo(options) {
4081
4438
  const root = process.cwd();
@@ -4086,7 +4443,7 @@ async function runUndo(options) {
4086
4443
  log("Nothing to remove: no haus-managed files found in this directory.");
4087
4444
  return;
4088
4445
  }
4089
- const relTargets = managed.map((p) => path23.relative(root, p));
4446
+ const relTargets = managed.map((p) => path24.relative(root, p));
4090
4447
  const summaryParts = [...relTargets];
4091
4448
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
4092
4449
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -4102,9 +4459,9 @@ User-owned .claude/ files will be preserved.`
4102
4459
  }
4103
4460
  }
4104
4461
  for (const abs of managed) {
4105
- if (!await fs17.pathExists(abs)) continue;
4106
- await fs17.remove(abs);
4107
- log(`Removed ${path23.relative(root, abs)}`);
4462
+ if (!await fs18.pathExists(abs)) continue;
4463
+ await fs18.remove(abs);
4464
+ log(`Removed ${path24.relative(root, abs)}`);
4108
4465
  }
4109
4466
  if (stripSettings) await stripProjectSettings(root);
4110
4467
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -4115,8 +4472,8 @@ User-owned .claude/ files will be preserved.`
4115
4472
 
4116
4473
  // src/install/uninstall.ts
4117
4474
  import crypto3 from "crypto";
4118
- import path24 from "path";
4119
- import fs18 from "fs-extra";
4475
+ import path25 from "path";
4476
+ import fs19 from "fs-extra";
4120
4477
  async function runUninstall(options = {}) {
4121
4478
  const { force = false } = options;
4122
4479
  const manifest = await readManifest();
@@ -4126,7 +4483,7 @@ async function runUninstall(options = {}) {
4126
4483
  return result;
4127
4484
  }
4128
4485
  for (const entry of manifest.files) {
4129
- const exists = fs18.pathExistsSync(entry.destPath);
4486
+ const exists = fs19.pathExistsSync(entry.destPath);
4130
4487
  if (!exists) continue;
4131
4488
  const content2 = await readText(entry.destPath);
4132
4489
  if (content2 === void 0) continue;
@@ -4144,22 +4501,22 @@ async function runUninstall(options = {}) {
4144
4501
  result.skipped.push(entry.destPath);
4145
4502
  continue;
4146
4503
  }
4147
- await fs18.remove(entry.destPath);
4148
- await pruneEmptyDir(path24.dirname(entry.destPath));
4504
+ await fs19.remove(entry.destPath);
4505
+ await pruneEmptyDir(path25.dirname(entry.destPath));
4149
4506
  result.deleted.push(entry.destPath);
4150
4507
  }
4151
4508
  const settings = await readSettings();
4152
- const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
4509
+ const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4153
4510
  await writeSettings(stripped);
4154
4511
  result.hooksStripped = true;
4155
- const hausDir = path24.join(globalClaudeDir(), "haus");
4512
+ const hausDir = path25.join(globalClaudeDir(), "haus");
4156
4513
  const manifestPath2 = hausManifestPath();
4157
- if (fs18.pathExistsSync(manifestPath2)) {
4158
- await fs18.remove(manifestPath2);
4514
+ if (fs19.pathExistsSync(manifestPath2)) {
4515
+ await fs19.remove(manifestPath2);
4159
4516
  }
4160
- if (fs18.pathExistsSync(hausDir)) {
4161
- const remaining = await fs18.readdir(hausDir);
4162
- if (remaining.length === 0) await fs18.remove(hausDir);
4517
+ if (fs19.pathExistsSync(hausDir)) {
4518
+ const remaining = await fs19.readdir(hausDir);
4519
+ if (remaining.length === 0) await fs19.remove(hausDir);
4163
4520
  }
4164
4521
  return result;
4165
4522
  }
@@ -4190,7 +4547,7 @@ async function runUninstallCommand(options) {
4190
4547
  }
4191
4548
 
4192
4549
  // src/commands/update.ts
4193
- import path26 from "path";
4550
+ import path27 from "path";
4194
4551
 
4195
4552
  // src/update/diff-generated-files.ts
4196
4553
  function diffGeneratedFiles() {
@@ -4217,7 +4574,7 @@ function summarizeLockDiff(before, after) {
4217
4574
 
4218
4575
  // src/update/lockfile.ts
4219
4576
  import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
4220
- import path25 from "path";
4577
+ import path26 from "path";
4221
4578
  async function checkLock(root) {
4222
4579
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4223
4580
  const hasValidVersions = lock.every(
@@ -4248,7 +4605,7 @@ async function applyLock(root) {
4248
4605
  try {
4249
4606
  const backupDir = hausPath(root, "backups");
4250
4607
  await mkdir(backupDir, { recursive: true });
4251
- await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
4608
+ await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
4252
4609
  } catch {
4253
4610
  }
4254
4611
  const enriched = await Promise.all(
@@ -4270,7 +4627,7 @@ function diffLock(before, after) {
4270
4627
  }
4271
4628
  async function hasLocalOverrides(root) {
4272
4629
  try {
4273
- await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
4630
+ await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
4274
4631
  return true;
4275
4632
  } catch {
4276
4633
  return false;
@@ -4282,7 +4639,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4282
4639
  async function runUpdate(options) {
4283
4640
  const root = process.cwd();
4284
4641
  if (options.check) {
4285
- const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
4642
+ const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
4286
4643
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4287
4644
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4288
4645
  checkLock(root),
@@ -4312,7 +4669,7 @@ async function runUpdate(options) {
4312
4669
  if (status.driftCount > 0) process.exitCode = 1;
4313
4670
  return;
4314
4671
  }
4315
- const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
4672
+ const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
4316
4673
  const currentVersion = pkgJson?.version ?? "0.0.0";
4317
4674
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4318
4675
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4389,8 +4746,8 @@ async function detectGlobalInstallDrift() {
4389
4746
  }
4390
4747
 
4391
4748
  // src/commands/validate-catalog.ts
4392
- import fs19 from "fs";
4393
- import path27 from "path";
4749
+ import fs20 from "fs";
4750
+ import path28 from "path";
4394
4751
  function auditForbiddenStacks(items) {
4395
4752
  const failures = [];
4396
4753
  for (const item of items) {
@@ -4467,49 +4824,40 @@ function auditShippedFiles(manifestDir, items) {
4467
4824
  const failures = [];
4468
4825
  for (const item of items) {
4469
4826
  if (!item.path) continue;
4470
- const absPath = path27.join(manifestDir, item.path);
4827
+ const absPath = path28.join(manifestDir, item.path);
4471
4828
  if (item.type === "skill") {
4472
- const skillMd = path27.join(absPath, "SKILL.md");
4473
- if (!fs19.existsSync(skillMd)) {
4474
- failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
4829
+ const skillMd = path28.join(absPath, "SKILL.md");
4830
+ if (!fs20.existsSync(skillMd)) {
4831
+ failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
4475
4832
  continue;
4476
4833
  }
4477
- const text = fs19.readFileSync(skillMd, "utf8");
4834
+ const text = fs20.readFileSync(skillMd, "utf8");
4478
4835
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4479
4836
  failures.push(
4480
- ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
4837
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
4481
4838
  );
4482
4839
  } else if (item.type === "agent") {
4483
- if (!fs19.existsSync(absPath)) {
4840
+ if (!fs20.existsSync(absPath)) {
4484
4841
  failures.push(`${item.id}: missing agent file ${item.path}`);
4485
4842
  continue;
4486
4843
  }
4487
- const text = fs19.readFileSync(absPath, "utf8");
4488
- if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
4489
- for (const section of REQUIRED_AGENT_SECTIONS) {
4490
- if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
4491
- }
4492
- const lower = text.toLowerCase();
4493
- for (const phrase of BANNED_AGENT_PHRASES) {
4494
- if (lower.includes(phrase))
4495
- failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
4496
- }
4497
- failures.push(
4498
- ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, absPath)}`)
4499
- );
4844
+ const text = fs20.readFileSync(absPath, "utf8");
4845
+ const rel = path28.relative(manifestDir, absPath);
4846
+ failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4847
+ failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
4500
4848
  } else if (item.type === "template") {
4501
- if (!fs19.existsSync(absPath)) {
4849
+ if (!fs20.existsSync(absPath)) {
4502
4850
  failures.push(`${item.id}: missing template file ${item.path}`);
4503
4851
  continue;
4504
4852
  }
4505
4853
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4506
4854
  } else if (item.type === "command") {
4507
- if (!fs19.existsSync(absPath)) {
4855
+ if (!fs20.existsSync(absPath)) {
4508
4856
  failures.push(`${item.id}: missing command file ${item.path}`);
4509
4857
  continue;
4510
4858
  }
4511
- const text = fs19.readFileSync(absPath, "utf8");
4512
- const rel = path27.relative(manifestDir, absPath);
4859
+ const text = fs20.readFileSync(absPath, "utf8");
4860
+ const rel = path28.relative(manifestDir, absPath);
4513
4861
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4514
4862
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4515
4863
  }
@@ -4517,8 +4865,8 @@ function auditShippedFiles(manifestDir, items) {
4517
4865
  return failures;
4518
4866
  }
4519
4867
  function auditTemplateContent(manifestDir, absPath, itemId) {
4520
- const rel = path27.relative(manifestDir, absPath);
4521
- const text = fs19.readFileSync(absPath, "utf8");
4868
+ const rel = path28.relative(manifestDir, absPath);
4869
+ const text = fs20.readFileSync(absPath, "utf8");
4522
4870
  const failures = [];
4523
4871
  const lines = text.split(/\r?\n/);
4524
4872
  for (let i = 0; i < lines.length; i++) {
@@ -4540,11 +4888,11 @@ function auditMarkdownContent(manifestDir) {
4540
4888
  const failures = [];
4541
4889
  const dirs = ["skills", "agents", "templates", "commands"];
4542
4890
  for (const dir of dirs) {
4543
- const abs = path27.join(manifestDir, dir);
4544
- if (!fs19.existsSync(abs)) continue;
4891
+ const abs = path28.join(manifestDir, dir);
4892
+ if (!fs20.existsSync(abs)) continue;
4545
4893
  walkMd(abs, (file) => {
4546
- const text = fs19.readFileSync(file, "utf8");
4547
- const rel = path27.relative(manifestDir, file);
4894
+ const text = fs20.readFileSync(file, "utf8");
4895
+ const rel = path28.relative(manifestDir, file);
4548
4896
  const lines = text.split(/\r?\n/);
4549
4897
  for (let i = 0; i < lines.length; i++) {
4550
4898
  const line2 = lines[i] ?? "";
@@ -4563,8 +4911,8 @@ function auditMarkdownContent(manifestDir) {
4563
4911
  return failures;
4564
4912
  }
4565
4913
  function walkMd(dir, fn) {
4566
- for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
4567
- const full = path27.join(dir, entry.name);
4914
+ for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4915
+ const full = path28.join(dir, entry.name);
4568
4916
  if (entry.isDirectory()) walkMd(full, fn);
4569
4917
  else if (entry.name.endsWith(".md")) fn(full);
4570
4918
  }
@@ -4575,8 +4923,8 @@ async function runValidateCatalog(manifestPath2) {
4575
4923
  process.exitCode = 1;
4576
4924
  return;
4577
4925
  }
4578
- const abs = path27.resolve(process.cwd(), manifestPath2);
4579
- const manifestDir = path27.dirname(abs);
4926
+ const abs = path28.resolve(process.cwd(), manifestPath2);
4927
+ const manifestDir = path28.dirname(abs);
4580
4928
  const data = await readJson(abs);
4581
4929
  if (!data?.items) {
4582
4930
  error(`Could not read catalog manifest at ${abs}`);
@@ -4606,7 +4954,7 @@ async function runValidateCatalog(manifestPath2) {
4606
4954
 
4607
4955
  // src/commands/workspace.ts
4608
4956
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4609
- import path34 from "path";
4957
+ import path35 from "path";
4610
4958
 
4611
4959
  // src/commands/workspace/aggregate.ts
4612
4960
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4660,7 +5008,7 @@ ${summaries.map(
4660
5008
  }
4661
5009
 
4662
5010
  // src/commands/workspace/config.ts
4663
- import path28 from "path";
5011
+ import path29 from "path";
4664
5012
  import YAML from "yaml";
4665
5013
  var WORKSPACE_FILE = "haus.workspace.yaml";
4666
5014
  function parseWorkspaceConfig(text) {
@@ -4683,12 +5031,12 @@ function parseWorkspaceConfig(text) {
4683
5031
  };
4684
5032
  }
4685
5033
  async function readWorkspaceConfig(workspaceRoot) {
4686
- return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
5034
+ return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4687
5035
  }
4688
5036
 
4689
5037
  // src/commands/workspace/discover.ts
4690
- import path29 from "path";
4691
- import fg3 from "fast-glob";
5038
+ import path30 from "path";
5039
+ import fg4 from "fast-glob";
4692
5040
  import YAML2 from "yaml";
4693
5041
  var DEFAULT_MAX_DEPTH = 3;
4694
5042
  var REPO_MARKERS = ["**/.git", "**/package.json", "**/composer.json"];
@@ -4704,7 +5052,7 @@ function isDescendant(child, ancestor) {
4704
5052
  return child === ancestor ? false : child.startsWith(`${ancestor}/`);
4705
5053
  }
4706
5054
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4707
- const matches = await fg3(REPO_MARKERS, {
5055
+ const matches = await fg4(REPO_MARKERS, {
4708
5056
  cwd: workspaceRoot,
4709
5057
  dot: true,
4710
5058
  onlyFiles: false,
@@ -4715,8 +5063,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4715
5063
  const gitDirs = /* @__PURE__ */ new Set();
4716
5064
  const manifestDirs = /* @__PURE__ */ new Set();
4717
5065
  for (const match of matches) {
4718
- const base = path29.posix.basename(match);
4719
- const dir = path29.posix.dirname(match);
5066
+ const base = path30.posix.basename(match);
5067
+ const dir = path30.posix.dirname(match);
4720
5068
  const owner = dir === "." ? "." : dir;
4721
5069
  if (base === ".git") gitDirs.add(owner);
4722
5070
  else manifestDirs.add(owner);
@@ -4732,9 +5080,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4732
5080
  }
4733
5081
  repoRoots.sort((a, b) => a.localeCompare(b));
4734
5082
  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);
5083
+ const absDir = path30.resolve(workspaceRoot, relDir);
5084
+ const pkg = await readJson(path30.join(absDir, "package.json"));
5085
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
4738
5086
  let role = "auto";
4739
5087
  try {
4740
5088
  const scan = await scanProject(absDir, "fast");
@@ -4777,7 +5125,7 @@ function renderWorkspaceYaml(config2) {
4777
5125
  });
4778
5126
  }
4779
5127
  async function runDiscover(workspaceRoot, opts = {}) {
4780
- const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
5128
+ const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
4781
5129
  const existingText = await readText(yamlPath);
4782
5130
  const existing = parseWorkspaceConfig(existingText);
4783
5131
  if (existingText && !existing) {
@@ -4811,18 +5159,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
4811
5159
 
4812
5160
  // src/commands/workspace/doctor.ts
4813
5161
  import { existsSync as existsSync3 } from "fs";
4814
- import path31 from "path";
5162
+ import path32 from "path";
4815
5163
 
4816
5164
  // src/commands/workspace/manifest.ts
4817
5165
  import { readFileSync as readFileSync3 } from "fs";
4818
- import path30 from "path";
5166
+ import path31 from "path";
4819
5167
  var MANIFEST_FILE = "workspace.manifest.json";
4820
5168
  function manifestPath(workspaceRoot) {
4821
5169
  return hausPath(workspaceRoot, MANIFEST_FILE);
4822
5170
  }
4823
5171
  function hausVersion() {
4824
5172
  try {
4825
- const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
5173
+ const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
4826
5174
  return pkg.version ?? "0.0.0";
4827
5175
  } catch {
4828
5176
  return "0.0.0";
@@ -4888,7 +5236,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4888
5236
  }
4889
5237
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
4890
5238
  for (const repo of config2.repos) {
4891
- const repoRoot = path31.resolve(workspaceRoot, repo.path);
5239
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
4892
5240
  const entry = manifestByName.get(repo.name);
4893
5241
  if (!entry) {
4894
5242
  flag({
@@ -4962,11 +5310,11 @@ function emit(args) {
4962
5310
 
4963
5311
  // src/commands/workspace/setup.ts
4964
5312
  import { existsSync as existsSync4, statSync } from "fs";
4965
- import path33 from "path";
5313
+ import path34 from "path";
4966
5314
 
4967
5315
  // src/claude/write-workspace-claude-md.ts
4968
- import path32 from "path";
4969
- import fs20 from "fs-extra";
5316
+ import path33 from "path";
5317
+ import fs21 from "fs-extra";
4970
5318
  function buildWorkspaceImportBlock(client, members) {
4971
5319
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
4972
5320
  const body = [
@@ -4984,8 +5332,8 @@ ${BLOCK_END}`;
4984
5332
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4985
5333
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
4986
5334
  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") : "";
5335
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
5336
+ const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
4989
5337
  const next = opts.collision ? `${block}
4990
5338
  ` : injectHausBlock(prev, block);
4991
5339
  const printable = displayPath(workspaceRoot, filePath);
@@ -5010,21 +5358,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5010
5358
 
5011
5359
  // src/commands/workspace/setup.ts
5012
5360
  function resolveWorkspaceRoot(start = process.cwd()) {
5013
- let dir = path33.resolve(start);
5361
+ let dir = path34.resolve(start);
5014
5362
  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);
5363
+ if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
5364
+ const parent = path34.dirname(dir);
5365
+ if (parent === dir) return path34.resolve(start);
5018
5366
  dir = parent;
5019
5367
  }
5020
5368
  }
5021
5369
  function isRootRepo(workspaceRoot, repoPath) {
5022
- return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
5370
+ return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
5023
5371
  }
5024
5372
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5025
5373
  const mode = options.mode ?? "fast";
5026
5374
  const apply = options.write ?? false;
5027
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5375
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5028
5376
  if (!configText) {
5029
5377
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5030
5378
  process.exitCode = 1;
@@ -5041,7 +5389,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5041
5389
  const statuses = [];
5042
5390
  const aggregateInputs = [];
5043
5391
  for (const repo of repos) {
5044
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
5392
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5045
5393
  log(`
5046
5394
  \u2192 ${repo.name} (${repo.path})`);
5047
5395
  try {
@@ -5110,7 +5458,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5110
5458
  const status = statusByName.get(repo.name);
5111
5459
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5112
5460
  if (status?.status === "ok") {
5113
- const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
5461
+ const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
5114
5462
  manifestRepos.push({
5115
5463
  name: repo.name,
5116
5464
  path: repo.path,
@@ -5190,7 +5538,7 @@ relationships: []
5190
5538
  log("Workspace initialized.");
5191
5539
  }
5192
5540
  async function scanWorkspace(workspaceRoot, opts) {
5193
- const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5541
+ const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
5194
5542
  if (!configText) {
5195
5543
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5196
5544
  process.exitCode = 1;
@@ -5211,7 +5559,7 @@ async function scanWorkspace(workspaceRoot, opts) {
5211
5559
  }
5212
5560
  const inputs = [];
5213
5561
  for (const repo of config2.repos) {
5214
- const repoRoot = path34.resolve(workspaceRoot, repo.path);
5562
+ const repoRoot = path35.resolve(workspaceRoot, repo.path);
5215
5563
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5216
5564
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5217
5565
  }
@@ -5262,7 +5610,7 @@ async function runWorkspace(action, options = {}) {
5262
5610
  // src/cli.ts
5263
5611
  function cliVersion() {
5264
5612
  try {
5265
- const pkgPath = path35.join(packageRoot(), "package.json");
5613
+ const pkgPath = path36.join(packageRoot(), "package.json");
5266
5614
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5267
5615
  return pkg.version ?? "0.0.0";
5268
5616
  } catch {
@@ -5272,7 +5620,7 @@ function cliVersion() {
5272
5620
  var program = new Command();
5273
5621
  function validateRuntimeNodeVersion() {
5274
5622
  try {
5275
- const pkgPath = path35.join(packageRoot(), "package.json");
5623
+ const pkgPath = path36.join(packageRoot(), "package.json");
5276
5624
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5277
5625
  const requiredRange = pkg.engines?.node;
5278
5626
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {