@haus-tech/haus-workflow 1.5.8 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -29,12 +29,15 @@ async function runCommand(command, args = [], options = {}) {
29
29
  // non-zero exits are returned, not thrown
30
30
  ...options
31
31
  });
32
+ const killed = result.timedOut === true || result.isCanceled === true || Boolean(result.signal);
32
33
  return {
33
34
  command,
34
35
  args,
35
36
  stdout: String(result.stdout ?? ""),
36
- stderr: String(result.stderr ?? ""),
37
- exitCode: result.exitCode ?? 0
37
+ stderr: String(
38
+ result.stderr ?? (result.timedOut === true ? `timed out after ${options.timeout}ms` : "")
39
+ ),
40
+ exitCode: result.exitCode ?? (killed || result.failed === true ? 1 : 0)
38
41
  };
39
42
  } catch (error2) {
40
43
  const message = error2 instanceof Error ? error2.message : String(error2);
@@ -44,9 +47,9 @@ async function runCommand(command, args = [], options = {}) {
44
47
  async function runGit(args, options = {}) {
45
48
  return runCommand("git", args, options);
46
49
  }
47
- async function commandExists(command) {
50
+ async function commandExists(command, timeoutMs = 1e4) {
48
51
  try {
49
- const result = await runCommand(command, ["--version"]);
52
+ const result = await runCommand(command, ["--version"], { timeout: timeoutMs });
50
53
  return result.exitCode === 0;
51
54
  } catch {
52
55
  return false;
@@ -60,11 +63,11 @@ var init_exec = __esm({
60
63
 
61
64
  // src/cli.ts
62
65
  import { readFileSync as readFileSync5 } from "node:fs";
63
- import path72 from "node:path";
66
+ import path73 from "node:path";
64
67
  import { Command } from "commander";
65
68
 
66
69
  // src/commands/apply.ts
67
- import path24 from "node:path";
70
+ import path25 from "node:path";
68
71
  import checkbox from "@inquirer/checkbox";
69
72
 
70
73
  // src/catalog/load-catalog.ts
@@ -216,6 +219,11 @@ function hausPath(root, ...parts) {
216
219
  function claudePath(root, ...parts) {
217
220
  return path2.join(root, ".claude", ...parts);
218
221
  }
222
+ function claudeConfigDir() {
223
+ const override = process.env.CLAUDE_CONFIG_DIR;
224
+ if (override && override.trim().length > 0) return override;
225
+ return path2.join(os.homedir(), ".claude");
226
+ }
219
227
  function displayPath(root, targetPath) {
220
228
  const rel = path2.relative(root, targetPath).replace(/\\/g, "/");
221
229
  if (rel && !rel.startsWith("../") && rel !== "..") {
@@ -894,7 +902,15 @@ function parseManifest(json) {
894
902
  if (formerIdFailures.length > 0) {
895
903
  return { ok: false, error: formerIdFailures[0] };
896
904
  }
897
- return { ok: true, manifest: { version: root.version, items }, warnings };
905
+ return {
906
+ ok: true,
907
+ manifest: {
908
+ version: root.version,
909
+ items,
910
+ ...root.globalPlugins !== void 0 ? { globalPlugins: root.globalPlugins } : {}
911
+ },
912
+ warnings
913
+ };
898
914
  }
899
915
 
900
916
  // src/catalog/remote-catalog/manifest.ts
@@ -1484,13 +1500,16 @@ async function syncRemoteCatalog() {
1484
1500
  warn("Remote catalog fetch failed \u2014 using bundled catalog");
1485
1501
  return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
1486
1502
  }
1487
- const { version, items } = manifest;
1503
+ const { version, items, globalPlugins } = manifest;
1488
1504
  const cacheDir = getCacheDir();
1489
1505
  try {
1490
1506
  await fs6.ensureDir(cacheDir);
1491
1507
  await fs6.writeFile(
1492
1508
  path8.join(cacheDir, "manifest.json"),
1493
- `${JSON.stringify({ version, items }, null, 2)}
1509
+ // globalPlugins must round-trip into the cache: the cache is what `haus update`
1510
+ // and `haus refresh` read pins from, so dropping it here would make every
1511
+ // catalog pin bump invisible and pin resolution fall back to the bundled default.
1512
+ `${JSON.stringify({ version, items, ...globalPlugins !== void 0 ? { globalPlugins } : {} }, null, 2)}
1494
1513
  `,
1495
1514
  "utf8"
1496
1515
  );
@@ -1544,14 +1563,20 @@ async function loadCatalogContext(root) {
1544
1563
  return {
1545
1564
  items: data2?.items ?? [],
1546
1565
  contentRoot: path9.dirname(envPath),
1547
- source: "fixture"
1566
+ source: "fixture",
1567
+ globalPlugins: data2?.globalPlugins
1548
1568
  };
1549
1569
  }
1550
1570
  const cacheDir = getCacheDir();
1551
1571
  const cacheManifestPath = path9.join(cacheDir, "manifest.json");
1552
1572
  const cacheData = await readJson(cacheManifestPath);
1553
1573
  if (cacheData?.items?.length) {
1554
- return { items: cacheData.items, contentRoot: cacheDir, source: "cache" };
1574
+ return {
1575
+ items: cacheData.items,
1576
+ contentRoot: cacheDir,
1577
+ source: "cache",
1578
+ globalPlugins: cacheData.globalPlugins
1579
+ };
1555
1580
  }
1556
1581
  const localManifest = path9.join(root, "library/catalog/manifest.json");
1557
1582
  const localData = await readJson(localManifest);
@@ -1559,7 +1584,8 @@ async function loadCatalogContext(root) {
1559
1584
  return {
1560
1585
  items: localData.items,
1561
1586
  contentRoot: path9.dirname(localManifest),
1562
- source: "local"
1587
+ source: "local",
1588
+ globalPlugins: localData.globalPlugins
1563
1589
  };
1564
1590
  }
1565
1591
  const packageManifest = path9.join(packageRoot(), "library/catalog/manifest.json");
@@ -1567,7 +1593,8 @@ async function loadCatalogContext(root) {
1567
1593
  return {
1568
1594
  items: data?.items ?? [],
1569
1595
  contentRoot: path9.dirname(packageManifest),
1570
- source: "bundled"
1596
+ source: "bundled",
1597
+ globalPlugins: data?.globalPlugins
1571
1598
  };
1572
1599
  }
1573
1600
  async function loadCatalog(root) {
@@ -1857,6 +1884,26 @@ function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
1857
1884
  return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
1858
1885
  }
1859
1886
 
1887
+ // src/recommender/superseded-by-plugin.ts
1888
+ var RULES = [
1889
+ {
1890
+ pinId: "superpowers",
1891
+ originSourceId: "superpowers-pcvelz",
1892
+ idPrefix: "haus.superpowers-"
1893
+ }
1894
+ ];
1895
+ function isItemSupersededByPlugin(item, activePinIds) {
1896
+ if (item.originSourceId === void 0) return false;
1897
+ return RULES.some(
1898
+ (rule) => activePinIds.includes(rule.pinId) && rule.originSourceId === item.originSourceId
1899
+ );
1900
+ }
1901
+ function isSupersededByPlugin(lockId, activePinIds) {
1902
+ return RULES.some(
1903
+ (rule) => (activePinIds === void 0 || activePinIds.includes(rule.pinId)) && lockId.startsWith(rule.idPrefix)
1904
+ );
1905
+ }
1906
+
1860
1907
  // src/update/hash-installed.ts
1861
1908
  import path11 from "node:path";
1862
1909
  import fg2 from "fast-glob";
@@ -2025,13 +2072,22 @@ async function readManifest() {
2025
2072
  async function writeManifest(manifest) {
2026
2073
  await writeJson(hausManifestPath(), manifest);
2027
2074
  }
2028
- function buildManifest(source, files, hooks) {
2075
+ async function recordManifestPlugins(plugins) {
2076
+ if (plugins.length === 0) return;
2077
+ const manifest = await readManifest();
2078
+ if (!manifest) return;
2079
+ const byId = new Map((manifest.plugins ?? []).map((p) => [p.id, p]));
2080
+ for (const plugin of plugins) byId.set(plugin.id, plugin);
2081
+ await writeManifest({ ...manifest, plugins: [...byId.values()] });
2082
+ }
2083
+ function buildManifest(source, files, hooks, plugins) {
2029
2084
  return {
2030
2085
  _schema: MANIFEST_SCHEMA,
2031
2086
  source,
2032
2087
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2033
2088
  files,
2034
- hooks
2089
+ hooks,
2090
+ ...plugins && plugins.length > 0 ? { plugins } : {}
2035
2091
  };
2036
2092
  }
2037
2093
 
@@ -2287,8 +2343,8 @@ function buildDenyRules() {
2287
2343
  for (const command of DENY_COMMANDS) {
2288
2344
  rules.push(`Bash(${command}:*)`);
2289
2345
  }
2290
- for (const path73 of DENY_PATHS) {
2291
- const pattern = DENY_DIRS.has(path73) ? `${path73}/**` : path73;
2346
+ for (const path74 of DENY_PATHS) {
2347
+ const pattern = DENY_DIRS.has(path74) ? `${path74}/**` : path74;
2292
2348
  for (const tool of FILE_TOOLS) {
2293
2349
  rules.push(`${tool}(${pattern})`);
2294
2350
  }
@@ -2371,6 +2427,7 @@ import path14 from "node:path";
2371
2427
  import fg3 from "fast-glob";
2372
2428
  import fs10 from "fs-extra";
2373
2429
  var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
2430
+ var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
2374
2431
  var PATH_REWRITES = [
2375
2432
  ["skills/shared/", ".claude/skills/shared/"]
2376
2433
  ];
@@ -3246,6 +3303,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
3246
3303
  warn(`Skipping ${item.id}: reviewStatus is deprecated`);
3247
3304
  continue;
3248
3305
  }
3306
+ if (isItemSupersededByPlugin(manifestItem, opts.activePluginIds ?? [])) {
3307
+ warn(`Skipping ${item.id}: provided by an installed global plugin`);
3308
+ continue;
3309
+ }
3249
3310
  const manifestItemTags = (manifestItem.tags ?? []).map((t) => t.toLowerCase());
3250
3311
  if (UNSUPPORTED.some((x) => manifestItemTags.includes(x))) {
3251
3312
  warn(`Skipping ${item.id}: unsupported stack policy`);
@@ -3371,7 +3432,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
3371
3432
  `${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
3372
3433
  );
3373
3434
  }
3374
- await cleanupStaleCatalogItems(root, cleanupManifestById, dryRun, opts.quiet);
3435
+ await cleanupStaleCatalogItems(
3436
+ root,
3437
+ cleanupManifestById,
3438
+ dryRun,
3439
+ opts.quiet,
3440
+ opts.activePluginIds ?? []
3441
+ );
3375
3442
  const refuseDestructiveLockChange = recommendationUnusable && prevLock.length > 0 && !opts.force;
3376
3443
  if (opts.prune) {
3377
3444
  if (refuseDestructiveLockChange) {
@@ -3515,25 +3582,46 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
3515
3582
  }
3516
3583
  return existing;
3517
3584
  }
3518
- async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
3585
+ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activePluginIds = []) {
3519
3586
  const say = quiet ? () => {
3520
3587
  } : log;
3521
3588
  const prevLock = await readJson(hausPath(root, "haus.lock.json"));
3522
3589
  if (!prevLock?.length) return;
3590
+ const supersededEntries = prevLock.filter(
3591
+ (entry) => entry.id && isSupersededByPlugin(entry.id, activePluginIds)
3592
+ );
3593
+ let keepShared = false;
3594
+ for (const entry of supersededEntries) {
3595
+ const relPaths = entry.paths ?? [];
3596
+ if (relPaths.length === 0) continue;
3597
+ const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
3598
+ if (existing.length === 0) continue;
3599
+ if (entry.hash === void 0) {
3600
+ keepShared = true;
3601
+ continue;
3602
+ }
3603
+ const currentHash = await hashInstalledPaths(
3604
+ root,
3605
+ existing.map((e) => e.rel)
3606
+ );
3607
+ if (currentHash !== entry.hash) keepShared = true;
3608
+ }
3523
3609
  for (const entry of prevLock) {
3524
3610
  if (!entry.id) continue;
3525
3611
  const manifestItem = manifestById.get(entry.id);
3526
3612
  const removedFromManifest = manifestItem === void 0;
3527
3613
  const deprecated = manifestItem?.reviewStatus === "deprecated";
3528
- if (!removedFromManifest && !deprecated) continue;
3614
+ const superseded = isSupersededByPlugin(entry.id, activePluginIds);
3615
+ if (!removedFromManifest && !deprecated && !superseded) continue;
3529
3616
  const relPaths = entry.paths ?? [];
3530
3617
  if (relPaths.length === 0) continue;
3531
3618
  const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
3532
3619
  if (existing.length === 0) continue;
3533
- const pruneReason = deprecated ? "deprecated" : "stale";
3620
+ const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
3621
+ const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
3534
3622
  if (entry.hash === void 0) {
3535
3623
  warn(
3536
- `${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
3624
+ `${label} catalog item ${entry.id} has no lock hash \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
3537
3625
  );
3538
3626
  continue;
3539
3627
  }
@@ -3543,11 +3631,28 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
3543
3631
  );
3544
3632
  if (currentHash !== entry.hash) {
3545
3633
  warn(
3546
- `${deprecated ? "Deprecated" : "Stale"} catalog item ${entry.id} was modified locally \u2014 leaving in place: ${existing.map((e) => e.rel).join(", ")}`
3634
+ `${label} catalog item ${entry.id} was modified locally \u2014 leaving in place${superseded ? " (the global plugin provides it; delete the copy manually if you no longer want it)" : ""}: ${existing.map((e) => e.rel).join(", ")}`
3547
3635
  );
3548
3636
  continue;
3549
3637
  }
3550
- for (const { abs } of existing) {
3638
+ const removable = superseded ? existing.filter(({ rel }) => {
3639
+ const isShared = rel.split(path20.sep).join("/") === SUPERPOWERS_SHARED_INSTALL_REL;
3640
+ if (isShared && keepShared) {
3641
+ say(
3642
+ `Keeping ${displayPath(root, path20.join(root, rel))} \u2014 a locally-edited superpowers skill still uses it`
3643
+ );
3644
+ return false;
3645
+ }
3646
+ return true;
3647
+ }) : existing;
3648
+ if (superseded && !dryRun && removable.length > 0) {
3649
+ await backupBeforePrune(
3650
+ root,
3651
+ removable.map((e) => e.abs),
3652
+ say
3653
+ );
3654
+ }
3655
+ for (const { abs } of removable) {
3551
3656
  if (dryRun) {
3552
3657
  say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
3553
3658
  continue;
@@ -3786,16 +3891,431 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
3786
3891
  return untracked;
3787
3892
  }
3788
3893
 
3894
+ // src/install/ensure-global-plugins.ts
3895
+ import path22 from "node:path";
3896
+ init_exec();
3897
+
3898
+ // src/install/global-plugins.ts
3899
+ var DEFAULT_GLOBAL_PLUGINS = [
3900
+ {
3901
+ id: "superpowers",
3902
+ plugin: "superpowers-extended-cc",
3903
+ marketplace: "superpowers-extended-cc-marketplace",
3904
+ url: "https://github.com/pcvelz/superpowers.git",
3905
+ ref: "v6.4.0"
3906
+ }
3907
+ ];
3908
+ var MANAGED_SCOPE = "user";
3909
+ function pluginId(pin) {
3910
+ return `${pin.plugin}@${pin.marketplace}`;
3911
+ }
3912
+ function canonicalUrl(url) {
3913
+ return url.trim().toLowerCase().replace(/\.git$/, "").replace(/\/+$/, "");
3914
+ }
3915
+ function isNonEmptyString3(value) {
3916
+ return typeof value === "string" && value.trim().length > 0;
3917
+ }
3918
+ function normalizeMarketplaceSource(marketplace) {
3919
+ if (marketplace.source === "git" && isNonEmptyString3(marketplace.url)) {
3920
+ return { url: canonicalUrl(marketplace.url), ref: marketplace.ref };
3921
+ }
3922
+ if (marketplace.source === "github" && isNonEmptyString3(marketplace.repo)) {
3923
+ return { url: canonicalUrl(`https://github.com/${marketplace.repo}`), ref: marketplace.ref };
3924
+ }
3925
+ return null;
3926
+ }
3927
+ function resolveGlobalPlugins(manifest, deps = {}) {
3928
+ const warn2 = deps.warn ?? warn;
3929
+ const declared = manifest?.globalPlugins;
3930
+ if (declared === void 0) return [...DEFAULT_GLOBAL_PLUGINS];
3931
+ if (!Array.isArray(declared)) {
3932
+ warn2("Catalog `globalPlugins` is not an array \u2014 falling back to the bundled pin.");
3933
+ return [...DEFAULT_GLOBAL_PLUGINS];
3934
+ }
3935
+ const pins = [];
3936
+ for (const entry of declared) {
3937
+ if (!isValidPin(entry)) {
3938
+ const label = entry !== null && typeof entry === "object" && isNonEmptyString3(entry.id) ? entry.id : JSON.stringify(entry);
3939
+ warn2(`Skipping malformed catalog globalPlugins entry: ${label}`);
3940
+ continue;
3941
+ }
3942
+ if (!isImmutableRef(entry.ref)) {
3943
+ warn2(
3944
+ `Refusing catalog pin ${entry.id}: ref "${entry.ref}" is not a release tag \u2014 haus only installs immutable, reviewed plugin content.`
3945
+ );
3946
+ continue;
3947
+ }
3948
+ pins.push({
3949
+ id: entry.id,
3950
+ plugin: entry.plugin,
3951
+ marketplace: entry.marketplace,
3952
+ url: entry.url,
3953
+ ref: entry.ref
3954
+ });
3955
+ }
3956
+ return pins;
3957
+ }
3958
+ function isValidPin(entry) {
3959
+ if (entry === null || typeof entry !== "object") return false;
3960
+ const candidate = entry;
3961
+ return isNonEmptyString3(candidate.id) && isNonEmptyString3(candidate.plugin) && isNonEmptyString3(candidate.marketplace) && isNonEmptyString3(candidate.url) && isNonEmptyString3(candidate.ref);
3962
+ }
3963
+ var RELEASE_TAG = /^v?\d+(\.\d+)*([-+][0-9A-Za-z.-]+)?$/;
3964
+ function isImmutableRef(ref) {
3965
+ return RELEASE_TAG.test(ref.trim());
3966
+ }
3967
+ function findManagedInstall(state, pin) {
3968
+ const id = pluginId(pin);
3969
+ return state.installed.find((p) => p.id === id && (p.scope ?? MANAGED_SCOPE) === MANAGED_SCOPE);
3970
+ }
3971
+ function isPluginActive(state, pin) {
3972
+ if (!state.available) return false;
3973
+ const id = pluginId(pin);
3974
+ return state.installed.some((p) => p.id === id && p.enabled !== false);
3975
+ }
3976
+ function planPluginActions(desired, state) {
3977
+ return desired.map((pin) => planOne(pin, state));
3978
+ }
3979
+ function planOne(pin, state) {
3980
+ if (!state.available) {
3981
+ return {
3982
+ pin,
3983
+ kind: "unavailable",
3984
+ steps: [],
3985
+ reason: "the `claude` CLI is not on PATH \u2014 plugin state is unknown"
3986
+ };
3987
+ }
3988
+ const marketplace = state.marketplaces.find((m) => m.name === pin.marketplace);
3989
+ const normalized = marketplace ? normalizeMarketplaceSource(marketplace) : null;
3990
+ const marketplaceMatches = normalized !== null && normalized.url === canonicalUrl(pin.url) && normalized.ref === pin.ref;
3991
+ const installed = findManagedInstall(state, pin);
3992
+ const disabled = installed?.enabled === false;
3993
+ const enableStep = disabled ? ["plugin-enable"] : [];
3994
+ if (!installed) {
3995
+ const steps = marketplaceMatches ? ["plugin-install"] : ["marketplace-add", "plugin-install"];
3996
+ return {
3997
+ pin,
3998
+ kind: "install",
3999
+ steps,
4000
+ reason: marketplace ? `plugin ${pluginId(pin)} is not installed at ${MANAGED_SCOPE} scope` : `marketplace ${pin.marketplace} is not configured`
4001
+ };
4002
+ }
4003
+ if (!marketplaceMatches) {
4004
+ return {
4005
+ pin,
4006
+ kind: "repin",
4007
+ steps: [
4008
+ "marketplace-add",
4009
+ "marketplace-update",
4010
+ "plugin-uninstall",
4011
+ "plugin-install",
4012
+ ...enableStep
4013
+ ],
4014
+ reason: `marketplace ${pin.marketplace} does not track the pinned ref ${pin.ref}`,
4015
+ ...normalized?.ref !== void 0 ? { overriddenRef: normalized.ref } : {}
4016
+ };
4017
+ }
4018
+ const versionsDiffer = isNonEmptyString3(marketplace?.pluginVersion) && isNonEmptyString3(installed.version) && marketplace.pluginVersion !== installed.version;
4019
+ if (versionsDiffer) {
4020
+ return {
4021
+ pin,
4022
+ kind: "repin",
4023
+ steps: ["marketplace-update", "plugin-uninstall", "plugin-install", ...enableStep],
4024
+ reason: `installed ${installed.version} does not match marketplace ${marketplace?.pluginVersion} at ref ${pin.ref}`
4025
+ };
4026
+ }
4027
+ if (disabled) {
4028
+ return {
4029
+ pin,
4030
+ kind: "re-enable",
4031
+ steps: ["plugin-enable"],
4032
+ reason: `plugin ${pluginId(pin)} is disabled \u2014 haus workflows depend on it`
4033
+ };
4034
+ }
4035
+ return { pin, kind: "up-to-date", steps: [], reason: `pinned to ${pin.ref}` };
4036
+ }
4037
+ function pluginDriftStatus(actions, opts) {
4038
+ if (actions.length === 0) return false;
4039
+ if (actions.some((a) => a.kind === "unavailable")) return null;
4040
+ return actions.some((a) => {
4041
+ if (a.kind === "install" || a.kind === "re-enable") return true;
4042
+ if (a.kind === "repin") return opts.matchRef;
4043
+ return false;
4044
+ });
4045
+ }
4046
+
4047
+ // src/install/ensure-global-plugins.ts
4048
+ async function loadCatalogPluginPins(root) {
4049
+ const ctx = await loadCatalogContext(root);
4050
+ return resolveGlobalPlugins({ globalPlugins: ctx.globalPlugins });
4051
+ }
4052
+ async function activeGlobalPluginIds(root, deps = {}) {
4053
+ try {
4054
+ const [pins, state] = await Promise.all([loadCatalogPluginPins(root), probePluginEnv(deps)]);
4055
+ if (!state.available) return [];
4056
+ return pins.filter((pin) => isPluginActive(state, pin)).map((pin) => pin.id);
4057
+ } catch {
4058
+ return [];
4059
+ }
4060
+ }
4061
+ var EXEC_TIMEOUT_MS = 12e4;
4062
+ var EXEC_ENV = { GIT_TERMINAL_PROMPT: "0" };
4063
+ function globalPluginsDisabled() {
4064
+ return process.env.HAUS_NO_GLOBAL_PLUGINS === "1";
4065
+ }
4066
+ var realExec = (args) => runCommand("claude", args, {
4067
+ timeout: EXEC_TIMEOUT_MS,
4068
+ env: EXEC_ENV,
4069
+ extendEnv: true
4070
+ });
4071
+ function parseInstalled(stdout) {
4072
+ try {
4073
+ const parsed = JSON.parse(stdout);
4074
+ const list = Array.isArray(parsed) ? parsed : parsed !== null && typeof parsed === "object" ? parsed.installed : void 0;
4075
+ if (!Array.isArray(list)) return null;
4076
+ return list.flatMap((entry) => {
4077
+ const item = entry;
4078
+ if (typeof item.id !== "string") return [];
4079
+ return [
4080
+ {
4081
+ id: item.id,
4082
+ ...typeof item.version === "string" ? { version: item.version } : {},
4083
+ ...typeof item.scope === "string" ? { scope: item.scope } : {},
4084
+ ...typeof item.enabled === "boolean" ? { enabled: item.enabled } : {}
4085
+ }
4086
+ ];
4087
+ });
4088
+ } catch {
4089
+ return null;
4090
+ }
4091
+ }
4092
+ async function readMarketplaceRefs(configDir) {
4093
+ const settings = await readJson(path22.join(configDir, "settings.json"));
4094
+ const out = {};
4095
+ for (const [name, entry] of Object.entries(settings?.extraKnownMarketplaces ?? {})) {
4096
+ const ref = entry?.source?.ref;
4097
+ out[name] = typeof ref === "string" ? ref : void 0;
4098
+ }
4099
+ return out;
4100
+ }
4101
+ async function readClonedPluginVersion(installLocation) {
4102
+ const manifest = await readJson(
4103
+ path22.join(installLocation, ".claude-plugin", "plugin.json")
4104
+ );
4105
+ return typeof manifest?.version === "string" ? manifest.version : void 0;
4106
+ }
4107
+ async function parseMarketplaces(stdout, configDir) {
4108
+ let rows = [];
4109
+ try {
4110
+ const parsed = JSON.parse(stdout);
4111
+ if (!Array.isArray(parsed)) return null;
4112
+ rows = parsed;
4113
+ } catch {
4114
+ return null;
4115
+ }
4116
+ const refs = await readMarketplaceRefs(configDir);
4117
+ const out = [];
4118
+ for (const row of rows) {
4119
+ if (typeof row.name !== "string") continue;
4120
+ const installLocation = typeof row.installLocation === "string" ? row.installLocation : void 0;
4121
+ out.push({
4122
+ name: row.name,
4123
+ ...typeof row.source === "string" ? { source: row.source } : {},
4124
+ ...typeof row.url === "string" ? { url: row.url } : {},
4125
+ ...typeof row.repo === "string" ? { repo: row.repo } : {},
4126
+ ...refs[row.name] !== void 0 ? { ref: refs[row.name] } : {},
4127
+ ...installLocation ? { pluginVersion: await readClonedPluginVersion(installLocation) } : {}
4128
+ });
4129
+ }
4130
+ return out;
4131
+ }
4132
+ async function probePluginEnv(deps = {}) {
4133
+ const exec = deps.exec ?? realExec;
4134
+ const isAvailable = deps.isAvailable ?? (() => commandExists("claude"));
4135
+ const warn2 = deps.warn ?? warn;
4136
+ if (globalPluginsDisabled() && deps.exec === void 0) {
4137
+ return { available: false, installed: [], marketplaces: [] };
4138
+ }
4139
+ if (!await isAvailable()) return { available: false, installed: [], marketplaces: [] };
4140
+ const [listed, markets] = await Promise.all([
4141
+ exec(["plugin", "list", "--json"]),
4142
+ exec(["plugin", "marketplace", "list", "--json"])
4143
+ ]);
4144
+ if (listed.exitCode !== 0 || markets.exitCode !== 0) {
4145
+ warn2(
4146
+ `Could not read Claude Code plugin state (${listed.exitCode !== 0 ? "plugin list" : "marketplace list"} exited ${listed.exitCode !== 0 ? listed.exitCode : markets.exitCode}) \u2014 leaving global plugins untouched.`
4147
+ );
4148
+ return { available: false, installed: [], marketplaces: [] };
4149
+ }
4150
+ const configDir = deps.configDir ?? claudeConfigDir();
4151
+ const installed = parseInstalled(listed.stdout);
4152
+ const marketplaces = await parseMarketplaces(markets.stdout, configDir);
4153
+ if (installed === null || marketplaces === null) {
4154
+ warn2("Claude Code returned unreadable plugin state \u2014 leaving global plugins untouched.");
4155
+ return { available: false, installed: [], marketplaces: [] };
4156
+ }
4157
+ return { available: true, installed, marketplaces };
4158
+ }
4159
+ function stepArgs(step, pin) {
4160
+ const id = pluginId(pin);
4161
+ switch (step) {
4162
+ case "marketplace-add":
4163
+ return ["plugin", "marketplace", "add", `${pin.url}#${pin.ref}`, "--scope", "user"];
4164
+ case "marketplace-update":
4165
+ return ["plugin", "marketplace", "update", pin.marketplace];
4166
+ case "plugin-uninstall":
4167
+ return ["plugin", "uninstall", id, "--scope", "user"];
4168
+ case "plugin-install":
4169
+ return ["plugin", "install", id, "--scope", "user"];
4170
+ case "plugin-update":
4171
+ return ["plugin", "update", id, "--scope", "user"];
4172
+ case "plugin-enable":
4173
+ return ["plugin", "enable", id, "--scope", "user"];
4174
+ }
4175
+ }
4176
+ var RESTART_STEPS = /* @__PURE__ */ new Set([
4177
+ "plugin-uninstall",
4178
+ "plugin-install",
4179
+ "plugin-update",
4180
+ "plugin-enable"
4181
+ ]);
4182
+ function manualCommands(pin) {
4183
+ return [
4184
+ `claude plugin marketplace add ${pin.url}#${pin.ref}`,
4185
+ `claude plugin install ${pluginId(pin)}`
4186
+ ];
4187
+ }
4188
+ async function ensureGlobalPlugins(pins, deps = {}) {
4189
+ const exec = deps.exec ?? realExec;
4190
+ const log2 = deps.log ?? log;
4191
+ const warn2 = deps.warn ?? warn;
4192
+ const dryRun = deps.dryRun ?? false;
4193
+ if (globalPluginsDisabled() && deps.exec === void 0) {
4194
+ return { actions: [], ensured: [], failed: [], unavailable: true, restartRequired: false };
4195
+ }
4196
+ const state = await probePluginEnv(deps);
4197
+ const actions = planPluginActions(pins, state);
4198
+ const result = {
4199
+ actions,
4200
+ ensured: [],
4201
+ failed: [],
4202
+ unavailable: !state.available,
4203
+ restartRequired: false
4204
+ };
4205
+ for (const action of actions) {
4206
+ if (action.kind === "unavailable") {
4207
+ warn2(
4208
+ `Claude Code CLI not found \u2014 cannot manage the ${action.pin.id} plugin. Install it manually:`
4209
+ );
4210
+ manualCommands(action.pin).forEach((cmd) => warn2(` ${cmd}`));
4211
+ continue;
4212
+ }
4213
+ if (action.kind === "up-to-date") {
4214
+ result.ensured.push(action.pin.id);
4215
+ continue;
4216
+ }
4217
+ if (action.overriddenRef !== void 0 && action.overriddenRef !== action.pin.ref) {
4218
+ warn2(
4219
+ `Re-pointing marketplace ${action.pin.marketplace} from ${action.overriddenRef} to the haus-pinned ${action.pin.ref} \u2014 haus manages this plugin's version.`
4220
+ );
4221
+ }
4222
+ if (dryRun) {
4223
+ log2(`[dry-run] would ${action.kind} ${action.pin.id}: ${action.reason}`);
4224
+ action.steps.forEach(
4225
+ (step) => log2(`[dry-run] claude ${stepArgs(step, action.pin).join(" ")}`)
4226
+ );
4227
+ continue;
4228
+ }
4229
+ let ok = true;
4230
+ for (const step of action.steps) {
4231
+ const args = stepArgs(step, action.pin);
4232
+ const run = await exec(args);
4233
+ if (run.exitCode !== 0) {
4234
+ ok = false;
4235
+ warn2(`\`claude ${args.join(" ")}\` failed: ${run.stderr.trim() || `exit ${run.exitCode}`}`);
4236
+ break;
4237
+ }
4238
+ if (RESTART_STEPS.has(step)) result.restartRequired = true;
4239
+ }
4240
+ if (ok) {
4241
+ result.ensured.push(action.pin.id);
4242
+ log2(
4243
+ `Global plugin ${action.pin.id} ${action.kind === "install" ? "installed" : "updated"} (${action.pin.ref}).`
4244
+ );
4245
+ } else {
4246
+ result.failed.push(action.pin.id);
4247
+ warn2(`Could not set up the ${action.pin.id} plugin. Run manually:`);
4248
+ manualCommands(action.pin).forEach((cmd) => warn2(` ${cmd}`));
4249
+ }
4250
+ }
4251
+ if (result.restartRequired && !dryRun) {
4252
+ log2("Restart Claude Code to load the updated plugin.");
4253
+ }
4254
+ return result;
4255
+ }
4256
+ async function removeGlobalPlugins(plugins, deps = {}) {
4257
+ const exec = deps.exec ?? realExec;
4258
+ const warn2 = deps.warn ?? warn;
4259
+ const log2 = deps.log ?? log;
4260
+ const result = { removed: [], failed: [], unavailable: false };
4261
+ if (plugins.length === 0) return result;
4262
+ if (globalPluginsDisabled() && deps.exec === void 0) {
4263
+ return { ...result, unavailable: true };
4264
+ }
4265
+ const state = await probePluginEnv(deps);
4266
+ if (!state.available) {
4267
+ result.unavailable = true;
4268
+ warn2("Claude Code CLI not found \u2014 leaving the haus-managed global plugin(s) in place.");
4269
+ return result;
4270
+ }
4271
+ const scopeKey = (id, scope) => `${id}@@${scope ?? "user"}`;
4272
+ const stillInstalled = new Set(state.installed.map((p) => scopeKey(p.id, p.scope)));
4273
+ for (const pin of plugins) {
4274
+ const id = pluginId(pin);
4275
+ const managedKey = scopeKey(id, "user");
4276
+ if (deps.dryRun) {
4277
+ if (stillInstalled.has(managedKey))
4278
+ log2(`[dry-run] would remove global plugin ${pin.id} (${id})`);
4279
+ continue;
4280
+ }
4281
+ if (stillInstalled.has(managedKey)) {
4282
+ const uninstalled = await exec(["plugin", "uninstall", id, "--scope", "user"]);
4283
+ if (uninstalled.exitCode !== 0) {
4284
+ result.failed.push(pin.id);
4285
+ warn2(
4286
+ `Could not remove plugin ${id}: ${uninstalled.stderr.trim() || `exit ${uninstalled.exitCode}`}`
4287
+ );
4288
+ continue;
4289
+ }
4290
+ stillInstalled.delete(managedKey);
4291
+ result.removed.push(pin.id);
4292
+ }
4293
+ const marketplaceStillConfigured = state.marketplaces.some((m) => m.name === pin.marketplace);
4294
+ const othersFromMarketplace = [...stillInstalled].some(
4295
+ (key) => key.split("@@")[0]?.endsWith(`@${pin.marketplace}`)
4296
+ );
4297
+ if (marketplaceStillConfigured && !othersFromMarketplace) {
4298
+ const removedMarket = await exec(["plugin", "marketplace", "remove", pin.marketplace]);
4299
+ if (removedMarket.exitCode !== 0) {
4300
+ warn2(
4301
+ `Could not remove marketplace ${pin.marketplace}: ${removedMarket.stderr.trim() || `exit ${removedMarket.exitCode}`}`
4302
+ );
4303
+ }
4304
+ }
4305
+ }
4306
+ return result;
4307
+ }
4308
+
3789
4309
  // src/refs/fetch-refs.ts
3790
- import path23 from "node:path";
4310
+ import path24 from "node:path";
3791
4311
  import fs21 from "fs-extra";
3792
4312
 
3793
4313
  // src/refs/cache-meta.ts
3794
- import path22 from "node:path";
4314
+ import path23 from "node:path";
3795
4315
  import fs20 from "fs-extra";
3796
4316
  var META_FILENAME = "cache-meta.json";
3797
4317
  async function readCacheMeta(cacheDir) {
3798
- const metaPath = path22.join(cacheDir, META_FILENAME);
4318
+ const metaPath = path23.join(cacheDir, META_FILENAME);
3799
4319
  try {
3800
4320
  const raw = await fs20.readFile(metaPath, "utf8");
3801
4321
  return JSON.parse(raw);
@@ -3805,7 +4325,7 @@ async function readCacheMeta(cacheDir) {
3805
4325
  }
3806
4326
  async function writeCacheMeta(cacheDir, meta) {
3807
4327
  await fs20.ensureDir(cacheDir);
3808
- const metaPath = path22.join(cacheDir, META_FILENAME);
4328
+ const metaPath = path23.join(cacheDir, META_FILENAME);
3809
4329
  await fs20.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
3810
4330
  }
3811
4331
 
@@ -3826,8 +4346,8 @@ function isLlmsTxtUrl(ref) {
3826
4346
  async function fetchSingleRef(url, cacheDir, meta) {
3827
4347
  const existing = meta[url];
3828
4348
  const headers = {};
3829
- const safeFile = existing?.file && existing.file === path23.basename(existing.file);
3830
- const cachedFileExists = safeFile && await fs21.pathExists(path23.join(cacheDir, existing.file));
4349
+ const safeFile = existing?.file && existing.file === path24.basename(existing.file);
4350
+ const cachedFileExists = safeFile && await fs21.pathExists(path24.join(cacheDir, existing.file));
3831
4351
  if (cachedFileExists) {
3832
4352
  if (existing?.etag) headers["If-None-Match"] = existing.etag;
3833
4353
  if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
@@ -3839,7 +4359,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
3839
4359
  const text = await res.text();
3840
4360
  const file = `${urlToSlug(url)}.md`;
3841
4361
  await fs21.ensureDir(cacheDir);
3842
- await fs21.writeFile(path23.join(cacheDir, file), text, "utf8");
4362
+ await fs21.writeFile(path24.join(cacheDir, file), text, "utf8");
3843
4363
  meta[url] = {
3844
4364
  url,
3845
4365
  etag: res.headers.get("etag") ?? void 0,
@@ -3862,8 +4382,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
3862
4382
  if (keepUrls.has(url)) continue;
3863
4383
  delete meta[url];
3864
4384
  removed++;
3865
- const safeFile = typeof entry.file === "string" && entry.file === path23.basename(entry.file);
3866
- if (safeFile) await fs21.remove(path23.join(cacheDir, entry.file)).catch(() => {
4385
+ const safeFile = typeof entry.file === "string" && entry.file === path24.basename(entry.file);
4386
+ if (safeFile) await fs21.remove(path24.join(cacheDir, entry.file)).catch(() => {
3867
4387
  });
3868
4388
  }
3869
4389
  if (removed > 0) await writeCacheMeta(cacheDir, meta);
@@ -3885,10 +4405,10 @@ async function fetchRefsForItems(items, cacheDir) {
3885
4405
  const outcome = await fetchSingleRef(url, cacheDir, meta);
3886
4406
  if (outcome.result === "fetched") {
3887
4407
  summary.fetched++;
3888
- summary.cachedFiles[url] = path23.join(cacheDir, outcome.file);
4408
+ summary.cachedFiles[url] = path24.join(cacheDir, outcome.file);
3889
4409
  } else if (outcome.result === "unchanged") {
3890
4410
  summary.unchanged++;
3891
- if (meta[url]?.file) summary.cachedFiles[url] = path23.join(cacheDir, meta[url].file);
4411
+ if (meta[url]?.file) summary.cachedFiles[url] = path24.join(cacheDir, meta[url].file);
3892
4412
  } else {
3893
4413
  summary.failed++;
3894
4414
  summary.failedUrls.push(url);
@@ -3907,11 +4427,17 @@ function parseIdList(value) {
3907
4427
  const raw = Array.isArray(value) ? value : [value];
3908
4428
  return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
3909
4429
  }
4430
+ function normalizeOnly(only) {
4431
+ if (!only) return void 0;
4432
+ const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
4433
+ const cleaned = list.map((s) => s.trim()).filter(Boolean);
4434
+ return cleaned.length > 0 ? cleaned : void 0;
4435
+ }
3910
4436
 
3911
4437
  // src/commands/apply.ts
3912
4438
  var STALE_CACHE_THRESHOLD_DAYS = 7;
3913
4439
  async function cacheHasItems() {
3914
- const data = await readJson(path24.join(getCacheDir(), "manifest.json"));
4440
+ const data = await readJson(path25.join(getCacheDir(), "manifest.json"));
3915
4441
  return Array.isArray(data?.items) && data.items.length > 0;
3916
4442
  }
3917
4443
  function installableRecommendedItems(recommended) {
@@ -4015,7 +4541,9 @@ async function runApply(options) {
4015
4541
  const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
4016
4542
  refillConfig: options.refillConfig,
4017
4543
  force: options.force,
4018
- prune: options.prune
4544
+ prune: options.prune,
4545
+ // Copies a global plugin already provides are removed here (issue #272).
4546
+ activePluginIds: await activeGlobalPluginIds(root)
4019
4547
  });
4020
4548
  await writeGitignore(root, isDryRun);
4021
4549
  await untrackMachineLocalArtifacts(root, isDryRun, warn);
@@ -4062,7 +4590,7 @@ async function runApply(options) {
4062
4590
  }
4063
4591
 
4064
4592
  // src/commands/backups.ts
4065
- import path25 from "node:path";
4593
+ import path26 from "node:path";
4066
4594
  import fs22 from "fs-extra";
4067
4595
 
4068
4596
  // src/utils/prompts.ts
@@ -4099,7 +4627,7 @@ async function listBackups(root) {
4099
4627
  for (const name of names) {
4100
4628
  const kind = classify(name);
4101
4629
  if (!kind) continue;
4102
- const absPath = path25.join(dir, name);
4630
+ const absPath = path26.join(dir, name);
4103
4631
  const stat = await fs22.lstat(absPath);
4104
4632
  if (stat.isSymbolicLink()) continue;
4105
4633
  const expectDirectory = kind !== "lock";
@@ -4129,10 +4657,10 @@ async function runList(root) {
4129
4657
  }
4130
4658
  }
4131
4659
  async function isUnsafeWriteTarget(root, target) {
4132
- const segments = path25.relative(root, target).split(path25.sep).filter(Boolean);
4660
+ const segments = path26.relative(root, target).split(path26.sep).filter(Boolean);
4133
4661
  let current = root;
4134
4662
  for (let i = 0; i < segments.length; i++) {
4135
- current = path25.join(current, segments[i]);
4663
+ current = path26.join(current, segments[i]);
4136
4664
  const isLastSegment = i === segments.length - 1;
4137
4665
  try {
4138
4666
  const stat = await fs22.lstat(current);
@@ -4196,7 +4724,7 @@ async function collectFilesRecursive(dir) {
4196
4724
  const skippedOther = [];
4197
4725
  const names = await fs22.readdir(dir);
4198
4726
  for (const name of names) {
4199
- const abs = path25.join(dir, name);
4727
+ const abs = path26.join(dir, name);
4200
4728
  const stat = await fs22.lstat(abs);
4201
4729
  if (stat.isSymbolicLink()) {
4202
4730
  skippedLinks.push(abs);
@@ -4219,12 +4747,12 @@ async function restoreDirBackup(root, entry, yes) {
4219
4747
  const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
4220
4748
  if (skippedLinks.length > 0) {
4221
4749
  warn(
4222
- `Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path25.relative(entry.absPath, abs)).join(", ")}`
4750
+ `Skipped ${skippedLinks.length} symlink(s) inside backup ${entry.id} \u2014 never followed: ${skippedLinks.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
4223
4751
  );
4224
4752
  }
4225
4753
  if (skippedOther.length > 0) {
4226
4754
  warn(
4227
- `Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path25.relative(entry.absPath, abs)).join(", ")}`
4755
+ `Skipped ${skippedOther.length} non-regular file(s) inside backup ${entry.id} (not restorable): ${skippedOther.map((abs) => path26.relative(entry.absPath, abs)).join(", ")}`
4228
4756
  );
4229
4757
  }
4230
4758
  if (files.length === 0) {
@@ -4234,8 +4762,8 @@ async function restoreDirBackup(root, entry, yes) {
4234
4762
  const safeFiles = [];
4235
4763
  const unsafeTargets = [];
4236
4764
  for (const abs of files) {
4237
- const rel = path25.relative(entry.absPath, abs);
4238
- const target = path25.join(root, rel);
4765
+ const rel = path26.relative(entry.absPath, abs);
4766
+ const target = path26.join(root, rel);
4239
4767
  if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
4240
4768
  else safeFiles.push(abs);
4241
4769
  }
@@ -4252,14 +4780,14 @@ async function restoreDirBackup(root, entry, yes) {
4252
4780
  const vanished = [];
4253
4781
  const toRestore = [];
4254
4782
  for (const abs of safeFiles) {
4255
- const rel = path25.relative(entry.absPath, abs);
4783
+ const rel = path26.relative(entry.absPath, abs);
4256
4784
  const backupFileStat = await lstatIfRegularFile(abs);
4257
4785
  if (!backupFileStat) {
4258
4786
  vanished.push(rel);
4259
4787
  continue;
4260
4788
  }
4261
4789
  toRestore.push(abs);
4262
- const target = path25.join(root, rel);
4790
+ const target = path26.join(root, rel);
4263
4791
  if (await fs22.pathExists(target)) {
4264
4792
  const targetStat = await fs22.stat(target);
4265
4793
  if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
@@ -4300,13 +4828,13 @@ async function restoreDirBackup(root, entry, yes) {
4300
4828
  let restoredCount = 0;
4301
4829
  const skippedAtWrite = [];
4302
4830
  for (const abs of toRestore) {
4303
- const rel = path25.relative(entry.absPath, abs);
4304
- const target = path25.join(root, rel);
4831
+ const rel = path26.relative(entry.absPath, abs);
4832
+ const target = path26.join(root, rel);
4305
4833
  if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
4306
4834
  skippedAtWrite.push(rel);
4307
4835
  continue;
4308
4836
  }
4309
- await fs22.ensureDir(path25.dirname(target));
4837
+ await fs22.ensureDir(path26.dirname(target));
4310
4838
  await fs22.copy(abs, target, { overwrite: true });
4311
4839
  restoredCount++;
4312
4840
  }
@@ -4445,18 +4973,19 @@ async function runCatalogAudit() {
4445
4973
  }
4446
4974
 
4447
4975
  // src/commands/doctor.ts
4448
- import path39 from "node:path";
4976
+ import path40 from "node:path";
4449
4977
  import fs30 from "fs-extra";
4450
4978
 
4451
4979
  // src/claude/skill-reference-check.ts
4452
- import path26 from "node:path";
4980
+ import path27 from "node:path";
4453
4981
  import fg4 from "fast-glob";
4454
4982
  import fs23 from "fs-extra";
4455
4983
  var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
4456
4984
  function stripFencedCodeBlocks(text) {
4457
4985
  return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
4458
4986
  }
4459
- async function findUnresolvedSkillReferences(root) {
4987
+ async function findUnresolvedSkillReferences(root, options = {}) {
4988
+ const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
4460
4989
  const skillsRoot = claudePath(root, "skills");
4461
4990
  if (!await fs23.pathExists(skillsRoot)) return [];
4462
4991
  const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
@@ -4465,15 +4994,16 @@ async function findUnresolvedSkillReferences(root) {
4465
4994
  const issues = [];
4466
4995
  for (const rel of files) {
4467
4996
  if (rel.startsWith("shared/")) continue;
4468
- const abs = path26.join(skillsRoot, rel);
4997
+ const abs = path27.join(skillsRoot, rel);
4469
4998
  const raw = await fs23.readFile(abs, "utf8");
4470
4999
  const lines = stripFencedCodeBlocks(raw).split("\n");
4471
5000
  lines.forEach((line2, idx) => {
4472
5001
  for (const match of line2.matchAll(REFERENCE_PATTERN)) {
4473
- const [reference, , bareName] = match;
5002
+ const [reference, namespace, bareName] = match;
4474
5003
  if (!reference || !bareName) continue;
5004
+ if (namespace && resolvableNamespaces.has(namespace)) continue;
4475
5005
  issues.push({
4476
- file: path26.join(".claude/skills", rel),
5006
+ file: path27.join(".claude/skills", rel),
4477
5007
  line: idx + 1,
4478
5008
  reference,
4479
5009
  bareName,
@@ -4486,11 +5016,11 @@ async function findUnresolvedSkillReferences(root) {
4486
5016
  }
4487
5017
 
4488
5018
  // src/decisions/doctor.ts
4489
- import path31 from "node:path";
5019
+ import path32 from "node:path";
4490
5020
  import fs27 from "fs-extra";
4491
5021
 
4492
5022
  // src/decisions/check.ts
4493
- import path30 from "node:path";
5023
+ import path31 from "node:path";
4494
5024
  import fs26 from "fs-extra";
4495
5025
 
4496
5026
  // src/decisions/dep-bump.ts
@@ -4550,7 +5080,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
4550
5080
 
4551
5081
  // src/decisions/diff.ts
4552
5082
  init_exec();
4553
- import path27 from "node:path";
5083
+ import path28 from "node:path";
4554
5084
  function isPackageJsonPath(file) {
4555
5085
  return file === "package.json" || file.endsWith("/package.json");
4556
5086
  }
@@ -4611,7 +5141,7 @@ async function resolvePackageJsonPair(root, file, opts) {
4611
5141
  return { before: before2, after: after2 };
4612
5142
  }
4613
5143
  const before = await readBlob(root, `${range}:${file}`) ?? empty;
4614
- const after = await readText(path27.join(root, file)) ?? empty;
5144
+ const after = await readText(path28.join(root, file)) ?? empty;
4615
5145
  return { before, after };
4616
5146
  }
4617
5147
  async function collectDiffStats(root, opts) {
@@ -4682,21 +5212,21 @@ function firstMatchingRegex(filePath, patterns) {
4682
5212
  }
4683
5213
 
4684
5214
  // src/decisions/paths.ts
4685
- import path28 from "node:path";
5215
+ import path29 from "node:path";
4686
5216
  import fs24 from "fs-extra";
4687
5217
  async function resolveDecisionsDir(root) {
4688
- const primary = path28.join(root, DECISIONS_TRIGGERS.decisionsDir);
5218
+ const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
4689
5219
  if (await fs24.pathExists(primary)) return primary;
4690
- const legacy = path28.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
5220
+ const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4691
5221
  if (await fs24.pathExists(legacy)) return legacy;
4692
5222
  return primary;
4693
5223
  }
4694
5224
  function relativeDecisionPath(decisionsDir, root) {
4695
- return path28.relative(root, decisionsDir).replace(/\\/g, "/");
5225
+ return path29.relative(root, decisionsDir).replace(/\\/g, "/");
4696
5226
  }
4697
5227
 
4698
5228
  // src/decisions/validate.ts
4699
- import path29 from "node:path";
5229
+ import path30 from "node:path";
4700
5230
  import fs25 from "fs-extra";
4701
5231
  function validateDecisionContent(content2) {
4702
5232
  const errors = [];
@@ -4726,14 +5256,14 @@ function decisionNumberFromFile(name) {
4726
5256
  return match?.[1] ?? null;
4727
5257
  }
4728
5258
  async function validateIndexSync(decisionsDir, decisionFiles) {
4729
- const readmePath = path29.join(decisionsDir, "README.md");
5259
+ const readmePath = path30.join(decisionsDir, "README.md");
4730
5260
  if (!await fs25.pathExists(readmePath)) {
4731
5261
  return ["docs/decisions/README.md is missing"];
4732
5262
  }
4733
5263
  const readme = await readText(readmePath) ?? "";
4734
5264
  const errors = [];
4735
5265
  for (const file of decisionFiles) {
4736
- const num = decisionNumberFromFile(path29.basename(file));
5266
+ const num = decisionNumberFromFile(path30.basename(file));
4737
5267
  if (num && !indexReferencesNumber(readme, num)) {
4738
5268
  errors.push(`README.md missing index row for ADR-${num}`);
4739
5269
  }
@@ -4828,22 +5358,22 @@ async function checkSatisfaction(input2) {
4828
5358
  const errors = [];
4829
5359
  const decisionsPrefix = `${input2.decisionsRel}/`;
4830
5360
  const newDecisionFiles = input2.changedFiles.filter(
4831
- (f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path30.basename(f))
5361
+ (f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path31.basename(f))
4832
5362
  );
4833
5363
  if (newDecisionFiles.length === 0) {
4834
5364
  errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
4835
5365
  return errors;
4836
5366
  }
4837
- const decisionsDir = path30.join(input2.root, input2.decisionsRel);
5367
+ const decisionsDir = path31.join(input2.root, input2.decisionsRel);
4838
5368
  for (const rel of newDecisionFiles) {
4839
- const abs = path30.join(input2.root, rel);
5369
+ const abs = path31.join(input2.root, rel);
4840
5370
  const content2 = await readText(abs) ?? "";
4841
5371
  const valid = validateDecisionContent(content2);
4842
5372
  if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
4843
5373
  }
4844
5374
  const indexErrors = await validateIndexSync(
4845
5375
  decisionsDir,
4846
- newDecisionFiles.map((f) => path30.basename(f))
5376
+ newDecisionFiles.map((f) => path31.basename(f))
4847
5377
  );
4848
5378
  errors.push(...indexErrors);
4849
5379
  return errors;
@@ -4899,7 +5429,7 @@ ${opts.commitMessages ?? ""}`;
4899
5429
  changedFiles: stats.files
4900
5430
  });
4901
5431
  if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
4902
- const legacyExists = await fs26.pathExists(path30.join(root, legacyRel));
5432
+ const legacyExists = await fs26.pathExists(path31.join(root, legacyRel));
4903
5433
  if (legacyExists && !await fs26.pathExists(decisionsDir)) {
4904
5434
  satisfactionErrors = await checkSatisfaction({
4905
5435
  root,
@@ -4930,8 +5460,8 @@ async function listDecisionNumbers(decisionsDir) {
4930
5460
  // src/decisions/doctor.ts
4931
5461
  async function auditDecisionsLayout(root) {
4932
5462
  const findings = [];
4933
- const primary = path31.join(root, DECISIONS_TRIGGERS.decisionsDir);
4934
- const legacy = path31.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
5463
+ const primary = path32.join(root, DECISIONS_TRIGGERS.decisionsDir);
5464
+ const legacy = path32.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4935
5465
  const hasPrimary = await fs27.pathExists(primary);
4936
5466
  const hasLegacy = await fs27.pathExists(legacy);
4937
5467
  if (!hasPrimary && hasLegacy) {
@@ -4946,7 +5476,7 @@ async function auditDecisionsLayout(root) {
4946
5476
  if (!hasPrimary) return findings;
4947
5477
  const decisionsDir = await resolveDecisionsDir(root);
4948
5478
  const rel = relativeDecisionPath(decisionsDir, root);
4949
- const readmePath = path31.join(decisionsDir, "README.md");
5479
+ const readmePath = path32.join(decisionsDir, "README.md");
4950
5480
  if (!await fs27.pathExists(readmePath)) {
4951
5481
  findings.push({
4952
5482
  level: "advisory",
@@ -4971,7 +5501,7 @@ async function auditDecisionsLayout(root) {
4971
5501
  });
4972
5502
  }
4973
5503
  }
4974
- const claude = await readText(path31.join(root, "CLAUDE.md"));
5504
+ const claude = await readText(path32.join(root, "CLAUDE.md"));
4975
5505
  if (claude && !claude.includes(`@${rel}/README.md`)) {
4976
5506
  findings.push({
4977
5507
  level: "advisory",
@@ -5002,11 +5532,11 @@ async function auditDecisionsLayout(root) {
5002
5532
  }
5003
5533
 
5004
5534
  // src/scanner/read-context.ts
5005
- import path38 from "node:path";
5535
+ import path39 from "node:path";
5006
5536
  import fs29 from "fs-extra";
5007
5537
 
5008
5538
  // src/commands/workspace/config.ts
5009
- import path32 from "node:path";
5539
+ import path33 from "node:path";
5010
5540
  import YAML from "yaml";
5011
5541
  var WORKSPACE_FILE = "haus.workspace.yaml";
5012
5542
  function parseWorkspaceConfig(text) {
@@ -5029,11 +5559,11 @@ function parseWorkspaceConfig(text) {
5029
5559
  };
5030
5560
  }
5031
5561
  async function readWorkspaceConfig(workspaceRoot) {
5032
- return parseWorkspaceConfig(await readText(path32.join(workspaceRoot, WORKSPACE_FILE)));
5562
+ return parseWorkspaceConfig(await readText(path33.join(workspaceRoot, WORKSPACE_FILE)));
5033
5563
  }
5034
5564
 
5035
5565
  // src/workspace/members.ts
5036
- import path33 from "node:path";
5566
+ import path34 from "node:path";
5037
5567
  var REPOS_MANIFEST_FILE = "repos.manifest.json";
5038
5568
  var REPOS_LOCAL_FILE = "repos.local.json";
5039
5569
  var MemberConfigError = class extends Error {
@@ -5046,14 +5576,14 @@ var MemberConfigError = class extends Error {
5046
5576
  };
5047
5577
  function resolveAbsPath(workspaceRoot, folder, overrides) {
5048
5578
  const override = overrides[folder];
5049
- if (override) return path33.resolve(workspaceRoot, override);
5050
- return path33.resolve(workspaceRoot, folder);
5579
+ if (override) return path34.resolve(workspaceRoot, override);
5580
+ return path34.resolve(workspaceRoot, folder);
5051
5581
  }
5052
5582
  function isSelfMember(workspaceRoot, member) {
5053
- return path33.resolve(member.absPath) === path33.resolve(workspaceRoot);
5583
+ return path34.resolve(member.absPath) === path34.resolve(workspaceRoot);
5054
5584
  }
5055
5585
  async function readPathOverrides(workspaceRoot) {
5056
- const filePath = path33.join(workspaceRoot, REPOS_LOCAL_FILE);
5586
+ const filePath = path34.join(workspaceRoot, REPOS_LOCAL_FILE);
5057
5587
  const result = await readJsonDetailed(filePath);
5058
5588
  if (result.status === "missing") return {};
5059
5589
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -5069,7 +5599,7 @@ async function readPathOverrides(workspaceRoot) {
5069
5599
  return overrides;
5070
5600
  }
5071
5601
  async function readFromWorkspaceYaml(workspaceRoot, overrides) {
5072
- const filePath = path33.join(workspaceRoot, WORKSPACE_FILE);
5602
+ const filePath = path34.join(workspaceRoot, WORKSPACE_FILE);
5073
5603
  const text = await readText(filePath);
5074
5604
  if (text === void 0) return void 0;
5075
5605
  const config = parseWorkspaceConfig(text);
@@ -5082,7 +5612,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
5082
5612
  }));
5083
5613
  }
5084
5614
  async function readFromManifest(workspaceRoot, overrides) {
5085
- const filePath = path33.join(workspaceRoot, REPOS_MANIFEST_FILE);
5615
+ const filePath = path34.join(workspaceRoot, REPOS_MANIFEST_FILE);
5086
5616
  const result = await readJsonDetailed(filePath);
5087
5617
  if (result.status === "missing") return void 0;
5088
5618
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -5124,11 +5654,11 @@ async function readMembers(rootInfo) {
5124
5654
  }
5125
5655
 
5126
5656
  // src/scanner/scan-project.ts
5127
- import path37 from "node:path";
5657
+ import path38 from "node:path";
5128
5658
 
5129
5659
  // src/utils/git-root.ts
5130
5660
  init_exec();
5131
- import path34 from "node:path";
5661
+ import path35 from "node:path";
5132
5662
  function fallback(start) {
5133
5663
  return {
5134
5664
  cwd: start,
@@ -5144,11 +5674,11 @@ function fallback(start) {
5144
5674
  async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
5145
5675
  const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
5146
5676
  const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
5147
- if (raw && path34.isAbsolute(raw)) return path34.resolve(raw);
5677
+ if (raw && path35.isAbsolute(raw)) return path35.resolve(raw);
5148
5678
  const plain = await runGit(["rev-parse", flag], { cwd });
5149
5679
  const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
5150
5680
  if (!plainRaw) return "";
5151
- return path34.isAbsolute(plainRaw) ? path34.resolve(plainRaw) : path34.resolve(repoRoot, plainRaw);
5681
+ return path35.isAbsolute(plainRaw) ? path35.resolve(plainRaw) : path35.resolve(repoRoot, plainRaw);
5152
5682
  }
5153
5683
  async function resolveRoots(start) {
5154
5684
  const cwd = start ?? process.cwd();
@@ -5157,15 +5687,15 @@ async function resolveRoots(start) {
5157
5687
  if (toplevel.exitCode !== 0) return fallback(cwd);
5158
5688
  const toplevelRaw = toplevel.stdout.trim();
5159
5689
  if (!toplevelRaw) return fallback(cwd);
5160
- const repoRoot = path34.resolve(toplevelRaw);
5690
+ const repoRoot = path35.resolve(toplevelRaw);
5161
5691
  const [gitDir, gitCommonDir] = await Promise.all([
5162
5692
  resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
5163
5693
  resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
5164
5694
  ]);
5165
5695
  if (!gitDir || !gitCommonDir) return fallback(cwd);
5166
5696
  const isLinkedWorktree = gitDir !== gitCommonDir;
5167
- const mainRoot = isLinkedWorktree && path34.basename(gitCommonDir) === ".git" ? path34.dirname(gitCommonDir) : repoRoot;
5168
- const worktreeName = isLinkedWorktree ? path34.basename(gitDir) : null;
5697
+ const mainRoot = isLinkedWorktree && path35.basename(gitCommonDir) === ".git" ? path35.dirname(gitCommonDir) : repoRoot;
5698
+ const worktreeName = isLinkedWorktree ? path35.basename(gitDir) : null;
5169
5699
  return {
5170
5700
  cwd,
5171
5701
  repoRoot,
@@ -5210,7 +5740,7 @@ function resolveMajorVersion(range) {
5210
5740
  }
5211
5741
 
5212
5742
  // src/scanner/detect-package-manager.ts
5213
- import path35 from "node:path";
5743
+ import path36 from "node:path";
5214
5744
  import fs28 from "fs-extra";
5215
5745
  function detectPackageManager(root, packageManagerField) {
5216
5746
  const field = String(packageManagerField ?? "").trim();
@@ -5229,9 +5759,9 @@ function detectPackageManager(root, packageManagerField) {
5229
5759
  if (satisfiesVersion(version, ">=9")) return "npm";
5230
5760
  return "unknown";
5231
5761
  }
5232
- if (fs28.existsSync(path35.join(root, "yarn.lock"))) return "yarn";
5233
- if (fs28.existsSync(path35.join(root, "pnpm-lock.yaml"))) return "pnpm";
5234
- if (fs28.existsSync(path35.join(root, "package-lock.json"))) return "npm";
5762
+ if (fs28.existsSync(path36.join(root, "yarn.lock"))) return "yarn";
5763
+ if (fs28.existsSync(path36.join(root, "pnpm-lock.yaml"))) return "pnpm";
5764
+ if (fs28.existsSync(path36.join(root, "package-lock.json"))) return "npm";
5235
5765
  return "unknown";
5236
5766
  }
5237
5767
 
@@ -5456,7 +5986,7 @@ function runDetection(ctx, rules = STACK_RULES) {
5456
5986
 
5457
5987
  // src/scanner/render.ts
5458
5988
  import { readFile } from "node:fs/promises";
5459
- import path36 from "node:path";
5989
+ import path37 from "node:path";
5460
5990
  async function buildContentBlob(root, files) {
5461
5991
  const candidates = files.filter(
5462
5992
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -5464,7 +5994,7 @@ async function buildContentBlob(root, files) {
5464
5994
  const slice = candidates.slice(0, 300);
5465
5995
  const parts = await mapWithConcurrency(slice, async (rel) => {
5466
5996
  try {
5467
- return await readFile(path36.join(root, rel), "utf8");
5997
+ return await readFile(path37.join(root, rel), "utf8");
5468
5998
  } catch {
5469
5999
  return "";
5470
6000
  }
@@ -5556,8 +6086,8 @@ var SAFE_FILES = [
5556
6086
  "Gemfile"
5557
6087
  ];
5558
6088
  async function scanProject(root) {
5559
- const pkg = await readJson(path37.join(root, "package.json"));
5560
- const composer = await readJson(path37.join(root, "composer.json"));
6089
+ const pkg = await readJson(path38.join(root, "package.json"));
6090
+ const composer = await readJson(path38.join(root, "composer.json"));
5561
6091
  const files = await listFiles(root, SAFE_FILES);
5562
6092
  const safeFiles = files.filter((f) => !blocked(f));
5563
6093
  const depVersions = dependencyVersionMap(pkg, composer);
@@ -5587,7 +6117,7 @@ async function scanProject(root) {
5587
6117
  crossRepoHints.push("Monorepo orchestration detected");
5588
6118
  const unsupportedSignals = collectUnsupportedSignals(safeFiles);
5589
6119
  const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
5590
- const repoNameFallback = pkg?.name ? String(pkg.name) : path37.basename((await resolveRoots(root)).mainRoot);
6120
+ const repoNameFallback = pkg?.name ? String(pkg.name) : path38.basename((await resolveRoots(root)).mainRoot);
5591
6121
  const context = {
5592
6122
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5593
6123
  repoName: repoNameFallback,
@@ -5609,10 +6139,10 @@ async function scanProject(root) {
5609
6139
  // src/scanner/read-context.ts
5610
6140
  async function isCacheFresh(root, cachePath) {
5611
6141
  const markerPaths = [
5612
- path38.join(root, "package.json"),
5613
- path38.join(root, "composer.json"),
5614
- path38.join(root, WORKSPACE_FILE),
5615
- path38.join(root, REPOS_MANIFEST_FILE)
6142
+ path39.join(root, "package.json"),
6143
+ path39.join(root, "composer.json"),
6144
+ path39.join(root, WORKSPACE_FILE),
6145
+ path39.join(root, REPOS_MANIFEST_FILE)
5616
6146
  ];
5617
6147
  const cacheStat = await fs29.stat(cachePath);
5618
6148
  for (const markerPath of markerPaths) {
@@ -5718,7 +6248,7 @@ async function runDoctor(options) {
5718
6248
  } else {
5719
6249
  ok(`- HOOKS OK: ${hooks.message}`);
5720
6250
  }
5721
- const rootClaudeMdPath = path39.join(root, "CLAUDE.md");
6251
+ const rootClaudeMdPath = path40.join(root, "CLAUDE.md");
5722
6252
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
5723
6253
  if (!rootClaudeMdContent) {
5724
6254
  flag(
@@ -5761,9 +6291,9 @@ async function runDoctor(options) {
5761
6291
  ok("- CLAUDE.md: import block present");
5762
6292
  }
5763
6293
  for (const target of actualTargets) {
5764
- const resolved = path39.resolve(root, target);
5765
- const relFromRoot = path39.relative(root, resolved);
5766
- const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path39.sep}`) || path39.isAbsolute(relFromRoot);
6294
+ const resolved = path40.resolve(root, target);
6295
+ const relFromRoot = path40.relative(root, resolved);
6296
+ const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path40.sep}`) || path40.isAbsolute(relFromRoot);
5767
6297
  if (escapesRoot || !await fs30.pathExists(resolved)) {
5768
6298
  flag(
5769
6299
  `- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
@@ -5797,8 +6327,8 @@ async function runDoctor(options) {
5797
6327
  } else {
5798
6328
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
5799
6329
  const bodyContent = workflowContent.slice(firstLine.length + 1);
5800
- const cachePath = path39.join(getCacheDir(), "templates/agentic-workflow-standard.md");
5801
- const bundledPath = path39.join(
6330
+ const cachePath = path40.join(getCacheDir(), "templates/agentic-workflow-standard.md");
6331
+ const bundledPath = path40.join(
5802
6332
  packageRoot(),
5803
6333
  "library",
5804
6334
  "global",
@@ -5847,7 +6377,7 @@ async function runDoctor(options) {
5847
6377
  }
5848
6378
  }
5849
6379
  if (workflowExists) {
5850
- const prettierIgnore = await readText(path39.join(root, ".prettierignore")) ?? "";
6380
+ const prettierIgnore = await readText(path40.join(root, ".prettierignore")) ?? "";
5851
6381
  const lines = prettierIgnore.split("\n");
5852
6382
  const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
5853
6383
  if (missing.length > 0) {
@@ -5887,7 +6417,50 @@ async function runDoctor(options) {
5887
6417
  if (finding.level === "ok") ok(finding.line);
5888
6418
  else suggest(finding.line, finding.sentence, finding.fix);
5889
6419
  }
5890
- const skillRefIssues = await findUnresolvedSkillReferences(root);
6420
+ const pluginPins = await loadCatalogPluginPins(root);
6421
+ const pluginState = await probePluginEnv();
6422
+ const activePins = pluginPins.filter((pin) => isPluginActive(pluginState, pin));
6423
+ const activeNamespaces = activePins.map((pin) => pin.plugin);
6424
+ if (pluginPins.length === 0) {
6425
+ } else if (!pluginState.available) {
6426
+ suggest(
6427
+ "- GLOBAL PLUGINS: not checked (the `claude` CLI is not on PATH)",
6428
+ "Claude Code's CLI isn't on PATH, so haus can't verify the global plugin(s) it manages",
6429
+ "install Claude Code, then run `haus install`"
6430
+ );
6431
+ } else {
6432
+ const pending = planPluginActions(pluginPins, pluginState).filter(
6433
+ (action) => action.kind !== "up-to-date"
6434
+ );
6435
+ if (pending.length > 0) {
6436
+ suggest(
6437
+ `- GLOBAL PLUGINS: ${pending.length} plugin(s) not at the pinned state (${pending.map((a) => `${a.pin.id}: ${a.reason}`).join("; ")})`,
6438
+ `${pending.length} haus-managed global plugin(s) are missing, disabled, or off the pinned ref`,
6439
+ "haus update (or `haus install` outside a project)"
6440
+ );
6441
+ } else {
6442
+ ok(`- GLOBAL PLUGINS: OK (${pluginPins.map((p) => `${p.id}@${p.ref}`).join(", ")})`);
6443
+ }
6444
+ }
6445
+ if (activePins.length > 0) {
6446
+ const lockEntries = await readJson(hausPath(root, "haus.lock.json"));
6447
+ const superseded = (lockEntries ?? []).map((entry) => entry.id).filter(
6448
+ (id) => typeof id === "string" && isSupersededByPlugin(
6449
+ id,
6450
+ activePins.map((pin) => pin.id)
6451
+ )
6452
+ );
6453
+ if (superseded.length > 0) {
6454
+ suggest(
6455
+ `- GLOBAL PLUGINS: ${superseded.length} installed item(s) are superseded by the global plugin (${superseded.slice(0, 3).join(", ")}${superseded.length > 3 ? ", \u2026" : ""})`,
6456
+ `${superseded.length} project skill(s) duplicate what the global plugin already provides`,
6457
+ "haus update (removes the superseded copies)"
6458
+ );
6459
+ }
6460
+ }
6461
+ const skillRefIssues = await findUnresolvedSkillReferences(root, {
6462
+ pluginNamespaces: activeNamespaces
6463
+ });
5891
6464
  if (skillRefIssues.length > 0) {
5892
6465
  const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
5893
6466
  const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
@@ -5951,7 +6524,7 @@ async function runDoctor(options) {
5951
6524
  }
5952
6525
  }
5953
6526
  }
5954
- const pkgJson = await readJson(path39.join(packageRoot(), "package.json"));
6527
+ const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
5955
6528
  const currentVersion = pkgJson?.version ?? "0.0.0";
5956
6529
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
5957
6530
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -5984,7 +6557,7 @@ async function runDoctor(options) {
5984
6557
  }
5985
6558
 
5986
6559
  // src/commands/update.ts
5987
- import path42 from "node:path";
6560
+ import path43 from "node:path";
5988
6561
 
5989
6562
  // src/claude/refresh-project.ts
5990
6563
  import fs31 from "fs-extra";
@@ -5996,12 +6569,16 @@ async function isHausProject(root) {
5996
6569
  }
5997
6570
  async function refreshProjectApply(root) {
5998
6571
  if (!await isHausProject(root)) return [];
5999
- return writeClaudeFiles(root, false, void 0, { refillConfig: false });
6572
+ return writeClaudeFiles(root, false, void 0, {
6573
+ refillConfig: false,
6574
+ // Superseded copies are removed in the same pass that re-applies the catalog.
6575
+ activePluginIds: await activeGlobalPluginIds(root)
6576
+ });
6000
6577
  }
6001
6578
 
6002
6579
  // src/install/apply.ts
6003
6580
  import crypto2 from "node:crypto";
6004
- import path40 from "node:path";
6581
+ import path41 from "node:path";
6005
6582
  import fs32 from "fs-extra";
6006
6583
 
6007
6584
  // src/install/header.ts
@@ -6095,7 +6672,7 @@ function destFileMissing(destPath) {
6095
6672
  }
6096
6673
  function sourceVersion() {
6097
6674
  try {
6098
- const pkgPath = path40.join(packageRoot(), "package.json");
6675
+ const pkgPath = path41.join(packageRoot(), "package.json");
6099
6676
  const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
6100
6677
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
6101
6678
  } catch {
@@ -6103,49 +6680,49 @@ function sourceVersion() {
6103
6680
  }
6104
6681
  }
6105
6682
  function globalSrcDir() {
6106
- return path40.join(packageRoot(), "library", "global");
6683
+ return path41.join(packageRoot(), "library", "global");
6107
6684
  }
6108
6685
  function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
6109
- const skillDir = path40.join(skillsDir, skillName);
6686
+ const skillDir = path41.join(skillsDir, skillName);
6110
6687
  const entries = [];
6111
6688
  for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
6112
6689
  if (rel === "SKILL.md") continue;
6113
- const abs = path40.join(skillDir, rel);
6690
+ const abs = path41.join(skillDir, rel);
6114
6691
  if (!fs32.statSync(abs).isFile()) continue;
6115
- const posixRel = rel.split(path40.sep).join("/");
6692
+ const posixRel = rel.split(path41.sep).join("/");
6116
6693
  entries.push({
6117
6694
  stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
6118
- srcRelPath: path40.join("library", "global", "skills", skillName, rel),
6119
- destPath: path40.join(claudeDir, "skills", skillName, rel)
6695
+ srcRelPath: path41.join("library", "global", "skills", skillName, rel),
6696
+ destPath: path41.join(claudeDir, "skills", skillName, rel)
6120
6697
  });
6121
6698
  }
6122
6699
  return entries;
6123
6700
  }
6124
6701
  function collectSourceFiles(srcDir, claudeDir) {
6125
6702
  const entries = [];
6126
- const skillsDir = path40.join(srcDir, "skills");
6703
+ const skillsDir = path41.join(srcDir, "skills");
6127
6704
  if (fs32.pathExistsSync(skillsDir)) {
6128
6705
  for (const skillName of fs32.readdirSync(skillsDir)) {
6129
- const skillFile = path40.join(skillsDir, skillName, "SKILL.md");
6706
+ const skillFile = path41.join(skillsDir, skillName, "SKILL.md");
6130
6707
  if (fs32.pathExistsSync(skillFile)) {
6131
6708
  entries.push({
6132
6709
  stableId: `skill.${skillName}`,
6133
- srcRelPath: path40.join("library", "global", "skills", skillName, "SKILL.md"),
6134
- destPath: path40.join(claudeDir, "skills", skillName, "SKILL.md")
6710
+ srcRelPath: path41.join("library", "global", "skills", skillName, "SKILL.md"),
6711
+ destPath: path41.join(claudeDir, "skills", skillName, "SKILL.md")
6135
6712
  });
6136
6713
  entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
6137
6714
  }
6138
6715
  }
6139
6716
  }
6140
- const commandsDir = path40.join(srcDir, "commands");
6717
+ const commandsDir = path41.join(srcDir, "commands");
6141
6718
  if (fs32.pathExistsSync(commandsDir)) {
6142
6719
  for (const fileName of fs32.readdirSync(commandsDir)) {
6143
6720
  if (!fileName.endsWith(".md")) continue;
6144
6721
  const commandName = fileName.slice(0, -".md".length);
6145
6722
  entries.push({
6146
6723
  stableId: `command.${commandName}`,
6147
- srcRelPath: path40.join("library", "global", "commands", fileName),
6148
- destPath: path40.join(claudeDir, "commands", fileName)
6724
+ srcRelPath: path41.join("library", "global", "commands", fileName),
6725
+ destPath: path41.join(claudeDir, "commands", fileName)
6149
6726
  });
6150
6727
  }
6151
6728
  }
@@ -6171,7 +6748,7 @@ async function applyInstall(options = {}) {
6171
6748
  };
6172
6749
  const manifestFiles = [];
6173
6750
  for (const entry of sourceFiles) {
6174
- const srcPath = path40.join(packageRoot(), entry.srcRelPath);
6751
+ const srcPath = path41.join(packageRoot(), entry.srcRelPath);
6175
6752
  const rawContent = await readText(srcPath);
6176
6753
  if (rawContent === void 0) {
6177
6754
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -6232,7 +6809,7 @@ async function applyInstall(options = {}) {
6232
6809
  }
6233
6810
  let mergedSettings;
6234
6811
  if (!check) {
6235
- const fragmentPath = path40.join(srcDir, "settings-fragments", "hooks.json");
6812
+ const fragmentPath = path41.join(srcDir, "settings-fragments", "hooks.json");
6236
6813
  const fragments = await loadHooksFragment(fragmentPath);
6237
6814
  const settings = await readSettings();
6238
6815
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -6262,9 +6839,12 @@ async function applyInstall(options = {}) {
6262
6839
  }
6263
6840
  if (!dryRun && !check && mergedSettings) {
6264
6841
  await writeSettings(mergedSettings);
6265
- const manifest = buildManifest(source, manifestFiles, [
6266
- .../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])
6267
- ]);
6842
+ const manifest = buildManifest(
6843
+ source,
6844
+ manifestFiles,
6845
+ [.../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])],
6846
+ existingManifest?.plugins
6847
+ );
6268
6848
  await writeManifest(manifest);
6269
6849
  }
6270
6850
  return result;
@@ -6321,7 +6901,7 @@ function summarizeLockDiff(before, after) {
6321
6901
 
6322
6902
  // src/update/lockfile.ts
6323
6903
  import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
6324
- import path41 from "node:path";
6904
+ import path42 from "node:path";
6325
6905
  async function readLockSummary(root) {
6326
6906
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
6327
6907
  const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
@@ -6359,7 +6939,7 @@ async function applyLock(root) {
6359
6939
  try {
6360
6940
  const backupDir = hausPath(root, "backups");
6361
6941
  await mkdir(backupDir, { recursive: true });
6362
- await copyFile(lockPath, path41.join(backupDir, `haus.lock.${Date.now()}.json`));
6942
+ await copyFile(lockPath, path42.join(backupDir, `haus.lock.${Date.now()}.json`));
6363
6943
  } catch {
6364
6944
  }
6365
6945
  const enriched = await Promise.all(
@@ -6381,7 +6961,7 @@ function diffLock(before, after) {
6381
6961
  }
6382
6962
  async function hasLocalOverrides(root) {
6383
6963
  try {
6384
- await readFile2(path41.join(root, ".claude", "settings.json"), "utf8");
6964
+ await readFile2(path42.join(root, ".claude", "settings.json"), "utf8");
6385
6965
  return true;
6386
6966
  } catch {
6387
6967
  return false;
@@ -6397,15 +6977,26 @@ async function runUpdate(options) {
6397
6977
  return;
6398
6978
  }
6399
6979
  if (options.check) {
6400
- const pkgJson2 = await readJson(path42.join(packageRoot(), "package.json"));
6980
+ const pkgJson2 = await readJson(path43.join(packageRoot(), "package.json"));
6401
6981
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
6402
- const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
6982
+ const [
6983
+ status,
6984
+ npmVersion,
6985
+ latestCatalogTag,
6986
+ globalInstallDrift,
6987
+ catalogItems,
6988
+ lockItems,
6989
+ globalPluginDrift
6990
+ ] = await Promise.all([
6403
6991
  options.fast ? fastLockStatus(root) : checkLock(root),
6404
6992
  fetchNpmVersionStatus(currentVersion2),
6405
6993
  fetchLatestCatalogTag(),
6406
6994
  options.fast ? Promise.resolve(null) : detectGlobalInstallDrift(),
6407
6995
  loadCatalog(root),
6408
- readJson(hausPath(root, "haus.lock.json"))
6996
+ readJson(hausPath(root, "haus.lock.json")),
6997
+ // Same tier split as globalInstallDrift: --fast is the cheap probe the
6998
+ // SessionStart hook uses and must not shell out to the `claude` CLI.
6999
+ options.fast ? Promise.resolve(null) : detectGlobalPluginDrift(root)
6409
7000
  ]);
6410
7001
  const formerIdMigrations = findFormerIdMigrations(lockItems ?? [], catalogItems);
6411
7002
  const installedRef = options.fast ? status.catalogRef : status.catalogRef ?? "main";
@@ -6420,6 +7011,7 @@ async function runUpdate(options) {
6420
7011
  catalogRefBehind,
6421
7012
  formerIdMigrations,
6422
7013
  globalInstallDrift,
7014
+ globalPluginDrift,
6423
7015
  localOverrides: await hasLocalOverrides(root),
6424
7016
  summary: diffGeneratedFiles(),
6425
7017
  npmVersion
@@ -6429,12 +7021,12 @@ async function runUpdate(options) {
6429
7021
  )
6430
7022
  );
6431
7023
  const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
6432
- if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
7024
+ if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true || globalPluginDrift === true) {
6433
7025
  process.exitCode = 1;
6434
7026
  }
6435
7027
  return;
6436
7028
  }
6437
- const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
7029
+ const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
6438
7030
  const currentVersion = pkgJson?.version ?? "0.0.0";
6439
7031
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
6440
7032
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -6467,6 +7059,7 @@ async function runUpdate(options) {
6467
7059
  warn(formatGithubRateLimitMessage(sync.rateLimit));
6468
7060
  }
6469
7061
  await refreshGlobalInstall();
7062
+ await ensureCatalogPinnedPlugins(root);
6470
7063
  await refreshProjectFiles(root);
6471
7064
  const { after } = await applyLock(root);
6472
7065
  log(diffLock(lockBefore, after));
@@ -6477,7 +7070,7 @@ async function runFromHookCheck(root) {
6477
7070
  try {
6478
7071
  const summary = await readLockSummary(root);
6479
7072
  if (summary.count === 0) return;
6480
- const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
7073
+ const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
6481
7074
  const currentVersion = pkgJson?.version ?? "0.0.0";
6482
7075
  const [npmVersion, latestCatalogTag] = await Promise.all([
6483
7076
  fetchNpmVersionStatus(currentVersion),
@@ -6535,6 +7128,26 @@ async function refreshGlobalInstall() {
6535
7128
  warn(`Could not refresh ~/.claude/: ${err instanceof Error ? err.message : String(err)}`);
6536
7129
  }
6537
7130
  }
7131
+ async function ensureCatalogPinnedPlugins(root) {
7132
+ try {
7133
+ const pins = await loadCatalogPluginPins(root);
7134
+ const ensured = await ensureGlobalPlugins(pins);
7135
+ await recordManifestPlugins(
7136
+ pins.filter((pin) => ensured.ensured.includes(pin.id)).map((pin) => ({ ...pin, installedAt: (/* @__PURE__ */ new Date()).toISOString() }))
7137
+ );
7138
+ } catch (err) {
7139
+ warn(`Could not update global plugins: ${err instanceof Error ? err.message : String(err)}`);
7140
+ }
7141
+ }
7142
+ async function detectGlobalPluginDrift(root) {
7143
+ try {
7144
+ const pins = await loadCatalogPluginPins(root);
7145
+ const state = await probePluginEnv();
7146
+ return pluginDriftStatus(planPluginActions(pins, state), { matchRef: true });
7147
+ } catch {
7148
+ return null;
7149
+ }
7150
+ }
6538
7151
  async function detectGlobalInstallDrift() {
6539
7152
  try {
6540
7153
  const result = await applyInstall({ check: true });
@@ -6628,7 +7241,7 @@ async function runCiGate(options = {}) {
6628
7241
  init_exec();
6629
7242
  import { existsSync as existsSync2 } from "node:fs";
6630
7243
  import { readdir } from "node:fs/promises";
6631
- import path43 from "node:path";
7244
+ import path44 from "node:path";
6632
7245
  var GIT_LOCATION_VARS = [
6633
7246
  "GIT_DIR",
6634
7247
  "GIT_WORK_TREE",
@@ -6688,11 +7301,11 @@ async function runClone(url, opts = {}) {
6688
7301
  process.exitCode = 1;
6689
7302
  return;
6690
7303
  }
6691
- const target = path43.resolve(opts.dir?.trim() || repoNameFromUrl(url));
7304
+ const target = path44.resolve(opts.dir?.trim() || repoNameFromUrl(url));
6692
7305
  if (existsSync2(target) && !await isEmptyDirectory(target)) {
6693
7306
  const check = await checkExistingTarget(target, url);
6694
7307
  if (check.kind === "matches") {
6695
- log(`\u2022 ${path43.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
7308
+ log(`\u2022 ${path44.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
6696
7309
  return;
6697
7310
  }
6698
7311
  const message = 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].`;
@@ -6801,7 +7414,7 @@ async function runDecisionsGuard(options = {}) {
6801
7414
  }
6802
7415
 
6803
7416
  // src/commands/decisions.ts
6804
- import path44 from "node:path";
7417
+ import path45 from "node:path";
6805
7418
 
6806
7419
  // src/decisions/next-number.ts
6807
7420
  async function nextDecisionNumber(root) {
@@ -6911,7 +7524,7 @@ async function runDecisions(sub, arg, options = {}) {
6911
7524
  return;
6912
7525
  }
6913
7526
  if (sub === "validate") {
6914
- const target = arg ? path44.resolve(root, arg) : void 0;
7527
+ const target = arg ? path45.resolve(root, arg) : void 0;
6915
7528
  if (!target) {
6916
7529
  error("usage: haus decisions validate <path-to-decision.md>");
6917
7530
  process.exitCode = 1;
@@ -7180,7 +7793,7 @@ async function runGuard(kind, _options) {
7180
7793
  }
7181
7794
 
7182
7795
  // src/commands/init.ts
7183
- import path46 from "node:path";
7796
+ import path47 from "node:path";
7184
7797
  import fs33 from "fs-extra";
7185
7798
 
7186
7799
  // src/recommender/git-signal.ts
@@ -7223,6 +7836,7 @@ async function readChangedFiles(root) {
7223
7836
 
7224
7837
  // src/recommender/recommend.ts
7225
7838
  async function recommend(root, context, opts = {}) {
7839
+ const activePluginIds = opts.activePluginIds ?? [];
7226
7840
  const items = await loadCatalog(root);
7227
7841
  const sources = buildSourcesReport(items);
7228
7842
  const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
@@ -7280,6 +7894,15 @@ async function recommend(root, context, opts = {}) {
7280
7894
  "Catalog item is deprecated",
7281
7895
  "reviewStatus:deprecated"
7282
7896
  );
7897
+ if (isItemSupersededByPlugin(item, activePluginIds)) {
7898
+ add(
7899
+ "superseded-by-global-plugin",
7900
+ false,
7901
+ "superseded-by-global-plugin",
7902
+ "Provided by an installed global Claude Code plugin",
7903
+ `plugin:${activePluginIds.join(",")}`
7904
+ );
7905
+ }
7283
7906
  if (normSource === "curated") {
7284
7907
  const rs = item.reviewStatus;
7285
7908
  add(
@@ -7619,7 +8242,7 @@ function buildOptInEligible(items, skipped) {
7619
8242
  }
7620
8243
 
7621
8244
  // src/commands/workspace/discover.ts
7622
- import path45 from "node:path";
8245
+ import path46 from "node:path";
7623
8246
  import fg5 from "fast-glob";
7624
8247
  import YAML2 from "yaml";
7625
8248
  var DEFAULT_MAX_DEPTH = 3;
@@ -7660,10 +8283,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7660
8283
  const gitDirs = /* @__PURE__ */ new Set();
7661
8284
  const manifestDirs = /* @__PURE__ */ new Set();
7662
8285
  for (const match of gitMatches) {
7663
- gitDirs.add(path45.posix.dirname(match));
8286
+ gitDirs.add(path46.posix.dirname(match));
7664
8287
  }
7665
8288
  for (const match of manifestMatches) {
7666
- manifestDirs.add(path45.posix.dirname(match));
8289
+ manifestDirs.add(path46.posix.dirname(match));
7667
8290
  }
7668
8291
  const repoRoots = [...gitDirs];
7669
8292
  const manifestSorted = [...manifestDirs].sort(
@@ -7680,9 +8303,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7680
8303
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7681
8304
  const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
7682
8305
  return mapWithConcurrency(repoRoots, async (relDir) => {
7683
- const absDir = path45.resolve(workspaceRoot, relDir);
7684
- const pkg = await readJson(path45.join(absDir, "package.json"));
7685
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path45.basename(absDir);
8306
+ const absDir = path46.resolve(workspaceRoot, relDir);
8307
+ const pkg = await readJson(path46.join(absDir, "package.json"));
8308
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path46.basename(absDir);
7686
8309
  let role = "auto";
7687
8310
  try {
7688
8311
  const scan = await scanProject(absDir);
@@ -7725,7 +8348,7 @@ function renderWorkspaceYaml(config) {
7725
8348
  });
7726
8349
  }
7727
8350
  async function runDiscover(workspaceRoot, opts = {}) {
7728
- const yamlPath = path45.join(workspaceRoot, "haus.workspace.yaml");
8351
+ const yamlPath = path46.join(workspaceRoot, "haus.workspace.yaml");
7729
8352
  const existingText = await readText(yamlPath);
7730
8353
  const existing = parseWorkspaceConfig(existingText);
7731
8354
  if (existingText && !existing) {
@@ -7792,7 +8415,11 @@ async function runSetupCore(root, opts) {
7792
8415
  const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
7793
8416
  const context = await readContextOrScan(root);
7794
8417
  const priorManualIncludes = await readPriorManualIncludes(root);
7795
- const recommendation = await recommend(root, context, { include: priorManualIncludes });
8418
+ const activePluginIds = await activeGlobalPluginIds(root);
8419
+ const recommendation = await recommend(root, context, {
8420
+ include: priorManualIncludes,
8421
+ activePluginIds
8422
+ });
7796
8423
  if (recommendation.recommended.length === 0 && !force) {
7797
8424
  const warningLines2 = [
7798
8425
  .../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
@@ -7806,7 +8433,10 @@ async function runSetupCore(root, opts) {
7806
8433
  say(`Repo: ${context.repoName}`);
7807
8434
  for (const warning of warningLines2) say(`- WARN: ${warning}`);
7808
8435
  }
7809
- const message = "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus setup-project --force` to write anyway.";
8436
+ const supersededCount = recommendation.skipped.filter(
8437
+ (s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
8438
+ ).length;
8439
+ const message = supersededCount > 0 ? `No catalog items matched this project after ${supersededCount} item(s) were gated as already provided by an installed global plugin \u2014 refusing to write recommendation.json/haus.lock.json. Those skills are available globally; re-run with \`haus setup-project --force\` to write anyway.` : "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus setup-project --force` to write anyway.";
7810
8440
  warn(message);
7811
8441
  process.exitCode = 1;
7812
8442
  return {
@@ -7872,7 +8502,10 @@ async function runSetupCore(root, opts) {
7872
8502
  warn(formatGithubRateLimitMessage(sync.rateLimit));
7873
8503
  }
7874
8504
  }
7875
- const files = await writeClaudeFiles(root, dryRun ?? false, void 0, { quiet: opts.quiet });
8505
+ const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
8506
+ quiet: opts.quiet,
8507
+ activePluginIds
8508
+ });
7876
8509
  say("Applied files:");
7877
8510
  files.forEach((f) => say(`- ${displayPath(root, f)}`));
7878
8511
  const hooksAfter = await verifyProjectSettingsHooksContract(root);
@@ -7903,7 +8536,7 @@ async function runSetupProject(options) {
7903
8536
  // src/commands/init.ts
7904
8537
  async function runInit(options) {
7905
8538
  const root = process.cwd();
7906
- const hausDir = path46.join(root, ".haus-workflow");
8539
+ const hausDir = path47.join(root, ".haus-workflow");
7907
8540
  const alreadyInit = await fs33.pathExists(hausDir);
7908
8541
  if (alreadyInit) {
7909
8542
  log("Haus AI already initialized in this project.");
@@ -7968,7 +8601,19 @@ function printCompanionToolSuggestions(deps = {}) {
7968
8601
  }
7969
8602
 
7970
8603
  // src/commands/install.ts
7971
- async function runInstall(options) {
8604
+ function toManifestPlugin(pin) {
8605
+ return {
8606
+ id: pin.id,
8607
+ plugin: pin.plugin,
8608
+ marketplace: pin.marketplace,
8609
+ url: pin.url,
8610
+ ref: pin.ref,
8611
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
8612
+ };
8613
+ }
8614
+ async function runInstall(options, deps = {}) {
8615
+ const ensure = deps.ensureGlobalPlugins ?? ensureGlobalPlugins;
8616
+ const probe = deps.probePluginEnv ?? probePluginEnv;
7972
8617
  try {
7973
8618
  const result = await applyInstall({
7974
8619
  dryRun: options.dryRun,
@@ -7976,9 +8621,23 @@ async function runInstall(options) {
7976
8621
  check: options.check
7977
8622
  });
7978
8623
  if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
7979
- if (options.check && result.drift) {
7980
- process.exitCode = 1;
7981
- } else if (!options.check && !options.dryRun) {
8624
+ const pins = [...DEFAULT_GLOBAL_PLUGINS];
8625
+ if (options.check) {
8626
+ const state = await probe();
8627
+ const pluginDrift = pluginDriftStatus(planPluginActions(pins, state), { matchRef: false });
8628
+ if (pluginDrift === true) {
8629
+ log("Global plugin drift: a haus-managed plugin is missing or disabled.");
8630
+ }
8631
+ if (result.drift || pluginDrift === true) process.exitCode = 1;
8632
+ return;
8633
+ }
8634
+ const ensured = await ensure(pins, { dryRun: options.dryRun });
8635
+ if (!options.dryRun) {
8636
+ await recordManifestPlugins(
8637
+ pins.filter((pin) => ensured.ensured.includes(pin.id)).map(toManifestPlugin)
8638
+ );
8639
+ }
8640
+ if (!options.dryRun) {
7982
8641
  printCompanionToolSuggestions();
7983
8642
  const total = result.created.length + result.updated.length;
7984
8643
  if (options.postinstall) {
@@ -7990,6 +8649,9 @@ async function runInstall(options) {
7990
8649
  parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
7991
8650
  );
7992
8651
  log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
8652
+ if (ensured.ensured.length) {
8653
+ log(` \u2022 global plugin(s) pinned: ${ensured.ensured.join(", ")}`);
8654
+ }
7993
8655
  log("Undo any time with: haus uninstall");
7994
8656
  log("Disable this on install: HAUS_NO_POSTINSTALL=1");
7995
8657
  } else {
@@ -8009,7 +8671,8 @@ async function runRecommend(options) {
8009
8671
  const root = process.cwd();
8010
8672
  const context = await readContextOrScan(root);
8011
8673
  const include = parseIdList(options.include);
8012
- const result = await recommend(root, context, { include });
8674
+ const activePluginIds = await activeGlobalPluginIds(root);
8675
+ const result = await recommend(root, context, { include, activePluginIds });
8013
8676
  await writeJson(hausPath(root, "recommendation.json"), result);
8014
8677
  if (options.json) {
8015
8678
  log(JSON.stringify(result, null, 2));
@@ -8030,7 +8693,14 @@ async function runRecommend(options) {
8030
8693
  async function runRefresh() {
8031
8694
  const root = process.cwd();
8032
8695
  const context = await scanProject(root);
8033
- const recommendation = await recommend(root, context);
8696
+ try {
8697
+ await ensureGlobalPlugins(await loadCatalogPluginPins(root));
8698
+ } catch (err) {
8699
+ warn(`Could not refresh global plugins: ${err instanceof Error ? err.message : String(err)}`);
8700
+ }
8701
+ const recommendation = await recommend(root, context, {
8702
+ activePluginIds: await activeGlobalPluginIds(root)
8703
+ });
8034
8704
  await writeJson(hausPath(root, "recommendation.json"), recommendation);
8035
8705
  log("Haus refresh complete");
8036
8706
  log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
@@ -8039,7 +8709,7 @@ async function runRefresh() {
8039
8709
  }
8040
8710
 
8041
8711
  // src/install/scaffold.ts
8042
- import path47 from "node:path";
8712
+ import path48 from "node:path";
8043
8713
  import fs34 from "fs-extra";
8044
8714
  async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8045
8715
  const scaffolded = [];
@@ -8056,9 +8726,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8056
8726
  const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
8057
8727
  for (const item of items) {
8058
8728
  if (item.type !== "config") continue;
8059
- const sourcePath = path47.resolve(catalogRoot, item.path);
8060
- const rel = path47.relative(catalogRoot, sourcePath);
8061
- const escapes = rel === "" || path47.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
8729
+ const sourcePath = path48.resolve(catalogRoot, item.path);
8730
+ const rel = path48.relative(catalogRoot, sourcePath);
8731
+ const escapes = rel === "" || path48.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
8062
8732
  if (escapes) {
8063
8733
  warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
8064
8734
  continue;
@@ -8068,8 +8738,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8068
8738
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
8069
8739
  continue;
8070
8740
  }
8071
- const realRel = path47.relative(realRoot, realSource);
8072
- if (realRel !== "" && (path47.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
8741
+ const realRel = path48.relative(realRoot, realSource);
8742
+ if (realRel !== "" && (path48.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
8073
8743
  warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
8074
8744
  continue;
8075
8745
  }
@@ -8083,14 +8753,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8083
8753
  continue;
8084
8754
  }
8085
8755
  if (stat.isFile()) {
8086
- const filename = path47.basename(sourcePath);
8087
- const dest = path47.join(projectRoot, filename);
8756
+ const filename = path48.basename(sourcePath);
8757
+ const dest = path48.join(projectRoot, filename);
8088
8758
  record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
8089
8759
  } else if (stat.isDirectory()) {
8090
8760
  const entries = await fs34.readdir(sourcePath);
8091
8761
  for (const entry of entries) {
8092
- const src = path47.join(sourcePath, entry);
8093
- const dest = path47.join(projectRoot, entry);
8762
+ const src = path48.join(sourcePath, entry);
8763
+ const dest = path48.join(projectRoot, entry);
8094
8764
  record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
8095
8765
  }
8096
8766
  }
@@ -8098,14 +8768,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8098
8768
  return { scaffolded, skipped, skippedExisting };
8099
8769
  }
8100
8770
  async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
8101
- const destDisplay = path47.relative(projectRoot, dest) || path47.basename(dest);
8771
+ const destDisplay = path48.relative(projectRoot, dest) || path48.basename(dest);
8102
8772
  const srcStat = await fs34.lstat(src).catch(() => null);
8103
8773
  if (!srcStat) {
8104
- warn(`Skipping ${path47.basename(src)}: source not found`);
8774
+ warn(`Skipping ${path48.basename(src)}: source not found`);
8105
8775
  return "skipped-refused";
8106
8776
  }
8107
8777
  if (srcStat.isSymbolicLink()) {
8108
- warn(`Skipping ${path47.basename(src)}: source is a symlink`);
8778
+ warn(`Skipping ${path48.basename(src)}: source is a symlink`);
8109
8779
  return "skipped-refused";
8110
8780
  }
8111
8781
  const exists = await fs34.pathExists(dest);
@@ -8114,13 +8784,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
8114
8784
  return "skipped-exists";
8115
8785
  }
8116
8786
  if (opts.dryRun) {
8117
- log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path47.basename(dest)} (${itemId})`);
8787
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path48.basename(dest)} (${itemId})`);
8118
8788
  return "scaffolded";
8119
8789
  }
8120
8790
  if (exists && opts.force) {
8121
8791
  await fs34.remove(dest);
8122
8792
  }
8123
- await fs34.ensureDir(path47.dirname(dest));
8793
+ await fs34.ensureDir(path48.dirname(dest));
8124
8794
  await fs34.copy(src, dest, {
8125
8795
  overwrite: true,
8126
8796
  dereference: false,
@@ -8133,7 +8803,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
8133
8803
  }
8134
8804
  }
8135
8805
  });
8136
- log(`${exists ? "Overwrote" : "Created"} ${path47.basename(dest)} (${itemId})`);
8806
+ log(`${exists ? "Overwrote" : "Created"} ${path48.basename(dest)} (${itemId})`);
8137
8807
  return "scaffolded";
8138
8808
  }
8139
8809
 
@@ -8198,7 +8868,7 @@ async function runScan(options) {
8198
8868
  }
8199
8869
 
8200
8870
  // src/commands/undo.ts
8201
- import path48 from "node:path";
8871
+ import path49 from "node:path";
8202
8872
  import fs35 from "fs-extra";
8203
8873
 
8204
8874
  // src/claude/managed-paths.ts
@@ -8226,18 +8896,18 @@ async function collectLockTrackedPaths(root) {
8226
8896
  const relPaths = row.paths ?? [];
8227
8897
  const existingRel = [];
8228
8898
  for (const rel of relPaths) {
8229
- if (await fs35.pathExists(path48.resolve(root, rel))) existingRel.push(rel);
8899
+ if (await fs35.pathExists(path49.resolve(root, rel))) existingRel.push(rel);
8230
8900
  }
8231
8901
  if (existingRel.length === 0) continue;
8232
8902
  if (row.hash === void 0) {
8233
- removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
8903
+ removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
8234
8904
  continue;
8235
8905
  }
8236
8906
  const currentHash = await hashInstalledPaths(root, existingRel);
8237
8907
  if (currentHash === row.hash) {
8238
- removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
8908
+ removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
8239
8909
  } else {
8240
- preserved.push(...existingRel.map((rel) => path48.resolve(root, rel)));
8910
+ preserved.push(...existingRel.map((rel) => path49.resolve(root, rel)));
8241
8911
  log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
8242
8912
  }
8243
8913
  }
@@ -8250,7 +8920,7 @@ async function settingsHasHausContent(root) {
8250
8920
  return settings._haus != null;
8251
8921
  }
8252
8922
  async function claudeMdHasHausBlock(root) {
8253
- const filePath = path48.join(root, "CLAUDE.md");
8923
+ const filePath = path49.join(root, "CLAUDE.md");
8254
8924
  if (!await fs35.pathExists(filePath)) return false;
8255
8925
  const text = await fs35.readFile(filePath, "utf8");
8256
8926
  return text.includes(BLOCK_BEGIN);
@@ -8263,15 +8933,15 @@ async function stripProjectSettings(root) {
8263
8933
  const hasContent = Object.keys(settings).length > 0;
8264
8934
  if (hasContent) {
8265
8935
  await writeProjectSettings(root, settings);
8266
- log(`Stripped haus rules from ${path48.relative(root, settingsPath)} (user settings preserved).`);
8936
+ log(`Stripped haus rules from ${path49.relative(root, settingsPath)} (user settings preserved).`);
8267
8937
  return true;
8268
8938
  }
8269
8939
  await fs35.remove(settingsPath);
8270
- log(`Removed ${path48.relative(root, settingsPath)} (no user-owned settings remained).`);
8940
+ log(`Removed ${path49.relative(root, settingsPath)} (no user-owned settings remained).`);
8271
8941
  return true;
8272
8942
  }
8273
8943
  async function stripRootClaudeMd(root) {
8274
- const filePath = path48.join(root, "CLAUDE.md");
8944
+ const filePath = path49.join(root, "CLAUDE.md");
8275
8945
  if (!await fs35.pathExists(filePath)) return false;
8276
8946
  const prev = await fs35.readFile(filePath, "utf8");
8277
8947
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -8299,12 +8969,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
8299
8969
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
8300
8970
  for (const abs of managedAbsPaths) {
8301
8971
  if (!await fs35.pathExists(abs)) continue;
8302
- const rel = path48.relative(root, abs);
8303
- const backupPath = path48.join(backupRoot, rel);
8304
- await fs35.ensureDir(path48.dirname(backupPath));
8972
+ const rel = path49.relative(root, abs);
8973
+ const backupPath = path49.join(backupRoot, rel);
8974
+ await fs35.ensureDir(path49.dirname(backupPath));
8305
8975
  await fs35.copy(abs, backupPath);
8306
8976
  }
8307
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path48.relative(root, backupRoot)}.`);
8977
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path49.relative(root, backupRoot)}.`);
8308
8978
  }
8309
8979
  async function runUndo(options) {
8310
8980
  const root = options.root ?? process.cwd();
@@ -8317,7 +8987,7 @@ async function runUndo(options) {
8317
8987
  log("Nothing to remove: no haus-managed files found in this directory.");
8318
8988
  return;
8319
8989
  }
8320
- const relTargets = managed.map((p) => path48.relative(root, p));
8990
+ const relTargets = managed.map((p) => path49.relative(root, p));
8321
8991
  const summaryParts = [...relTargets];
8322
8992
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
8323
8993
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -8341,7 +9011,7 @@ User-owned .claude/ files will be preserved.`
8341
9011
  for (const abs of managed) {
8342
9012
  if (!await fs35.pathExists(abs)) continue;
8343
9013
  await fs35.remove(abs);
8344
- log(`Removed ${path48.relative(root, abs)}`);
9014
+ log(`Removed ${path49.relative(root, abs)}`);
8345
9015
  }
8346
9016
  if (stripSettings) await stripProjectSettings(root);
8347
9017
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -8352,16 +9022,43 @@ User-owned .claude/ files will be preserved.`
8352
9022
 
8353
9023
  // src/install/uninstall.ts
8354
9024
  import crypto3 from "node:crypto";
8355
- import path49 from "node:path";
9025
+ import path50 from "node:path";
8356
9026
  import fs36 from "fs-extra";
8357
9027
  async function runUninstall(options = {}) {
8358
- const { force = false } = options;
9028
+ const { force = false, removePlugins = removeGlobalPlugins } = options;
8359
9029
  const manifest = await readManifest();
8360
- const result = { deleted: [], skipped: [], hooksStripped: false };
9030
+ const result = {
9031
+ deleted: [],
9032
+ skipped: [],
9033
+ hooksStripped: false,
9034
+ pluginsRemoved: []
9035
+ };
8361
9036
  if (!manifest) {
8362
9037
  warn("No install manifest found \u2014 nothing to uninstall.");
8363
9038
  return result;
8364
9039
  }
9040
+ const recordedPlugins = Array.isArray(manifest.plugins) ? manifest.plugins : [];
9041
+ const malformed = recordedPlugins.filter((entry) => !isValidPin(entry)).length;
9042
+ if (malformed > 0) {
9043
+ warn(
9044
+ `Ignoring ${malformed} malformed plugin record(s) in the install manifest \u2014 continuing with the rest of the uninstall.`
9045
+ );
9046
+ }
9047
+ const managedPlugins = recordedPlugins.filter(isValidPin).map((p) => ({
9048
+ id: p.id,
9049
+ plugin: p.plugin,
9050
+ marketplace: p.marketplace,
9051
+ url: p.url,
9052
+ ref: p.ref
9053
+ }));
9054
+ try {
9055
+ const pluginRemoval = await removePlugins(managedPlugins);
9056
+ result.pluginsRemoved = pluginRemoval.removed;
9057
+ } catch (err) {
9058
+ warn(
9059
+ `Could not remove global plugin(s): ${err instanceof Error ? err.message : String(err)} \u2014 continuing with the rest of the uninstall.`
9060
+ );
9061
+ }
8365
9062
  for (const entry of manifest.files) {
8366
9063
  const exists = fs36.pathExistsSync(entry.destPath);
8367
9064
  if (!exists) continue;
@@ -8382,14 +9079,14 @@ async function runUninstall(options = {}) {
8382
9079
  continue;
8383
9080
  }
8384
9081
  await fs36.remove(entry.destPath);
8385
- await pruneEmptyDir(path49.dirname(entry.destPath));
9082
+ await pruneEmptyDir(path50.dirname(entry.destPath));
8386
9083
  result.deleted.push(entry.destPath);
8387
9084
  }
8388
9085
  const settings = await readSettings();
8389
9086
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
8390
9087
  await writeSettings(stripped);
8391
9088
  result.hooksStripped = true;
8392
- const hausDir = path49.join(globalClaudeDir(), "haus");
9089
+ const hausDir = path50.join(globalClaudeDir(), "haus");
8393
9090
  const manifestPath2 = hausManifestPath();
8394
9091
  if (fs36.pathExistsSync(manifestPath2)) {
8395
9092
  await fs36.remove(manifestPath2);
@@ -8415,6 +9112,10 @@ function printUninstallResult(result) {
8415
9112
  if (result.hooksStripped) {
8416
9113
  log("Haus hook entries removed from ~/.claude/settings.json");
8417
9114
  }
9115
+ if (result.pluginsRemoved.length) {
9116
+ log(`Global plugin(s) removed: ${result.pluginsRemoved.join(", ")}`);
9117
+ log("Projects relying on those plugin skills get them back by reinstalling haus.");
9118
+ }
8418
9119
  }
8419
9120
 
8420
9121
  // src/commands/uninstall.ts
@@ -8443,11 +9144,11 @@ async function runUninstallCommand(options) {
8443
9144
  }
8444
9145
 
8445
9146
  // src/commands/validate-catalog.ts
8446
- import path51 from "node:path";
9147
+ import path52 from "node:path";
8447
9148
 
8448
9149
  // src/catalog/validate-core.ts
8449
9150
  import fs37 from "node:fs";
8450
- import path50 from "node:path";
9151
+ import path51 from "node:path";
8451
9152
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
8452
9153
  var ITEM_ID_PATTERN = /^haus\./;
8453
9154
  function auditForbiddenStacks(items) {
@@ -8596,7 +9297,7 @@ function readForAudit(absPath, label, failures) {
8596
9297
  }
8597
9298
  }
8598
9299
  function auditTemplateContent(manifestDir, absPath, itemId) {
8599
- const rel = path50.relative(manifestDir, absPath);
9300
+ const rel = path51.relative(manifestDir, absPath);
8600
9301
  const failures = [];
8601
9302
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
8602
9303
  if (text === null) return failures;
@@ -8618,25 +9319,25 @@ function auditShippedFiles(manifestDir, items) {
8618
9319
  const failures = [];
8619
9320
  for (const item of items) {
8620
9321
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
8621
- const absPath = path50.join(manifestDir, item.path);
9322
+ const absPath = path51.join(manifestDir, item.path);
8622
9323
  if (item.type === "skill") {
8623
- const skillMd = path50.join(absPath, "SKILL.md");
9324
+ const skillMd = path51.join(absPath, "SKILL.md");
8624
9325
  if (!fs37.existsSync(skillMd)) {
8625
- failures.push(`${item.id}: missing ${path50.relative(manifestDir, skillMd)}`);
9326
+ failures.push(`${item.id}: missing ${path51.relative(manifestDir, skillMd)}`);
8626
9327
  continue;
8627
9328
  }
8628
9329
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
8629
9330
  if (text === null) continue;
8630
9331
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
8631
9332
  failures.push(
8632
- ...auditForbiddenTagsInText(text, `${item.id}: ${path50.relative(manifestDir, skillMd)}`)
9333
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path51.relative(manifestDir, skillMd)}`)
8633
9334
  );
8634
9335
  } else if (item.type === "agent") {
8635
9336
  if (!fs37.existsSync(absPath)) {
8636
9337
  failures.push(`${item.id}: missing agent file ${item.path}`);
8637
9338
  continue;
8638
9339
  }
8639
- const rel = path50.relative(manifestDir, absPath);
9340
+ const rel = path51.relative(manifestDir, absPath);
8640
9341
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8641
9342
  if (text === null) continue;
8642
9343
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8652,7 +9353,7 @@ function auditShippedFiles(manifestDir, items) {
8652
9353
  failures.push(`${item.id}: missing command file ${item.path}`);
8653
9354
  continue;
8654
9355
  }
8655
- const rel = path50.relative(manifestDir, absPath);
9356
+ const rel = path51.relative(manifestDir, absPath);
8656
9357
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8657
9358
  if (text === null) continue;
8658
9359
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8684,7 +9385,7 @@ function walkAllFiles(dir, fn, failures) {
8684
9385
  }
8685
9386
  for (const entry of entries) {
8686
9387
  if (entry.name === ".DS_Store") continue;
8687
- const full = path50.join(dir, entry.name);
9388
+ const full = path51.join(dir, entry.name);
8688
9389
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
8689
9390
  else fn(full);
8690
9391
  }
@@ -8702,12 +9403,12 @@ function auditDiskOrphans(manifestDir, items) {
8702
9403
  const failures = [];
8703
9404
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
8704
9405
  for (const dir of CONTENT_ROOTS) {
8705
- const abs = path50.join(manifestDir, dir);
9406
+ const abs = path51.join(manifestDir, dir);
8706
9407
  if (!fs37.existsSync(abs)) continue;
8707
9408
  walkAllFiles(
8708
9409
  abs,
8709
9410
  (file) => {
8710
- const relPath = path50.relative(manifestDir, file).replace(/\\/g, "/");
9411
+ const relPath = path51.relative(manifestDir, file).replace(/\\/g, "/");
8711
9412
  if (!isPathClaimed(relPath, claimedPaths)) {
8712
9413
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
8713
9414
  }
@@ -8728,7 +9429,7 @@ function walkMd(dir, fn, failures) {
8728
9429
  return;
8729
9430
  }
8730
9431
  for (const entry of entries) {
8731
- const full = path50.join(dir, entry.name);
9432
+ const full = path51.join(dir, entry.name);
8732
9433
  if (entry.isDirectory()) walkMd(full, fn, failures);
8733
9434
  else if (entry.name.endsWith(".md")) fn(full);
8734
9435
  }
@@ -8737,13 +9438,13 @@ function auditMarkdownContent(manifestDir, items) {
8737
9438
  const failures = [];
8738
9439
  const pathSourceMap = buildItemPathSourceMap(items);
8739
9440
  for (const dir of ["skills", "agents", "templates", "commands"]) {
8740
- const abs = path50.join(manifestDir, dir);
9441
+ const abs = path51.join(manifestDir, dir);
8741
9442
  if (!fs37.existsSync(abs)) continue;
8742
9443
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
8743
9444
  walkMd(
8744
9445
  abs,
8745
9446
  (file) => {
8746
- const rel = path50.relative(manifestDir, file);
9447
+ const rel = path51.relative(manifestDir, file);
8747
9448
  const text = readForAudit(file, rel, failures);
8748
9449
  if (text === null) return;
8749
9450
  const norm = rel.replace(/\\/g, "/");
@@ -8788,8 +9489,8 @@ async function runValidateCatalog(manifestPath2) {
8788
9489
  process.exitCode = 1;
8789
9490
  return;
8790
9491
  }
8791
- const abs = path51.resolve(process.cwd(), manifestPath2);
8792
- const manifestDir = path51.dirname(abs);
9492
+ const abs = path52.resolve(process.cwd(), manifestPath2);
9493
+ const manifestDir = path52.dirname(abs);
8793
9494
  const data = await readJson(abs);
8794
9495
  if (!data?.items) {
8795
9496
  error(`Could not read catalog manifest at ${abs}`);
@@ -8806,7 +9507,7 @@ async function runValidateCatalog(manifestPath2) {
8806
9507
  }
8807
9508
 
8808
9509
  // src/workspace/worktree/add.ts
8809
- import path58 from "node:path";
9510
+ import path59 from "node:path";
8810
9511
  import fs41 from "fs-extra";
8811
9512
 
8812
9513
  // src/workspace/worktree/git-worktree.ts
@@ -8926,12 +9627,12 @@ async function hasUnpushedWork(cwd) {
8926
9627
  }
8927
9628
 
8928
9629
  // src/workspace/worktree/hydrate.ts
8929
- import path54 from "node:path";
9630
+ import path55 from "node:path";
8930
9631
  import fs39 from "fs-extra";
8931
9632
 
8932
9633
  // src/workspace/worktree/cow-copy.ts
8933
9634
  init_exec();
8934
- import path52 from "node:path";
9635
+ import path53 from "node:path";
8935
9636
  import fs38 from "fs-extra";
8936
9637
  async function getMacFilesystemType(dir) {
8937
9638
  const df = await runCommand("df", ["-P", dir]);
@@ -8966,11 +9667,11 @@ async function cowCopyDir(src, dest) {
8966
9667
  const srcExists = await fs38.pathExists(src);
8967
9668
  if (!srcExists)
8968
9669
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
8969
- const strategy = await detectCowStrategy(path52.dirname(dest));
9670
+ const strategy = await detectCowStrategy(path53.dirname(dest));
8970
9671
  if (strategy === "unsupported" || strategy === "unknown-platform") {
8971
9672
  return { strategy, attempted: false, ok: false };
8972
9673
  }
8973
- await fs38.ensureDir(path52.dirname(dest));
9674
+ await fs38.ensureDir(path53.dirname(dest));
8974
9675
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
8975
9676
  const result = await runCommand("cp", args);
8976
9677
  return {
@@ -8984,7 +9685,7 @@ async function cowCopyDir(src, dest) {
8984
9685
  // src/workspace/worktree/install.ts
8985
9686
  init_exec();
8986
9687
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8987
- import path53 from "node:path";
9688
+ import path54 from "node:path";
8988
9689
  import fg6 from "fast-glob";
8989
9690
  function detectInstallPlan(signals) {
8990
9691
  if (signals.hasYarnLock) {
@@ -9005,10 +9706,10 @@ function detectInstallPlan(signals) {
9005
9706
  return { manager: "none" };
9006
9707
  }
9007
9708
  async function computeLockfileSignals(dir) {
9008
- const hasYarnLock = existsSync3(path53.join(dir, "yarn.lock"));
9009
- const hasPnpmLock = existsSync3(path53.join(dir, "pnpm-lock.yaml"));
9010
- const hasPackageLock = existsSync3(path53.join(dir, "package-lock.json"));
9011
- const hasComposerLock = existsSync3(path53.join(dir, "composer.lock"));
9709
+ const hasYarnLock = existsSync3(path54.join(dir, "yarn.lock"));
9710
+ const hasPnpmLock = existsSync3(path54.join(dir, "pnpm-lock.yaml"));
9711
+ const hasPackageLock = existsSync3(path54.join(dir, "package-lock.json"));
9712
+ const hasComposerLock = existsSync3(path54.join(dir, "composer.lock"));
9012
9713
  const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
9013
9714
  cwd: dir,
9014
9715
  onlyFiles: true,
@@ -9022,7 +9723,7 @@ async function computeLockfileSignals(dir) {
9022
9723
  });
9023
9724
  const hasDotnetProject = dotnetFiles.length > 0;
9024
9725
  let hasPackageManagerField = false;
9025
- const pkgPath = path53.join(dir, "package.json");
9726
+ const pkgPath = path54.join(dir, "package.json");
9026
9727
  if (existsSync3(pkgPath)) {
9027
9728
  try {
9028
9729
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -9063,8 +9764,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
9063
9764
  const start = Date.now();
9064
9765
  const targets = [];
9065
9766
  for (const target of HYDRATION_TARGETS) {
9066
- const src = path54.join(mainMemberPath, target);
9067
- const dest = path54.join(worktreeMemberPath, target);
9767
+ const src = path55.join(mainMemberPath, target);
9768
+ const dest = path55.join(worktreeMemberPath, target);
9068
9769
  if (!await fs39.pathExists(src)) continue;
9069
9770
  const destExists = await fs39.pathExists(dest);
9070
9771
  if (!opts.force && destExists) continue;
@@ -9082,11 +9783,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
9082
9783
 
9083
9784
  // src/workspace/worktree/local-files.ts
9084
9785
  init_exec();
9085
- import path55 from "node:path";
9786
+ import path56 from "node:path";
9086
9787
  import fs40 from "fs-extra";
9087
9788
  var MACHINE_LOCAL_FILES = [
9088
9789
  "docker-compose.local.yml",
9089
- path55.join(".claude", "settings.local.json")
9790
+ path56.join(".claude", "settings.local.json")
9090
9791
  ];
9091
9792
  async function isTracked(repoRoot, relFile) {
9092
9793
  const gitPath = relFile.replace(/\\/g, "/");
@@ -9096,11 +9797,11 @@ async function isTracked(repoRoot, relFile) {
9096
9797
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
9097
9798
  const copied = [];
9098
9799
  for (const rel of MACHINE_LOCAL_FILES) {
9099
- const src = path55.join(mainMemberPath, rel);
9800
+ const src = path56.join(mainMemberPath, rel);
9100
9801
  if (!await fs40.pathExists(src)) continue;
9101
9802
  if (await isTracked(mainMemberPath, rel)) continue;
9102
- const dest = path55.join(worktreeMemberPath, rel);
9103
- await fs40.ensureDir(path55.dirname(dest));
9803
+ const dest = path56.join(worktreeMemberPath, rel);
9804
+ await fs40.ensureDir(path56.dirname(dest));
9104
9805
  await fs40.copy(src, dest, { overwrite: true });
9105
9806
  copied.push(rel);
9106
9807
  }
@@ -9109,15 +9810,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
9109
9810
 
9110
9811
  // src/workspace/worktree/root.ts
9111
9812
  import { existsSync as existsSync4 } from "node:fs";
9112
- import path56 from "node:path";
9813
+ import path57 from "node:path";
9113
9814
  async function resolveWorkspaceForWorktree(start) {
9114
9815
  const rootInfo = await resolveRoots(start);
9115
9816
  if (!rootInfo.isGitRepo) {
9116
9817
  return { ok: false, reason: "Not inside a git repository." };
9117
9818
  }
9118
9819
  const workspaceRoot = rootInfo.mainRoot;
9119
- const hasYaml = existsSync4(path56.join(workspaceRoot, WORKSPACE_FILE));
9120
- const hasManifest = existsSync4(path56.join(workspaceRoot, REPOS_MANIFEST_FILE));
9820
+ const hasYaml = existsSync4(path57.join(workspaceRoot, WORKSPACE_FILE));
9821
+ const hasManifest = existsSync4(path57.join(workspaceRoot, REPOS_MANIFEST_FILE));
9121
9822
  if (!hasYaml && !hasManifest) {
9122
9823
  return {
9123
9824
  ok: false,
@@ -9142,10 +9843,10 @@ function selectMembers(members, only) {
9142
9843
  }
9143
9844
 
9144
9845
  // src/workspace/worktree/state.ts
9145
- import path57 from "node:path";
9846
+ import path58 from "node:path";
9146
9847
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
9147
9848
  function worktreesDir(mainRoot) {
9148
- return path57.join(mainRoot, ".claude", "worktrees");
9849
+ return path58.join(mainRoot, ".claude", "worktrees");
9149
9850
  }
9150
9851
  function isValidWorktreeSlug(slug) {
9151
9852
  if (slug === "" || slug === "." || slug === "..") return false;
@@ -9158,15 +9859,15 @@ function worktreePath(mainRoot, slug) {
9158
9859
  `Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
9159
9860
  );
9160
9861
  }
9161
- return path57.join(worktreesDir(mainRoot), slug);
9862
+ return path58.join(worktreesDir(mainRoot), slug);
9162
9863
  }
9163
9864
  async function readWorktreeState(worktreeRoot) {
9164
- const state = await readJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE));
9865
+ const state = await readJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE));
9165
9866
  if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
9166
9867
  return state;
9167
9868
  }
9168
9869
  async function writeWorktreeState(worktreeRoot, state) {
9169
- await writeJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE), state);
9870
+ await writeJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE), state);
9170
9871
  }
9171
9872
 
9172
9873
  // src/workspace/worktree/add.ts
@@ -9200,7 +9901,7 @@ async function runAdd(opts) {
9200
9901
  const memberResults = [];
9201
9902
  for (const member of selected) {
9202
9903
  const start = Date.now();
9203
- const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
9904
+ const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
9204
9905
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
9205
9906
  preferDefaultBranchFrom: true
9206
9907
  });
@@ -9230,7 +9931,7 @@ async function runAdd(opts) {
9230
9931
  await mapWithConcurrency(okResults, async (result) => {
9231
9932
  const member = selected.find((m) => m.id === result.member);
9232
9933
  if (!member) return;
9233
- const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
9934
+ const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
9234
9935
  const hydrationStart = Date.now();
9235
9936
  if (opts.hydrate !== false) {
9236
9937
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -9297,10 +9998,10 @@ async function runDryRun(args) {
9297
9998
 
9298
9999
  // src/workspace/worktree/doctor.ts
9299
10000
  import { existsSync as existsSync5 } from "node:fs";
9300
- import path59 from "node:path";
10001
+ import path60 from "node:path";
9301
10002
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
9302
10003
  function looksHydrated(memberPath) {
9303
- return HYDRATION_MARKERS.some((marker) => existsSync5(path59.join(memberPath, marker)));
10004
+ return HYDRATION_MARKERS.some((marker) => existsSync5(path60.join(memberPath, marker)));
9304
10005
  }
9305
10006
  async function runDoctor2() {
9306
10007
  const rootInfo = await resolveRoots();
@@ -9314,7 +10015,7 @@ async function runDoctor2() {
9314
10015
  };
9315
10016
  }
9316
10017
  const workspaceRoot = rootInfo.mainRoot;
9317
- const isWorkspace = existsSync5(path59.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path59.join(workspaceRoot, REPOS_MANIFEST_FILE));
10018
+ const isWorkspace = existsSync5(path60.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path60.join(workspaceRoot, REPOS_MANIFEST_FILE));
9318
10019
  if (!isWorkspace) {
9319
10020
  return {
9320
10021
  isWorkspace: false,
@@ -9327,7 +10028,7 @@ async function runDoctor2() {
9327
10028
  };
9328
10029
  }
9329
10030
  const wsWorktreesDir = worktreesDir(workspaceRoot);
9330
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path59.sep}`);
10031
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path60.sep}`);
9331
10032
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
9332
10033
  const members = await readMembers(rootInfo);
9333
10034
  const problems = [];
@@ -9335,10 +10036,10 @@ async function runDoctor2() {
9335
10036
  let slug;
9336
10037
  if (inWorkspaceWorktree) {
9337
10038
  const wtPath = rootInfo.repoRoot;
9338
- slug = rootInfo.worktreeName ?? path59.basename(wtPath);
10039
+ slug = rootInfo.worktreeName ?? path60.basename(wtPath);
9339
10040
  const state = await readWorktreeState(wtPath);
9340
10041
  for (const member of members) {
9341
- const memberPath = path59.join(wtPath, member.folder);
10042
+ const memberPath = path60.join(wtPath, member.folder);
9342
10043
  const materialized = existsSync5(memberPath);
9343
10044
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
9344
10045
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -9369,16 +10070,16 @@ async function runDoctor2() {
9369
10070
  for (const member of members) {
9370
10071
  const entries = await listWorktrees(member.absPath);
9371
10072
  for (const entry of entries) {
9372
- const entryPath = path59.normalize(entry.path);
9373
- const marker = `${path59.sep}.claude${path59.sep}worktrees${path59.sep}`;
10073
+ const entryPath = path60.normalize(entry.path);
10074
+ const marker = `${path60.sep}.claude${path60.sep}worktrees${path60.sep}`;
9374
10075
  const idx = entryPath.indexOf(marker);
9375
10076
  if (idx === -1) continue;
9376
10077
  const rest = entryPath.slice(idx + marker.length);
9377
- const slugName = rest.split(path59.sep)[0];
10078
+ const slugName = rest.split(path60.sep)[0];
9378
10079
  if (!slugName) continue;
9379
- if (!existsSync5(path59.join(wsWorktreesDir, slugName))) {
10080
+ if (!existsSync5(path60.join(wsWorktreesDir, slugName))) {
9380
10081
  orphans.push(
9381
- `${member.id}: worktree registered at ${entry.path} but ${path59.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
10082
+ `${member.id}: worktree registered at ${entry.path} but ${path60.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
9382
10083
  );
9383
10084
  }
9384
10085
  }
@@ -9389,7 +10090,7 @@ async function runDoctor2() {
9389
10090
 
9390
10091
  // src/workspace/worktree/hydrate-command.ts
9391
10092
  import { existsSync as existsSync6 } from "node:fs";
9392
- import path60 from "node:path";
10093
+ import path61 from "node:path";
9393
10094
  async function runHydrateWorktree(opts) {
9394
10095
  const rootInfo = await resolveRoots();
9395
10096
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -9400,7 +10101,7 @@ async function runHydrateWorktree(opts) {
9400
10101
  }
9401
10102
  const workspaceRoot = rootInfo.mainRoot;
9402
10103
  const wtPath = rootInfo.repoRoot;
9403
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path60.sep}`)) {
10104
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path61.sep}`)) {
9404
10105
  return {
9405
10106
  ok: false,
9406
10107
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -9412,13 +10113,13 @@ async function runHydrateWorktree(opts) {
9412
10113
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
9413
10114
  }
9414
10115
  const state = await readWorktreeState(wtPath);
9415
- const slug = state?.slug ?? path60.basename(wtPath);
10116
+ const slug = state?.slug ?? path61.basename(wtPath);
9416
10117
  const detectedBranch = await currentBranch(wtPath);
9417
10118
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
9418
10119
  if (opts.dryRun) {
9419
10120
  const results2 = await Promise.all(
9420
10121
  selected.map(async (member) => {
9421
- const memberPath = path60.join(wtPath, member.folder);
10122
+ const memberPath = path61.join(wtPath, member.folder);
9422
10123
  if (existsSync6(memberPath)) {
9423
10124
  return { member: member.id, folder: member.folder, status: "planned" };
9424
10125
  }
@@ -9439,7 +10140,7 @@ async function runHydrateWorktree(opts) {
9439
10140
  const results = await mapWithConcurrency(
9440
10141
  selected,
9441
10142
  async (member) => {
9442
- const memberPath = path60.join(wtPath, member.folder);
10143
+ const memberPath = path61.join(wtPath, member.folder);
9443
10144
  let branchAction;
9444
10145
  if (!existsSync6(memberPath)) {
9445
10146
  if (!opts.materialize) {
@@ -9502,7 +10203,7 @@ async function runHydrateWorktree(opts) {
9502
10203
  }
9503
10204
 
9504
10205
  // src/workspace/worktree/list.ts
9505
- import path61 from "node:path";
10206
+ import path62 from "node:path";
9506
10207
  import fs42 from "fs-extra";
9507
10208
  async function runList2() {
9508
10209
  const resolved = await resolveWorkspaceForWorktree();
@@ -9515,12 +10216,12 @@ async function runList2() {
9515
10216
  const configuredMembers = await readMembers(rootInfo);
9516
10217
  const worktrees = [];
9517
10218
  for (const slug of slugs) {
9518
- const wtPath = path61.join(dir, slug);
10219
+ const wtPath = path62.join(dir, slug);
9519
10220
  const state = await readWorktreeState(wtPath);
9520
10221
  const members = [];
9521
10222
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
9522
10223
  for (const m of source) {
9523
- const memberPath = path61.join(wtPath, m.folder);
10224
+ const memberPath = path62.join(wtPath, m.folder);
9524
10225
  const materialized = await fs42.pathExists(memberPath);
9525
10226
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
9526
10227
  members.push({
@@ -9542,7 +10243,7 @@ async function runList2() {
9542
10243
  }
9543
10244
 
9544
10245
  // src/workspace/worktree/remove.ts
9545
- import path62 from "node:path";
10246
+ import path63 from "node:path";
9546
10247
  import fs43 from "fs-extra";
9547
10248
  async function runRemove(opts) {
9548
10249
  const resolved = await resolveWorkspaceForWorktree();
@@ -9569,7 +10270,7 @@ async function runRemove(opts) {
9569
10270
  targets.push({
9570
10271
  repo: configMember?.id ?? stateMember.id,
9571
10272
  ownerRepo,
9572
- path: path62.join(wsWorktreePath, stateMember.folder)
10273
+ path: path63.join(wsWorktreePath, stateMember.folder)
9573
10274
  });
9574
10275
  }
9575
10276
  const existingTargets = [];
@@ -9637,12 +10338,6 @@ async function runRemove(opts) {
9637
10338
  }
9638
10339
 
9639
10340
  // src/commands/workspace/worktree.ts
9640
- function normalizeOnly(only) {
9641
- if (!only) return void 0;
9642
- const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
9643
- const cleaned = list.map((s) => s.trim()).filter(Boolean);
9644
- return cleaned.length > 0 ? cleaned : void 0;
9645
- }
9646
10341
  function describeInstall(hydration) {
9647
10342
  if (hydration.install.plan.manager === "none") return "no install step (no recognized lockfile)";
9648
10343
  if (!hydration.install.ran)
@@ -9846,7 +10541,7 @@ async function runWorktree(action, opts = {}) {
9846
10541
 
9847
10542
  // src/commands/workspace.ts
9848
10543
  import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
9849
- import path71 from "node:path";
10544
+ import path72 from "node:path";
9850
10545
 
9851
10546
  // src/commands/workspace/aggregate.ts
9852
10547
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -9901,18 +10596,18 @@ ${summaries.map(
9901
10596
 
9902
10597
  // src/commands/workspace/doctor.ts
9903
10598
  import { existsSync as existsSync7 } from "node:fs";
9904
- import path64 from "node:path";
10599
+ import path65 from "node:path";
9905
10600
 
9906
10601
  // src/commands/workspace/manifest.ts
9907
10602
  import { readFileSync as readFileSync4 } from "node:fs";
9908
- import path63 from "node:path";
10603
+ import path64 from "node:path";
9909
10604
  var MANIFEST_FILE = "workspace.manifest.json";
9910
10605
  function manifestPath(workspaceRoot) {
9911
10606
  return hausPath(workspaceRoot, MANIFEST_FILE);
9912
10607
  }
9913
10608
  function hausVersion() {
9914
10609
  try {
9915
- const pkg = JSON.parse(readFileSync4(path63.join(packageRoot(), "package.json"), "utf8"));
10610
+ const pkg = JSON.parse(readFileSync4(path64.join(packageRoot(), "package.json"), "utf8"));
9916
10611
  return pkg.version ?? "0.0.0";
9917
10612
  } catch {
9918
10613
  return "0.0.0";
@@ -10004,7 +10699,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
10004
10699
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
10005
10700
  const catalogRefByRepo = [];
10006
10701
  for (const repo of config.repos) {
10007
- const repoRoot = path64.resolve(workspaceRoot, repo.path);
10702
+ const repoRoot = path65.resolve(workspaceRoot, repo.path);
10008
10703
  const entry = manifestByName.get(repo.name);
10009
10704
  if (!entry) {
10010
10705
  flag({
@@ -10077,7 +10772,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
10077
10772
  });
10078
10773
  continue;
10079
10774
  }
10080
- if (!existsSync7(path64.join(workspaceRoot, entry.path))) {
10775
+ if (!existsSync7(path65.join(workspaceRoot, entry.path))) {
10081
10776
  flag({
10082
10777
  repo: entry.repo,
10083
10778
  kind: "missing-linked-context-copy",
@@ -10120,15 +10815,15 @@ function emit(args) {
10120
10815
  }
10121
10816
 
10122
10817
  // src/workspace/link-context/link.ts
10123
- import path67 from "node:path";
10818
+ import path68 from "node:path";
10124
10819
  import fs46 from "fs-extra";
10125
10820
 
10126
10821
  // src/workspace/link-context/plan.ts
10127
- import path66 from "node:path";
10822
+ import path67 from "node:path";
10128
10823
  import fs45 from "fs-extra";
10129
10824
 
10130
10825
  // src/workspace/link-context/scan-source.ts
10131
- import path65 from "node:path";
10826
+ import path66 from "node:path";
10132
10827
  import fs44 from "fs-extra";
10133
10828
  var ASSET_SUBDIR = {
10134
10829
  skill: "skills",
@@ -10137,36 +10832,36 @@ var ASSET_SUBDIR = {
10137
10832
  };
10138
10833
  async function hasLinkableContext(member) {
10139
10834
  for (const subdir of Object.values(ASSET_SUBDIR)) {
10140
- if (await fs44.pathExists(path65.join(member.absPath, ".claude", subdir))) return true;
10835
+ if (await fs44.pathExists(path66.join(member.absPath, ".claude", subdir))) return true;
10141
10836
  }
10142
10837
  return false;
10143
10838
  }
10144
10839
  async function collectSkills(member) {
10145
- const dir = path65.join(member.absPath, ".claude", "skills");
10840
+ const dir = path66.join(member.absPath, ".claude", "skills");
10146
10841
  if (!await fs44.pathExists(dir)) return [];
10147
10842
  const names = await fs44.readdir(dir);
10148
10843
  const out = [];
10149
10844
  for (const name of [...names].sort()) {
10150
- const abs = path65.join(dir, name);
10845
+ const abs = path66.join(dir, name);
10151
10846
  const stat = await fs44.lstat(abs);
10152
10847
  if (!stat.isDirectory()) continue;
10153
- if (!await fs44.pathExists(path65.join(abs, "SKILL.md"))) continue;
10154
- out.push({ type: "skill", name, sourceRelPath: path65.posix.join(".claude", "skills", name) });
10848
+ if (!await fs44.pathExists(path66.join(abs, "SKILL.md"))) continue;
10849
+ out.push({ type: "skill", name, sourceRelPath: path66.posix.join(".claude", "skills", name) });
10155
10850
  }
10156
10851
  return out;
10157
10852
  }
10158
10853
  async function collectFlatMarkdown(member, type) {
10159
10854
  const subdir = ASSET_SUBDIR[type];
10160
- const dir = path65.join(member.absPath, ".claude", subdir);
10855
+ const dir = path66.join(member.absPath, ".claude", subdir);
10161
10856
  if (!await fs44.pathExists(dir)) return [];
10162
10857
  const names = await fs44.readdir(dir);
10163
10858
  const out = [];
10164
10859
  for (const fileName of [...names].sort()) {
10165
10860
  if (!fileName.endsWith(".md")) continue;
10166
- const abs = path65.join(dir, fileName);
10861
+ const abs = path66.join(dir, fileName);
10167
10862
  if (!(await fs44.lstat(abs)).isFile()) continue;
10168
10863
  const name = fileName.slice(0, -".md".length);
10169
- out.push({ type, name, sourceRelPath: path65.posix.join(".claude", subdir, fileName) });
10864
+ out.push({ type, name, sourceRelPath: path66.posix.join(".claude", subdir, fileName) });
10170
10865
  }
10171
10866
  return out;
10172
10867
  }
@@ -10181,9 +10876,9 @@ async function collectSourceAssets(member) {
10181
10876
 
10182
10877
  // src/workspace/link-context/plan.ts
10183
10878
  function repoFolderPrefix(member) {
10184
- const base = path66.posix.basename(member.folder);
10879
+ const base = path67.posix.basename(member.folder);
10185
10880
  if (base && base !== "." && base !== "/") return base;
10186
- const fromAbs = path66.basename(member.absPath);
10881
+ const fromAbs = path67.basename(member.absPath);
10187
10882
  return fromAbs || member.id;
10188
10883
  }
10189
10884
  function assetKey(type, destKey) {
@@ -10240,7 +10935,7 @@ async function buildLinkPlan(members) {
10240
10935
  name: asset.name,
10241
10936
  destKey,
10242
10937
  sourceRelPath: asset.sourceRelPath,
10243
- sourceAbsPath: path66.join(member.absPath, asset.sourceRelPath),
10938
+ sourceAbsPath: path67.join(member.absPath, asset.sourceRelPath),
10244
10939
  sourceHash
10245
10940
  });
10246
10941
  }
@@ -10251,7 +10946,7 @@ async function buildLinkPlan(members) {
10251
10946
  function destRelPathFor(type, destKey) {
10252
10947
  const subdir = `${type}s`;
10253
10948
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
10254
- return path67.posix.join(".claude", subdir, leaf);
10949
+ return path68.posix.join(".claude", subdir, leaf);
10255
10950
  }
10256
10951
  async function runLinkContext(workspaceRoot, opts = {}) {
10257
10952
  const write = opts.write ?? false;
@@ -10277,9 +10972,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
10277
10972
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
10278
10973
  for (const entry of plan.entries) {
10279
10974
  const destRel = destRelPathFor(entry.type, entry.destKey);
10280
- const destAbs = path67.join(workspaceRoot, destRel);
10975
+ const destAbs = path68.join(workspaceRoot, destRel);
10281
10976
  if (write) {
10282
- await fs46.ensureDir(path67.dirname(destAbs));
10977
+ await fs46.ensureDir(path68.dirname(destAbs));
10283
10978
  if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
10284
10979
  await fs46.copy(entry.sourceAbsPath, destAbs, {
10285
10980
  overwrite: true,
@@ -10368,10 +11063,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
10368
11063
 
10369
11064
  // src/commands/workspace/setup.ts
10370
11065
  import { existsSync as existsSync8, statSync } from "node:fs";
10371
- import path69 from "node:path";
11066
+ import path70 from "node:path";
10372
11067
 
10373
11068
  // src/claude/write-workspace-claude-md.ts
10374
- import path68 from "node:path";
11069
+ import path69 from "node:path";
10375
11070
  import fs47 from "fs-extra";
10376
11071
  var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
10377
11072
  function buildWorkspaceImportBlock(client, members) {
@@ -10391,7 +11086,7 @@ ${WORKSPACE_BLOCK_END}`;
10391
11086
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
10392
11087
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
10393
11088
  const dryRun = opts.dryRun ?? false;
10394
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path68.join(workspaceRoot, "CLAUDE.md");
11089
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path69.join(workspaceRoot, "CLAUDE.md");
10395
11090
  const say = opts.quiet ? () => {
10396
11091
  } : log;
10397
11092
  const existed = await fs47.pathExists(filePath);
@@ -10422,20 +11117,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
10422
11117
 
10423
11118
  // src/commands/workspace/setup.ts
10424
11119
  function resolveWorkspaceRoot(start = process.cwd()) {
10425
- let dir = path69.resolve(start);
11120
+ let dir = path70.resolve(start);
10426
11121
  for (; ; ) {
10427
- if (existsSync8(path69.join(dir, WORKSPACE_FILE))) return dir;
10428
- const parent = path69.dirname(dir);
10429
- if (parent === dir) return path69.resolve(start);
11122
+ if (existsSync8(path70.join(dir, WORKSPACE_FILE))) return dir;
11123
+ const parent = path70.dirname(dir);
11124
+ if (parent === dir) return path70.resolve(start);
10430
11125
  dir = parent;
10431
11126
  }
10432
11127
  }
10433
11128
  function isRootRepo(workspaceRoot, repoPath) {
10434
- return path69.resolve(workspaceRoot, repoPath) === path69.resolve(workspaceRoot);
11129
+ return path70.resolve(workspaceRoot, repoPath) === path70.resolve(workspaceRoot);
10435
11130
  }
10436
11131
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
10437
11132
  const apply = options.write ?? false;
10438
- const configText = await readText(path69.join(workspaceRoot, WORKSPACE_FILE));
11133
+ const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
10439
11134
  if (!configText) {
10440
11135
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
10441
11136
  process.exitCode = 1;
@@ -10462,7 +11157,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10462
11157
  const statuses = [];
10463
11158
  const aggregateInputs = [];
10464
11159
  for (const repo of repos) {
10465
- const repoRoot = path69.resolve(workspaceRoot, repo.path);
11160
+ const repoRoot = path70.resolve(workspaceRoot, repo.path);
10466
11161
  say(`
10467
11162
  \u2192 ${repo.name} (${repo.path})`);
10468
11163
  try {
@@ -10502,7 +11197,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10502
11197
  process.exitCode = 1;
10503
11198
  return result2;
10504
11199
  }
10505
- throw err;
11200
+ throw new Error(`Setup failed for ${repo.name}: ${message}`, { cause: err });
10506
11201
  }
10507
11202
  error(`Setup failed for ${repo.name}: ${message}`);
10508
11203
  }
@@ -10552,7 +11247,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10552
11247
  const status = statusByName.get(repo.name);
10553
11248
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
10554
11249
  if (status?.status === "ok") {
10555
- const lock = await checkLock(path69.resolve(workspaceRoot, repo.path));
11250
+ const lock = await checkLock(path70.resolve(workspaceRoot, repo.path));
10556
11251
  manifestRepos.push({
10557
11252
  name: repo.name,
10558
11253
  path: repo.path,
@@ -10608,7 +11303,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10608
11303
  try {
10609
11304
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
10610
11305
  if (linkResult.ok) {
10611
- written.push(...linkResult.added.map((rel) => path69.join(workspaceRoot, rel)));
11306
+ written.push(...linkResult.added.map((rel) => path70.join(workspaceRoot, rel)));
10612
11307
  if (!options.json) {
10613
11308
  say(
10614
11309
  `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` : ""}.`
@@ -10640,7 +11335,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
10640
11335
 
10641
11336
  // src/commands/workspace/undo.ts
10642
11337
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
10643
- import path70 from "node:path";
11338
+ import path71 from "node:path";
10644
11339
  import fs48 from "fs-extra";
10645
11340
  var WORKSPACE_AGGREGATE_FILES = [
10646
11341
  "workspace-summary.json",
@@ -10677,7 +11372,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10677
11372
  }
10678
11373
  let anyRepoFailed = false;
10679
11374
  for (const repo of config.repos) {
10680
- const repoRoot = path70.resolve(workspaceRoot, repo.path);
11375
+ const repoRoot = path71.resolve(workspaceRoot, repo.path);
10681
11376
  log(`
10682
11377
  \u2192 ${repo.name} (${repo.path})`);
10683
11378
  try {
@@ -10706,20 +11401,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10706
11401
  const abs = hausPath(workspaceRoot, rel);
10707
11402
  if (await fs48.pathExists(abs)) {
10708
11403
  await fs48.remove(abs);
10709
- log(`Removed ${path70.relative(workspaceRoot, abs)}`);
11404
+ log(`Removed ${path71.relative(workspaceRoot, abs)}`);
10710
11405
  }
10711
11406
  }
10712
11407
  const manifestFile = manifestPath(workspaceRoot);
10713
11408
  if (await fs48.pathExists(manifestFile)) {
10714
11409
  await fs48.remove(manifestFile);
10715
- log(`Removed ${path70.relative(workspaceRoot, manifestFile)}`);
11410
+ log(`Removed ${path71.relative(workspaceRoot, manifestFile)}`);
10716
11411
  }
10717
11412
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
10718
11413
  if (await fs48.pathExists(workspaceMdPath)) {
10719
11414
  await fs48.remove(workspaceMdPath);
10720
- log(`Removed ${path70.relative(workspaceRoot, workspaceMdPath)}`);
11415
+ log(`Removed ${path71.relative(workspaceRoot, workspaceMdPath)}`);
10721
11416
  } else {
10722
- const claudeMdPath = path70.join(workspaceRoot, "CLAUDE.md");
11417
+ const claudeMdPath = path71.join(workspaceRoot, "CLAUDE.md");
10723
11418
  if (existsSync9(claudeMdPath)) {
10724
11419
  const prev = await readText(claudeMdPath);
10725
11420
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
@@ -10758,12 +11453,6 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10758
11453
  }
10759
11454
 
10760
11455
  // src/commands/workspace.ts
10761
- function normalizeOnly2(only) {
10762
- if (!only) return void 0;
10763
- const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
10764
- const cleaned = list.map((s) => s.trim()).filter(Boolean);
10765
- return cleaned.length > 0 ? cleaned : void 0;
10766
- }
10767
11456
  function normalizeMaxDepth(maxDepth) {
10768
11457
  if (maxDepth === void 0) return void 0;
10769
11458
  const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
@@ -10794,7 +11483,7 @@ relationships: []
10794
11483
  log("Workspace initialized.");
10795
11484
  }
10796
11485
  async function scanWorkspace(workspaceRoot, opts) {
10797
- const configText = await readText(path71.join(workspaceRoot, WORKSPACE_FILE));
11486
+ const configText = await readText(path72.join(workspaceRoot, WORKSPACE_FILE));
10798
11487
  if (!configText) {
10799
11488
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
10800
11489
  process.exitCode = 1;
@@ -10816,7 +11505,7 @@ async function scanWorkspace(workspaceRoot, opts) {
10816
11505
  const inputs = [];
10817
11506
  const failures = [];
10818
11507
  for (const repo of config.repos) {
10819
- const repoRoot = path71.resolve(workspaceRoot, repo.path);
11508
+ const repoRoot = path72.resolve(workspaceRoot, repo.path);
10820
11509
  try {
10821
11510
  if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
10822
11511
  throw new Error("path is not a directory");
@@ -10864,7 +11553,7 @@ async function runWorkspace(action, options = {}) {
10864
11553
  dryRun: options.dryRun,
10865
11554
  json: options.json,
10866
11555
  continueOnError: options.continueOnError,
10867
- only: normalizeOnly2(options.only)
11556
+ only: normalizeOnly(options.only)
10868
11557
  });
10869
11558
  return;
10870
11559
  case "doctor":
@@ -10882,7 +11571,7 @@ async function runWorkspace(action, options = {}) {
10882
11571
  // src/cli.ts
10883
11572
  function cliVersion() {
10884
11573
  try {
10885
- const pkgPath = path72.join(packageRoot(), "package.json");
11574
+ const pkgPath = path73.join(packageRoot(), "package.json");
10886
11575
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10887
11576
  return pkg.version ?? "0.0.0";
10888
11577
  } catch {
@@ -10892,7 +11581,7 @@ function cliVersion() {
10892
11581
  var program = new Command();
10893
11582
  function validateRuntimeNodeVersion() {
10894
11583
  try {
10895
- const pkgPath = path72.join(packageRoot(), "package.json");
11584
+ const pkgPath = path73.join(packageRoot(), "package.json");
10896
11585
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10897
11586
  const requiredRange = pkg.engines?.node;
10898
11587
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -10906,18 +11595,22 @@ function validateRuntimeNodeVersion() {
10906
11595
  }
10907
11596
  validateRuntimeNodeVersion();
10908
11597
  program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
10909
- program.command("scan").option("--json").action(runScan);
10910
- program.command("recommend").option("--json").option(
11598
+ program.command("scan").description(
11599
+ "Scan the repo for roles, dependencies, and package manager; writes context-map.json"
11600
+ ).option("--json").action(runScan);
11601
+ program.command("recommend").description("Score catalog items against the scanned context and write recommendation.json").option("--json").option(
10911
11602
  "--include <ids...>",
10912
11603
  "Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
10913
11604
  ).action(runRecommend);
10914
- program.command("setup-project").option("--json").option(
11605
+ program.command("setup-project").description("Fast full setup: scan, recommend, and optionally apply Claude files").option("--json").option(
10915
11606
  "--force",
10916
11607
  "Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
10917
11608
  ).action(runSetupProject);
10918
- program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
11609
+ program.command("doctor").description(
11610
+ "Validate project setup: hooks contract, managed files, cache freshness, CLI version"
11611
+ ).option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
10919
11612
  program.command("fetch-refs").description("Fetch and cache llms.txt content from catalog item references").option("--all", "Fetch for all catalog items (default)").option("--id <id>", "Fetch for a single catalog item by id").option("--json", "Emit machine-readable JSON summary to stdout").action(runFetchRefs);
10920
- program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
11613
+ program.command("apply").description("Write recommended catalog items and core Claude files to .claude/").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
10921
11614
  "--ids <ids...>",
10922
11615
  "Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
10923
11616
  ).option(
@@ -10930,33 +11623,33 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
10930
11623
  "--prune",
10931
11624
  "Remove lock-tracked catalog items no longer in the current recommendation (hash-gated, backed up first)"
10932
11625
  ).action(runApply);
10933
- program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
10934
- var backups = program.command("backups");
11626
+ program.command("undo").description("Remove haus-managed project files; preserves user-owned .claude/ content").option("-y, --yes", "Skip confirmation").action(runUndo);
11627
+ var backups = program.command("backups").description("List, restore, or prune backups under .haus-workflow/backups/");
10935
11628
  backups.command("list").description("List backups under .haus-workflow/backups/").action(() => runBackups("list"));
10936
11629
  backups.command("restore <id>").description("Restore a specific backup by id (see `haus backups list`)").option("-y, --yes", "Skip confirmation").action((id, opts) => runBackups("restore", { id, yes: opts.yes }));
10937
11630
  backups.command("prune").description("Delete old backups by age and/or count (requires --older-than or --keep)").option("--older-than <days>", "Remove backups older than N days").option("--keep <n>", "Keep only the N most recent backups").option("-y, --yes", "Skip confirmation").action(
10938
11631
  (opts) => runBackups("prune", { olderThan: opts.olderThan, keep: opts.keep, yes: opts.yes })
10939
11632
  );
10940
- program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
10941
- program.command("init").option("--json").action(runInit);
11633
+ program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
11634
+ program.command("init").description("First-run setup; delegates to setup-project if .haus-workflow/ does not exist").option("--json").action(runInit);
10942
11635
  program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
10943
11636
  (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
10944
11637
  );
10945
- program.command("refresh").action(runRefresh);
10946
- program.command("catalog-audit").action(runCatalogAudit);
10947
- program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
10948
- program.command("update").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
11638
+ program.command("refresh").description("Re-scan the project, refresh recommendations, and update scan artifacts").action(runRefresh);
11639
+ program.command("catalog-audit").description("Check the bundled catalog manifest for unsupported or forbidden stacks").action(runCatalogAudit);
11640
+ program.command("validate-catalog").description("Validate a catalog manifest at an explicit path").argument("[manifest]").action(runValidateCatalog);
11641
+ program.command("update").description("Sync the remote catalog cache, refresh the lockfile, and check for CLI updates").option("--check").option("--fast", "With --check, skip per-item content hashing (cheap count+ref only)").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
10949
11642
  program.command("ci-gate").description(
10950
11643
  "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."
10951
11644
  ).option("--json", "Emit machine-readable {doctor, decisions, update, ok}").action((opts) => runCiGate(opts));
10952
- program.command("install").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);
11645
+ 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);
10953
11646
  program.command("scaffold [ids...]").description(
10954
11647
  "Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
10955
11648
  ).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(runScaffold);
10956
- program.command("uninstall").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
10957
- var guard = program.command("guard");
10958
- guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
10959
- guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
11649
+ program.command("uninstall").description("Remove Haus-managed skills, agents, and hook entries from ~/.claude/").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
11650
+ var guard = program.command("guard").description("PreToolUse hook guards: block dangerous bash commands and sensitive file access");
11651
+ guard.command("file-access").description("Block reads/edits of sensitive paths (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("file-access", opts));
11652
+ guard.command("bash").description("Block dangerous shell commands (run by the Claude Code hook)").option("--from-hook").action((opts) => runGuard("bash", opts));
10960
11653
  function registerDecisionsCommand(name) {
10961
11654
  const cmd = program.command(name).description("Architecture decision records (ADR) gate and drafts");
10962
11655
  cmd.command("check").option("--staged", "Check staged changes only").option("--range <range>", "Git revision range (e.g. origin/main..HEAD)").action((opts) => runDecisions("check", void 0, opts));
@@ -10969,7 +11662,7 @@ function registerDecisionsCommand(name) {
10969
11662
  }
10970
11663
  registerDecisionsCommand("decisions");
10971
11664
  registerDecisionsCommand("adr");
10972
- var workspace = program.command("workspace");
11665
+ var workspace = program.command("workspace").description("Multi-repo workspace commands: discover, setup, link-context, worktree, doctor");
10973
11666
  workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
10974
11667
  workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
10975
11668
  workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));