@haus-tech/haus-workflow 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +847 -499
  3. package/library/catalog/manifest.json +97 -34
  4. package/library/global/agents/request-assessor.md +2 -2
  5. package/library/global/agents/request-scorer.md +13 -8
  6. package/library/global/agents/request-specifier-eskalerad.md +1 -1
  7. package/library/global/agents/request-specifier.md +1 -1
  8. package/library/global/skills/request/SKILL.md +675 -363
  9. package/library/global/skills/request/scripts/archive.mjs +1376 -0
  10. package/library/global/skills/request/scripts/check-lasta.mjs +64 -0
  11. package/library/global/skills/request/scripts/compare-scores.mjs +23 -15
  12. package/library/global/skills/request/scripts/contract-check.mjs +108 -36
  13. package/library/global/skills/request/scripts/lib.mjs +66 -0
  14. package/library/global/skills/request/scripts/precheck.mjs +30 -27
  15. package/library/global/skills/request/scripts/report.mjs +571 -0
  16. package/library/global/skills/request/scripts/resolve-anchors.mjs +243 -0
  17. package/library/global/skills/request-assess-impact/SKILL.md +10 -10
  18. package/library/global/skills/request-score/SKILL.md +19 -10
  19. package/library/global/skills/request-sync/SKILL.md +4 -4
  20. package/library/global/skills/request-write-spec/SKILL.md +6 -6
  21. package/library/global/skills/request-write-spec/references/specformat.md +2 -2
  22. package/package.json +1 -1
  23. package/library/global/agents/request-classifier.md +0 -55
  24. package/library/global/skills/request/scripts/compare-classifications.mjs +0 -94
  25. package/library/global/skills/request/scripts/quote-repair.mjs +0 -166
  26. package/library/global/skills/request-classify/SKILL.md +0 -79
  27. package/library/global/skills/request-classify/references/klassningsinstruktion.md +0 -152
package/dist/cli.js CHANGED
@@ -1998,7 +1998,7 @@ var init_sync = __esm({
1998
1998
  // src/cli.ts
1999
1999
  init_offline();
2000
2000
  import { readFileSync as readFileSync5 } from "node:fs";
2001
- import path89 from "node:path";
2001
+ import path90 from "node:path";
2002
2002
  import { Command, Option } from "commander";
2003
2003
 
2004
2004
  // src/commands/apply.ts
@@ -2787,41 +2787,12 @@ function parseHausManagedHeader(line2) {
2787
2787
  }
2788
2788
 
2789
2789
  // src/claude/superpowers-install.ts
2790
- init_constants();
2791
2790
  import path14 from "node:path";
2792
2791
  import fg2 from "fast-glob";
2793
2792
  import fs10 from "fs-extra";
2794
2793
  init_logger();
2795
- init_paths();
2796
- var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
2797
- var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
2798
- var PATH_REWRITES = [
2799
- ["skills/shared/", ".claude/skills/shared/"]
2800
- ];
2801
- var PREFIX_REWRITES = ["superpowers-extended-cc:"];
2802
2794
  var SKILL_DEP_ARTIFACTS = ["node_modules/**"];
2803
2795
  var SKILL_DEP_ARTIFACT_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
2804
- function rewriteSuperpowersMarkdown(text) {
2805
- let out = text;
2806
- for (const [from, to] of PATH_REWRITES) {
2807
- out = out.split(from).join(to);
2808
- }
2809
- for (const prefix of PREFIX_REWRITES) {
2810
- out = out.split(prefix).join("");
2811
- }
2812
- return out;
2813
- }
2814
- async function rewriteMarkdownTree(dir) {
2815
- const files = await fg2("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
2816
- for (const rel of files) {
2817
- const abs = path14.join(dir, rel);
2818
- const text = await fs10.readFile(abs, "utf8");
2819
- const rewritten = rewriteSuperpowersMarkdown(text);
2820
- if (rewritten !== text) {
2821
- await fs10.writeFile(abs, rewritten, "utf8");
2822
- }
2823
- }
2824
- }
2825
2796
  async function extraDestFiles(sourcePath, destination) {
2826
2797
  const [sourceFiles, destFiles] = await Promise.all([
2827
2798
  fg2("**/*", { cwd: sourcePath, onlyFiles: true, dot: true, ignore: [...SKILL_DEP_ARTIFACTS] }),
@@ -2832,8 +2803,7 @@ async function extraDestFiles(sourcePath, destination) {
2832
2803
  }
2833
2804
  var SKILL_ENTRY_FILE = "SKILL.md";
2834
2805
  function installedSkillText(text, opts) {
2835
- const rewritten = opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID ? rewriteSuperpowersMarkdown(text) : text;
2836
- return opts.description ? rewriteSkillDescription(rewritten, opts.description) : rewritten;
2806
+ return opts.description ? rewriteSkillDescription(text, opts.description) : text;
2837
2807
  }
2838
2808
  async function previewInstalledSkill(srcFile, opts) {
2839
2809
  const text = await readRewritableText(srcFile);
@@ -2870,30 +2840,8 @@ async function installCatalogSkill(sourcePath, destination, opts) {
2870
2840
  }
2871
2841
  }
2872
2842
  await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
2873
- if (opts.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID) {
2874
- await rewriteMarkdownTree(destination);
2875
- }
2876
2843
  if (opts.description) await composeInstalledDescription(destination, opts.description);
2877
2844
  }
2878
- async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
2879
- const source = path14.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
2880
- if (!await fs10.pathExists(source)) return null;
2881
- const destination = claudePath(projectRoot, "skills", "shared");
2882
- if (dryRun) return path14.relative(projectRoot, destination);
2883
- await fs10.ensureDir(path14.dirname(destination));
2884
- if (await fs10.pathExists(destination)) {
2885
- const extra = await extraDestFiles(source, destination);
2886
- if (extra.length > 0) {
2887
- warn(
2888
- `${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
2889
- );
2890
- }
2891
- await fs10.remove(destination);
2892
- }
2893
- await fs10.copy(source, destination, { overwrite: true, errorOnExist: false });
2894
- await rewriteMarkdownTree(destination);
2895
- return path14.relative(projectRoot, destination);
2896
- }
2897
2845
 
2898
2846
  // src/update/hash-installed.ts
2899
2847
  init_fs();
@@ -3103,6 +3051,15 @@ async function recordManifestPlugins(plugins) {
3103
3051
  for (const plugin of plugins) byId.set(plugin.id, plugin);
3104
3052
  await writeManifest({ ...manifest, plugins: [...byId.values()] });
3105
3053
  }
3054
+ async function pruneManifestPlugins(pluginKeys) {
3055
+ if (pluginKeys.length === 0) return;
3056
+ const manifest = await readManifest();
3057
+ if (!manifest || !manifest.plugins || manifest.plugins.length === 0) return;
3058
+ const drop = new Set(pluginKeys);
3059
+ const kept = manifest.plugins.filter((p) => !drop.has(`${p.plugin}@${p.marketplace}`));
3060
+ if (kept.length === manifest.plugins.length) return;
3061
+ await writeManifest({ ...manifest, plugins: kept });
3062
+ }
3106
3063
  function buildManifest(source, files, hooks, plugins) {
3107
3064
  return {
3108
3065
  _schema: MANIFEST_SCHEMA,
@@ -3437,8 +3394,8 @@ function buildDenyRules() {
3437
3394
  if (RUNTIME_ONLY_DENY_COMMANDS.includes(command)) continue;
3438
3395
  rules.push(`Bash(${command}:*)`);
3439
3396
  }
3440
- for (const path90 of DENY_PATHS) {
3441
- const pattern = DENY_DIRS.has(path90) ? `${path90}/**` : path90;
3397
+ for (const path91 of DENY_PATHS) {
3398
+ const pattern = DENY_DIRS.has(path91) ? `${path91}/**` : path91;
3442
3399
  for (const tool of FILE_TOOLS) {
3443
3400
  rules.push(`${tool}(${pattern})`);
3444
3401
  }
@@ -3870,6 +3827,7 @@ async function verifyProjectSettingsHooksContract(root) {
3870
3827
  init_logger();
3871
3828
  import path20 from "node:path";
3872
3829
  import fs17 from "fs-extra";
3830
+ import YAML from "yaml";
3873
3831
  var CI_GATE_WORKFLOW_REL = path20.join(".github", "workflows", "haus-gate.yml");
3874
3832
  var WORKFLOW = `# Written by \`haus init\`. Yours to edit \u2014 haus will not rewrite this file.
3875
3833
  #
@@ -3898,12 +3856,21 @@ jobs:
3898
3856
  node-version: '22'
3899
3857
  - name: Install haus
3900
3858
  run: npm install -g @haus-tech/haus-workflow@latest
3859
+ # \`.haus-workflow/recommendation.json\` is machine-local and gitignored
3860
+ # (ADR-0025), so it is absent on every CI clone \u2014 and without it \`doctor\`
3861
+ # cannot tell "the catalog gates this agent out of your stack" from "you are
3862
+ # missing an install", so it keeps the stricter reading and fails the gate over
3863
+ # upstream prose you cannot act on. Computing one first is what makes the
3864
+ # severity rules in ADR-0070/ADR-0095 work here. Writes only gitignored files.
3865
+ - name: Compute a recommendation for this stack
3866
+ run: haus recommend
3901
3867
  - name: haus ci-gate
3902
3868
  run: haus ci-gate
3903
3869
  `;
3904
3870
  async function writeCiGateWorkflow(root, dryRun, quiet = false) {
3905
3871
  const dest = path20.join(root, CI_GATE_WORKFLOW_REL);
3906
3872
  if (await fs17.pathExists(dest)) return void 0;
3873
+ if (await projectRunsCiGate(root)) return void 0;
3907
3874
  if (dryRun) {
3908
3875
  if (!quiet) log(`Would write ${CI_GATE_WORKFLOW_REL} (CI gate: haus ci-gate on every PR)`);
3909
3876
  return dest;
@@ -3913,6 +3880,20 @@ async function writeCiGateWorkflow(root, dryRun, quiet = false) {
3913
3880
  if (!quiet) log(`Wrote ${CI_GATE_WORKFLOW_REL} \u2014 runs \`haus ci-gate\` on every pull request`);
3914
3881
  return dest;
3915
3882
  }
3883
+ var CI_GATE_COMMAND = /\bci-gate\b/;
3884
+ function runScripts(node, found = []) {
3885
+ if (Array.isArray(node)) {
3886
+ for (const item of node) runScripts(item, found);
3887
+ return found;
3888
+ }
3889
+ if (node && typeof node === "object") {
3890
+ for (const [key, value] of Object.entries(node)) {
3891
+ if (key === "run" && typeof value === "string") found.push(value);
3892
+ else runScripts(value, found);
3893
+ }
3894
+ }
3895
+ return found;
3896
+ }
3916
3897
  async function projectRunsCiGate(root) {
3917
3898
  const dir = path20.join(root, ".github", "workflows");
3918
3899
  let entries;
@@ -3925,7 +3906,7 @@ async function projectRunsCiGate(root) {
3925
3906
  if (!/\.ya?ml$/i.test(entry)) continue;
3926
3907
  try {
3927
3908
  const text = await fs17.readFile(path20.join(dir, entry), "utf8");
3928
- if (/\bci-gate\b/.test(text)) return true;
3909
+ if (runScripts(YAML.parse(text)).some((script) => CI_GATE_COMMAND.test(script))) return true;
3929
3910
  } catch {
3930
3911
  }
3931
3912
  }
@@ -4667,7 +4648,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4667
4648
  }
4668
4649
  const catalogItems = [...catalogItemsById.values()];
4669
4650
  let curatedReviewStatusSkips = 0;
4670
- let superpowersSharedInstalled = false;
4671
4651
  const projectPackageManager = detectPackageManager(
4672
4652
  root,
4673
4653
  (await readJson(path26.join(root, "package.json")))?.packageManager
@@ -4814,14 +4794,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4814
4794
  }
4815
4795
  files.push(destination);
4816
4796
  const relPaths = [path26.relative(root, destination)];
4817
- if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
4818
- const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
4819
- if (sharedRel) {
4820
- superpowersSharedInstalled = true;
4821
- relPaths.push(sharedRel);
4822
- files.push(path26.join(root, sharedRel));
4823
- }
4824
- }
4825
4797
  const current = installedPathsByItem.get(item.id) ?? [];
4826
4798
  installedPathsByItem.set(item.id, [...current, ...relPaths]);
4827
4799
  installedIds.add(item.id);
@@ -5014,6 +4986,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
5014
4986
  }
5015
4987
  }
5016
4988
  }
4989
+ var LEGACY_SUPERPOWERS_SHARED_REL_PATH = ".claude/skills/shared";
5017
4990
  async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activePluginIds = []) {
5018
4991
  const say = quiet ? () => {
5019
4992
  } : log;
@@ -5067,7 +5040,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
5067
5040
  continue;
5068
5041
  }
5069
5042
  const removable = superseded ? existing.filter(({ rel }) => {
5070
- const isShared = rel.split(path26.sep).join("/") === SUPERPOWERS_SHARED_INSTALL_REL;
5043
+ const isShared = rel.split(path26.sep).join("/") === LEGACY_SUPERPOWERS_SHARED_REL_PATH;
5071
5044
  if (isShared && keepShared) {
5072
5045
  say(
5073
5046
  `Keeping ${displayPath(root, path26.join(root, rel))} \u2014 a locally-edited superpowers skill still uses it`
@@ -5326,12 +5299,39 @@ init_logger();
5326
5299
  var DEFAULT_GLOBAL_PLUGINS = [
5327
5300
  {
5328
5301
  id: "superpowers",
5302
+ plugin: "superpowers",
5303
+ marketplace: "superpowers-dev",
5304
+ url: "https://github.com/obra/superpowers.git",
5305
+ ref: "v6.3.0"
5306
+ }
5307
+ ];
5308
+ var FORMER_GLOBAL_PLUGINS = [
5309
+ {
5310
+ id: "superpowers-extended-cc",
5329
5311
  plugin: "superpowers-extended-cc",
5330
5312
  marketplace: "superpowers-extended-cc-marketplace",
5331
5313
  url: "https://github.com/pcvelz/superpowers.git",
5332
5314
  ref: "v6.4.0"
5333
5315
  }
5334
5316
  ];
5317
+ function planStalePluginRemovals(desired, candidates, state) {
5318
+ if (!state.available) return [];
5319
+ const desiredIds = new Set(desired.map(pluginId));
5320
+ const desiredMarketplaces = new Set(desired.map((pin) => pin.marketplace));
5321
+ const seen = /* @__PURE__ */ new Set();
5322
+ const stale = [];
5323
+ for (const candidate of candidates) {
5324
+ const id = pluginId(candidate);
5325
+ if (desiredIds.has(id) || seen.has(id)) continue;
5326
+ seen.add(id);
5327
+ const installedAtManagedScope = state.installed.some(
5328
+ (p) => p.id === id && (p.scope ?? MANAGED_SCOPE) === MANAGED_SCOPE
5329
+ );
5330
+ const marketplaceLeftover = !desiredMarketplaces.has(candidate.marketplace) && state.marketplaces.some((m) => m.name === candidate.marketplace);
5331
+ if (installedAtManagedScope || marketplaceLeftover) stale.push(candidate);
5332
+ }
5333
+ return stale;
5334
+ }
5335
5335
  var MANAGED_SCOPE = "user";
5336
5336
  function pluginId(pin) {
5337
5337
  return `${pin.plugin}@${pin.marketplace}`;
@@ -5712,7 +5712,7 @@ async function removeGlobalPlugins(plugins, deps = {}) {
5712
5712
  const exec = deps.exec ?? realExec;
5713
5713
  const warn2 = deps.warn ?? warn;
5714
5714
  const log2 = deps.log ?? log;
5715
- const result = { removed: [], failed: [], unavailable: false };
5715
+ const result = { removed: [], failed: [], unavailable: false, converged: [] };
5716
5716
  if (plugins.length === 0) return result;
5717
5717
  if (globalPluginMutationsDisabled() && deps.exec === void 0) {
5718
5718
  return { ...result, unavailable: true };
@@ -5725,15 +5725,28 @@ async function removeGlobalPlugins(plugins, deps = {}) {
5725
5725
  }
5726
5726
  const scopeKey = (id, scope) => `${id}@@${scope ?? "user"}`;
5727
5727
  const stillInstalled = new Set(state.installed.map((p) => scopeKey(p.id, p.scope)));
5728
+ const marketplaceNeedsRemoval = (pin) => {
5729
+ const marketplaceStillConfigured = state.marketplaces.some((m) => m.name === pin.marketplace);
5730
+ const othersFromMarketplace = [...stillInstalled].some(
5731
+ (key) => key.split("@@")[0]?.endsWith(`@${pin.marketplace}`)
5732
+ );
5733
+ return marketplaceStillConfigured && !othersFromMarketplace;
5734
+ };
5728
5735
  for (const pin of plugins) {
5729
5736
  const id = pluginId(pin);
5730
5737
  const managedKey = scopeKey(id, "user");
5738
+ const wasInstalledAtManagedScope = stillInstalled.has(managedKey);
5731
5739
  if (deps.dryRun) {
5732
- if (stillInstalled.has(managedKey))
5740
+ if (wasInstalledAtManagedScope) {
5733
5741
  log2(`[dry-run] would remove global plugin ${pin.id} (${id})`);
5742
+ stillInstalled.delete(managedKey);
5743
+ }
5744
+ if (marketplaceNeedsRemoval(pin)) {
5745
+ log2(`[dry-run] would remove marketplace ${pin.marketplace}`);
5746
+ }
5734
5747
  continue;
5735
5748
  }
5736
- if (stillInstalled.has(managedKey)) {
5749
+ if (wasInstalledAtManagedScope) {
5737
5750
  const uninstalled = await exec(["plugin", "uninstall", id, "--scope", "user"]);
5738
5751
  if (uninstalled.exitCode !== 0) {
5739
5752
  result.failed.push(pin.id);
@@ -5745,21 +5758,60 @@ async function removeGlobalPlugins(plugins, deps = {}) {
5745
5758
  stillInstalled.delete(managedKey);
5746
5759
  result.removed.push(pin.id);
5747
5760
  }
5748
- const marketplaceStillConfigured = state.marketplaces.some((m) => m.name === pin.marketplace);
5749
- const othersFromMarketplace = [...stillInstalled].some(
5750
- (key) => key.split("@@")[0]?.endsWith(`@${pin.marketplace}`)
5751
- );
5752
- if (marketplaceStillConfigured && !othersFromMarketplace) {
5761
+ let marketplaceRemovalFailed = false;
5762
+ if (marketplaceNeedsRemoval(pin)) {
5753
5763
  const removedMarket = await exec(["plugin", "marketplace", "remove", pin.marketplace]);
5754
5764
  if (removedMarket.exitCode !== 0) {
5765
+ marketplaceRemovalFailed = true;
5755
5766
  warn2(
5756
5767
  `Could not remove marketplace ${pin.marketplace}: ${removedMarket.stderr.trim() || `exit ${removedMarket.exitCode}`}`
5757
5768
  );
5758
5769
  }
5759
5770
  }
5771
+ if (!wasInstalledAtManagedScope && !marketplaceRemovalFailed) {
5772
+ result.converged.push(pin.id);
5773
+ }
5760
5774
  }
5761
5775
  return result;
5762
5776
  }
5777
+ async function cleanupStaleGlobalPlugins(desired, deps = {}) {
5778
+ const none = { planned: [], removed: [], failed: [], unavailable: false };
5779
+ if (globalPluginMutationsDisabled() && deps.exec === void 0) {
5780
+ return { ...none, unavailable: true };
5781
+ }
5782
+ const state = await probePluginEnv(deps);
5783
+ if (!state.available) return { ...none, unavailable: true };
5784
+ const manifest = await readManifest();
5785
+ const recorded = (manifest?.plugins ?? []).filter(isValidPin);
5786
+ const protectedPins = [...desired, ...DEFAULT_GLOBAL_PLUGINS];
5787
+ const planned = planStalePluginRemovals(
5788
+ protectedPins,
5789
+ [...recorded, ...FORMER_GLOBAL_PLUGINS],
5790
+ state
5791
+ );
5792
+ if (planned.length === 0) return none;
5793
+ const removal = await removeGlobalPlugins(planned, deps);
5794
+ const removedPins = planned.filter((pin) => removal.removed.includes(pin.id));
5795
+ const convergedPins = planned.filter((pin) => removal.converged.includes(pin.id));
5796
+ if (!deps.dryRun) {
5797
+ const toPrune = [...removedPins, ...convergedPins];
5798
+ if (toPrune.length > 0) {
5799
+ await pruneManifestPlugins(toPrune.map((pin) => pluginId(pin)));
5800
+ }
5801
+ if (removedPins.length > 0) {
5802
+ const log2 = deps.log ?? log;
5803
+ log2(
5804
+ `Removed stale global plugin(s): ${removedPins.map((pin) => pluginId(pin)).join(", ")}. Restart Claude Code to unload them.`
5805
+ );
5806
+ }
5807
+ }
5808
+ return {
5809
+ planned,
5810
+ removed: removal.removed,
5811
+ failed: removal.failed,
5812
+ unavailable: removal.unavailable
5813
+ };
5814
+ }
5763
5815
 
5764
5816
  // src/refs/fetch-refs.ts
5765
5817
  import path30 from "node:path";
@@ -7080,8 +7132,34 @@ import path34 from "node:path";
7080
7132
  import fg4 from "fast-glob";
7081
7133
  import fs33 from "fs-extra";
7082
7134
  var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
7135
+ function escapeRegExp2(value) {
7136
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7137
+ }
7138
+ function buildKnownNamespacePattern(names) {
7139
+ if (names.length === 0) return null;
7140
+ const alternation = names.map(escapeRegExp2).join("|");
7141
+ return new RegExp(`\\b(${alternation}):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\\b(?!:\\/\\/)`, "g");
7142
+ }
7143
+ function collectReferenceMatches(line2, knownPattern) {
7144
+ const seenSpans = /* @__PURE__ */ new Set();
7145
+ const matches = [];
7146
+ const collectFrom = (regex) => {
7147
+ for (const match of line2.matchAll(regex)) {
7148
+ const [reference, namespace, bareName] = match;
7149
+ if (!reference || !namespace || !bareName || match.index === void 0) continue;
7150
+ const span = `${match.index}:${reference.length}`;
7151
+ if (seenSpans.has(span)) continue;
7152
+ seenSpans.add(span);
7153
+ matches.push({ reference, namespace, bareName, index: match.index });
7154
+ }
7155
+ };
7156
+ collectFrom(REFERENCE_PATTERN);
7157
+ if (knownPattern) collectFrom(knownPattern);
7158
+ return matches.sort((a, b) => a.index - b.index);
7159
+ }
7160
+ var FENCED_BLOCK = /(```|~~~)[\s\S]*?\1/g;
7083
7161
  function stripFencedCodeBlocks(text) {
7084
- return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
7162
+ return text.replace(FENCED_BLOCK, (block) => block.replace(/[^\n]/g, " "));
7085
7163
  }
7086
7164
  var INLINE_CODE_SPAN = /(`+)(?!`)[\s\S]*?\1/g;
7087
7165
  function inlineCodeSpanRanges(line2) {
@@ -7093,18 +7171,19 @@ function inlineCodeSpanRanges(line2) {
7093
7171
  }
7094
7172
  async function findUnresolvedSkillReferences(root, options = {}) {
7095
7173
  const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
7174
+ const knownNamespacePattern = buildKnownNamespacePattern(options.knownPluginNamespaces ?? []);
7096
7175
  const installedSkillNames = await readInstalledSkillNames(root);
7097
7176
  const issues = [];
7098
7177
  for (const { file, lines } of await readScannedMarkdown(root)) {
7099
7178
  lines.forEach((line2, idx) => {
7100
7179
  const codeSpans = inlineCodeSpanRanges(line2);
7101
- for (const match of line2.matchAll(REFERENCE_PATTERN)) {
7102
- const [reference, namespace, bareName] = match;
7180
+ for (const { reference, namespace, bareName, index } of collectReferenceMatches(
7181
+ line2,
7182
+ knownNamespacePattern
7183
+ )) {
7103
7184
  if (!reference || !bareName) continue;
7104
7185
  if (namespace && resolvableNamespaces.has(namespace)) continue;
7105
- const inCodeSpan = codeSpans.some(
7106
- ([start, end]) => match.index >= start && match.index < end
7107
- );
7186
+ const inCodeSpan = codeSpans.some(([start, end]) => index >= start && index < end);
7108
7187
  if (inCodeSpan && !installedSkillNames.has(bareName)) continue;
7109
7188
  issues.push({
7110
7189
  file,
@@ -7118,37 +7197,107 @@ async function findUnresolvedSkillReferences(root, options = {}) {
7118
7197
  }
7119
7198
  return issues;
7120
7199
  }
7121
- var BOLD_AGENT_PATTERN = /\*\*([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\*\*/g;
7200
+ var BOLD_SPAN = /\*\*([^*\n]+)\*\*/g;
7201
+ var BARE_NAME = /^[a-z0-9][a-z0-9-]{2,}$/;
7202
+ var KEBAB_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
7122
7203
  var AGENT_WORD = /\bagents?\b/i;
7204
+ var ITEMS_HEADING = /\b(?:skills?|agents?)\b/i;
7205
+ var LIST_ITEM = /^(\s*(?:[-*+]|\d+[.)])\s+)(.*)$/;
7206
+ var LIST_LABEL = /^[A-Za-z][A-Za-z /]{0,24}:\s*/;
7207
+ var KEYWORD_BEFORE = /(?:sub)?(?:skills?|agents?)\s*[:=-]*\s*$/i;
7208
+ var KEYWORD_AFTER = /^\s+(?:[A-Za-z]+\s+)?(?:sub)?(?:skills?|agents?)\b/i;
7209
+ var ROUTER_BEFORE = /\b(?:routes?|routing|delegates?|dispatch(?:es)?|hands? off)\s*(?:to|→|->|:)?\s*$/i;
7210
+ var RUN_SEPARATOR = /^\s*(?:\([^)\n]*\))?\s*(?:,|;|\/|\band\b|\bor\b|\bplus\b)\s*(?:the\s+)?$/i;
7211
+ var INLINE_KEYWORD_NAME = /^(?:sub)?(?:skills?|agents?)\s*[:=]\s*(\S+)$/i;
7212
+ var SUBAGENT = /subagent_type\s*=\s*["']([^"']+)["']/g;
7213
+ function isBareNameLike(name) {
7214
+ return BARE_NAME.test(name);
7215
+ }
7216
+ function normalizeName(ref) {
7217
+ return ref.split("#")[0].split("?")[0].trim().replace(/[.,;:!)\]]+$/, "");
7218
+ }
7219
+ function boldNameSpans(line2) {
7220
+ const spans = [];
7221
+ for (const match of line2.matchAll(BOLD_SPAN)) {
7222
+ const text = match[1];
7223
+ if (text && isBareNameLike(text)) {
7224
+ spans.push({ text, start: match.index, end: match.index + match[0].length });
7225
+ }
7226
+ }
7227
+ return spans;
7228
+ }
7229
+ function groupRuns(line2, spans) {
7230
+ const runs = [];
7231
+ for (const span of spans) {
7232
+ const current = runs.at(-1);
7233
+ const previous = current?.at(-1);
7234
+ if (current && previous && RUN_SEPARATOR.test(line2.slice(previous.end, span.start))) {
7235
+ current.push(span);
7236
+ } else {
7237
+ runs.push([span]);
7238
+ }
7239
+ }
7240
+ return runs;
7241
+ }
7123
7242
  function isAgentContext(line2, nearestHeading) {
7124
- const outsideBold = line2.replace(BOLD_AGENT_PATTERN, " ");
7125
- return AGENT_WORD.test(outsideBold) || AGENT_WORD.test(nearestHeading);
7243
+ let blanked = "";
7244
+ let cursor = 0;
7245
+ for (const span of boldNameSpans(line2)) {
7246
+ blanked += line2.slice(cursor, span.start) + " ".repeat(span.end - span.start);
7247
+ cursor = span.end;
7248
+ }
7249
+ blanked += line2.slice(cursor);
7250
+ return AGENT_WORD.test(blanked) || AGENT_WORD.test(nearestHeading);
7251
+ }
7252
+ function extractNameReferences(lines, { knownNames = /* @__PURE__ */ new Set() } = {}) {
7253
+ const out = [];
7254
+ let nearestHeading = "";
7255
+ lines.forEach((line2, idx) => {
7256
+ if (/^#{1,6}\s/.test(line2)) nearestHeading = line2.replace(/^#{1,6}\s*/, "");
7257
+ const lineNumber = idx + 1;
7258
+ const codeSpans = inlineCodeSpanRanges(line2);
7259
+ const inCodeSpan = (index) => codeSpans.some(([start, end]) => index >= start && index < end);
7260
+ const listMatch = line2.match(LIST_ITEM);
7261
+ const itemBody = listMatch?.[2]?.replace(LIST_LABEL, "");
7262
+ const itemBodyStart = itemBody === void 0 ? -1 : line2.length - itemBody.length;
7263
+ const underItemsHeading = ITEMS_HEADING.test(nearestHeading);
7264
+ const looseContext = isAgentContext(line2, nearestHeading);
7265
+ for (const run of groupRuns(line2, boldNameSpans(line2))) {
7266
+ const before = line2.slice(0, run[0].start);
7267
+ const after = line2.slice(run.at(-1).end);
7268
+ const positional = KEYWORD_BEFORE.test(before) || ROUTER_BEFORE.test(before) || KEYWORD_AFTER.test(after) || // Two co-listed installed items make the run an item list. One is not enough:
7269
+ // package names share basenames with skills.
7270
+ run.filter((span) => knownNames.has(span.text)).length >= 2 || underItemsHeading && run[0].start === itemBodyStart;
7271
+ for (const span of run) {
7272
+ if (inCodeSpan(span.start)) continue;
7273
+ if (!positional && !(KEBAB_NAME.test(span.text) && looseContext)) continue;
7274
+ out.push({ line: lineNumber, name: span.text, kind: "bold" });
7275
+ }
7276
+ }
7277
+ for (const [start, end] of codeSpans) {
7278
+ const keyword = line2.slice(start, end).replace(/^`+|`+$/g, "").trim().match(INLINE_KEYWORD_NAME);
7279
+ if (!keyword) continue;
7280
+ const name = normalizeName(keyword[1]);
7281
+ if (isBareNameLike(name)) out.push({ line: lineNumber, name, kind: "label" });
7282
+ }
7283
+ for (const match of line2.matchAll(SUBAGENT)) {
7284
+ if (match[1]) out.push({ line: lineNumber, name: match[1], kind: "subagent" });
7285
+ }
7286
+ });
7287
+ return out;
7126
7288
  }
7127
7289
  async function findUnresolvedAgentReferences(root, options = {}) {
7128
7290
  const installedAgentNames = await readInstalledAgentNames(root);
7129
7291
  const catalogAgentNames = new Set(options.catalogAgentNames ?? []);
7130
7292
  const issues = [];
7293
+ const installedNames = /* @__PURE__ */ new Set([...installedAgentNames, ...await readInstalledSkillNames(root)]);
7131
7294
  for (const { file, lines } of await readScannedMarkdown(root)) {
7132
- let nearestHeading = "";
7133
- lines.forEach((line2, idx) => {
7134
- if (/^#{1,6}\s/.test(line2)) nearestHeading = line2.replace(/^#{1,6}\s*/, "");
7135
- if (!isAgentContext(line2, nearestHeading)) return;
7136
- const codeSpans = inlineCodeSpanRanges(line2);
7137
- for (const match of line2.matchAll(BOLD_AGENT_PATTERN)) {
7138
- const agentName = match[1];
7139
- if (!agentName || installedAgentNames.has(agentName)) continue;
7140
- const inCodeSpan = codeSpans.some(
7141
- ([start, end]) => match.index >= start && match.index < end
7142
- );
7143
- if (inCodeSpan) continue;
7144
- issues.push({
7145
- file,
7146
- line: idx + 1,
7147
- agentName,
7148
- inCatalog: catalogAgentNames.has(agentName)
7149
- });
7150
- }
7151
- });
7295
+ for (const { line: line2, name, kind } of extractNameReferences(lines, {
7296
+ knownNames: installedNames
7297
+ })) {
7298
+ if (installedNames.has(name)) continue;
7299
+ issues.push({ file, line: line2, agentName: name, kind, inCatalog: catalogAgentNames.has(name) });
7300
+ }
7152
7301
  }
7153
7302
  return issues;
7154
7303
  }
@@ -8005,13 +8154,13 @@ import fs39 from "fs-extra";
8005
8154
  // src/commands/workspace/config.ts
8006
8155
  init_fs();
8007
8156
  import path41 from "node:path";
8008
- import YAML from "yaml";
8157
+ import YAML2 from "yaml";
8009
8158
  var WORKSPACE_FILE = "haus.workspace.yaml";
8010
8159
  function parseWorkspaceConfig(text) {
8011
8160
  if (!text) return void 0;
8012
8161
  let parsed;
8013
8162
  try {
8014
- parsed = YAML.parse(text);
8163
+ parsed = YAML2.parse(text);
8015
8164
  } catch {
8016
8165
  return void 0;
8017
8166
  }
@@ -8496,7 +8645,7 @@ async function buildContentBlob(root, files) {
8496
8645
  // src/scanner/workspace-packages.ts
8497
8646
  import path45 from "node:path";
8498
8647
  import fg5 from "fast-glob";
8499
- import YAML2 from "yaml";
8648
+ import YAML3 from "yaml";
8500
8649
  init_fs();
8501
8650
  var PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
8502
8651
  var MAX_WORKSPACE_MEMBERS = 200;
@@ -8518,7 +8667,7 @@ function workspaceGlobsFromPackageJson(pkg) {
8518
8667
  }
8519
8668
  function workspaceGlobsFromPnpmYaml(text) {
8520
8669
  try {
8521
- const doc = YAML2.parse(text);
8670
+ const doc = YAML3.parse(text);
8522
8671
  if (!isRecord(doc)) return [];
8523
8672
  return stringEntries(doc.packages);
8524
8673
  } catch {
@@ -8830,9 +8979,7 @@ async function runDoctor(options) {
8830
8979
  return;
8831
8980
  }
8832
8981
  const context = await readContextOrScan(root);
8833
- const recommendation = await readJson(
8834
- hausPath(root, "recommendation.json")
8835
- );
8982
+ const recommendation = await readJson(hausPath(root, "recommendation.json"));
8836
8983
  const detail = [];
8837
8984
  const attention = [];
8838
8985
  const suggestions = [];
@@ -9142,8 +9289,16 @@ async function runDoctor(options) {
9142
9289
  );
9143
9290
  }
9144
9291
  }
9292
+ const knownNamespaces = [
9293
+ .../* @__PURE__ */ new Set([
9294
+ ...pluginPins.map((pin) => pin.plugin),
9295
+ ...DEFAULT_GLOBAL_PLUGINS.map((pin) => pin.plugin),
9296
+ ...FORMER_GLOBAL_PLUGINS.map((pin) => pin.plugin)
9297
+ ])
9298
+ ];
9145
9299
  const skillRefIssues = await findUnresolvedSkillReferences(root, {
9146
- pluginNamespaces: activeNamespaces
9300
+ pluginNamespaces: activeNamespaces,
9301
+ knownPluginNamespaces: knownNamespaces
9147
9302
  });
9148
9303
  if (skillRefIssues.length > 0) {
9149
9304
  const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
@@ -9165,27 +9320,83 @@ async function runDoctor(options) {
9165
9320
  );
9166
9321
  }
9167
9322
  }
9168
- const agentCatalogNames = catalogItems === null ? [] : catalogItems.filter((item) => item.type === "agent").map((item) => installTargetFor(item)).flatMap((target) => target.ok ? [path48.basename(target.relPath, ".md")] : []);
9323
+ const recommendedEntries = recommendation && Array.isArray(recommendation.recommended) ? recommendation.recommended : null;
9324
+ const recommendedIds = new Set(
9325
+ (recommendedEntries ?? []).map((entry) => entry.id).filter((id) => typeof id === "string")
9326
+ );
9327
+ const eligibilityKnown = recommendedEntries !== null;
9328
+ const catalogAgents = catalogItems === null ? [] : catalogItems.flatMap((item) => {
9329
+ if (item.type !== "agent") return [];
9330
+ const target = installTargetFor(item);
9331
+ return target.ok ? [{ id: item.id, name: path48.basename(target.relPath, ".md") }] : [];
9332
+ });
9333
+ const STACK_GATE_SKIP_CODES = /* @__PURE__ */ new Set([
9334
+ "requires-any-unsatisfied",
9335
+ "no-role-stack-match",
9336
+ "required-role-missing"
9337
+ ]);
9338
+ const skippedEntries = Array.isArray(recommendation?.skipped) ? recommendation.skipped : [];
9339
+ const stackGatedIds = /* @__PURE__ */ new Map();
9340
+ for (const entry of skippedEntries) {
9341
+ if (typeof entry.id !== "string" || !Array.isArray(entry.skipReasons)) continue;
9342
+ for (const reason of entry.skipReasons) {
9343
+ if (typeof reason?.code !== "string" || !STACK_GATE_SKIP_CODES.has(reason.code)) continue;
9344
+ if (!stackGatedIds.has(entry.id)) {
9345
+ stackGatedIds.set(entry.id, typeof reason.signal === "string" ? reason.signal : "");
9346
+ }
9347
+ }
9348
+ }
9349
+ const gatedOutAgentSignals = /* @__PURE__ */ new Map();
9350
+ for (const agent of catalogAgents) {
9351
+ if (recommendedIds.has(agent.id)) continue;
9352
+ const signal = stackGatedIds.get(agent.id);
9353
+ if (signal !== void 0) gatedOutAgentSignals.set(agent.name, signal);
9354
+ }
9169
9355
  const agentRefIssues = await findUnresolvedAgentReferences(root, {
9170
- catalogAgentNames: agentCatalogNames
9356
+ catalogAgentNames: catalogAgents.map((agent) => agent.name)
9171
9357
  });
9172
9358
  if (agentRefIssues.length > 0) {
9173
9359
  const describeAgent = (i) => `${i.file}:${i.line} "${i.agentName}"`;
9174
- const installable = agentRefIssues.filter((i) => i.inCatalog);
9360
+ const referent = (group) => {
9361
+ const kinds = new Set(group.map((i) => i.kind));
9362
+ if (kinds.size === 1 && kinds.has("label")) return "a skill";
9363
+ return kinds.has("label") ? "an agent or skill" : "an agent";
9364
+ };
9365
+ const routedTo = (group, names) => {
9366
+ const list = names.join(", ");
9367
+ const kinds = new Set(group.map((i) => i.kind));
9368
+ if (kinds.size === 1 && kinds.has("label")) return `hand work to the skill ${list}`;
9369
+ return kinds.has("label") ? `point at ${list}` : `send agents after ${list}`;
9370
+ };
9371
+ const stackGatedHere = (i) => eligibilityKnown && gatedOutAgentSignals.has(i.agentName);
9372
+ const installable = agentRefIssues.filter((i) => i.inCatalog && !stackGatedHere(i));
9373
+ const gatedOut = agentRefIssues.filter((i) => i.inCatalog && stackGatedHere(i));
9175
9374
  const unknown = agentRefIssues.filter((i) => !i.inCatalog);
9176
9375
  if (installable.length > 0) {
9177
9376
  const names = [...new Set(installable.map((i) => i.agentName))];
9178
9377
  flag(
9179
- `- AGENT REFS: ${installable.length} reference(s) point at an agent the catalog can install but this project has not (${names.join(", ")}) (${installable.slice(0, 3).map(describeAgent).join("; ")}${installable.length > 3 ? "; \u2026" : ""})`,
9180
- `${installable.length} rule/agent reference(s) send agents after ${names.join(", ")}, which is available but not installed`,
9378
+ `- AGENT REFS: ${installable.length} reference(s) point at ${referent(installable)} the catalog can install but this project has not (${names.join(", ")}) (${installable.slice(0, 3).map(describeAgent).join("; ")}${installable.length > 3 ? "; \u2026" : ""})`,
9379
+ `${installable.length} rule/agent reference(s) ${routedTo(installable, names)}, which is available but not installed`,
9181
9380
  "find its catalog id and run `haus recommend --include <id>` then `haus apply --write`"
9182
9381
  );
9183
9382
  }
9383
+ if (gatedOut.length > 0) {
9384
+ const names = [...new Set(gatedOut.map((i) => i.agentName))];
9385
+ const unmet = names.map((name) => {
9386
+ const signal = gatedOutAgentSignals.get(name);
9387
+ return signal ? `${name} needs ${signal}` : name;
9388
+ }).join("; ");
9389
+ suggest(
9390
+ `- AGENT REFS: ${gatedOut.length} reference(s) name ${referent(gatedOut)} the catalog gates out of this project's stack (${unmet}) (${gatedOut.slice(0, 3).map(describeAgent).join("; ")}${gatedOut.length > 3 ? "; \u2026" : ""})`,
9391
+ `${gatedOut.length} rule/agent reference(s) ${routedTo(gatedOut, names)}, which the catalog does not offer a project with this stack (${unmet}) \u2014 the reference is upstream prose, not a missing install`,
9392
+ `nothing to install here: correct or drop the reference upstream (catalog-owned prose) \u2014 the catalog gates ${names.join(", ")} out of this stack on purpose`
9393
+ );
9394
+ }
9184
9395
  if (unknown.length > 0) {
9185
9396
  const names = [...new Set(unknown.map((i) => i.agentName))];
9186
9397
  suggest(
9187
- `- AGENT REFS: ${unknown.length} reference(s) name an agent that exists neither here nor in the catalog (${names.join(", ")}) (${unknown.slice(0, 3).map(describeAgent).join("; ")}${unknown.length > 3 ? "; \u2026" : ""})`,
9188
- `${unknown.length} rule/agent reference(s) send agents after ${names.join(", ")}, which was never shipped`,
9398
+ `- AGENT REFS: ${unknown.length} reference(s) name ${referent(unknown)} that exists neither here nor in the catalog (${names.join(", ")}) (${unknown.slice(0, 3).map(describeAgent).join("; ")}${unknown.length > 3 ? "; \u2026" : ""})`,
9399
+ `${unknown.length} rule/agent reference(s) ${routedTo(unknown, names)}, which was never shipped`,
9189
9400
  "correct or drop the reference upstream (catalog-owned prose), so agents stop being sent after a tool that does not exist"
9190
9401
  );
9191
9402
  }
@@ -9193,9 +9404,6 @@ async function runDoctor(options) {
9193
9404
  if (skillRefIssues.length === 0 && agentRefIssues.length === 0) {
9194
9405
  ok("- SKILL REFS: OK (no unresolved cross-skill or agent references)");
9195
9406
  }
9196
- const recommendedIds = new Set(
9197
- (recommendation?.recommended ?? []).map((entry) => entry.id).filter((id) => typeof id === "string")
9198
- );
9199
9407
  if (catalogItems !== null && recommendedIds.size > 0) {
9200
9408
  const catalogIds = new Set(catalogItems.map((item) => item.id));
9201
9409
  const excludedIds = new Set(await readProjectExcludedIds(root));
@@ -9415,7 +9623,7 @@ async function runDoctor(options) {
9415
9623
  }
9416
9624
 
9417
9625
  // src/commands/update.ts
9418
- import path51 from "node:path";
9626
+ import path52 from "node:path";
9419
9627
  init_github_rate_limit();
9420
9628
  init_private_catalog_access();
9421
9629
 
@@ -9442,8 +9650,8 @@ async function refreshProjectApply(root) {
9442
9650
 
9443
9651
  // src/install/apply.ts
9444
9652
  import crypto3 from "node:crypto";
9445
- import path49 from "node:path";
9446
- import fs42 from "fs-extra";
9653
+ import path50 from "node:path";
9654
+ import fs43 from "fs-extra";
9447
9655
  init_fs();
9448
9656
  init_logger();
9449
9657
  init_paths();
@@ -9550,72 +9758,146 @@ ${rest}`;
9550
9758
  ${content2}`;
9551
9759
  }
9552
9760
 
9761
+ // src/install/internal-channel.ts
9762
+ init_fs();
9763
+ import path49 from "node:path";
9764
+ import fs42 from "fs-extra";
9765
+ var INSTALL_CONFIG_SCHEMA = "haus-install-config/1";
9766
+ var INTERNAL_NAME_PREFIXES = ["request", "slack-cowork"];
9767
+ function internalPrefixMatch(name) {
9768
+ return INTERNAL_NAME_PREFIXES.some(
9769
+ (prefix) => name === prefix || name.startsWith(`${prefix}-`) || name.startsWith(`${prefix}.`)
9770
+ );
9771
+ }
9772
+ function isInternalAsset(stableId) {
9773
+ const separator = stableId.indexOf(".");
9774
+ if (separator === -1) return false;
9775
+ const kind = stableId.slice(0, separator);
9776
+ const name = stableId.slice(separator + 1);
9777
+ if (kind !== "skill" && kind !== "agent") return false;
9778
+ return internalPrefixMatch(name);
9779
+ }
9780
+ function installConfigPath() {
9781
+ return path49.join(globalClaudeDir(), "haus", "install-config.json");
9782
+ }
9783
+ async function readInstallConfig() {
9784
+ const raw = await readJson(installConfigPath());
9785
+ if (!raw || typeof raw.internal !== "boolean") return void 0;
9786
+ return raw;
9787
+ }
9788
+ async function writeInstallConfig(config) {
9789
+ await writeJson(installConfigPath(), {
9790
+ _schema: INSTALL_CONFIG_SCHEMA,
9791
+ internal: config.internal,
9792
+ source: config.source,
9793
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9794
+ });
9795
+ }
9796
+ async function removeInstallConfig() {
9797
+ const p = installConfigPath();
9798
+ if (fs42.pathExistsSync(p)) await fs42.remove(p);
9799
+ }
9800
+ function envOverride() {
9801
+ const raw = process.env.HAUS_INTERNAL_CHANNEL?.trim().toLowerCase();
9802
+ if (raw === void 0 || raw === "") return void 0;
9803
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
9804
+ if (["0", "false", "no", "off"].includes(raw)) return false;
9805
+ return void 0;
9806
+ }
9807
+ function manifestHasInternal(manifest) {
9808
+ return (manifest?.files ?? []).some((file) => isInternalAsset(file.stableId));
9809
+ }
9810
+ async function resolveInternalChannel(manifest, requested) {
9811
+ if (requested !== void 0) return { enabled: requested, source: "flag" };
9812
+ const override = envOverride();
9813
+ if (override !== void 0) return { enabled: override, source: "env" };
9814
+ const config = await readInstallConfig();
9815
+ if (config) return { enabled: config.internal, source: "config" };
9816
+ if (manifestHasInternal(manifest)) return { enabled: true, source: "grandfathered" };
9817
+ return { enabled: false, source: "default" };
9818
+ }
9819
+
9553
9820
  // src/install/apply.ts
9554
9821
  var SCHEMA_VERSION2 = "1";
9822
+ async function pruneEmptyDirsUpward(dirs, stopAt) {
9823
+ const ordered = [...new Set(dirs)].sort((a, b) => b.length - a.length);
9824
+ for (const start of ordered) {
9825
+ let dir = start;
9826
+ while (dir.startsWith(stopAt) && dir !== stopAt) {
9827
+ try {
9828
+ if ((await fs43.readdir(dir)).length > 0) break;
9829
+ await fs43.remove(dir);
9830
+ } catch {
9831
+ break;
9832
+ }
9833
+ dir = path50.dirname(dir);
9834
+ }
9835
+ }
9836
+ }
9555
9837
  function hashContent(content2) {
9556
9838
  return `sha256-${crypto3.createHash("sha256").update(normaliseLF(content2)).digest("hex")}`;
9557
9839
  }
9558
9840
  function destFileMissing(destPath) {
9559
9841
  try {
9560
- return !fs42.statSync(destPath).isFile();
9842
+ return !fs43.statSync(destPath).isFile();
9561
9843
  } catch {
9562
9844
  return true;
9563
9845
  }
9564
9846
  }
9565
9847
  function sourceVersion() {
9566
9848
  try {
9567
- const pkgPath = path49.join(packageRoot(), "package.json");
9568
- const pkg = JSON.parse(fs42.readFileSync(pkgPath, "utf8"));
9849
+ const pkgPath = path50.join(packageRoot(), "package.json");
9850
+ const pkg = JSON.parse(fs43.readFileSync(pkgPath, "utf8"));
9569
9851
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
9570
9852
  } catch {
9571
9853
  return "haus@0.0.0";
9572
9854
  }
9573
9855
  }
9574
9856
  function globalSrcDir() {
9575
- return path49.join(packageRoot(), "library", "global");
9857
+ return path50.join(packageRoot(), "library", "global");
9576
9858
  }
9577
9859
  function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
9578
- const skillDir = path49.join(skillsDir, skillName);
9860
+ const skillDir = path50.join(skillsDir, skillName);
9579
9861
  const entries = [];
9580
- for (const rel of fs42.readdirSync(skillDir, { recursive: true })) {
9862
+ for (const rel of fs43.readdirSync(skillDir, { recursive: true })) {
9581
9863
  if (rel === "SKILL.md") continue;
9582
- const abs = path49.join(skillDir, rel);
9583
- if (!fs42.statSync(abs).isFile()) continue;
9584
- const posixRel = rel.split(path49.sep).join("/");
9864
+ const abs = path50.join(skillDir, rel);
9865
+ if (!fs43.statSync(abs).isFile()) continue;
9866
+ const posixRel = rel.split(path50.sep).join("/");
9585
9867
  entries.push({
9586
9868
  stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
9587
- srcRelPath: path49.join("library", "global", "skills", skillName, rel),
9588
- destPath: path49.join(claudeDir, "skills", skillName, rel)
9869
+ srcRelPath: path50.join("library", "global", "skills", skillName, rel),
9870
+ destPath: path50.join(claudeDir, "skills", skillName, rel)
9589
9871
  });
9590
9872
  }
9591
9873
  return entries;
9592
9874
  }
9593
9875
  function collectFlatChannel(srcDir, claudeDir, dirName, idPrefix) {
9594
9876
  const entries = [];
9595
- const channelDir = path49.join(srcDir, dirName);
9596
- if (!fs42.pathExistsSync(channelDir)) return entries;
9597
- for (const fileName of fs42.readdirSync(channelDir)) {
9877
+ const channelDir = path50.join(srcDir, dirName);
9878
+ if (!fs43.pathExistsSync(channelDir)) return entries;
9879
+ for (const fileName of fs43.readdirSync(channelDir)) {
9598
9880
  if (!fileName.endsWith(".md")) continue;
9599
9881
  const name = fileName.slice(0, -".md".length);
9600
9882
  entries.push({
9601
9883
  stableId: `${idPrefix}.${name}`,
9602
- srcRelPath: path49.join("library", "global", dirName, fileName),
9603
- destPath: path49.join(claudeDir, dirName, fileName)
9884
+ srcRelPath: path50.join("library", "global", dirName, fileName),
9885
+ destPath: path50.join(claudeDir, dirName, fileName)
9604
9886
  });
9605
9887
  }
9606
9888
  return entries;
9607
9889
  }
9608
9890
  function collectSourceFiles(srcDir, claudeDir) {
9609
9891
  const entries = [];
9610
- const skillsDir = path49.join(srcDir, "skills");
9611
- if (fs42.pathExistsSync(skillsDir)) {
9612
- for (const skillName of fs42.readdirSync(skillsDir)) {
9613
- const skillFile = path49.join(skillsDir, skillName, "SKILL.md");
9614
- if (fs42.pathExistsSync(skillFile)) {
9892
+ const skillsDir = path50.join(srcDir, "skills");
9893
+ if (fs43.pathExistsSync(skillsDir)) {
9894
+ for (const skillName of fs43.readdirSync(skillsDir)) {
9895
+ const skillFile = path50.join(skillsDir, skillName, "SKILL.md");
9896
+ if (fs43.pathExistsSync(skillFile)) {
9615
9897
  entries.push({
9616
9898
  stableId: `skill.${skillName}`,
9617
- srcRelPath: path49.join("library", "global", "skills", skillName, "SKILL.md"),
9618
- destPath: path49.join(claudeDir, "skills", skillName, "SKILL.md")
9899
+ srcRelPath: path50.join("library", "global", "skills", skillName, "SKILL.md"),
9900
+ destPath: path50.join(claudeDir, "skills", skillName, "SKILL.md")
9619
9901
  });
9620
9902
  entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
9621
9903
  }
@@ -9626,7 +9908,7 @@ function collectSourceFiles(srcDir, claudeDir) {
9626
9908
  return entries;
9627
9909
  }
9628
9910
  async function applyInstall(options = {}) {
9629
- const { dryRun = false, force = false, check = false } = options;
9911
+ const { dryRun = false, force = false, check = false, internal } = options;
9630
9912
  const claudeDir = globalClaudeDir();
9631
9913
  const srcDir = globalSrcDir();
9632
9914
  const source = sourceVersion();
@@ -9637,7 +9919,10 @@ async function applyInstall(options = {}) {
9637
9919
  ]);
9638
9920
  const existingManifest = await readManifest();
9639
9921
  const manifestByDest = new Map(existingManifest?.files.map((f) => [f.destPath, f]) ?? []);
9640
- const sourceFiles = collectSourceFiles(srcDir, claudeDir);
9922
+ const internalChannel = await resolveInternalChannel(existingManifest, internal);
9923
+ const sourceFiles = collectSourceFiles(srcDir, claudeDir).filter(
9924
+ (entry) => internalChannel.enabled || !isInternalAsset(entry.stableId)
9925
+ );
9641
9926
  const result = {
9642
9927
  created: [],
9643
9928
  updated: [],
@@ -9647,11 +9932,12 @@ async function applyInstall(options = {}) {
9647
9932
  deleted: [],
9648
9933
  hookIds: [],
9649
9934
  configDirMigration,
9935
+ internalChannel,
9650
9936
  drift: false
9651
9937
  };
9652
9938
  const manifestFiles = [];
9653
9939
  for (const entry of sourceFiles) {
9654
- const srcPath = path49.join(packageRoot(), entry.srcRelPath);
9940
+ const srcPath = path50.join(packageRoot(), entry.srcRelPath);
9655
9941
  const rawContent = await readText(srcPath);
9656
9942
  if (rawContent === void 0) {
9657
9943
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -9680,7 +9966,7 @@ async function applyInstall(options = {}) {
9680
9966
  }
9681
9967
  continue;
9682
9968
  }
9683
- const destExists = fs42.pathExistsSync(entry.destPath);
9969
+ const destExists = fs43.pathExistsSync(entry.destPath);
9684
9970
  if (destExists) {
9685
9971
  const currentContent = await readText(entry.destPath);
9686
9972
  if (currentContent !== void 0) {
@@ -9720,7 +10006,7 @@ async function applyInstall(options = {}) {
9720
10006
  }
9721
10007
  let mergedSettings;
9722
10008
  if (!check) {
9723
- const fragmentPath = path49.join(srcDir, "settings-fragments", "hooks.json");
10009
+ const fragmentPath = path50.join(srcDir, "settings-fragments", "hooks.json");
9724
10010
  const fragments = await loadHooksFragment(fragmentPath);
9725
10011
  const settings = await readSettings();
9726
10012
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -9731,6 +10017,7 @@ async function applyInstall(options = {}) {
9731
10017
  result.hookIds = addedIds;
9732
10018
  }
9733
10019
  if (!check && existingManifest) {
10020
+ const emptiedDirs = /* @__PURE__ */ new Set();
9734
10021
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
9735
10022
  for (const entry of existingManifest.files) {
9736
10023
  if (currentDestPaths.has(entry.destPath)) continue;
@@ -9739,19 +10026,21 @@ async function applyInstall(options = {}) {
9739
10026
  manifestFiles.push(entry);
9740
10027
  continue;
9741
10028
  }
9742
- if (!fs42.pathExistsSync(entry.destPath)) continue;
10029
+ if (!fs43.pathExistsSync(entry.destPath)) continue;
9743
10030
  const content2 = await readText(entry.destPath);
9744
10031
  if (!content2) continue;
9745
10032
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
9746
10033
  const currentHash = hashContent(content2);
9747
10034
  if (hasHeader && currentHash === entry.hash) {
9748
- if (!dryRun) await fs42.remove(entry.destPath);
10035
+ if (!dryRun) await fs43.remove(entry.destPath);
10036
+ emptiedDirs.add(path50.dirname(entry.destPath));
9749
10037
  result.deleted.push(entry.destPath);
9750
10038
  } else {
9751
10039
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
9752
10040
  result.skipped.push(entry.destPath);
9753
10041
  }
9754
10042
  }
10043
+ if (!dryRun) await pruneEmptyDirsUpward(emptiedDirs, claudeDir);
9755
10044
  }
9756
10045
  if (!dryRun && !check && mergedSettings) {
9757
10046
  await writeSettings(mergedSettings);
@@ -9762,9 +10051,27 @@ async function applyInstall(options = {}) {
9762
10051
  existingManifest?.plugins
9763
10052
  );
9764
10053
  await writeManifest(manifest);
10054
+ if (internalChannel.source === "grandfathered")
10055
+ await writeInstallConfig({ internal: true, source: "grandfathered" });
10056
+ if (internalChannel.source === "flag")
10057
+ await writeInstallConfig({ internal: internalChannel.enabled, source: "explicit" });
9765
10058
  }
9766
10059
  return result;
9767
10060
  }
10061
+ function printInternalChannel(result, prefix) {
10062
+ const { source, enabled } = result.internalChannel;
10063
+ if (source === "grandfathered") {
10064
+ log(
10065
+ `${prefix}Internal channel kept on: this machine already had the request skills. Turn it off with \`haus install --no-internal\`.`
10066
+ );
10067
+ return;
10068
+ }
10069
+ if (!enabled && result.deleted.some((p) => p.includes("request") || p.includes("slack-cowork"))) {
10070
+ log(
10071
+ `${prefix}Internal channel is off \u2014 the request skills and slack-cowork were removed. Bring them back with \`haus install --internal\`.`
10072
+ );
10073
+ }
10074
+ }
9768
10075
  function printApplyResult(result, dryRun) {
9769
10076
  const prefix = dryRun ? "[dry-run] " : "";
9770
10077
  printConfigDirMigration(result.configDirMigration);
@@ -9788,6 +10095,7 @@ function printApplyResult(result, dryRun) {
9788
10095
  if (result.hookIds.length) {
9789
10096
  log(`${prefix}Hooks added: ${result.hookIds.join(", ")}`);
9790
10097
  }
10098
+ printInternalChannel(result, prefix);
9791
10099
  if (result.drift) {
9792
10100
  warn("Install drift detected \u2014 run `haus install` to sync.");
9793
10101
  }
@@ -9819,7 +10127,7 @@ function summarizeLockDiff(before, after) {
9819
10127
  // src/update/lockfile.ts
9820
10128
  import { constants as fsConstants } from "node:fs";
9821
10129
  import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
9822
- import path50 from "node:path";
10130
+ import path51 from "node:path";
9823
10131
  init_fs();
9824
10132
  init_paths();
9825
10133
  async function readLockSummary(root) {
@@ -9859,7 +10167,7 @@ async function backupLockfile(root) {
9859
10167
  await mkdir(backupDir, { recursive: true });
9860
10168
  let stamp = Date.now();
9861
10169
  for (; ; ) {
9862
- const dest = path50.join(backupDir, lockBackupFileName(stamp));
10170
+ const dest = path51.join(backupDir, lockBackupFileName(stamp));
9863
10171
  try {
9864
10172
  await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
9865
10173
  return;
@@ -9910,7 +10218,7 @@ function diffLock(before, after) {
9910
10218
  }
9911
10219
  async function hasLocalOverrides(root) {
9912
10220
  try {
9913
- await readFile2(path50.join(root, ".claude", "settings.json"), "utf8");
10221
+ await readFile2(path51.join(root, ".claude", "settings.json"), "utf8");
9914
10222
  return true;
9915
10223
  } catch {
9916
10224
  return false;
@@ -9955,7 +10263,7 @@ async function runUpdate(options) {
9955
10263
  return;
9956
10264
  }
9957
10265
  if (options.check) {
9958
- const pkgJson2 = await readJson(path51.join(packageRoot(), "package.json"));
10266
+ const pkgJson2 = await readJson(path52.join(packageRoot(), "package.json"));
9959
10267
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
9960
10268
  const [
9961
10269
  status,
@@ -10007,7 +10315,7 @@ async function runUpdate(options) {
10007
10315
  }
10008
10316
  return;
10009
10317
  }
10010
- const pkgJson = await readJson(path51.join(packageRoot(), "package.json"));
10318
+ const pkgJson = await readJson(path52.join(packageRoot(), "package.json"));
10011
10319
  const currentVersion = pkgJson?.version ?? "0.0.0";
10012
10320
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
10013
10321
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -10078,7 +10386,7 @@ async function runFromHookCheck(root) {
10078
10386
  try {
10079
10387
  const summary = await readLockSummary(root);
10080
10388
  if (summary.count === 0) return;
10081
- const pkgJson = await readJson(path51.join(packageRoot(), "package.json"));
10389
+ const pkgJson = await readJson(path52.join(packageRoot(), "package.json"));
10082
10390
  const currentVersion = pkgJson?.version ?? "0.0.0";
10083
10391
  const pin = readProjectCatalogPin(root);
10084
10392
  const honoursPin = pin.kind === "pinned" && summary.catalogRef === pin.ref;
@@ -10145,6 +10453,7 @@ async function refreshGlobalInstall() {
10145
10453
  async function ensureCatalogPinnedPlugins(root) {
10146
10454
  try {
10147
10455
  const pins = await loadCatalogPluginPins(root);
10456
+ await cleanupStaleGlobalPlugins(pins);
10148
10457
  const ensured = await ensureGlobalPlugins(pins);
10149
10458
  await recordManifestPlugins(
10150
10459
  pins.filter((pin) => ensured.ensured.includes(pin.id)).map((pin) => ({ ...pin, installedAt: (/* @__PURE__ */ new Date()).toISOString() }))
@@ -10153,11 +10462,21 @@ async function ensureCatalogPinnedPlugins(root) {
10153
10462
  warn(`Could not update global plugins: ${err instanceof Error ? err.message : String(err)}`);
10154
10463
  }
10155
10464
  }
10156
- async function detectGlobalPluginDrift(root) {
10465
+ async function detectGlobalPluginDrift(root, deps = {}) {
10157
10466
  try {
10158
10467
  const pins = await loadCatalogPluginPins(root);
10159
- const state = await probePluginEnv();
10160
- return pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
10468
+ const state = await probePluginEnv(deps);
10469
+ const manifest = await readManifest();
10470
+ const recorded = (manifest?.plugins ?? []).filter(isValidPin);
10471
+ const protectedPins = [...pins, ...DEFAULT_GLOBAL_PLUGINS];
10472
+ const stale = planStalePluginRemovals(
10473
+ protectedPins,
10474
+ [...recorded, ...FORMER_GLOBAL_PLUGINS],
10475
+ state
10476
+ );
10477
+ const pinned = pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
10478
+ if (pinned === null) return null;
10479
+ return pinned || stale.length > 0;
10161
10480
  } catch {
10162
10481
  return null;
10163
10482
  }
@@ -10262,7 +10581,7 @@ init_exec();
10262
10581
  init_logger();
10263
10582
  import { existsSync as existsSync2 } from "node:fs";
10264
10583
  import { readdir } from "node:fs/promises";
10265
- import path52 from "node:path";
10584
+ import path53 from "node:path";
10266
10585
  var GIT_LOCATION_VARS = [
10267
10586
  "GIT_DIR",
10268
10587
  "GIT_WORK_TREE",
@@ -10322,11 +10641,11 @@ async function runClone(url, opts = {}) {
10322
10641
  process.exitCode = 1;
10323
10642
  return;
10324
10643
  }
10325
- const target = path52.resolve(opts.dir?.trim() || repoNameFromUrl(url));
10644
+ const target = path53.resolve(opts.dir?.trim() || repoNameFromUrl(url));
10326
10645
  if (existsSync2(target) && !await isEmptyDirectory(target)) {
10327
10646
  const check = await checkExistingTarget(target, url);
10328
10647
  if (check.kind === "matches") {
10329
- log(`\u2022 ${path52.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
10648
+ log(`\u2022 ${path53.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
10330
10649
  return;
10331
10650
  }
10332
10651
  const message2 = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
@@ -10375,8 +10694,8 @@ function isCatalogItemInstallable(item) {
10375
10694
 
10376
10695
  // src/install/scaffold.ts
10377
10696
  init_logger();
10378
- import path53 from "node:path";
10379
- import fs43 from "fs-extra";
10697
+ import path54 from "node:path";
10698
+ import fs44 from "fs-extra";
10380
10699
  async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
10381
10700
  const scaffolded = [];
10382
10701
  const skipped = [];
@@ -10389,27 +10708,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
10389
10708
  skipped.push(name);
10390
10709
  if (outcome === "skipped-exists") skippedExisting.push(name);
10391
10710
  };
10392
- const realRoot = await fs43.realpath(catalogRoot).catch(() => null);
10711
+ const realRoot = await fs44.realpath(catalogRoot).catch(() => null);
10393
10712
  for (const item of items) {
10394
10713
  if (item.type !== "config") continue;
10395
- const sourcePath = path53.resolve(catalogRoot, item.path);
10396
- const rel = path53.relative(catalogRoot, sourcePath);
10397
- const escapes = rel === "" || path53.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
10714
+ const sourcePath = path54.resolve(catalogRoot, item.path);
10715
+ const rel = path54.relative(catalogRoot, sourcePath);
10716
+ const escapes = rel === "" || path54.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
10398
10717
  if (escapes) {
10399
10718
  warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
10400
10719
  continue;
10401
10720
  }
10402
- const realSource = await fs43.realpath(sourcePath).catch(() => null);
10721
+ const realSource = await fs44.realpath(sourcePath).catch(() => null);
10403
10722
  if (!realRoot || !realSource) {
10404
10723
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
10405
10724
  continue;
10406
10725
  }
10407
- const realRel = path53.relative(realRoot, realSource);
10408
- if (realRel !== "" && (path53.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
10726
+ const realRel = path54.relative(realRoot, realSource);
10727
+ if (realRel !== "" && (path54.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
10409
10728
  warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
10410
10729
  continue;
10411
10730
  }
10412
- const stat = await fs43.lstat(sourcePath).catch(() => null);
10731
+ const stat = await fs44.lstat(sourcePath).catch(() => null);
10413
10732
  if (!stat) {
10414
10733
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
10415
10734
  continue;
@@ -10419,14 +10738,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
10419
10738
  continue;
10420
10739
  }
10421
10740
  if (stat.isFile()) {
10422
- const filename = path53.basename(sourcePath);
10423
- const dest = path53.join(projectRoot, filename);
10741
+ const filename = path54.basename(sourcePath);
10742
+ const dest = path54.join(projectRoot, filename);
10424
10743
  record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
10425
10744
  } else if (stat.isDirectory()) {
10426
- const entries = await fs43.readdir(sourcePath);
10745
+ const entries = await fs44.readdir(sourcePath);
10427
10746
  for (const entry of entries) {
10428
- const src = path53.join(sourcePath, entry);
10429
- const dest = path53.join(projectRoot, entry);
10747
+ const src = path54.join(sourcePath, entry);
10748
+ const dest = path54.join(projectRoot, entry);
10430
10749
  record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
10431
10750
  }
10432
10751
  }
@@ -10434,42 +10753,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
10434
10753
  return { scaffolded, skipped, skippedExisting };
10435
10754
  }
10436
10755
  async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
10437
- const destDisplay = path53.relative(projectRoot, dest) || path53.basename(dest);
10438
- const srcStat = await fs43.lstat(src).catch(() => null);
10756
+ const destDisplay = path54.relative(projectRoot, dest) || path54.basename(dest);
10757
+ const srcStat = await fs44.lstat(src).catch(() => null);
10439
10758
  if (!srcStat) {
10440
- warn(`Skipping ${path53.basename(src)}: source not found`);
10759
+ warn(`Skipping ${path54.basename(src)}: source not found`);
10441
10760
  return "skipped-refused";
10442
10761
  }
10443
10762
  if (srcStat.isSymbolicLink()) {
10444
- warn(`Skipping ${path53.basename(src)}: source is a symlink`);
10763
+ warn(`Skipping ${path54.basename(src)}: source is a symlink`);
10445
10764
  return "skipped-refused";
10446
10765
  }
10447
- const exists = await fs43.pathExists(dest);
10766
+ const exists = await fs44.pathExists(dest);
10448
10767
  if (exists && !opts.force) {
10449
10768
  warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
10450
10769
  return "skipped-exists";
10451
10770
  }
10452
10771
  if (opts.dryRun) {
10453
- log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path53.basename(dest)} (${itemId})`);
10772
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path54.basename(dest)} (${itemId})`);
10454
10773
  return "scaffolded";
10455
10774
  }
10456
10775
  if (exists && opts.force) {
10457
- await fs43.remove(dest);
10776
+ await fs44.remove(dest);
10458
10777
  }
10459
- await fs43.ensureDir(path53.dirname(dest));
10460
- await fs43.copy(src, dest, {
10778
+ await fs44.ensureDir(path54.dirname(dest));
10779
+ await fs44.copy(src, dest, {
10461
10780
  overwrite: true,
10462
10781
  dereference: false,
10463
10782
  // A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
10464
10783
  filter: (s) => {
10465
10784
  try {
10466
- return !fs43.lstatSync(s).isSymbolicLink();
10785
+ return !fs44.lstatSync(s).isSymbolicLink();
10467
10786
  } catch {
10468
10787
  return false;
10469
10788
  }
10470
10789
  }
10471
10790
  });
10472
- if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path53.basename(dest)} (${itemId})`);
10791
+ if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path54.basename(dest)} (${itemId})`);
10473
10792
  return "scaffolded";
10474
10793
  }
10475
10794
 
@@ -10601,7 +10920,7 @@ async function runDecisionsGuard(options = {}) {
10601
10920
  }
10602
10921
 
10603
10922
  // src/commands/decisions.ts
10604
- import path54 from "node:path";
10923
+ import path55 from "node:path";
10605
10924
 
10606
10925
  // src/decisions/next-number.ts
10607
10926
  async function nextDecisionNumber(root) {
@@ -10712,7 +11031,7 @@ async function runDecisions(sub, arg, options = {}) {
10712
11031
  return;
10713
11032
  }
10714
11033
  if (sub === "validate") {
10715
- const target = arg ? path54.resolve(root, arg) : void 0;
11034
+ const target = arg ? path55.resolve(root, arg) : void 0;
10716
11035
  if (!target) {
10717
11036
  error("usage: haus decisions validate <path-to-decision.md>");
10718
11037
  process.exitCode = 1;
@@ -11181,27 +11500,27 @@ function guardBash(command) {
11181
11500
  }
11182
11501
 
11183
11502
  // src/security/guard-file-access.ts
11184
- import fs44 from "node:fs";
11185
- import path55 from "node:path";
11503
+ import fs45 from "node:fs";
11504
+ import path56 from "node:path";
11186
11505
  function matchesDenyTier(candidate) {
11187
11506
  const normalized = normalizePathForMatch(candidate);
11188
11507
  return DENY_PATH_REGEXES.some((re) => re.test(normalized));
11189
11508
  }
11190
11509
  function resolveOnDisk(candidate) {
11191
11510
  try {
11192
- const resolved = fs44.realpathSync(candidate);
11193
- return resolved === path55.resolve(candidate) ? {} : { resolved };
11511
+ const resolved = fs45.realpathSync(candidate);
11512
+ return resolved === path56.resolve(candidate) ? {} : { resolved };
11194
11513
  } catch {
11195
11514
  try {
11196
- if (fs44.lstatSync(candidate).isSymbolicLink()) return { unverifiable: true };
11515
+ if (fs45.lstatSync(candidate).isSymbolicLink()) return { unverifiable: true };
11197
11516
  } catch {
11198
11517
  }
11199
- const parent = path55.dirname(candidate);
11518
+ const parent = path56.dirname(candidate);
11200
11519
  if (parent === candidate) return {};
11201
11520
  const viaParent = resolveOnDisk(parent);
11202
11521
  if (viaParent.unverifiable) return { unverifiable: true };
11203
11522
  if (viaParent.resolved)
11204
- return { resolved: path55.join(viaParent.resolved, path55.basename(candidate)) };
11523
+ return { resolved: path56.join(viaParent.resolved, path56.basename(candidate)) };
11205
11524
  return {};
11206
11525
  }
11207
11526
  }
@@ -11751,9 +12070,9 @@ function buildOptInEligible(items, skipped) {
11751
12070
  }
11752
12071
 
11753
12072
  // src/commands/workspace/discover.ts
11754
- import path56 from "node:path";
12073
+ import path57 from "node:path";
11755
12074
  import fg6 from "fast-glob";
11756
- import YAML3 from "yaml";
12075
+ import YAML4 from "yaml";
11757
12076
  init_fs();
11758
12077
  init_logger();
11759
12078
  var DEFAULT_MAX_DEPTH = 3;
@@ -11794,10 +12113,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
11794
12113
  const gitDirs = /* @__PURE__ */ new Set();
11795
12114
  const manifestDirs = /* @__PURE__ */ new Set();
11796
12115
  for (const match of gitMatches) {
11797
- gitDirs.add(path56.posix.dirname(match));
12116
+ gitDirs.add(path57.posix.dirname(match));
11798
12117
  }
11799
12118
  for (const match of manifestMatches) {
11800
- manifestDirs.add(path56.posix.dirname(match));
12119
+ manifestDirs.add(path57.posix.dirname(match));
11801
12120
  }
11802
12121
  const repoRoots = [...gitDirs];
11803
12122
  const manifestSorted = [...manifestDirs].sort(
@@ -11814,9 +12133,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
11814
12133
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
11815
12134
  const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
11816
12135
  return mapWithConcurrency(repoRoots, async (relDir) => {
11817
- const absDir = path56.resolve(workspaceRoot, relDir);
11818
- const pkg = await readJson(path56.join(absDir, "package.json"));
11819
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path56.basename(absDir);
12136
+ const absDir = path57.resolve(workspaceRoot, relDir);
12137
+ const pkg = await readJson(path57.join(absDir, "package.json"));
12138
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path57.basename(absDir);
11820
12139
  let role = "auto";
11821
12140
  try {
11822
12141
  const scan = await scanProject(absDir);
@@ -11852,14 +12171,14 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
11852
12171
  };
11853
12172
  }
11854
12173
  function renderWorkspaceYaml(config) {
11855
- return YAML3.stringify({
12174
+ return YAML4.stringify({
11856
12175
  client: config.client,
11857
12176
  repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
11858
12177
  relationships: config.relationships
11859
12178
  });
11860
12179
  }
11861
12180
  async function runDiscover(workspaceRoot, opts = {}) {
11862
- const yamlPath = path56.join(workspaceRoot, "haus.workspace.yaml");
12181
+ const yamlPath = path57.join(workspaceRoot, "haus.workspace.yaml");
11863
12182
  const existingText = await readText(yamlPath);
11864
12183
  const existing = parseWorkspaceConfig(existingText);
11865
12184
  if (existingText && !existing) {
@@ -12221,23 +12540,39 @@ function toManifestPlugin(pin) {
12221
12540
  async function runInstall(options, deps = {}) {
12222
12541
  const ensure = deps.ensureGlobalPlugins ?? ensureGlobalPlugins;
12223
12542
  const probe = deps.probePluginEnv ?? probePluginEnv;
12543
+ const cleanup = deps.cleanupStaleGlobalPlugins ?? cleanupStaleGlobalPlugins;
12224
12544
  try {
12225
12545
  const result = await applyInstall({
12226
12546
  dryRun: options.dryRun,
12227
12547
  force: options.force,
12228
- check: options.check
12548
+ check: options.check,
12549
+ internal: options.internal
12229
12550
  });
12230
12551
  if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
12231
12552
  const pins = [...DEFAULT_GLOBAL_PLUGINS];
12232
12553
  if (options.check) {
12233
12554
  const state = await probe();
12234
12555
  const pluginDrift = pluginDriftStatus(planPluginActions(pins, state), { matchRef: false });
12556
+ const manifest = await readManifest();
12557
+ const recorded = (manifest?.plugins ?? []).filter(isValidPin);
12558
+ const protectedPins = [...pins, ...DEFAULT_GLOBAL_PLUGINS];
12559
+ const stale = planStalePluginRemovals(
12560
+ protectedPins,
12561
+ [...recorded, ...FORMER_GLOBAL_PLUGINS],
12562
+ state
12563
+ );
12235
12564
  if (pluginDrift === true) {
12236
12565
  log("Global plugin drift: a haus-managed plugin is missing or disabled.");
12237
12566
  }
12238
- if (result.drift || pluginDrift === true) process.exitCode = 1;
12567
+ if (stale.length > 0) {
12568
+ log(
12569
+ `Global plugin drift: formerly managed plugin(s) still installed: ${stale.map((p) => `${p.plugin}@${p.marketplace}`).join(", ")}.`
12570
+ );
12571
+ }
12572
+ if (result.drift || pluginDrift === true || stale.length > 0) process.exitCode = 1;
12239
12573
  return;
12240
12574
  }
12575
+ await cleanup(pins, { dryRun: options.dryRun });
12241
12576
  const ensured = await ensure(pins, { dryRun: options.dryRun });
12242
12577
  if (!options.dryRun) {
12243
12578
  await recordManifestPlugins(
@@ -12276,11 +12611,11 @@ async function runInstall(options, deps = {}) {
12276
12611
  }
12277
12612
 
12278
12613
  // src/commands/new.ts
12279
- import path58 from "node:path";
12614
+ import path59 from "node:path";
12280
12615
 
12281
12616
  // src/new-project/create-project.ts
12282
- import path57 from "node:path";
12283
- import fs45 from "fs-extra";
12617
+ import path58 from "node:path";
12618
+ import fs46 from "fs-extra";
12284
12619
  init_exec();
12285
12620
  init_logger();
12286
12621
 
@@ -12465,10 +12800,10 @@ var defaultDeps = {
12465
12800
  };
12466
12801
  var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
12467
12802
  async function recordPackageManager(projectPath, pm, deps) {
12468
- const pkgPath = path57.join(projectPath, "package.json");
12803
+ const pkgPath = path58.join(projectPath, "package.json");
12469
12804
  let pkg;
12470
12805
  try {
12471
- pkg = await fs45.readJson(pkgPath);
12806
+ pkg = await fs46.readJson(pkgPath);
12472
12807
  } catch {
12473
12808
  return {};
12474
12809
  }
@@ -12483,7 +12818,7 @@ async function recordPackageManager(projectPath, pm, deps) {
12483
12818
  note: `installed ${pm} is ${version}, which haus does not support \u2014 the package manager was not recorded, so the scanner will report it as unknown until you upgrade`
12484
12819
  };
12485
12820
  }
12486
- await fs45.writeJson(pkgPath, { ...pkg, packageManager: field }, { spaces: 2 });
12821
+ await fs46.writeJson(pkgPath, { ...pkg, packageManager: field }, { spaces: 2 });
12487
12822
  return {};
12488
12823
  }
12489
12824
  var CONFIG_TOOLING = {
@@ -12496,12 +12831,12 @@ var CONFIG_TOOLING = {
12496
12831
  }
12497
12832
  };
12498
12833
  async function ensureConfigTooling(projectPath, scaffolded) {
12499
- const needed = scaffolded.map((file) => CONFIG_TOOLING[path57.basename(file)]).filter((entry) => entry !== void 0);
12834
+ const needed = scaffolded.map((file) => CONFIG_TOOLING[path58.basename(file)]).filter((entry) => entry !== void 0);
12500
12835
  if (needed.length === 0) return [];
12501
- const pkgPath = path57.join(projectPath, "package.json");
12836
+ const pkgPath = path58.join(projectPath, "package.json");
12502
12837
  let pkg;
12503
12838
  try {
12504
- pkg = await fs45.readJson(pkgPath);
12839
+ pkg = await fs46.readJson(pkgPath);
12505
12840
  } catch {
12506
12841
  return [];
12507
12842
  }
@@ -12525,12 +12860,12 @@ async function ensureConfigTooling(projectPath, scaffolded) {
12525
12860
  }
12526
12861
  }
12527
12862
  if (!changed) return [];
12528
- await fs45.writeJson(pkgPath, { ...pkg, devDependencies, scripts }, { spaces: 2 });
12863
+ await fs46.writeJson(pkgPath, { ...pkg, devDependencies, scripts }, { spaces: 2 });
12529
12864
  return added;
12530
12865
  }
12531
12866
  async function createProject(opts, deps = defaultDeps) {
12532
12867
  const steps = [];
12533
- const projectPath = path57.resolve(opts.parentDir, opts.name);
12868
+ const projectPath = path58.resolve(opts.parentDir, opts.name);
12534
12869
  const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
12535
12870
  const fail = (step, detail) => {
12536
12871
  steps.push({ step, status: "failed", detail });
@@ -12538,20 +12873,20 @@ async function createProject(opts, deps = defaultDeps) {
12538
12873
  };
12539
12874
  const stack = getStack(opts.stackId);
12540
12875
  if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
12541
- const parentStat = await fs45.stat(path57.dirname(projectPath)).catch(() => null);
12876
+ const parentStat = await fs46.stat(path58.dirname(projectPath)).catch(() => null);
12542
12877
  if (!parentStat?.isDirectory()) {
12543
12878
  return fail(
12544
12879
  "preflight",
12545
12880
  `Parent directory ${opts.parentDir} is not an existing directory \u2014 pass an existing one to --dir.`
12546
12881
  );
12547
12882
  }
12548
- if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path57.dirname(projectPath) !== path57.resolve(opts.parentDir)) {
12883
+ if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path58.dirname(projectPath) !== path58.resolve(opts.parentDir)) {
12549
12884
  return fail(
12550
12885
  "preflight",
12551
12886
  `Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
12552
12887
  );
12553
12888
  }
12554
- if (await fs45.pathExists(projectPath)) {
12889
+ if (await fs46.pathExists(projectPath)) {
12555
12890
  return fail("preflight", `Target ${projectPath} already exists \u2014 refusing to write into it.`);
12556
12891
  }
12557
12892
  const scaffoldArgs = stack.scaffold.args.map(
@@ -12623,13 +12958,13 @@ async function createProject(opts, deps = defaultDeps) {
12623
12958
  });
12624
12959
  if (scaffold.exitCode !== 0) {
12625
12960
  const why = scaffold.killed ? `${pinned} did not complete: ${scaffold.stderr.trim() || `timed out after ${SCAFFOLD_TIMEOUT_MS}ms`}. Scaffolders that stall this way are usually waiting on a prompt despite --yes \u2014 run it yourself to see: npx ${npxArgs.join(" ")}` : `${pinned} exited ${scaffold.exitCode}.`;
12626
- const leftBehind = await fs45.pathExists(projectPath) ? ` The directory was left for inspection \u2014 review it and remove it manually if unwanted (haus never auto-deletes).` : "";
12961
+ const leftBehind = await fs46.pathExists(projectPath) ? ` The directory was left for inspection \u2014 review it and remove it manually if unwanted (haus never auto-deletes).` : "";
12627
12962
  return fail("scaffold", `${why}${leftBehind}`);
12628
12963
  }
12629
12964
  steps.push({ step: "scaffold", status: "done" });
12630
12965
  phase = "git";
12631
12966
  if (!opts.json) log("\u25B6 git: init/branch/commit");
12632
- if (!await fs45.pathExists(path57.join(projectPath, ".git"))) {
12967
+ if (!await fs46.pathExists(path58.join(projectPath, ".git"))) {
12633
12968
  const init = await deps.git(["init"], { cwd: projectPath });
12634
12969
  if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
12635
12970
  }
@@ -12855,7 +13190,7 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
12855
13190
  if (flags.dir !== void 0 && flags.dir.trim() === "") {
12856
13191
  return { ok: false, error: "--dir needs a path (omit it to scaffold into the cwd)." };
12857
13192
  }
12858
- const parentDir = flags.dir === void 0 ? cwd : path58.resolve(cwd, flags.dir);
13193
+ const parentDir = flags.dir === void 0 ? cwd : path59.resolve(cwd, flags.dir);
12859
13194
  const pm = flags.pm ?? "yarn";
12860
13195
  if (!PMS.includes(pm)) {
12861
13196
  return { ok: false, error: `Unsupported --pm "${pm}" \u2014 use yarn, npm, or pnpm.` };
@@ -12953,7 +13288,7 @@ async function runRecommend(options) {
12953
13288
  }
12954
13289
 
12955
13290
  // src/commands/remove.ts
12956
- import path59 from "node:path";
13291
+ import path60 from "node:path";
12957
13292
  init_fs();
12958
13293
  init_logger();
12959
13294
  init_paths();
@@ -13060,7 +13395,7 @@ async function runRemove(ids, options = {}) {
13060
13395
  }
13061
13396
  const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
13062
13397
  const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
13063
- report.backupDir = backupRoot === null ? null : path59.basename(backupRoot);
13398
+ report.backupDir = backupRoot === null ? null : path60.basename(backupRoot);
13064
13399
  for (const { id, existing } of cleared) {
13065
13400
  await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
13066
13401
  }
@@ -13101,7 +13436,9 @@ async function runRescan(options = {}) {
13101
13436
  const context = await scanProject(root, { persist: !dryRun });
13102
13437
  if (!dryRun) {
13103
13438
  try {
13104
- await ensureGlobalPlugins(await loadCatalogPluginPins(root));
13439
+ const pins = await loadCatalogPluginPins(root);
13440
+ await cleanupStaleGlobalPlugins(pins);
13441
+ await ensureGlobalPlugins(pins);
13105
13442
  } catch (err) {
13106
13443
  warn(`Could not refresh global plugins: ${err instanceof Error ? err.message : String(err)}`);
13107
13444
  }
@@ -13148,8 +13485,8 @@ async function runScan(options) {
13148
13485
  }
13149
13486
 
13150
13487
  // src/commands/undo.ts
13151
- import path60 from "node:path";
13152
- import fs46 from "fs-extra";
13488
+ import path61 from "node:path";
13489
+ import fs47 from "fs-extra";
13153
13490
 
13154
13491
  // src/claude/managed-paths.ts
13155
13492
  init_paths();
@@ -13168,7 +13505,7 @@ init_paths();
13168
13505
  async function collectCoreManagedPaths(root) {
13169
13506
  const existing = [];
13170
13507
  for (const abs of coreManagedAbsolutePaths(root)) {
13171
- if (await fs46.pathExists(abs)) existing.push(abs);
13508
+ if (await fs47.pathExists(abs)) existing.push(abs);
13172
13509
  }
13173
13510
  return existing;
13174
13511
  }
@@ -13182,7 +13519,7 @@ async function collectLockTrackedPaths(root) {
13182
13519
  const unsafeByRel = await unsafeReasonsFor(root, relPaths);
13183
13520
  const existing = await resolveExistingTrackedPaths(root, id, relPaths, log, unsafeByRel);
13184
13521
  for (const [rel, reason] of unsafeByRel) {
13185
- if (reason !== null) preserved.push(path60.resolve(root, rel));
13522
+ if (reason !== null) preserved.push(path61.resolve(root, rel));
13186
13523
  }
13187
13524
  if (existing.length === 0) continue;
13188
13525
  const gate = await hashGateEntry(root, { hash: row.hash }, existing);
@@ -13198,51 +13535,51 @@ async function collectLockTrackedPaths(root) {
13198
13535
  }
13199
13536
  async function settingsHasHausContent(root) {
13200
13537
  const settingsPath = claudePath(root, "settings.json");
13201
- if (!await fs46.pathExists(settingsPath)) return false;
13538
+ if (!await fs47.pathExists(settingsPath)) return false;
13202
13539
  const settings = await readProjectSettings(root);
13203
13540
  return settings._haus != null;
13204
13541
  }
13205
13542
  async function claudeMdHasHausBlock(root) {
13206
- const filePath = path60.join(root, "CLAUDE.md");
13207
- if (!await fs46.pathExists(filePath)) return false;
13208
- const text = await fs46.readFile(filePath, "utf8");
13543
+ const filePath = path61.join(root, "CLAUDE.md");
13544
+ if (!await fs47.pathExists(filePath)) return false;
13545
+ const text = await fs47.readFile(filePath, "utf8");
13209
13546
  return text.includes(BLOCK_BEGIN);
13210
13547
  }
13211
13548
  async function stripProjectSettings(root) {
13212
13549
  const settingsPath = claudePath(root, "settings.json");
13213
- if (!await fs46.pathExists(settingsPath)) return false;
13550
+ if (!await fs47.pathExists(settingsPath)) return false;
13214
13551
  let settings = await readProjectSettings(root);
13215
13552
  settings = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
13216
13553
  const hasContent = Object.keys(settings).length > 0;
13217
13554
  if (hasContent) {
13218
13555
  await writeProjectSettings(root, settings);
13219
- log(`Stripped haus rules from ${path60.relative(root, settingsPath)} (user settings preserved).`);
13556
+ log(`Stripped haus rules from ${path61.relative(root, settingsPath)} (user settings preserved).`);
13220
13557
  return true;
13221
13558
  }
13222
- await fs46.remove(settingsPath);
13223
- log(`Removed ${path60.relative(root, settingsPath)} (no user-owned settings remained).`);
13559
+ await fs47.remove(settingsPath);
13560
+ log(`Removed ${path61.relative(root, settingsPath)} (no user-owned settings remained).`);
13224
13561
  return true;
13225
13562
  }
13226
13563
  async function stripRootClaudeMd(root) {
13227
- const filePath = path60.join(root, "CLAUDE.md");
13228
- if (!await fs46.pathExists(filePath)) return false;
13229
- const prev = await fs46.readFile(filePath, "utf8");
13564
+ const filePath = path61.join(root, "CLAUDE.md");
13565
+ if (!await fs47.pathExists(filePath)) return false;
13566
+ const prev = await fs47.readFile(filePath, "utf8");
13230
13567
  if (!prev.includes(BLOCK_BEGIN)) return false;
13231
13568
  const next = stripHausBlock(prev);
13232
13569
  if (next.length === 0) {
13233
- await fs46.remove(filePath);
13570
+ await fs47.remove(filePath);
13234
13571
  log("Removed CLAUDE.md (only contained haus import block).");
13235
13572
  } else {
13236
- await fs46.writeFile(filePath, next, "utf8");
13573
+ await fs47.writeFile(filePath, next, "utf8");
13237
13574
  log("Removed haus import block from CLAUDE.md (user content preserved).");
13238
13575
  }
13239
13576
  return true;
13240
13577
  }
13241
13578
  async function pruneDirIfEmpty(dir) {
13242
- if (!await fs46.pathExists(dir)) return;
13579
+ if (!await fs47.pathExists(dir)) return;
13243
13580
  try {
13244
- const entries = await fs46.readdir(dir);
13245
- if (entries.length === 0) await fs46.remove(dir);
13581
+ const entries = await fs47.readdir(dir);
13582
+ if (entries.length === 0) await fs47.remove(dir);
13246
13583
  } catch {
13247
13584
  }
13248
13585
  }
@@ -13251,13 +13588,13 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
13251
13588
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13252
13589
  const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
13253
13590
  for (const abs of managedAbsPaths) {
13254
- if (!await fs46.pathExists(abs)) continue;
13255
- const rel = path60.relative(root, abs);
13256
- const backupPath = path60.join(backupRoot, rel);
13257
- await fs46.ensureDir(path60.dirname(backupPath));
13258
- await fs46.copy(abs, backupPath);
13591
+ if (!await fs47.pathExists(abs)) continue;
13592
+ const rel = path61.relative(root, abs);
13593
+ const backupPath = path61.join(backupRoot, rel);
13594
+ await fs47.ensureDir(path61.dirname(backupPath));
13595
+ await fs47.copy(abs, backupPath);
13259
13596
  }
13260
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path60.relative(root, backupRoot)}.`);
13597
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path61.relative(root, backupRoot)}.`);
13261
13598
  }
13262
13599
  async function runUndo(options) {
13263
13600
  const root = options.root ?? process.cwd();
@@ -13271,7 +13608,7 @@ async function runUndo(options) {
13271
13608
  log("Nothing to remove: no haus-managed files found in this directory.");
13272
13609
  return;
13273
13610
  }
13274
- const relTargets = managed.map((p) => path60.relative(root, p));
13611
+ const relTargets = managed.map((p) => path61.relative(root, p));
13275
13612
  const summaryParts = [...relTargets];
13276
13613
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
13277
13614
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -13297,9 +13634,9 @@ User-owned .claude/ files will be preserved.`
13297
13634
  }
13298
13635
  await backupManagedFilesBeforeUndo(root, managed);
13299
13636
  for (const abs of coreManaged) {
13300
- if (!await fs46.pathExists(abs)) continue;
13301
- await fs46.remove(abs);
13302
- log(`Removed ${path60.relative(root, abs)}`);
13637
+ if (!await fs47.pathExists(abs)) continue;
13638
+ await fs47.remove(abs);
13639
+ log(`Removed ${path61.relative(root, abs)}`);
13303
13640
  }
13304
13641
  for (const { id, existing } of lockRemovable) {
13305
13642
  await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
@@ -13316,8 +13653,8 @@ init_fs();
13316
13653
  init_logger();
13317
13654
  init_paths();
13318
13655
  import crypto4 from "node:crypto";
13319
- import path61 from "node:path";
13320
- import fs47 from "fs-extra";
13656
+ import path62 from "node:path";
13657
+ import fs48 from "fs-extra";
13321
13658
  async function runUninstall(options = {}) {
13322
13659
  const { force = false, removePlugins = removeGlobalPlugins } = options;
13323
13660
  const manifest = await readManifest();
@@ -13354,7 +13691,7 @@ async function runUninstall(options = {}) {
13354
13691
  );
13355
13692
  }
13356
13693
  for (const entry of manifest.files) {
13357
- const exists = fs47.pathExistsSync(entry.destPath);
13694
+ const exists = fs48.pathExistsSync(entry.destPath);
13358
13695
  if (!exists) continue;
13359
13696
  const content2 = await readText(entry.destPath);
13360
13697
  if (content2 === void 0) continue;
@@ -13372,23 +13709,24 @@ async function runUninstall(options = {}) {
13372
13709
  result.skipped.push(entry.destPath);
13373
13710
  continue;
13374
13711
  }
13375
- await fs47.remove(entry.destPath);
13376
- await pruneEmptyDir(path61.dirname(entry.destPath));
13712
+ await fs48.remove(entry.destPath);
13713
+ await pruneEmptyDir(path62.dirname(entry.destPath));
13377
13714
  result.deleted.push(entry.destPath);
13378
13715
  }
13379
13716
  const settings = await readSettings();
13380
13717
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
13381
13718
  await writeSettings(stripped);
13382
13719
  result.hooksStripped = true;
13383
- const hausDir = path61.join(globalClaudeDir(), "haus");
13720
+ const hausDir = path62.join(globalClaudeDir(), "haus");
13384
13721
  const manifestPath2 = hausManifestPath();
13385
- if (fs47.pathExistsSync(manifestPath2)) {
13386
- await fs47.remove(manifestPath2);
13722
+ if (fs48.pathExistsSync(manifestPath2)) {
13723
+ await fs48.remove(manifestPath2);
13387
13724
  }
13388
- if (fs47.pathExistsSync(hausDir)) {
13725
+ await removeInstallConfig();
13726
+ if (fs48.pathExistsSync(hausDir)) {
13389
13727
  try {
13390
- const remaining = await fs47.readdir(hausDir);
13391
- if (remaining.length === 0) await fs47.remove(hausDir);
13728
+ const remaining = await fs48.readdir(hausDir);
13729
+ if (remaining.length === 0) await fs48.remove(hausDir);
13392
13730
  } catch {
13393
13731
  }
13394
13732
  }
@@ -13457,15 +13795,15 @@ async function runUninstallCommand(options) {
13457
13795
  }
13458
13796
 
13459
13797
  // src/commands/validate-catalog.ts
13460
- import path63 from "node:path";
13798
+ import path64 from "node:path";
13461
13799
 
13462
13800
  // src/catalog/validate-core.ts
13463
13801
  init_forbidden_content();
13464
13802
  init_manifest_item_fields();
13465
13803
  init_path_safety();
13466
13804
  init_validation_rules2();
13467
- import fs48 from "node:fs";
13468
- import path62 from "node:path";
13805
+ import fs49 from "node:fs";
13806
+ import path63 from "node:path";
13469
13807
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
13470
13808
  var ITEM_ID_PATTERN = /^haus\./;
13471
13809
  function auditForbiddenStacks(items) {
@@ -13625,14 +13963,14 @@ function checkRequiredFrontmatter(text, label) {
13625
13963
  }
13626
13964
  function readForAudit(absPath, label, failures) {
13627
13965
  try {
13628
- return fs48.readFileSync(absPath, "utf8");
13966
+ return fs49.readFileSync(absPath, "utf8");
13629
13967
  } catch (err) {
13630
13968
  failures.push(`${label}: unreadable (${err instanceof Error ? err.message : String(err)})`);
13631
13969
  return null;
13632
13970
  }
13633
13971
  }
13634
13972
  function auditTemplateContent(manifestDir, absPath, itemId) {
13635
- const rel = path62.relative(manifestDir, absPath);
13973
+ const rel = path63.relative(manifestDir, absPath);
13636
13974
  const failures = [];
13637
13975
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
13638
13976
  if (text === null) return failures;
@@ -13654,55 +13992,55 @@ function auditShippedFiles(manifestDir, items) {
13654
13992
  const failures = [];
13655
13993
  for (const item of items) {
13656
13994
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
13657
- const absPath = path62.join(manifestDir, item.path);
13995
+ const absPath = path63.join(manifestDir, item.path);
13658
13996
  if (item.type === "skill") {
13659
- const skillMd = path62.join(absPath, "SKILL.md");
13660
- if (!fs48.existsSync(skillMd)) {
13661
- failures.push(`${item.id}: missing ${path62.relative(manifestDir, skillMd)}`);
13997
+ const skillMd = path63.join(absPath, "SKILL.md");
13998
+ if (!fs49.existsSync(skillMd)) {
13999
+ failures.push(`${item.id}: missing ${path63.relative(manifestDir, skillMd)}`);
13662
14000
  continue;
13663
14001
  }
13664
14002
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
13665
14003
  if (text === null) continue;
13666
14004
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
13667
14005
  failures.push(
13668
- ...auditForbiddenTagsInText(text, `${item.id}: ${path62.relative(manifestDir, skillMd)}`)
14006
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path63.relative(manifestDir, skillMd)}`)
13669
14007
  );
13670
14008
  } else if (item.type === "agent") {
13671
- if (!fs48.existsSync(absPath)) {
14009
+ if (!fs49.existsSync(absPath)) {
13672
14010
  failures.push(`${item.id}: missing agent file ${item.path}`);
13673
14011
  continue;
13674
14012
  }
13675
- const rel = path62.relative(manifestDir, absPath);
14013
+ const rel = path63.relative(manifestDir, absPath);
13676
14014
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
13677
14015
  if (text === null) continue;
13678
14016
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
13679
14017
  failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
13680
14018
  } else if (item.type === "template") {
13681
- if (!fs48.existsSync(absPath)) {
14019
+ if (!fs49.existsSync(absPath)) {
13682
14020
  failures.push(`${item.id}: missing template file ${item.path}`);
13683
14021
  continue;
13684
14022
  }
13685
14023
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
13686
14024
  } else if (item.type === "command") {
13687
- if (!fs48.existsSync(absPath)) {
14025
+ if (!fs49.existsSync(absPath)) {
13688
14026
  failures.push(`${item.id}: missing command file ${item.path}`);
13689
14027
  continue;
13690
14028
  }
13691
- const rel = path62.relative(manifestDir, absPath);
14029
+ const rel = path63.relative(manifestDir, absPath);
13692
14030
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
13693
14031
  if (text === null) continue;
13694
14032
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
13695
14033
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
13696
14034
  } else if (item.type === "config") {
13697
- if (!fs48.existsSync(absPath)) {
14035
+ if (!fs49.existsSync(absPath)) {
13698
14036
  failures.push(`${item.id}: missing config path ${item.path}`);
13699
14037
  }
13700
14038
  } else if (item.type === "rule") {
13701
- if (!fs48.existsSync(absPath)) {
14039
+ if (!fs49.existsSync(absPath)) {
13702
14040
  failures.push(`${item.id}: missing rule file ${item.path}`);
13703
14041
  }
13704
14042
  } else if (item.type === "asset") {
13705
- if (!fs48.existsSync(absPath)) {
14043
+ if (!fs49.existsSync(absPath)) {
13706
14044
  failures.push(`${item.id}: missing asset file ${item.path}`);
13707
14045
  }
13708
14046
  }
@@ -13737,7 +14075,7 @@ function resolveItemType(relPath, pathTypeMap) {
13737
14075
  function walkAllFiles(dir, fn, failures) {
13738
14076
  let entries;
13739
14077
  try {
13740
- entries = fs48.readdirSync(dir, { withFileTypes: true });
14078
+ entries = fs49.readdirSync(dir, { withFileTypes: true });
13741
14079
  } catch (err) {
13742
14080
  failures.push(
13743
14081
  `${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
@@ -13746,7 +14084,7 @@ function walkAllFiles(dir, fn, failures) {
13746
14084
  }
13747
14085
  for (const entry of entries) {
13748
14086
  if (entry.name === ".DS_Store") continue;
13749
- const full = path62.join(dir, entry.name);
14087
+ const full = path63.join(dir, entry.name);
13750
14088
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
13751
14089
  else fn(full);
13752
14090
  }
@@ -13770,12 +14108,12 @@ function auditDiskOrphans(manifestDir, items) {
13770
14108
  const failures = [];
13771
14109
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
13772
14110
  for (const dir of CONTENT_ROOTS) {
13773
- const abs = path62.join(manifestDir, dir);
13774
- if (!fs48.existsSync(abs)) continue;
14111
+ const abs = path63.join(manifestDir, dir);
14112
+ if (!fs49.existsSync(abs)) continue;
13775
14113
  walkAllFiles(
13776
14114
  abs,
13777
14115
  (file) => {
13778
- const relPath = path62.relative(manifestDir, file).replace(/\\/g, "/");
14116
+ const relPath = path63.relative(manifestDir, file).replace(/\\/g, "/");
13779
14117
  if (!isPathClaimed(relPath, claimedPaths)) {
13780
14118
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
13781
14119
  }
@@ -13788,7 +14126,7 @@ function auditDiskOrphans(manifestDir, items) {
13788
14126
  function walkMd(dir, fn, failures) {
13789
14127
  let entries;
13790
14128
  try {
13791
- entries = fs48.readdirSync(dir, { withFileTypes: true });
14129
+ entries = fs49.readdirSync(dir, { withFileTypes: true });
13792
14130
  } catch (err) {
13793
14131
  failures.push(
13794
14132
  `${dir}: unreadable subtree (${err instanceof Error ? err.message : String(err)})`
@@ -13796,7 +14134,7 @@ function walkMd(dir, fn, failures) {
13796
14134
  return;
13797
14135
  }
13798
14136
  for (const entry of entries) {
13799
- const full = path62.join(dir, entry.name);
14137
+ const full = path63.join(dir, entry.name);
13800
14138
  if (entry.isDirectory()) walkMd(full, fn, failures);
13801
14139
  else if (entry.name.endsWith(".md")) fn(full);
13802
14140
  }
@@ -13806,13 +14144,13 @@ function auditMarkdownContent(manifestDir, items) {
13806
14144
  const pathSourceMap = buildItemPathSourceMap(items);
13807
14145
  const pathTypeMap = buildItemPathTypeMap(items);
13808
14146
  for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
13809
- const abs = path62.join(manifestDir, dir);
13810
- if (!fs48.existsSync(abs)) continue;
14147
+ const abs = path63.join(manifestDir, dir);
14148
+ if (!fs49.existsSync(abs)) continue;
13811
14149
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
13812
14150
  walkMd(
13813
14151
  abs,
13814
14152
  (file) => {
13815
- const rel = path62.relative(manifestDir, file);
14153
+ const rel = path63.relative(manifestDir, file);
13816
14154
  const text = readForAudit(file, rel, failures);
13817
14155
  if (text === null) return;
13818
14156
  const norm = rel.replace(/\\/g, "/");
@@ -13860,8 +14198,8 @@ async function runValidateCatalog(manifestPath2) {
13860
14198
  process.exitCode = 1;
13861
14199
  return;
13862
14200
  }
13863
- const abs = path63.resolve(process.cwd(), manifestPath2);
13864
- const manifestDir = path63.dirname(abs);
14201
+ const abs = path64.resolve(process.cwd(), manifestPath2);
14202
+ const manifestDir = path64.dirname(abs);
13865
14203
  const data = await readJson(abs);
13866
14204
  if (!data?.items) {
13867
14205
  error(`Could not read catalog manifest at ${abs}`);
@@ -13882,8 +14220,8 @@ init_logger();
13882
14220
 
13883
14221
  // src/workspace/worktree/add.ts
13884
14222
  init_fs();
13885
- import path70 from "node:path";
13886
- import fs52 from "fs-extra";
14223
+ import path71 from "node:path";
14224
+ import fs53 from "fs-extra";
13887
14225
 
13888
14226
  // src/workspace/worktree/git-worktree.ts
13889
14227
  init_exec();
@@ -14003,13 +14341,13 @@ async function hasUnpushedWork(cwd) {
14003
14341
  }
14004
14342
 
14005
14343
  // src/workspace/worktree/hydrate.ts
14006
- import path66 from "node:path";
14007
- import fs50 from "fs-extra";
14344
+ import path67 from "node:path";
14345
+ import fs51 from "fs-extra";
14008
14346
 
14009
14347
  // src/workspace/worktree/cow-copy.ts
14010
14348
  init_exec();
14011
- import path64 from "node:path";
14012
- import fs49 from "fs-extra";
14349
+ import path65 from "node:path";
14350
+ import fs50 from "fs-extra";
14013
14351
  async function getMacFilesystemType(dir) {
14014
14352
  const df = await runCommand("df", ["-P", dir]);
14015
14353
  if (df.exitCode !== 0) return void 0;
@@ -14040,14 +14378,14 @@ async function detectCowStrategy(dir) {
14040
14378
  return "unknown-platform";
14041
14379
  }
14042
14380
  async function cowCopyDir(src, dest) {
14043
- const srcExists = await fs49.pathExists(src);
14381
+ const srcExists = await fs50.pathExists(src);
14044
14382
  if (!srcExists)
14045
14383
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
14046
- const strategy = await detectCowStrategy(path64.dirname(dest));
14384
+ const strategy = await detectCowStrategy(path65.dirname(dest));
14047
14385
  if (strategy === "unsupported" || strategy === "unknown-platform") {
14048
14386
  return { strategy, attempted: false, ok: false };
14049
14387
  }
14050
- await fs49.ensureDir(path64.dirname(dest));
14388
+ await fs50.ensureDir(path65.dirname(dest));
14051
14389
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
14052
14390
  const result = await runCommand("cp", args);
14053
14391
  return {
@@ -14061,7 +14399,7 @@ async function cowCopyDir(src, dest) {
14061
14399
  // src/workspace/worktree/install.ts
14062
14400
  init_exec();
14063
14401
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
14064
- import path65 from "node:path";
14402
+ import path66 from "node:path";
14065
14403
  import fg7 from "fast-glob";
14066
14404
  function detectInstallPlan(signals) {
14067
14405
  if (signals.hasYarnLock) {
@@ -14082,10 +14420,10 @@ function detectInstallPlan(signals) {
14082
14420
  return { manager: "none" };
14083
14421
  }
14084
14422
  async function computeLockfileSignals(dir) {
14085
- const hasYarnLock = existsSync3(path65.join(dir, "yarn.lock"));
14086
- const hasPnpmLock = existsSync3(path65.join(dir, "pnpm-lock.yaml"));
14087
- const hasPackageLock = existsSync3(path65.join(dir, "package-lock.json"));
14088
- const hasComposerLock = existsSync3(path65.join(dir, "composer.lock"));
14423
+ const hasYarnLock = existsSync3(path66.join(dir, "yarn.lock"));
14424
+ const hasPnpmLock = existsSync3(path66.join(dir, "pnpm-lock.yaml"));
14425
+ const hasPackageLock = existsSync3(path66.join(dir, "package-lock.json"));
14426
+ const hasComposerLock = existsSync3(path66.join(dir, "composer.lock"));
14089
14427
  const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
14090
14428
  cwd: dir,
14091
14429
  onlyFiles: true,
@@ -14099,7 +14437,7 @@ async function computeLockfileSignals(dir) {
14099
14437
  });
14100
14438
  const hasDotnetProject = dotnetFiles.length > 0;
14101
14439
  let hasPackageManagerField = false;
14102
- const pkgPath = path65.join(dir, "package.json");
14440
+ const pkgPath = path66.join(dir, "package.json");
14103
14441
  if (existsSync3(pkgPath)) {
14104
14442
  try {
14105
14443
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -14142,7 +14480,7 @@ function missingYarnrcEnvVars(content2, env) {
14142
14480
  return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
14143
14481
  }
14144
14482
  function missingYarnrcEnvVarsInDir(dir, env) {
14145
- const yarnrcPath = path65.join(dir, ".yarnrc.yml");
14483
+ const yarnrcPath = path66.join(dir, ".yarnrc.yml");
14146
14484
  if (!existsSync3(yarnrcPath)) return [];
14147
14485
  try {
14148
14486
  return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
@@ -14238,13 +14576,13 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
14238
14576
  const start = Date.now();
14239
14577
  const targets = [];
14240
14578
  for (const target of HYDRATION_TARGETS) {
14241
- const src = path66.join(mainMemberPath, target);
14242
- const dest = path66.join(worktreeMemberPath, target);
14243
- if (!await fs50.pathExists(src)) continue;
14244
- const destExists = await fs50.pathExists(dest);
14579
+ const src = path67.join(mainMemberPath, target);
14580
+ const dest = path67.join(worktreeMemberPath, target);
14581
+ if (!await fs51.pathExists(src)) continue;
14582
+ const destExists = await fs51.pathExists(dest);
14245
14583
  if (!opts.force && destExists) continue;
14246
14584
  if (opts.force && destExists) {
14247
- await fs50.remove(dest);
14585
+ await fs51.remove(dest);
14248
14586
  }
14249
14587
  const copy = await cowCopyDir(src, dest);
14250
14588
  targets.push({ target, copy });
@@ -14257,11 +14595,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
14257
14595
 
14258
14596
  // src/workspace/worktree/local-files.ts
14259
14597
  init_exec();
14260
- import path67 from "node:path";
14261
- import fs51 from "fs-extra";
14598
+ import path68 from "node:path";
14599
+ import fs52 from "fs-extra";
14262
14600
  var MACHINE_LOCAL_FILES = [
14263
14601
  "docker-compose.local.yml",
14264
- path67.join(".claude", "settings.local.json")
14602
+ path68.join(".claude", "settings.local.json")
14265
14603
  ];
14266
14604
  async function isTracked(repoRoot, relFile) {
14267
14605
  const gitPath = relFile.replace(/\\/g, "/");
@@ -14271,12 +14609,12 @@ async function isTracked(repoRoot, relFile) {
14271
14609
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
14272
14610
  const copied = [];
14273
14611
  for (const rel of MACHINE_LOCAL_FILES) {
14274
- const src = path67.join(mainMemberPath, rel);
14275
- if (!await fs51.pathExists(src)) continue;
14612
+ const src = path68.join(mainMemberPath, rel);
14613
+ if (!await fs52.pathExists(src)) continue;
14276
14614
  if (await isTracked(mainMemberPath, rel)) continue;
14277
- const dest = path67.join(worktreeMemberPath, rel);
14278
- await fs51.ensureDir(path67.dirname(dest));
14279
- await fs51.copy(src, dest, { overwrite: true });
14615
+ const dest = path68.join(worktreeMemberPath, rel);
14616
+ await fs52.ensureDir(path68.dirname(dest));
14617
+ await fs52.copy(src, dest, { overwrite: true });
14280
14618
  copied.push(rel);
14281
14619
  }
14282
14620
  return copied;
@@ -14284,15 +14622,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
14284
14622
 
14285
14623
  // src/workspace/worktree/root.ts
14286
14624
  import { existsSync as existsSync4 } from "node:fs";
14287
- import path68 from "node:path";
14625
+ import path69 from "node:path";
14288
14626
  async function resolveWorkspaceForWorktree(start) {
14289
14627
  const rootInfo = await resolveRoots(start);
14290
14628
  if (!rootInfo.isGitRepo) {
14291
14629
  return { ok: false, reason: "Not inside a git repository." };
14292
14630
  }
14293
14631
  const workspaceRoot = rootInfo.mainRoot;
14294
- const hasYaml = existsSync4(path68.join(workspaceRoot, WORKSPACE_FILE));
14295
- const hasManifest = existsSync4(path68.join(workspaceRoot, REPOS_MANIFEST_FILE));
14632
+ const hasYaml = existsSync4(path69.join(workspaceRoot, WORKSPACE_FILE));
14633
+ const hasManifest = existsSync4(path69.join(workspaceRoot, REPOS_MANIFEST_FILE));
14296
14634
  if (!hasYaml && !hasManifest) {
14297
14635
  return {
14298
14636
  ok: false,
@@ -14318,10 +14656,10 @@ function selectMembers(members, only) {
14318
14656
 
14319
14657
  // src/workspace/worktree/state.ts
14320
14658
  init_fs();
14321
- import path69 from "node:path";
14659
+ import path70 from "node:path";
14322
14660
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
14323
14661
  function worktreesDir(mainRoot) {
14324
- return path69.join(mainRoot, ".claude", "worktrees");
14662
+ return path70.join(mainRoot, ".claude", "worktrees");
14325
14663
  }
14326
14664
  function isValidWorktreeSlug(slug) {
14327
14665
  if (slug === "" || slug === "." || slug === "..") return false;
@@ -14334,22 +14672,22 @@ function worktreePath(mainRoot, slug) {
14334
14672
  `Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
14335
14673
  );
14336
14674
  }
14337
- return path69.join(worktreesDir(mainRoot), slug);
14675
+ return path70.join(worktreesDir(mainRoot), slug);
14338
14676
  }
14339
14677
  function slugFromWorktreePath(mainRoot, somePath) {
14340
14678
  const dir = worktreesDir(mainRoot);
14341
- const prefix = `${dir}${path69.sep}`;
14679
+ const prefix = `${dir}${path70.sep}`;
14342
14680
  if (!somePath.startsWith(prefix)) return void 0;
14343
- const segment = somePath.slice(prefix.length).split(path69.sep)[0];
14681
+ const segment = somePath.slice(prefix.length).split(path70.sep)[0];
14344
14682
  return segment !== void 0 && segment !== "" ? segment : void 0;
14345
14683
  }
14346
14684
  async function readWorktreeState(worktreeRoot) {
14347
- const state = await readJson(path69.join(worktreeRoot, WORKTREE_STATE_FILE));
14685
+ const state = await readJson(path70.join(worktreeRoot, WORKTREE_STATE_FILE));
14348
14686
  if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
14349
14687
  return state;
14350
14688
  }
14351
14689
  async function writeWorktreeState(worktreeRoot, state) {
14352
- await writeJson(path69.join(worktreeRoot, WORKTREE_STATE_FILE), state);
14690
+ await writeJson(path70.join(worktreeRoot, WORKTREE_STATE_FILE), state);
14353
14691
  }
14354
14692
 
14355
14693
  // src/workspace/worktree/add.ts
@@ -14364,7 +14702,7 @@ async function runAdd(opts) {
14364
14702
  }
14365
14703
  const branch = opts.branch ?? opts.slug;
14366
14704
  const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
14367
- if (await fs52.pathExists(wsWorktreePath)) {
14705
+ if (await fs53.pathExists(wsWorktreePath)) {
14368
14706
  return {
14369
14707
  ok: false,
14370
14708
  error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
@@ -14383,7 +14721,7 @@ async function runAdd(opts) {
14383
14721
  const memberResults = [];
14384
14722
  for (const member of selected) {
14385
14723
  const start = Date.now();
14386
- const memberWorktreePath = path70.join(wsWorktreePath, member.folder);
14724
+ const memberWorktreePath = path71.join(wsWorktreePath, member.folder);
14387
14725
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
14388
14726
  preferDefaultBranchFrom: true
14389
14727
  });
@@ -14413,7 +14751,7 @@ async function runAdd(opts) {
14413
14751
  await mapWithConcurrency(okResults, async (result) => {
14414
14752
  const member = selected.find((m) => m.id === result.member);
14415
14753
  if (!member) return;
14416
- const memberWorktreePath = path70.join(wsWorktreePath, member.folder);
14754
+ const memberWorktreePath = path71.join(wsWorktreePath, member.folder);
14417
14755
  const hydrationStart = Date.now();
14418
14756
  if (opts.hydrate !== false) {
14419
14757
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -14481,17 +14819,17 @@ async function runDryRun(args) {
14481
14819
  // src/workspace/worktree/autohydrate.ts
14482
14820
  import { spawn } from "node:child_process";
14483
14821
  import fsSync from "node:fs";
14484
- import path75 from "node:path";
14485
- import fs54 from "fs-extra";
14822
+ import path76 from "node:path";
14823
+ import fs55 from "fs-extra";
14486
14824
 
14487
14825
  // src/workspace/worktree/doctor.ts
14488
14826
  import { existsSync as existsSync6 } from "node:fs";
14489
- import path73 from "node:path";
14827
+ import path74 from "node:path";
14490
14828
 
14491
14829
  // src/workspace/worktree/hydrate-lock.ts
14492
14830
  init_paths();
14493
- import path71 from "node:path";
14494
- import fs53 from "fs-extra";
14831
+ import path72 from "node:path";
14832
+ import fs54 from "fs-extra";
14495
14833
  var STALE_LOCK_MS = 30 * 60 * 1e3;
14496
14834
  function locksDir(mainRoot) {
14497
14835
  return hausPath(mainRoot, "locks");
@@ -14508,13 +14846,13 @@ function assertSlug(slug) {
14508
14846
  return slug;
14509
14847
  }
14510
14848
  function hydrateLockPath(mainRoot, slug) {
14511
- return path71.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
14849
+ return path72.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
14512
14850
  }
14513
14851
  function hydrateLogPath(mainRoot, slug) {
14514
- return path71.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
14852
+ return path72.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
14515
14853
  }
14516
14854
  function hydrateLastRunPath(mainRoot, slug) {
14517
- return path71.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
14855
+ return path72.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
14518
14856
  }
14519
14857
  function defaultIsAlive(pid) {
14520
14858
  try {
@@ -14526,7 +14864,7 @@ function defaultIsAlive(pid) {
14526
14864
  }
14527
14865
  async function readOwnerPid(lockPath) {
14528
14866
  try {
14529
- const raw = (await fs53.readFile(path71.join(lockPath, "pid"), "utf8")).trim();
14867
+ const raw = (await fs54.readFile(path72.join(lockPath, "pid"), "utf8")).trim();
14530
14868
  if (raw === "") return void 0;
14531
14869
  const pid = Number.parseInt(raw, 10);
14532
14870
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
@@ -14535,13 +14873,13 @@ async function readOwnerPid(lockPath) {
14535
14873
  }
14536
14874
  }
14537
14875
  async function claim(lockPath, pid) {
14538
- await fs53.ensureDir(path71.dirname(lockPath));
14876
+ await fs54.ensureDir(path72.dirname(lockPath));
14539
14877
  try {
14540
- await fs53.mkdir(lockPath);
14878
+ await fs54.mkdir(lockPath);
14541
14879
  } catch {
14542
14880
  return false;
14543
14881
  }
14544
- await fs53.writeFile(path71.join(lockPath, "pid"), String(pid), "utf8");
14882
+ await fs54.writeFile(path72.join(lockPath, "pid"), String(pid), "utf8");
14545
14883
  return true;
14546
14884
  }
14547
14885
  async function acquireHydrateLock(mainRoot, slug, deps = {}) {
@@ -14560,7 +14898,7 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
14560
14898
  }
14561
14899
  if (alive) return { acquired: false, reason: "live", ownerPid, lockPath };
14562
14900
  } else {
14563
- const stat = await fs53.stat(lockPath).catch(() => void 0);
14901
+ const stat = await fs54.stat(lockPath).catch(() => void 0);
14564
14902
  if (stat === void 0) {
14565
14903
  return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", lockPath };
14566
14904
  }
@@ -14568,18 +14906,18 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
14568
14906
  return { acquired: false, reason: "starting", lockPath };
14569
14907
  }
14570
14908
  }
14571
- await fs53.remove(lockPath);
14909
+ await fs54.remove(lockPath);
14572
14910
  return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", ownerPid, lockPath };
14573
14911
  }
14574
14912
  async function recordHydrateLockOwner(mainRoot, slug, pid) {
14575
- await fs53.writeFile(path71.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
14913
+ await fs54.writeFile(path72.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
14576
14914
  }
14577
14915
  async function releaseHydrateLock(mainRoot, slug) {
14578
- await fs53.remove(hydrateLockPath(mainRoot, slug));
14916
+ await fs54.remove(hydrateLockPath(mainRoot, slug));
14579
14917
  }
14580
14918
  async function readHydrateLock(mainRoot, slug) {
14581
14919
  const lockPath = hydrateLockPath(mainRoot, slug);
14582
- const stat = await fs53.stat(lockPath).catch(() => void 0);
14920
+ const stat = await fs54.stat(lockPath).catch(() => void 0);
14583
14921
  if (stat === void 0) return { held: false };
14584
14922
  const ownerPid = await readOwnerPid(lockPath);
14585
14923
  return {
@@ -14592,21 +14930,21 @@ async function readHydrateLock(mainRoot, slug) {
14592
14930
 
14593
14931
  // src/workspace/worktree/orphans.ts
14594
14932
  import { existsSync as existsSync5 } from "node:fs";
14595
- import path72 from "node:path";
14933
+ import path73 from "node:path";
14596
14934
  var WORKSPACE_REPO_ID = "(workspace)";
14597
14935
  async function findOrphanedRegistrations(repos, wsWorktreesDir) {
14598
14936
  const orphans = [];
14599
14937
  for (const repo of repos) {
14600
14938
  const entries = await listWorktrees(repo.absPath);
14601
14939
  for (const entry of entries) {
14602
- const entryPath = path72.normalize(entry.path);
14603
- const marker = `${path72.sep}.claude${path72.sep}worktrees${path72.sep}`;
14940
+ const entryPath = path73.normalize(entry.path);
14941
+ const marker = `${path73.sep}.claude${path73.sep}worktrees${path73.sep}`;
14604
14942
  const idx = entryPath.indexOf(marker);
14605
14943
  if (idx === -1) continue;
14606
14944
  const rest = entryPath.slice(idx + marker.length);
14607
- const slug = rest.split(path72.sep)[0];
14945
+ const slug = rest.split(path73.sep)[0];
14608
14946
  if (!slug) continue;
14609
- if (existsSync5(path72.join(wsWorktreesDir, slug))) continue;
14947
+ if (existsSync5(path73.join(wsWorktreesDir, slug))) continue;
14610
14948
  orphans.push({
14611
14949
  repoId: repo.id,
14612
14950
  ownerRepo: repo.absPath,
@@ -14621,7 +14959,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
14621
14959
  // src/workspace/worktree/doctor.ts
14622
14960
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
14623
14961
  function looksHydrated(memberPath) {
14624
- return HYDRATION_MARKERS.some((marker) => existsSync6(path73.join(memberPath, marker)));
14962
+ return HYDRATION_MARKERS.some((marker) => existsSync6(path74.join(memberPath, marker)));
14625
14963
  }
14626
14964
  async function runDoctor2() {
14627
14965
  const rootInfo = await resolveRoots();
@@ -14638,7 +14976,7 @@ async function runDoctor2() {
14638
14976
  };
14639
14977
  }
14640
14978
  const workspaceRoot = rootInfo.mainRoot;
14641
- const isWorkspace = existsSync6(path73.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path73.join(workspaceRoot, REPOS_MANIFEST_FILE));
14979
+ const isWorkspace = existsSync6(path74.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path74.join(workspaceRoot, REPOS_MANIFEST_FILE));
14642
14980
  if (!isWorkspace) {
14643
14981
  return {
14644
14982
  isWorkspace: false,
@@ -14654,7 +14992,7 @@ async function runDoctor2() {
14654
14992
  };
14655
14993
  }
14656
14994
  const wsWorktreesDir = worktreesDir(workspaceRoot);
14657
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path73.sep}`);
14995
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path74.sep}`);
14658
14996
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
14659
14997
  const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
14660
14998
  const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
@@ -14667,10 +15005,10 @@ async function runDoctor2() {
14667
15005
  let hydrationOwnerPid;
14668
15006
  if (inWorkspaceWorktree) {
14669
15007
  const wtPath = rootInfo.repoRoot;
14670
- slug = rootInfo.worktreeName ?? path73.basename(wtPath);
15008
+ slug = rootInfo.worktreeName ?? path74.basename(wtPath);
14671
15009
  const state = await readWorktreeState(wtPath);
14672
15010
  for (const member of members) {
14673
- const memberPath = path73.join(wtPath, member.folder);
15011
+ const memberPath = path74.join(wtPath, member.folder);
14674
15012
  const materialized = existsSync6(memberPath);
14675
15013
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
14676
15014
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -14715,7 +15053,7 @@ async function runDoctor2() {
14715
15053
  wsWorktreesDir
14716
15054
  );
14717
15055
  const orphans = orphanScan.map(
14718
- (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path73.join(wsWorktreesDir, o.slug)} no longer exists.`
15056
+ (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path74.join(wsWorktreesDir, o.slug)} no longer exists.`
14719
15057
  );
14720
15058
  if (orphans.length > 0) {
14721
15059
  problems.push(...orphans);
@@ -14738,7 +15076,7 @@ async function runDoctor2() {
14738
15076
  // src/workspace/worktree/hydrate-command.ts
14739
15077
  init_fs();
14740
15078
  import { existsSync as existsSync7 } from "node:fs";
14741
- import path74 from "node:path";
15079
+ import path75 from "node:path";
14742
15080
  async function runHydrateWorktree(opts) {
14743
15081
  const rootInfo = await resolveRoots();
14744
15082
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -14749,7 +15087,7 @@ async function runHydrateWorktree(opts) {
14749
15087
  }
14750
15088
  const workspaceRoot = rootInfo.mainRoot;
14751
15089
  const wtPath = rootInfo.repoRoot;
14752
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path74.sep}`)) {
15090
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path75.sep}`)) {
14753
15091
  return {
14754
15092
  ok: false,
14755
15093
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -14761,13 +15099,13 @@ async function runHydrateWorktree(opts) {
14761
15099
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
14762
15100
  }
14763
15101
  const state = await readWorktreeState(wtPath);
14764
- const slug = state?.slug ?? path74.basename(wtPath);
15102
+ const slug = state?.slug ?? path75.basename(wtPath);
14765
15103
  const detectedBranch = await currentBranch(wtPath);
14766
15104
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
14767
15105
  if (opts.dryRun) {
14768
15106
  const results2 = await Promise.all(
14769
15107
  selected.map(async (member) => {
14770
- const memberPath = path74.join(wtPath, member.folder);
15108
+ const memberPath = path75.join(wtPath, member.folder);
14771
15109
  if (existsSync7(memberPath)) {
14772
15110
  return { member: member.id, folder: member.folder, status: "planned" };
14773
15111
  }
@@ -14788,7 +15126,7 @@ async function runHydrateWorktree(opts) {
14788
15126
  const results = await mapWithConcurrency(
14789
15127
  selected,
14790
15128
  async (member) => {
14791
- const memberPath = path74.join(wtPath, member.folder);
15129
+ const memberPath = path75.join(wtPath, member.folder);
14792
15130
  let branchAction;
14793
15131
  if (!existsSync7(memberPath)) {
14794
15132
  if (!opts.materialize) {
@@ -14909,7 +15247,7 @@ function defaultSpawnDetached(spec) {
14909
15247
  }
14910
15248
  function appendLog(logPath, line2) {
14911
15249
  try {
14912
- fsSync.mkdirSync(path75.dirname(logPath), { recursive: true });
15250
+ fsSync.mkdirSync(path76.dirname(logPath), { recursive: true });
14913
15251
  const size = fsSync.statSync(logPath, { throwIfNoEntry: false })?.size ?? 0;
14914
15252
  if (size > LOG_SIZE_CAP_BYTES) {
14915
15253
  fsSync.writeFileSync(logPath, `--- log rotated (exceeded ${LOG_SIZE_CAP_BYTES} bytes) ---
@@ -14924,11 +15262,11 @@ async function consumeLastRunFailure(mainRoot, slug) {
14924
15262
  const file = hydrateLastRunPath(mainRoot, slug);
14925
15263
  let parsed;
14926
15264
  try {
14927
- parsed = JSON.parse(await fs54.readFile(file, "utf8"));
15265
+ parsed = JSON.parse(await fs55.readFile(file, "utf8"));
14928
15266
  } catch {
14929
15267
  return void 0;
14930
15268
  }
14931
- await fs54.remove(file).catch(() => void 0);
15269
+ await fs55.remove(file).catch(() => void 0);
14932
15270
  if (parsed && parsed.ok === false) return parsed;
14933
15271
  return void 0;
14934
15272
  }
@@ -15000,7 +15338,7 @@ async function runAutoHydrate(opts = {}, deps = {}) {
15000
15338
  reason: "could not determine the haus CLI entry point to re-invoke"
15001
15339
  };
15002
15340
  }
15003
- await fs54.ensureDir(logsDir(mainRoot));
15341
+ await fs55.ensureDir(logsDir(mainRoot));
15004
15342
  appendLog(
15005
15343
  logPath,
15006
15344
  `autohydrate: forking hydrate --materialize for "${slug}" (${missingMembers.join(", ")})`
@@ -15069,21 +15407,21 @@ async function runDetachedHydration() {
15069
15407
  async function writeLastRun(mainRoot, slug, ok, failures) {
15070
15408
  const file = hydrateLastRunPath(mainRoot, slug);
15071
15409
  if (ok) {
15072
- await fs54.remove(file).catch(() => void 0);
15410
+ await fs55.remove(file).catch(() => void 0);
15073
15411
  return;
15074
15412
  }
15075
15413
  const record = { slug, finishedAt: (/* @__PURE__ */ new Date()).toISOString(), ok, failures };
15076
- await fs54.ensureDir(path75.dirname(file));
15077
- await fs54.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
15414
+ await fs55.ensureDir(path76.dirname(file));
15415
+ await fs55.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
15078
15416
  }
15079
15417
 
15080
15418
  // src/workspace/worktree/dematerialize.ts
15081
- import path77 from "node:path";
15082
- import fs56 from "fs-extra";
15419
+ import path78 from "node:path";
15420
+ import fs57 from "fs-extra";
15083
15421
 
15084
15422
  // src/workspace/worktree/member-targets.ts
15085
- import path76 from "node:path";
15086
- import fs55 from "fs-extra";
15423
+ import path77 from "node:path";
15424
+ import fs56 from "fs-extra";
15087
15425
  function normalizeStateMembers(state, allMembers) {
15088
15426
  return state?.members.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath })) ?? allMembers.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath }));
15089
15427
  }
@@ -15100,7 +15438,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
15100
15438
  targets.push({
15101
15439
  repo: configMember?.id ?? stateMember.id,
15102
15440
  ownerRepo,
15103
- path: path76.join(wsWorktreePath, stateMember.folder)
15441
+ path: path77.join(wsWorktreePath, stateMember.folder)
15104
15442
  });
15105
15443
  }
15106
15444
  return { targets, unresolvedDroppedMembers };
@@ -15108,7 +15446,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
15108
15446
  async function filterExistingTargets(targets) {
15109
15447
  const existing = [];
15110
15448
  for (const target of targets) {
15111
- if (await fs55.pathExists(target.path)) existing.push(target);
15449
+ if (await fs56.pathExists(target.path)) existing.push(target);
15112
15450
  }
15113
15451
  return existing;
15114
15452
  }
@@ -15153,7 +15491,7 @@ async function runDematerialize(opts) {
15153
15491
  } catch (err) {
15154
15492
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
15155
15493
  }
15156
- if (!await fs56.pathExists(wsWorktreePath)) {
15494
+ if (!await fs57.pathExists(wsWorktreePath)) {
15157
15495
  return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
15158
15496
  }
15159
15497
  const lock = await readHydrateLock(workspaceRoot, slug);
@@ -15186,9 +15524,9 @@ async function runDematerialize(opts) {
15186
15524
  const removed = [];
15187
15525
  const failed = [];
15188
15526
  for (const folder of unresolvedDroppedMembers) {
15189
- const droppedPath = path77.join(wsWorktreePath, folder);
15190
- if (await fs56.pathExists(droppedPath)) {
15191
- await fs56.remove(droppedPath);
15527
+ const droppedPath = path78.join(wsWorktreePath, folder);
15528
+ if (await fs57.pathExists(droppedPath)) {
15529
+ await fs57.remove(droppedPath);
15192
15530
  failed.push({ repo: folder, error: unresolvedDroppedFailureReason() });
15193
15531
  }
15194
15532
  }
@@ -15202,26 +15540,26 @@ async function runDematerialize(opts) {
15202
15540
  }
15203
15541
 
15204
15542
  // src/workspace/worktree/list.ts
15205
- import path78 from "node:path";
15206
- import fs57 from "fs-extra";
15543
+ import path79 from "node:path";
15544
+ import fs58 from "fs-extra";
15207
15545
  async function runList2() {
15208
15546
  const resolved = await resolveWorkspaceForWorktree();
15209
15547
  if (!resolved.ok) return { ok: false, error: resolved.reason };
15210
15548
  const { rootInfo, workspaceRoot } = resolved;
15211
15549
  const dir = worktreesDir(workspaceRoot);
15212
- if (!await fs57.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
15213
- const entries = await fs57.readdir(dir, { withFileTypes: true });
15550
+ if (!await fs58.pathExists(dir)) return { ok: true, workspaceRoot, worktrees: [] };
15551
+ const entries = await fs58.readdir(dir, { withFileTypes: true });
15214
15552
  const slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
15215
15553
  const configuredMembers = await readMembers(rootInfo);
15216
15554
  const worktrees = [];
15217
15555
  for (const slug of slugs) {
15218
- const wtPath = path78.join(dir, slug);
15556
+ const wtPath = path79.join(dir, slug);
15219
15557
  const state = await readWorktreeState(wtPath);
15220
15558
  const members = [];
15221
15559
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
15222
15560
  for (const m of source) {
15223
- const memberPath = path78.join(wtPath, m.folder);
15224
- const materialized = await fs57.pathExists(memberPath);
15561
+ const memberPath = path79.join(wtPath, m.folder);
15562
+ const materialized = await fs58.pathExists(memberPath);
15225
15563
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
15226
15564
  members.push({
15227
15565
  id: m.id,
@@ -15243,23 +15581,23 @@ async function runList2() {
15243
15581
 
15244
15582
  // src/workspace/worktree/reclaim.ts
15245
15583
  import { existsSync as existsSync8 } from "node:fs";
15246
- import path79 from "node:path";
15247
- import fs58 from "fs-extra";
15584
+ import path80 from "node:path";
15585
+ import fs59 from "fs-extra";
15248
15586
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
15249
15587
  async function lastTouchedMs(worktreeRoot) {
15250
- const candidates = [worktreeRoot, path79.join(worktreeRoot, ".haus-worktree.json")];
15588
+ const candidates = [worktreeRoot, path80.join(worktreeRoot, ".haus-worktree.json")];
15251
15589
  try {
15252
- const dotGit = await fs58.readFile(path79.join(worktreeRoot, ".git"), "utf8");
15590
+ const dotGit = await fs59.readFile(path80.join(worktreeRoot, ".git"), "utf8");
15253
15591
  const match = /^gitdir:\s*(.+)$/m.exec(dotGit);
15254
15592
  if (match?.[1]) {
15255
15593
  const gitdir = match[1].trim();
15256
- candidates.push(path79.join(gitdir, "HEAD"), path79.join(gitdir, "index"));
15594
+ candidates.push(path80.join(gitdir, "HEAD"), path80.join(gitdir, "index"));
15257
15595
  }
15258
15596
  } catch {
15259
15597
  }
15260
15598
  let newest = 0;
15261
15599
  for (const candidate of candidates) {
15262
- const stat = await fs58.stat(candidate).catch(() => void 0);
15600
+ const stat = await fs59.stat(candidate).catch(() => void 0);
15263
15601
  if (stat && stat.mtimeMs > newest) newest = stat.mtimeMs;
15264
15602
  }
15265
15603
  return newest;
@@ -15286,7 +15624,7 @@ async function runReclaim(opts = {}) {
15286
15624
  if (!existsSync8(dir)) {
15287
15625
  return { ok: true, dryRun: !opts.write, olderThanDays, candidates: [], reclaimed: 0 };
15288
15626
  }
15289
- const allEntries = (await fs58.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
15627
+ const allEntries = (await fs59.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort();
15290
15628
  const entries = allEntries.filter((name) => isValidWorktreeSlug(name));
15291
15629
  const unaddressable = allEntries.filter((name) => !isValidWorktreeSlug(name));
15292
15630
  const members = await readMembers(rootInfo);
@@ -15296,17 +15634,17 @@ async function runReclaim(opts = {}) {
15296
15634
  for (const name of unaddressable) {
15297
15635
  candidates.push({
15298
15636
  slug: name,
15299
- ageDays: (Date.now() - await lastTouchedMs(path79.join(dir, name))) / MS_PER_DAY,
15637
+ ageDays: (Date.now() - await lastTouchedMs(path80.join(dir, name))) / MS_PER_DAY,
15300
15638
  materializedMembers: 0,
15301
15639
  action: "skip",
15302
15640
  reason: "directory name is not a valid worktree slug \u2014 no haus worktree command can address it; remove it by hand if it is debris"
15303
15641
  });
15304
15642
  }
15305
15643
  for (const slug of entries) {
15306
- const wtPath = path79.join(dir, slug);
15644
+ const wtPath = path80.join(dir, slug);
15307
15645
  const state = await readWorktreeState(wtPath);
15308
15646
  const stateMembers = normalizeStateMembers(state, members);
15309
- const materialized = stateMembers.filter((m) => existsSync8(path79.join(wtPath, m.folder)));
15647
+ const materialized = stateMembers.filter((m) => existsSync8(path80.join(wtPath, m.folder)));
15310
15648
  const ageDays = (Date.now() - await lastTouchedMs(wtPath)) / MS_PER_DAY;
15311
15649
  const base = { slug, ageDays, materializedMembers: materialized.length };
15312
15650
  if (slug === currentSlug) {
@@ -15367,13 +15705,13 @@ async function runReclaim(opts = {}) {
15367
15705
  }
15368
15706
 
15369
15707
  // src/workspace/worktree/remove.ts
15370
- import fs59 from "fs-extra";
15708
+ import fs60 from "fs-extra";
15371
15709
  async function runRemove2(opts) {
15372
15710
  const resolved = await resolveWorkspaceForWorktree();
15373
15711
  if (!resolved.ok) return { ok: false, error: resolved.reason };
15374
15712
  const { rootInfo, workspaceRoot } = resolved;
15375
15713
  const wsWorktreePath = worktreePath(workspaceRoot, opts.slug);
15376
- if (!await fs59.pathExists(wsWorktreePath)) {
15714
+ if (!await fs60.pathExists(wsWorktreePath)) {
15377
15715
  return { ok: false, error: `No workspace worktree found at ${wsWorktreePath}.` };
15378
15716
  }
15379
15717
  const state = await readWorktreeState(wsWorktreePath);
@@ -15441,8 +15779,8 @@ async function runRemove2(opts) {
15441
15779
  }
15442
15780
  await pruneWorktrees(workspaceRoot);
15443
15781
  }
15444
- if (wsRemoveOk && await fs59.pathExists(wsWorktreePath)) {
15445
- await fs59.remove(wsWorktreePath);
15782
+ if (wsRemoveOk && await fs60.pathExists(wsWorktreePath)) {
15783
+ await fs60.remove(wsWorktreePath);
15446
15784
  }
15447
15785
  return { ok: failed.length === 0, dryRun: false, slug: opts.slug, removed, failed };
15448
15786
  }
@@ -15903,7 +16241,7 @@ async function runWorktree(action, opts = {}) {
15903
16241
 
15904
16242
  // src/commands/workspace.ts
15905
16243
  import { existsSync as existsSync12, statSync as statSync3 } from "node:fs";
15906
- import path88 from "node:path";
16244
+ import path89 from "node:path";
15907
16245
  init_fs();
15908
16246
  init_logger();
15909
16247
 
@@ -15962,7 +16300,7 @@ ${summaries.map(
15962
16300
 
15963
16301
  // src/commands/workspace/doctor.ts
15964
16302
  import { existsSync as existsSync9 } from "node:fs";
15965
- import path81 from "node:path";
16303
+ import path82 from "node:path";
15966
16304
  init_logger();
15967
16305
  init_paths();
15968
16306
 
@@ -15970,14 +16308,14 @@ init_paths();
15970
16308
  init_fs();
15971
16309
  init_paths();
15972
16310
  import { readFileSync as readFileSync4 } from "node:fs";
15973
- import path80 from "node:path";
16311
+ import path81 from "node:path";
15974
16312
  var MANIFEST_FILE2 = "workspace.manifest.json";
15975
16313
  function manifestPath(workspaceRoot) {
15976
16314
  return hausPath(workspaceRoot, MANIFEST_FILE2);
15977
16315
  }
15978
16316
  function hausVersion() {
15979
16317
  try {
15980
- const pkg = JSON.parse(readFileSync4(path80.join(packageRoot(), "package.json"), "utf8"));
16318
+ const pkg = JSON.parse(readFileSync4(path81.join(packageRoot(), "package.json"), "utf8"));
15981
16319
  return pkg.version ?? "0.0.0";
15982
16320
  } catch {
15983
16321
  return "0.0.0";
@@ -16069,7 +16407,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
16069
16407
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
16070
16408
  const catalogRefByRepo = [];
16071
16409
  for (const repo of config.repos) {
16072
- const repoRoot = path81.resolve(workspaceRoot, repo.path);
16410
+ const repoRoot = path82.resolve(workspaceRoot, repo.path);
16073
16411
  const entry = manifestByName.get(repo.name);
16074
16412
  if (!entry) {
16075
16413
  flag({
@@ -16142,7 +16480,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
16142
16480
  });
16143
16481
  continue;
16144
16482
  }
16145
- if (!existsSync9(path81.join(workspaceRoot, entry.path))) {
16483
+ if (!existsSync9(path82.join(workspaceRoot, entry.path))) {
16146
16484
  flag({
16147
16485
  repo: entry.repo,
16148
16486
  kind: "missing-linked-context-copy",
@@ -16188,18 +16526,18 @@ function emit(args) {
16188
16526
  init_logger();
16189
16527
 
16190
16528
  // src/workspace/link-context/link.ts
16191
- import path84 from "node:path";
16192
- import fs62 from "fs-extra";
16529
+ import path85 from "node:path";
16530
+ import fs63 from "fs-extra";
16193
16531
  init_fs();
16194
16532
  init_logger();
16195
16533
 
16196
16534
  // src/workspace/link-context/plan.ts
16197
- import path83 from "node:path";
16198
- import fs61 from "fs-extra";
16535
+ import path84 from "node:path";
16536
+ import fs62 from "fs-extra";
16199
16537
 
16200
16538
  // src/workspace/link-context/scan-source.ts
16201
- import path82 from "node:path";
16202
- import fs60 from "fs-extra";
16539
+ import path83 from "node:path";
16540
+ import fs61 from "fs-extra";
16203
16541
  var ASSET_SUBDIR = {
16204
16542
  skill: "skills",
16205
16543
  agent: "agents",
@@ -16207,36 +16545,36 @@ var ASSET_SUBDIR = {
16207
16545
  };
16208
16546
  async function hasLinkableContext(member) {
16209
16547
  for (const subdir of Object.values(ASSET_SUBDIR)) {
16210
- if (await fs60.pathExists(path82.join(member.absPath, ".claude", subdir))) return true;
16548
+ if (await fs61.pathExists(path83.join(member.absPath, ".claude", subdir))) return true;
16211
16549
  }
16212
16550
  return false;
16213
16551
  }
16214
16552
  async function collectSkills(member) {
16215
- const dir = path82.join(member.absPath, ".claude", "skills");
16216
- if (!await fs60.pathExists(dir)) return [];
16217
- const names = await fs60.readdir(dir);
16553
+ const dir = path83.join(member.absPath, ".claude", "skills");
16554
+ if (!await fs61.pathExists(dir)) return [];
16555
+ const names = await fs61.readdir(dir);
16218
16556
  const out = [];
16219
16557
  for (const name of [...names].sort()) {
16220
- const abs = path82.join(dir, name);
16221
- const stat = await fs60.lstat(abs);
16558
+ const abs = path83.join(dir, name);
16559
+ const stat = await fs61.lstat(abs);
16222
16560
  if (!stat.isDirectory()) continue;
16223
- if (!await fs60.pathExists(path82.join(abs, "SKILL.md"))) continue;
16224
- out.push({ type: "skill", name, sourceRelPath: path82.posix.join(".claude", "skills", name) });
16561
+ if (!await fs61.pathExists(path83.join(abs, "SKILL.md"))) continue;
16562
+ out.push({ type: "skill", name, sourceRelPath: path83.posix.join(".claude", "skills", name) });
16225
16563
  }
16226
16564
  return out;
16227
16565
  }
16228
16566
  async function collectFlatMarkdown(member, type) {
16229
16567
  const subdir = ASSET_SUBDIR[type];
16230
- const dir = path82.join(member.absPath, ".claude", subdir);
16231
- if (!await fs60.pathExists(dir)) return [];
16232
- const names = await fs60.readdir(dir);
16568
+ const dir = path83.join(member.absPath, ".claude", subdir);
16569
+ if (!await fs61.pathExists(dir)) return [];
16570
+ const names = await fs61.readdir(dir);
16233
16571
  const out = [];
16234
16572
  for (const fileName of [...names].sort()) {
16235
16573
  if (!fileName.endsWith(".md")) continue;
16236
- const abs = path82.join(dir, fileName);
16237
- if (!(await fs60.lstat(abs)).isFile()) continue;
16574
+ const abs = path83.join(dir, fileName);
16575
+ if (!(await fs61.lstat(abs)).isFile()) continue;
16238
16576
  const name = fileName.slice(0, -".md".length);
16239
- out.push({ type, name, sourceRelPath: path82.posix.join(".claude", subdir, fileName) });
16577
+ out.push({ type, name, sourceRelPath: path83.posix.join(".claude", subdir, fileName) });
16240
16578
  }
16241
16579
  return out;
16242
16580
  }
@@ -16251,9 +16589,9 @@ async function collectSourceAssets(member) {
16251
16589
 
16252
16590
  // src/workspace/link-context/plan.ts
16253
16591
  function repoFolderPrefix(member) {
16254
- const base = path83.posix.basename(member.folder);
16592
+ const base = path84.posix.basename(member.folder);
16255
16593
  if (base && base !== "." && base !== "/") return base;
16256
- const fromAbs = path83.basename(member.absPath);
16594
+ const fromAbs = path84.basename(member.absPath);
16257
16595
  return fromAbs || member.id;
16258
16596
  }
16259
16597
  function assetKey(type, destKey) {
@@ -16268,7 +16606,7 @@ async function buildLinkPlan(members) {
16268
16606
  const byKey = /* @__PURE__ */ new Map();
16269
16607
  const membersById = new Map(members.map((m) => [m.id, m]));
16270
16608
  for (const member of members) {
16271
- if (!await fs61.pathExists(member.absPath)) {
16609
+ if (!await fs62.pathExists(member.absPath)) {
16272
16610
  skipped.push({ repo: member.id, reason: `not cloned locally (${member.absPath}) \u2014 skipped` });
16273
16611
  continue;
16274
16612
  }
@@ -16310,7 +16648,7 @@ async function buildLinkPlan(members) {
16310
16648
  name: asset.name,
16311
16649
  destKey,
16312
16650
  sourceRelPath: asset.sourceRelPath,
16313
- sourceAbsPath: path83.join(member.absPath, asset.sourceRelPath),
16651
+ sourceAbsPath: path84.join(member.absPath, asset.sourceRelPath),
16314
16652
  sourceHash
16315
16653
  });
16316
16654
  }
@@ -16321,7 +16659,7 @@ async function buildLinkPlan(members) {
16321
16659
  function destRelPathFor(type, destKey) {
16322
16660
  const subdir = `${type}s`;
16323
16661
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
16324
- return path84.posix.join(".claude", subdir, leaf);
16662
+ return path85.posix.join(".claude", subdir, leaf);
16325
16663
  }
16326
16664
  async function runLinkContext(workspaceRoot, opts = {}) {
16327
16665
  const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
@@ -16347,17 +16685,17 @@ async function runLinkContext(workspaceRoot, opts = {}) {
16347
16685
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
16348
16686
  for (const entry of plan.entries) {
16349
16687
  const destRel = destRelPathFor(entry.type, entry.destKey);
16350
- const destAbs = path84.join(workspaceRoot, destRel);
16688
+ const destAbs = path85.join(workspaceRoot, destRel);
16351
16689
  if (write) {
16352
- await fs62.ensureDir(path84.dirname(destAbs));
16353
- if (await fs62.pathExists(destAbs)) await fs62.remove(destAbs);
16354
- await fs62.copy(entry.sourceAbsPath, destAbs, {
16690
+ await fs63.ensureDir(path85.dirname(destAbs));
16691
+ if (await fs63.pathExists(destAbs)) await fs63.remove(destAbs);
16692
+ await fs63.copy(entry.sourceAbsPath, destAbs, {
16355
16693
  overwrite: true,
16356
16694
  errorOnExist: false,
16357
16695
  dereference: false,
16358
16696
  filter: (s) => {
16359
16697
  try {
16360
- return !fs62.lstatSync(s).isSymbolicLink();
16698
+ return !fs63.lstatSync(s).isSymbolicLink();
16361
16699
  } catch {
16362
16700
  return false;
16363
16701
  }
@@ -16386,7 +16724,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
16386
16724
  warn(`Refusing to remove linked context path outside the workspace root: ${old.path}`);
16387
16725
  continue;
16388
16726
  }
16389
- if (await fs62.pathExists(abs)) await fs62.remove(abs);
16727
+ if (await fs63.pathExists(abs)) await fs63.remove(abs);
16390
16728
  }
16391
16729
  }
16392
16730
  if (write) {
@@ -16438,11 +16776,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
16438
16776
 
16439
16777
  // src/commands/workspace/setup.ts
16440
16778
  import { existsSync as existsSync10, statSync } from "node:fs";
16441
- import path86 from "node:path";
16779
+ import path87 from "node:path";
16442
16780
 
16443
16781
  // src/claude/write-workspace-claude-md.ts
16444
- import path85 from "node:path";
16445
- import fs63 from "fs-extra";
16782
+ import path86 from "node:path";
16783
+ import fs64 from "fs-extra";
16446
16784
  init_fs();
16447
16785
  init_logger();
16448
16786
  init_paths();
@@ -16467,11 +16805,11 @@ ${WORKSPACE_BLOCK_END}`;
16467
16805
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
16468
16806
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
16469
16807
  const dryRun = opts.dryRun ?? false;
16470
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path85.join(workspaceRoot, "CLAUDE.md");
16808
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path86.join(workspaceRoot, "CLAUDE.md");
16471
16809
  const say = opts.quiet ? () => {
16472
16810
  } : log;
16473
- const existed = await fs63.pathExists(filePath);
16474
- const prev = existed ? await fs63.readFile(filePath, "utf8") : "";
16811
+ const existed = await fs64.pathExists(filePath);
16812
+ const prev = existed ? await fs64.readFile(filePath, "utf8") : "";
16475
16813
  const next = opts.collision ? `${block}
16476
16814
  ` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
16477
16815
  const printable = displayPath(workspaceRoot, filePath);
@@ -16500,20 +16838,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
16500
16838
  init_fs();
16501
16839
  init_logger();
16502
16840
  function resolveWorkspaceRoot(start = process.cwd()) {
16503
- let dir = path86.resolve(start);
16841
+ let dir = path87.resolve(start);
16504
16842
  for (; ; ) {
16505
- if (existsSync10(path86.join(dir, WORKSPACE_FILE))) return dir;
16506
- const parent = path86.dirname(dir);
16507
- if (parent === dir) return path86.resolve(start);
16843
+ if (existsSync10(path87.join(dir, WORKSPACE_FILE))) return dir;
16844
+ const parent = path87.dirname(dir);
16845
+ if (parent === dir) return path87.resolve(start);
16508
16846
  dir = parent;
16509
16847
  }
16510
16848
  }
16511
16849
  function isRootRepo(workspaceRoot, repoPath) {
16512
- return path86.resolve(workspaceRoot, repoPath) === path86.resolve(workspaceRoot);
16850
+ return path87.resolve(workspaceRoot, repoPath) === path87.resolve(workspaceRoot);
16513
16851
  }
16514
16852
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
16515
16853
  const apply = options.write ?? false;
16516
- const configText = await readText(path86.join(workspaceRoot, WORKSPACE_FILE));
16854
+ const configText = await readText(path87.join(workspaceRoot, WORKSPACE_FILE));
16517
16855
  if (!configText) {
16518
16856
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
16519
16857
  process.exitCode = 1;
@@ -16540,7 +16878,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
16540
16878
  const statuses = [];
16541
16879
  const aggregateInputs = [];
16542
16880
  for (const repo of repos) {
16543
- const repoRoot = path86.resolve(workspaceRoot, repo.path);
16881
+ const repoRoot = path87.resolve(workspaceRoot, repo.path);
16544
16882
  say(`
16545
16883
  \u2192 ${repo.name} (${repo.path})`);
16546
16884
  let res;
@@ -16656,7 +16994,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
16656
16994
  const status = statusByName.get(repo.name);
16657
16995
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
16658
16996
  if (status?.status === "ok") {
16659
- const lock = await checkLock(path86.resolve(workspaceRoot, repo.path));
16997
+ const lock = await checkLock(path87.resolve(workspaceRoot, repo.path));
16660
16998
  manifestRepos.push({
16661
16999
  name: repo.name,
16662
17000
  path: repo.path,
@@ -16712,7 +17050,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
16712
17050
  try {
16713
17051
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
16714
17052
  if (linkResult.ok) {
16715
- written.push(...linkResult.added.map((rel) => path86.join(workspaceRoot, rel)));
17053
+ written.push(...linkResult.added.map((rel) => path87.join(workspaceRoot, rel)));
16716
17054
  if (!options.json) {
16717
17055
  say(
16718
17056
  `Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
@@ -16745,8 +17083,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
16745
17083
 
16746
17084
  // src/commands/workspace/undo.ts
16747
17085
  import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
16748
- import path87 from "node:path";
16749
- import fs64 from "fs-extra";
17086
+ import path88 from "node:path";
17087
+ import fs65 from "fs-extra";
16750
17088
  init_fs();
16751
17089
  init_logger();
16752
17090
  init_paths();
@@ -16776,7 +17114,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
16776
17114
  }
16777
17115
  if (options.dryRun) {
16778
17116
  for (const repo of config.repos) {
16779
- const repoRoot = path87.resolve(workspaceRoot, repo.path);
17117
+ const repoRoot = path88.resolve(workspaceRoot, repo.path);
16780
17118
  log(`
16781
17119
  \u2192 ${repo.name} (${repo.path})`);
16782
17120
  try {
@@ -16797,7 +17135,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
16797
17135
  warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
16798
17136
  continue;
16799
17137
  }
16800
- if (await fs64.pathExists(abs)) {
17138
+ if (await fs65.pathExists(abs)) {
16801
17139
  log(`[dry-run] Would remove ${entry.path} (linked ${entry.type} from ${entry.repo})`);
16802
17140
  }
16803
17141
  }
@@ -16807,12 +17145,12 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
16807
17145
  hausPath(workspaceRoot, "WORKSPACE.md")
16808
17146
  ];
16809
17147
  for (const abs of workspaceArtifacts) {
16810
- if (await fs64.pathExists(abs)) {
16811
- log(`[dry-run] Would remove ${path87.relative(workspaceRoot, abs)}`);
17148
+ if (await fs65.pathExists(abs)) {
17149
+ log(`[dry-run] Would remove ${path88.relative(workspaceRoot, abs)}`);
16812
17150
  }
16813
17151
  }
16814
- if (!await fs64.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
16815
- const claudeMd = await readText(path87.join(workspaceRoot, "CLAUDE.md"));
17152
+ if (!await fs65.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
17153
+ const claudeMd = await readText(path88.join(workspaceRoot, "CLAUDE.md"));
16816
17154
  if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
16817
17155
  log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
16818
17156
  }
@@ -16831,7 +17169,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
16831
17169
  }
16832
17170
  let anyRepoFailed = false;
16833
17171
  for (const repo of config.repos) {
16834
- const repoRoot = path87.resolve(workspaceRoot, repo.path);
17172
+ const repoRoot = path88.resolve(workspaceRoot, repo.path);
16835
17173
  log(`
16836
17174
  \u2192 ${repo.name} (${repo.path})`);
16837
17175
  try {
@@ -16851,29 +17189,29 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
16851
17189
  warn(`Refusing to remove linked context path outside the workspace root: ${entry.path}`);
16852
17190
  continue;
16853
17191
  }
16854
- if (await fs64.pathExists(abs)) {
16855
- await fs64.remove(abs);
17192
+ if (await fs65.pathExists(abs)) {
17193
+ await fs65.remove(abs);
16856
17194
  log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
16857
17195
  }
16858
17196
  }
16859
17197
  for (const rel of WORKSPACE_AGGREGATE_FILES) {
16860
17198
  const abs = hausPath(workspaceRoot, rel);
16861
- if (await fs64.pathExists(abs)) {
16862
- await fs64.remove(abs);
16863
- log(`Removed ${path87.relative(workspaceRoot, abs)}`);
17199
+ if (await fs65.pathExists(abs)) {
17200
+ await fs65.remove(abs);
17201
+ log(`Removed ${path88.relative(workspaceRoot, abs)}`);
16864
17202
  }
16865
17203
  }
16866
17204
  const manifestFile = manifestPath(workspaceRoot);
16867
- if (await fs64.pathExists(manifestFile)) {
16868
- await fs64.remove(manifestFile);
16869
- log(`Removed ${path87.relative(workspaceRoot, manifestFile)}`);
17205
+ if (await fs65.pathExists(manifestFile)) {
17206
+ await fs65.remove(manifestFile);
17207
+ log(`Removed ${path88.relative(workspaceRoot, manifestFile)}`);
16870
17208
  }
16871
17209
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
16872
- if (await fs64.pathExists(workspaceMdPath)) {
16873
- await fs64.remove(workspaceMdPath);
16874
- log(`Removed ${path87.relative(workspaceRoot, workspaceMdPath)}`);
17210
+ if (await fs65.pathExists(workspaceMdPath)) {
17211
+ await fs65.remove(workspaceMdPath);
17212
+ log(`Removed ${path88.relative(workspaceRoot, workspaceMdPath)}`);
16875
17213
  } else {
16876
- const claudeMdPath = path87.join(workspaceRoot, "CLAUDE.md");
17214
+ const claudeMdPath = path88.join(workspaceRoot, "CLAUDE.md");
16877
17215
  if (existsSync11(claudeMdPath)) {
16878
17216
  const prev = await readText(claudeMdPath);
16879
17217
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
@@ -16882,19 +17220,19 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
16882
17220
  end: WORKSPACE_BLOCK_END
16883
17221
  });
16884
17222
  if (next.length === 0) {
16885
- await fs64.remove(claudeMdPath);
17223
+ await fs65.remove(claudeMdPath);
16886
17224
  log("Removed CLAUDE.md (only contained the workspace haus import block).");
16887
17225
  } else {
16888
- await fs64.writeFile(claudeMdPath, next, "utf8");
17226
+ await fs65.writeFile(claudeMdPath, next, "utf8");
16889
17227
  log("Removed haus import block from CLAUDE.md (user content preserved).");
16890
17228
  }
16891
17229
  } else if (prev && hasLegacyWorkspaceBlock(prev)) {
16892
17230
  const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
16893
17231
  if (next.length === 0) {
16894
- await fs64.remove(claudeMdPath);
17232
+ await fs65.remove(claudeMdPath);
16895
17233
  log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
16896
17234
  } else {
16897
- await fs64.writeFile(claudeMdPath, next, "utf8");
17235
+ await fs65.writeFile(claudeMdPath, next, "utf8");
16898
17236
  log(
16899
17237
  "Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
16900
17238
  );
@@ -16942,7 +17280,7 @@ relationships: []
16942
17280
  log("Workspace initialized.");
16943
17281
  }
16944
17282
  async function scanWorkspace(workspaceRoot, opts) {
16945
- const configText = await readText(path88.join(workspaceRoot, WORKSPACE_FILE));
17283
+ const configText = await readText(path89.join(workspaceRoot, WORKSPACE_FILE));
16946
17284
  if (!configText) {
16947
17285
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
16948
17286
  process.exitCode = 1;
@@ -16964,7 +17302,7 @@ async function scanWorkspace(workspaceRoot, opts) {
16964
17302
  const inputs = [];
16965
17303
  const failures = [];
16966
17304
  for (const repo of config.repos) {
16967
- const repoRoot = path88.resolve(workspaceRoot, repo.path);
17305
+ const repoRoot = path89.resolve(workspaceRoot, repo.path);
16968
17306
  try {
16969
17307
  if (!existsSync12(repoRoot) || !statSync3(repoRoot).isDirectory()) {
16970
17308
  throw new Error("path is not a directory");
@@ -17033,7 +17371,7 @@ init_logger();
17033
17371
  init_paths();
17034
17372
  function cliVersion() {
17035
17373
  try {
17036
- const pkgPath = path89.join(packageRoot(), "package.json");
17374
+ const pkgPath = path90.join(packageRoot(), "package.json");
17037
17375
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
17038
17376
  return pkg.version ?? "0.0.0";
17039
17377
  } catch {
@@ -17043,7 +17381,7 @@ function cliVersion() {
17043
17381
  var program = new Command();
17044
17382
  function validateRuntimeNodeVersion() {
17045
17383
  try {
17046
- const pkgPath = path89.join(packageRoot(), "package.json");
17384
+ const pkgPath = path90.join(packageRoot(), "package.json");
17047
17385
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
17048
17386
  const requiredRange = pkg.engines?.node;
17049
17387
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -17137,7 +17475,17 @@ program.command("update").description("Sync the remote catalog cache, refresh th
17137
17475
  program.command("ci-gate").description(
17138
17476
  "Aggregate `doctor` + `decisions check` + `update --check --fast` into one pass/fail gate for CI. Uses the fast/cheap update-check tier (no per-item content hashing) to keep CI runs quick \u2014 run `haus update --check` directly for the fully-hashed tier. Each constituent command remains independently runnable with its own unchanged exit-code contract."
17139
17477
  ).option("--json", "Emit machine-readable {doctor, decisions, update, ok}").action((opts) => runCiGate(opts));
17140
- program.command("install").description("Seed ~/.claude/ with Haus-managed skills, agents, and hook settings").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
17478
+ program.command("install").description("Seed ~/.claude/ with Haus-managed skills, agents, and hook settings").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").addOption(
17479
+ new Option(
17480
+ "--internal",
17481
+ "Install Haus's internal channel too (request pipeline + slack-cowork); remembered per machine (ADR-0100)"
17482
+ ).default(void 0)
17483
+ ).addOption(
17484
+ new Option(
17485
+ "--no-internal",
17486
+ "Remove the internal channel from this machine and remember that"
17487
+ ).default(void 0)
17488
+ ).action(runInstall);
17141
17489
  program.command("configs [ids...]").description(
17142
17490
  "Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to copy specific items, or omit to copy all available config items."
17143
17491
  ).option("--force", "Overwrite existing files").option("--dry-run", "Preview what would be written without changing anything").option("--root <path>", "Project root (defaults to cwd)", process.cwd()).action(runConfigs);