@haus-tech/haus-workflow 1.5.6 → 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
@@ -944,8 +960,6 @@ var validation_rules_default = {
944
960
  "java",
945
961
  "spring",
946
962
  "kotlin",
947
- "swift",
948
- "android",
949
963
  "flutter",
950
964
  "dart",
951
965
  "c++",
@@ -1167,6 +1181,16 @@ function extractUseWhenSection(text) {
1167
1181
  const next = tail.search(/\n##\s+/);
1168
1182
  return next < 0 ? tail : tail.slice(0, next);
1169
1183
  }
1184
+ function indentedContinuationLines(lines, idx) {
1185
+ const out = [];
1186
+ for (let j = idx + 1; j < lines.length; j++) {
1187
+ const line2 = lines[j];
1188
+ if (line2.trim() === "") continue;
1189
+ if (!/^\s+\S/.test(line2)) break;
1190
+ out.push(line2.trim());
1191
+ }
1192
+ return out;
1193
+ }
1170
1194
  function isYamlBlockScalarHeader(rest) {
1171
1195
  return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
1172
1196
  }
@@ -1180,7 +1204,7 @@ function extractFrontmatterValue(text, key) {
1180
1204
  const rest = lines[idx].replace(keyRe, "").trim();
1181
1205
  if (!rest) return "";
1182
1206
  if (!isYamlBlockScalarHeader(rest)) {
1183
- return rest.replace(/^["']|["']$/g, "").trim();
1207
+ return [rest, ...indentedContinuationLines(lines, idx)].join(" ").replace(/\s+/g, " ").replace(/^["']|["']$/g, "").trim();
1184
1208
  }
1185
1209
  const body = [];
1186
1210
  for (let j = idx + 1; j < lines.length; j++) {
@@ -1192,11 +1216,20 @@ function extractFrontmatterValue(text, key) {
1192
1216
  }
1193
1217
  return body.join(" ").replace(/\s+/g, " ").trim();
1194
1218
  }
1195
- function extractFrontmatterDescription(text) {
1196
- return extractFrontmatterValue(text, "description");
1219
+ function frontmatterDescriptionRegion(text) {
1220
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1221
+ if (!m) return "";
1222
+ const lines = m[1].split(/\r?\n/);
1223
+ const idx = lines.findIndex((l) => /^description:[ \t]*/.test(l));
1224
+ if (idx < 0) return "";
1225
+ const region = [
1226
+ lines[idx].replace(/^description:[ \t]*/, ""),
1227
+ ...indentedContinuationLines(lines, idx)
1228
+ ];
1229
+ return region.join(" ").replace(/\s+/g, " ").trim();
1197
1230
  }
1198
1231
  function auditForbiddenTagsInText(text, label) {
1199
- const body = `${extractFrontmatterDescription(text)}
1232
+ const body = `${frontmatterDescriptionRegion(text)}
1200
1233
  ${extractUseWhenSection(text)}`;
1201
1234
  if (!body.trim()) return [];
1202
1235
  const failures = [];
@@ -1467,13 +1500,16 @@ async function syncRemoteCatalog() {
1467
1500
  warn("Remote catalog fetch failed \u2014 using bundled catalog");
1468
1501
  return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
1469
1502
  }
1470
- const { version, items } = manifest;
1503
+ const { version, items, globalPlugins } = manifest;
1471
1504
  const cacheDir = getCacheDir();
1472
1505
  try {
1473
1506
  await fs6.ensureDir(cacheDir);
1474
1507
  await fs6.writeFile(
1475
1508
  path8.join(cacheDir, "manifest.json"),
1476
- `${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)}
1477
1513
  `,
1478
1514
  "utf8"
1479
1515
  );
@@ -1527,14 +1563,20 @@ async function loadCatalogContext(root) {
1527
1563
  return {
1528
1564
  items: data2?.items ?? [],
1529
1565
  contentRoot: path9.dirname(envPath),
1530
- source: "fixture"
1566
+ source: "fixture",
1567
+ globalPlugins: data2?.globalPlugins
1531
1568
  };
1532
1569
  }
1533
1570
  const cacheDir = getCacheDir();
1534
1571
  const cacheManifestPath = path9.join(cacheDir, "manifest.json");
1535
1572
  const cacheData = await readJson(cacheManifestPath);
1536
1573
  if (cacheData?.items?.length) {
1537
- 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
+ };
1538
1580
  }
1539
1581
  const localManifest = path9.join(root, "library/catalog/manifest.json");
1540
1582
  const localData = await readJson(localManifest);
@@ -1542,7 +1584,8 @@ async function loadCatalogContext(root) {
1542
1584
  return {
1543
1585
  items: localData.items,
1544
1586
  contentRoot: path9.dirname(localManifest),
1545
- source: "local"
1587
+ source: "local",
1588
+ globalPlugins: localData.globalPlugins
1546
1589
  };
1547
1590
  }
1548
1591
  const packageManifest = path9.join(packageRoot(), "library/catalog/manifest.json");
@@ -1550,7 +1593,8 @@ async function loadCatalogContext(root) {
1550
1593
  return {
1551
1594
  items: data?.items ?? [],
1552
1595
  contentRoot: path9.dirname(packageManifest),
1553
- source: "bundled"
1596
+ source: "bundled",
1597
+ globalPlugins: data?.globalPlugins
1554
1598
  };
1555
1599
  }
1556
1600
  async function loadCatalog(root) {
@@ -1840,6 +1884,26 @@ function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
1840
1884
  return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
1841
1885
  }
1842
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
+
1843
1907
  // src/update/hash-installed.ts
1844
1908
  import path11 from "node:path";
1845
1909
  import fg2 from "fast-glob";
@@ -2008,13 +2072,22 @@ async function readManifest() {
2008
2072
  async function writeManifest(manifest) {
2009
2073
  await writeJson(hausManifestPath(), manifest);
2010
2074
  }
2011
- 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) {
2012
2084
  return {
2013
2085
  _schema: MANIFEST_SCHEMA,
2014
2086
  source,
2015
2087
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2016
2088
  files,
2017
- hooks
2089
+ hooks,
2090
+ ...plugins && plugins.length > 0 ? { plugins } : {}
2018
2091
  };
2019
2092
  }
2020
2093
 
@@ -2270,8 +2343,8 @@ function buildDenyRules() {
2270
2343
  for (const command of DENY_COMMANDS) {
2271
2344
  rules.push(`Bash(${command}:*)`);
2272
2345
  }
2273
- for (const path73 of DENY_PATHS) {
2274
- const pattern = DENY_DIRS.has(path73) ? `${path73}/**` : path73;
2346
+ for (const path74 of DENY_PATHS) {
2347
+ const pattern = DENY_DIRS.has(path74) ? `${path74}/**` : path74;
2275
2348
  for (const tool of FILE_TOOLS) {
2276
2349
  rules.push(`${tool}(${pattern})`);
2277
2350
  }
@@ -2354,6 +2427,7 @@ import path14 from "node:path";
2354
2427
  import fg3 from "fast-glob";
2355
2428
  import fs10 from "fs-extra";
2356
2429
  var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
2430
+ var SUPERPOWERS_SHARED_INSTALL_REL = ".claude/skills/shared";
2357
2431
  var PATH_REWRITES = [
2358
2432
  ["skills/shared/", ".claude/skills/shared/"]
2359
2433
  ];
@@ -3229,6 +3303,10 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
3229
3303
  warn(`Skipping ${item.id}: reviewStatus is deprecated`);
3230
3304
  continue;
3231
3305
  }
3306
+ if (isItemSupersededByPlugin(manifestItem, opts.activePluginIds ?? [])) {
3307
+ warn(`Skipping ${item.id}: provided by an installed global plugin`);
3308
+ continue;
3309
+ }
3232
3310
  const manifestItemTags = (manifestItem.tags ?? []).map((t) => t.toLowerCase());
3233
3311
  if (UNSUPPORTED.some((x) => manifestItemTags.includes(x))) {
3234
3312
  warn(`Skipping ${item.id}: unsupported stack policy`);
@@ -3354,7 +3432,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
3354
3432
  `${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
3355
3433
  );
3356
3434
  }
3357
- await cleanupStaleCatalogItems(root, cleanupManifestById, dryRun, opts.quiet);
3435
+ await cleanupStaleCatalogItems(
3436
+ root,
3437
+ cleanupManifestById,
3438
+ dryRun,
3439
+ opts.quiet,
3440
+ opts.activePluginIds ?? []
3441
+ );
3358
3442
  const refuseDestructiveLockChange = recommendationUnusable && prevLock.length > 0 && !opts.force;
3359
3443
  if (opts.prune) {
3360
3444
  if (refuseDestructiveLockChange) {
@@ -3498,25 +3582,46 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn) {
3498
3582
  }
3499
3583
  return existing;
3500
3584
  }
3501
- async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
3585
+ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activePluginIds = []) {
3502
3586
  const say = quiet ? () => {
3503
3587
  } : log;
3504
3588
  const prevLock = await readJson(hausPath(root, "haus.lock.json"));
3505
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
+ }
3506
3609
  for (const entry of prevLock) {
3507
3610
  if (!entry.id) continue;
3508
3611
  const manifestItem = manifestById.get(entry.id);
3509
3612
  const removedFromManifest = manifestItem === void 0;
3510
3613
  const deprecated = manifestItem?.reviewStatus === "deprecated";
3511
- if (!removedFromManifest && !deprecated) continue;
3614
+ const superseded = isSupersededByPlugin(entry.id, activePluginIds);
3615
+ if (!removedFromManifest && !deprecated && !superseded) continue;
3512
3616
  const relPaths = entry.paths ?? [];
3513
3617
  if (relPaths.length === 0) continue;
3514
3618
  const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
3515
3619
  if (existing.length === 0) continue;
3516
- const pruneReason = deprecated ? "deprecated" : "stale";
3620
+ const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
3621
+ const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
3517
3622
  if (entry.hash === void 0) {
3518
3623
  warn(
3519
- `${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(", ")}`
3520
3625
  );
3521
3626
  continue;
3522
3627
  }
@@ -3526,11 +3631,28 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet) {
3526
3631
  );
3527
3632
  if (currentHash !== entry.hash) {
3528
3633
  warn(
3529
- `${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(", ")}`
3530
3635
  );
3531
3636
  continue;
3532
3637
  }
3533
- 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) {
3534
3656
  if (dryRun) {
3535
3657
  say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
3536
3658
  continue;
@@ -3769,16 +3891,431 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
3769
3891
  return untracked;
3770
3892
  }
3771
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
+
3772
4309
  // src/refs/fetch-refs.ts
3773
- import path23 from "node:path";
4310
+ import path24 from "node:path";
3774
4311
  import fs21 from "fs-extra";
3775
4312
 
3776
4313
  // src/refs/cache-meta.ts
3777
- import path22 from "node:path";
4314
+ import path23 from "node:path";
3778
4315
  import fs20 from "fs-extra";
3779
4316
  var META_FILENAME = "cache-meta.json";
3780
4317
  async function readCacheMeta(cacheDir) {
3781
- const metaPath = path22.join(cacheDir, META_FILENAME);
4318
+ const metaPath = path23.join(cacheDir, META_FILENAME);
3782
4319
  try {
3783
4320
  const raw = await fs20.readFile(metaPath, "utf8");
3784
4321
  return JSON.parse(raw);
@@ -3788,7 +4325,7 @@ async function readCacheMeta(cacheDir) {
3788
4325
  }
3789
4326
  async function writeCacheMeta(cacheDir, meta) {
3790
4327
  await fs20.ensureDir(cacheDir);
3791
- const metaPath = path22.join(cacheDir, META_FILENAME);
4328
+ const metaPath = path23.join(cacheDir, META_FILENAME);
3792
4329
  await fs20.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
3793
4330
  }
3794
4331
 
@@ -3809,8 +4346,8 @@ function isLlmsTxtUrl(ref) {
3809
4346
  async function fetchSingleRef(url, cacheDir, meta) {
3810
4347
  const existing = meta[url];
3811
4348
  const headers = {};
3812
- const safeFile = existing?.file && existing.file === path23.basename(existing.file);
3813
- 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));
3814
4351
  if (cachedFileExists) {
3815
4352
  if (existing?.etag) headers["If-None-Match"] = existing.etag;
3816
4353
  if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
@@ -3822,7 +4359,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
3822
4359
  const text = await res.text();
3823
4360
  const file = `${urlToSlug(url)}.md`;
3824
4361
  await fs21.ensureDir(cacheDir);
3825
- await fs21.writeFile(path23.join(cacheDir, file), text, "utf8");
4362
+ await fs21.writeFile(path24.join(cacheDir, file), text, "utf8");
3826
4363
  meta[url] = {
3827
4364
  url,
3828
4365
  etag: res.headers.get("etag") ?? void 0,
@@ -3845,8 +4382,8 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
3845
4382
  if (keepUrls.has(url)) continue;
3846
4383
  delete meta[url];
3847
4384
  removed++;
3848
- const safeFile = typeof entry.file === "string" && entry.file === path23.basename(entry.file);
3849
- 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(() => {
3850
4387
  });
3851
4388
  }
3852
4389
  if (removed > 0) await writeCacheMeta(cacheDir, meta);
@@ -3868,10 +4405,10 @@ async function fetchRefsForItems(items, cacheDir) {
3868
4405
  const outcome = await fetchSingleRef(url, cacheDir, meta);
3869
4406
  if (outcome.result === "fetched") {
3870
4407
  summary.fetched++;
3871
- summary.cachedFiles[url] = path23.join(cacheDir, outcome.file);
4408
+ summary.cachedFiles[url] = path24.join(cacheDir, outcome.file);
3872
4409
  } else if (outcome.result === "unchanged") {
3873
4410
  summary.unchanged++;
3874
- 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);
3875
4412
  } else {
3876
4413
  summary.failed++;
3877
4414
  summary.failedUrls.push(url);
@@ -3890,11 +4427,17 @@ function parseIdList(value) {
3890
4427
  const raw = Array.isArray(value) ? value : [value];
3891
4428
  return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
3892
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
+ }
3893
4436
 
3894
4437
  // src/commands/apply.ts
3895
4438
  var STALE_CACHE_THRESHOLD_DAYS = 7;
3896
4439
  async function cacheHasItems() {
3897
- const data = await readJson(path24.join(getCacheDir(), "manifest.json"));
4440
+ const data = await readJson(path25.join(getCacheDir(), "manifest.json"));
3898
4441
  return Array.isArray(data?.items) && data.items.length > 0;
3899
4442
  }
3900
4443
  function installableRecommendedItems(recommended) {
@@ -3998,7 +4541,9 @@ async function runApply(options) {
3998
4541
  const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
3999
4542
  refillConfig: options.refillConfig,
4000
4543
  force: options.force,
4001
- prune: options.prune
4544
+ prune: options.prune,
4545
+ // Copies a global plugin already provides are removed here (issue #272).
4546
+ activePluginIds: await activeGlobalPluginIds(root)
4002
4547
  });
4003
4548
  await writeGitignore(root, isDryRun);
4004
4549
  await untrackMachineLocalArtifacts(root, isDryRun, warn);
@@ -4045,7 +4590,7 @@ async function runApply(options) {
4045
4590
  }
4046
4591
 
4047
4592
  // src/commands/backups.ts
4048
- import path25 from "node:path";
4593
+ import path26 from "node:path";
4049
4594
  import fs22 from "fs-extra";
4050
4595
 
4051
4596
  // src/utils/prompts.ts
@@ -4082,7 +4627,7 @@ async function listBackups(root) {
4082
4627
  for (const name of names) {
4083
4628
  const kind = classify(name);
4084
4629
  if (!kind) continue;
4085
- const absPath = path25.join(dir, name);
4630
+ const absPath = path26.join(dir, name);
4086
4631
  const stat = await fs22.lstat(absPath);
4087
4632
  if (stat.isSymbolicLink()) continue;
4088
4633
  const expectDirectory = kind !== "lock";
@@ -4112,10 +4657,10 @@ async function runList(root) {
4112
4657
  }
4113
4658
  }
4114
4659
  async function isUnsafeWriteTarget(root, target) {
4115
- const segments = path25.relative(root, target).split(path25.sep).filter(Boolean);
4660
+ const segments = path26.relative(root, target).split(path26.sep).filter(Boolean);
4116
4661
  let current = root;
4117
4662
  for (let i = 0; i < segments.length; i++) {
4118
- current = path25.join(current, segments[i]);
4663
+ current = path26.join(current, segments[i]);
4119
4664
  const isLastSegment = i === segments.length - 1;
4120
4665
  try {
4121
4666
  const stat = await fs22.lstat(current);
@@ -4179,7 +4724,7 @@ async function collectFilesRecursive(dir) {
4179
4724
  const skippedOther = [];
4180
4725
  const names = await fs22.readdir(dir);
4181
4726
  for (const name of names) {
4182
- const abs = path25.join(dir, name);
4727
+ const abs = path26.join(dir, name);
4183
4728
  const stat = await fs22.lstat(abs);
4184
4729
  if (stat.isSymbolicLink()) {
4185
4730
  skippedLinks.push(abs);
@@ -4202,12 +4747,12 @@ async function restoreDirBackup(root, entry, yes) {
4202
4747
  const { files, skippedLinks, skippedOther } = await collectFilesRecursive(entry.absPath);
4203
4748
  if (skippedLinks.length > 0) {
4204
4749
  warn(
4205
- `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(", ")}`
4206
4751
  );
4207
4752
  }
4208
4753
  if (skippedOther.length > 0) {
4209
4754
  warn(
4210
- `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(", ")}`
4211
4756
  );
4212
4757
  }
4213
4758
  if (files.length === 0) {
@@ -4217,8 +4762,8 @@ async function restoreDirBackup(root, entry, yes) {
4217
4762
  const safeFiles = [];
4218
4763
  const unsafeTargets = [];
4219
4764
  for (const abs of files) {
4220
- const rel = path25.relative(entry.absPath, abs);
4221
- const target = path25.join(root, rel);
4765
+ const rel = path26.relative(entry.absPath, abs);
4766
+ const target = path26.join(root, rel);
4222
4767
  if (await isUnsafeWriteTarget(root, target)) unsafeTargets.push(rel);
4223
4768
  else safeFiles.push(abs);
4224
4769
  }
@@ -4235,14 +4780,14 @@ async function restoreDirBackup(root, entry, yes) {
4235
4780
  const vanished = [];
4236
4781
  const toRestore = [];
4237
4782
  for (const abs of safeFiles) {
4238
- const rel = path25.relative(entry.absPath, abs);
4783
+ const rel = path26.relative(entry.absPath, abs);
4239
4784
  const backupFileStat = await lstatIfRegularFile(abs);
4240
4785
  if (!backupFileStat) {
4241
4786
  vanished.push(rel);
4242
4787
  continue;
4243
4788
  }
4244
4789
  toRestore.push(abs);
4245
- const target = path25.join(root, rel);
4790
+ const target = path26.join(root, rel);
4246
4791
  if (await fs22.pathExists(target)) {
4247
4792
  const targetStat = await fs22.stat(target);
4248
4793
  if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
@@ -4283,13 +4828,13 @@ async function restoreDirBackup(root, entry, yes) {
4283
4828
  let restoredCount = 0;
4284
4829
  const skippedAtWrite = [];
4285
4830
  for (const abs of toRestore) {
4286
- const rel = path25.relative(entry.absPath, abs);
4287
- const target = path25.join(root, rel);
4831
+ const rel = path26.relative(entry.absPath, abs);
4832
+ const target = path26.join(root, rel);
4288
4833
  if (!await lstatIfRegularFile(abs) || await isUnsafeWriteTarget(root, target)) {
4289
4834
  skippedAtWrite.push(rel);
4290
4835
  continue;
4291
4836
  }
4292
- await fs22.ensureDir(path25.dirname(target));
4837
+ await fs22.ensureDir(path26.dirname(target));
4293
4838
  await fs22.copy(abs, target, { overwrite: true });
4294
4839
  restoredCount++;
4295
4840
  }
@@ -4428,18 +4973,19 @@ async function runCatalogAudit() {
4428
4973
  }
4429
4974
 
4430
4975
  // src/commands/doctor.ts
4431
- import path39 from "node:path";
4976
+ import path40 from "node:path";
4432
4977
  import fs30 from "fs-extra";
4433
4978
 
4434
4979
  // src/claude/skill-reference-check.ts
4435
- import path26 from "node:path";
4980
+ import path27 from "node:path";
4436
4981
  import fg4 from "fast-glob";
4437
4982
  import fs23 from "fs-extra";
4438
4983
  var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
4439
4984
  function stripFencedCodeBlocks(text) {
4440
4985
  return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
4441
4986
  }
4442
- async function findUnresolvedSkillReferences(root) {
4987
+ async function findUnresolvedSkillReferences(root, options = {}) {
4988
+ const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
4443
4989
  const skillsRoot = claudePath(root, "skills");
4444
4990
  if (!await fs23.pathExists(skillsRoot)) return [];
4445
4991
  const skillDirs = (await fs23.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
@@ -4448,15 +4994,16 @@ async function findUnresolvedSkillReferences(root) {
4448
4994
  const issues = [];
4449
4995
  for (const rel of files) {
4450
4996
  if (rel.startsWith("shared/")) continue;
4451
- const abs = path26.join(skillsRoot, rel);
4997
+ const abs = path27.join(skillsRoot, rel);
4452
4998
  const raw = await fs23.readFile(abs, "utf8");
4453
4999
  const lines = stripFencedCodeBlocks(raw).split("\n");
4454
5000
  lines.forEach((line2, idx) => {
4455
5001
  for (const match of line2.matchAll(REFERENCE_PATTERN)) {
4456
- const [reference, , bareName] = match;
5002
+ const [reference, namespace, bareName] = match;
4457
5003
  if (!reference || !bareName) continue;
5004
+ if (namespace && resolvableNamespaces.has(namespace)) continue;
4458
5005
  issues.push({
4459
- file: path26.join(".claude/skills", rel),
5006
+ file: path27.join(".claude/skills", rel),
4460
5007
  line: idx + 1,
4461
5008
  reference,
4462
5009
  bareName,
@@ -4469,11 +5016,11 @@ async function findUnresolvedSkillReferences(root) {
4469
5016
  }
4470
5017
 
4471
5018
  // src/decisions/doctor.ts
4472
- import path31 from "node:path";
5019
+ import path32 from "node:path";
4473
5020
  import fs27 from "fs-extra";
4474
5021
 
4475
5022
  // src/decisions/check.ts
4476
- import path30 from "node:path";
5023
+ import path31 from "node:path";
4477
5024
  import fs26 from "fs-extra";
4478
5025
 
4479
5026
  // src/decisions/dep-bump.ts
@@ -4533,7 +5080,7 @@ function isDependencyBumpOnly(beforeText, afterText) {
4533
5080
 
4534
5081
  // src/decisions/diff.ts
4535
5082
  init_exec();
4536
- import path27 from "node:path";
5083
+ import path28 from "node:path";
4537
5084
  function isPackageJsonPath(file) {
4538
5085
  return file === "package.json" || file.endsWith("/package.json");
4539
5086
  }
@@ -4594,7 +5141,7 @@ async function resolvePackageJsonPair(root, file, opts) {
4594
5141
  return { before: before2, after: after2 };
4595
5142
  }
4596
5143
  const before = await readBlob(root, `${range}:${file}`) ?? empty;
4597
- const after = await readText(path27.join(root, file)) ?? empty;
5144
+ const after = await readText(path28.join(root, file)) ?? empty;
4598
5145
  return { before, after };
4599
5146
  }
4600
5147
  async function collectDiffStats(root, opts) {
@@ -4665,21 +5212,21 @@ function firstMatchingRegex(filePath, patterns) {
4665
5212
  }
4666
5213
 
4667
5214
  // src/decisions/paths.ts
4668
- import path28 from "node:path";
5215
+ import path29 from "node:path";
4669
5216
  import fs24 from "fs-extra";
4670
5217
  async function resolveDecisionsDir(root) {
4671
- const primary = path28.join(root, DECISIONS_TRIGGERS.decisionsDir);
5218
+ const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
4672
5219
  if (await fs24.pathExists(primary)) return primary;
4673
- const legacy = path28.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
5220
+ const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4674
5221
  if (await fs24.pathExists(legacy)) return legacy;
4675
5222
  return primary;
4676
5223
  }
4677
5224
  function relativeDecisionPath(decisionsDir, root) {
4678
- return path28.relative(root, decisionsDir).replace(/\\/g, "/");
5225
+ return path29.relative(root, decisionsDir).replace(/\\/g, "/");
4679
5226
  }
4680
5227
 
4681
5228
  // src/decisions/validate.ts
4682
- import path29 from "node:path";
5229
+ import path30 from "node:path";
4683
5230
  import fs25 from "fs-extra";
4684
5231
  function validateDecisionContent(content2) {
4685
5232
  const errors = [];
@@ -4709,14 +5256,14 @@ function decisionNumberFromFile(name) {
4709
5256
  return match?.[1] ?? null;
4710
5257
  }
4711
5258
  async function validateIndexSync(decisionsDir, decisionFiles) {
4712
- const readmePath = path29.join(decisionsDir, "README.md");
5259
+ const readmePath = path30.join(decisionsDir, "README.md");
4713
5260
  if (!await fs25.pathExists(readmePath)) {
4714
5261
  return ["docs/decisions/README.md is missing"];
4715
5262
  }
4716
5263
  const readme = await readText(readmePath) ?? "";
4717
5264
  const errors = [];
4718
5265
  for (const file of decisionFiles) {
4719
- const num = decisionNumberFromFile(path29.basename(file));
5266
+ const num = decisionNumberFromFile(path30.basename(file));
4720
5267
  if (num && !indexReferencesNumber(readme, num)) {
4721
5268
  errors.push(`README.md missing index row for ADR-${num}`);
4722
5269
  }
@@ -4811,22 +5358,22 @@ async function checkSatisfaction(input2) {
4811
5358
  const errors = [];
4812
5359
  const decisionsPrefix = `${input2.decisionsRel}/`;
4813
5360
  const newDecisionFiles = input2.changedFiles.filter(
4814
- (f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path30.basename(f))
5361
+ (f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path31.basename(f))
4815
5362
  );
4816
5363
  if (newDecisionFiles.length === 0) {
4817
5364
  errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
4818
5365
  return errors;
4819
5366
  }
4820
- const decisionsDir = path30.join(input2.root, input2.decisionsRel);
5367
+ const decisionsDir = path31.join(input2.root, input2.decisionsRel);
4821
5368
  for (const rel of newDecisionFiles) {
4822
- const abs = path30.join(input2.root, rel);
5369
+ const abs = path31.join(input2.root, rel);
4823
5370
  const content2 = await readText(abs) ?? "";
4824
5371
  const valid = validateDecisionContent(content2);
4825
5372
  if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
4826
5373
  }
4827
5374
  const indexErrors = await validateIndexSync(
4828
5375
  decisionsDir,
4829
- newDecisionFiles.map((f) => path30.basename(f))
5376
+ newDecisionFiles.map((f) => path31.basename(f))
4830
5377
  );
4831
5378
  errors.push(...indexErrors);
4832
5379
  return errors;
@@ -4882,7 +5429,7 @@ ${opts.commitMessages ?? ""}`;
4882
5429
  changedFiles: stats.files
4883
5430
  });
4884
5431
  if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
4885
- const legacyExists = await fs26.pathExists(path30.join(root, legacyRel));
5432
+ const legacyExists = await fs26.pathExists(path31.join(root, legacyRel));
4886
5433
  if (legacyExists && !await fs26.pathExists(decisionsDir)) {
4887
5434
  satisfactionErrors = await checkSatisfaction({
4888
5435
  root,
@@ -4913,8 +5460,8 @@ async function listDecisionNumbers(decisionsDir) {
4913
5460
  // src/decisions/doctor.ts
4914
5461
  async function auditDecisionsLayout(root) {
4915
5462
  const findings = [];
4916
- const primary = path31.join(root, DECISIONS_TRIGGERS.decisionsDir);
4917
- 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");
4918
5465
  const hasPrimary = await fs27.pathExists(primary);
4919
5466
  const hasLegacy = await fs27.pathExists(legacy);
4920
5467
  if (!hasPrimary && hasLegacy) {
@@ -4929,7 +5476,7 @@ async function auditDecisionsLayout(root) {
4929
5476
  if (!hasPrimary) return findings;
4930
5477
  const decisionsDir = await resolveDecisionsDir(root);
4931
5478
  const rel = relativeDecisionPath(decisionsDir, root);
4932
- const readmePath = path31.join(decisionsDir, "README.md");
5479
+ const readmePath = path32.join(decisionsDir, "README.md");
4933
5480
  if (!await fs27.pathExists(readmePath)) {
4934
5481
  findings.push({
4935
5482
  level: "advisory",
@@ -4954,7 +5501,7 @@ async function auditDecisionsLayout(root) {
4954
5501
  });
4955
5502
  }
4956
5503
  }
4957
- const claude = await readText(path31.join(root, "CLAUDE.md"));
5504
+ const claude = await readText(path32.join(root, "CLAUDE.md"));
4958
5505
  if (claude && !claude.includes(`@${rel}/README.md`)) {
4959
5506
  findings.push({
4960
5507
  level: "advisory",
@@ -4985,11 +5532,11 @@ async function auditDecisionsLayout(root) {
4985
5532
  }
4986
5533
 
4987
5534
  // src/scanner/read-context.ts
4988
- import path38 from "node:path";
5535
+ import path39 from "node:path";
4989
5536
  import fs29 from "fs-extra";
4990
5537
 
4991
5538
  // src/commands/workspace/config.ts
4992
- import path32 from "node:path";
5539
+ import path33 from "node:path";
4993
5540
  import YAML from "yaml";
4994
5541
  var WORKSPACE_FILE = "haus.workspace.yaml";
4995
5542
  function parseWorkspaceConfig(text) {
@@ -5012,11 +5559,11 @@ function parseWorkspaceConfig(text) {
5012
5559
  };
5013
5560
  }
5014
5561
  async function readWorkspaceConfig(workspaceRoot) {
5015
- return parseWorkspaceConfig(await readText(path32.join(workspaceRoot, WORKSPACE_FILE)));
5562
+ return parseWorkspaceConfig(await readText(path33.join(workspaceRoot, WORKSPACE_FILE)));
5016
5563
  }
5017
5564
 
5018
5565
  // src/workspace/members.ts
5019
- import path33 from "node:path";
5566
+ import path34 from "node:path";
5020
5567
  var REPOS_MANIFEST_FILE = "repos.manifest.json";
5021
5568
  var REPOS_LOCAL_FILE = "repos.local.json";
5022
5569
  var MemberConfigError = class extends Error {
@@ -5029,14 +5576,14 @@ var MemberConfigError = class extends Error {
5029
5576
  };
5030
5577
  function resolveAbsPath(workspaceRoot, folder, overrides) {
5031
5578
  const override = overrides[folder];
5032
- if (override) return path33.resolve(workspaceRoot, override);
5033
- return path33.resolve(workspaceRoot, folder);
5579
+ if (override) return path34.resolve(workspaceRoot, override);
5580
+ return path34.resolve(workspaceRoot, folder);
5034
5581
  }
5035
5582
  function isSelfMember(workspaceRoot, member) {
5036
- return path33.resolve(member.absPath) === path33.resolve(workspaceRoot);
5583
+ return path34.resolve(member.absPath) === path34.resolve(workspaceRoot);
5037
5584
  }
5038
5585
  async function readPathOverrides(workspaceRoot) {
5039
- const filePath = path33.join(workspaceRoot, REPOS_LOCAL_FILE);
5586
+ const filePath = path34.join(workspaceRoot, REPOS_LOCAL_FILE);
5040
5587
  const result = await readJsonDetailed(filePath);
5041
5588
  if (result.status === "missing") return {};
5042
5589
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -5052,7 +5599,7 @@ async function readPathOverrides(workspaceRoot) {
5052
5599
  return overrides;
5053
5600
  }
5054
5601
  async function readFromWorkspaceYaml(workspaceRoot, overrides) {
5055
- const filePath = path33.join(workspaceRoot, WORKSPACE_FILE);
5602
+ const filePath = path34.join(workspaceRoot, WORKSPACE_FILE);
5056
5603
  const text = await readText(filePath);
5057
5604
  if (text === void 0) return void 0;
5058
5605
  const config = parseWorkspaceConfig(text);
@@ -5065,7 +5612,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
5065
5612
  }));
5066
5613
  }
5067
5614
  async function readFromManifest(workspaceRoot, overrides) {
5068
- const filePath = path33.join(workspaceRoot, REPOS_MANIFEST_FILE);
5615
+ const filePath = path34.join(workspaceRoot, REPOS_MANIFEST_FILE);
5069
5616
  const result = await readJsonDetailed(filePath);
5070
5617
  if (result.status === "missing") return void 0;
5071
5618
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -5107,11 +5654,11 @@ async function readMembers(rootInfo) {
5107
5654
  }
5108
5655
 
5109
5656
  // src/scanner/scan-project.ts
5110
- import path37 from "node:path";
5657
+ import path38 from "node:path";
5111
5658
 
5112
5659
  // src/utils/git-root.ts
5113
5660
  init_exec();
5114
- import path34 from "node:path";
5661
+ import path35 from "node:path";
5115
5662
  function fallback(start) {
5116
5663
  return {
5117
5664
  cwd: start,
@@ -5127,11 +5674,11 @@ function fallback(start) {
5127
5674
  async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
5128
5675
  const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
5129
5676
  const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
5130
- if (raw && path34.isAbsolute(raw)) return path34.resolve(raw);
5677
+ if (raw && path35.isAbsolute(raw)) return path35.resolve(raw);
5131
5678
  const plain = await runGit(["rev-parse", flag], { cwd });
5132
5679
  const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
5133
5680
  if (!plainRaw) return "";
5134
- return path34.isAbsolute(plainRaw) ? path34.resolve(plainRaw) : path34.resolve(repoRoot, plainRaw);
5681
+ return path35.isAbsolute(plainRaw) ? path35.resolve(plainRaw) : path35.resolve(repoRoot, plainRaw);
5135
5682
  }
5136
5683
  async function resolveRoots(start) {
5137
5684
  const cwd = start ?? process.cwd();
@@ -5140,15 +5687,15 @@ async function resolveRoots(start) {
5140
5687
  if (toplevel.exitCode !== 0) return fallback(cwd);
5141
5688
  const toplevelRaw = toplevel.stdout.trim();
5142
5689
  if (!toplevelRaw) return fallback(cwd);
5143
- const repoRoot = path34.resolve(toplevelRaw);
5690
+ const repoRoot = path35.resolve(toplevelRaw);
5144
5691
  const [gitDir, gitCommonDir] = await Promise.all([
5145
5692
  resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
5146
5693
  resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
5147
5694
  ]);
5148
5695
  if (!gitDir || !gitCommonDir) return fallback(cwd);
5149
5696
  const isLinkedWorktree = gitDir !== gitCommonDir;
5150
- const mainRoot = isLinkedWorktree && path34.basename(gitCommonDir) === ".git" ? path34.dirname(gitCommonDir) : repoRoot;
5151
- 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;
5152
5699
  return {
5153
5700
  cwd,
5154
5701
  repoRoot,
@@ -5193,7 +5740,7 @@ function resolveMajorVersion(range) {
5193
5740
  }
5194
5741
 
5195
5742
  // src/scanner/detect-package-manager.ts
5196
- import path35 from "node:path";
5743
+ import path36 from "node:path";
5197
5744
  import fs28 from "fs-extra";
5198
5745
  function detectPackageManager(root, packageManagerField) {
5199
5746
  const field = String(packageManagerField ?? "").trim();
@@ -5212,9 +5759,9 @@ function detectPackageManager(root, packageManagerField) {
5212
5759
  if (satisfiesVersion(version, ">=9")) return "npm";
5213
5760
  return "unknown";
5214
5761
  }
5215
- if (fs28.existsSync(path35.join(root, "yarn.lock"))) return "yarn";
5216
- if (fs28.existsSync(path35.join(root, "pnpm-lock.yaml"))) return "pnpm";
5217
- 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";
5218
5765
  return "unknown";
5219
5766
  }
5220
5767
 
@@ -5439,7 +5986,7 @@ function runDetection(ctx, rules = STACK_RULES) {
5439
5986
 
5440
5987
  // src/scanner/render.ts
5441
5988
  import { readFile } from "node:fs/promises";
5442
- import path36 from "node:path";
5989
+ import path37 from "node:path";
5443
5990
  async function buildContentBlob(root, files) {
5444
5991
  const candidates = files.filter(
5445
5992
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -5447,7 +5994,7 @@ async function buildContentBlob(root, files) {
5447
5994
  const slice = candidates.slice(0, 300);
5448
5995
  const parts = await mapWithConcurrency(slice, async (rel) => {
5449
5996
  try {
5450
- return await readFile(path36.join(root, rel), "utf8");
5997
+ return await readFile(path37.join(root, rel), "utf8");
5451
5998
  } catch {
5452
5999
  return "";
5453
6000
  }
@@ -5539,8 +6086,8 @@ var SAFE_FILES = [
5539
6086
  "Gemfile"
5540
6087
  ];
5541
6088
  async function scanProject(root) {
5542
- const pkg = await readJson(path37.join(root, "package.json"));
5543
- 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"));
5544
6091
  const files = await listFiles(root, SAFE_FILES);
5545
6092
  const safeFiles = files.filter((f) => !blocked(f));
5546
6093
  const depVersions = dependencyVersionMap(pkg, composer);
@@ -5570,7 +6117,7 @@ async function scanProject(root) {
5570
6117
  crossRepoHints.push("Monorepo orchestration detected");
5571
6118
  const unsupportedSignals = collectUnsupportedSignals(safeFiles);
5572
6119
  const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
5573
- 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);
5574
6121
  const context = {
5575
6122
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5576
6123
  repoName: repoNameFallback,
@@ -5592,10 +6139,10 @@ async function scanProject(root) {
5592
6139
  // src/scanner/read-context.ts
5593
6140
  async function isCacheFresh(root, cachePath) {
5594
6141
  const markerPaths = [
5595
- path38.join(root, "package.json"),
5596
- path38.join(root, "composer.json"),
5597
- path38.join(root, WORKSPACE_FILE),
5598
- 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)
5599
6146
  ];
5600
6147
  const cacheStat = await fs29.stat(cachePath);
5601
6148
  for (const markerPath of markerPaths) {
@@ -5701,7 +6248,7 @@ async function runDoctor(options) {
5701
6248
  } else {
5702
6249
  ok(`- HOOKS OK: ${hooks.message}`);
5703
6250
  }
5704
- const rootClaudeMdPath = path39.join(root, "CLAUDE.md");
6251
+ const rootClaudeMdPath = path40.join(root, "CLAUDE.md");
5705
6252
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
5706
6253
  if (!rootClaudeMdContent) {
5707
6254
  flag(
@@ -5744,9 +6291,9 @@ async function runDoctor(options) {
5744
6291
  ok("- CLAUDE.md: import block present");
5745
6292
  }
5746
6293
  for (const target of actualTargets) {
5747
- const resolved = path39.resolve(root, target);
5748
- const relFromRoot = path39.relative(root, resolved);
5749
- 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);
5750
6297
  if (escapesRoot || !await fs30.pathExists(resolved)) {
5751
6298
  flag(
5752
6299
  `- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
@@ -5780,8 +6327,8 @@ async function runDoctor(options) {
5780
6327
  } else {
5781
6328
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
5782
6329
  const bodyContent = workflowContent.slice(firstLine.length + 1);
5783
- const cachePath = path39.join(getCacheDir(), "templates/agentic-workflow-standard.md");
5784
- const bundledPath = path39.join(
6330
+ const cachePath = path40.join(getCacheDir(), "templates/agentic-workflow-standard.md");
6331
+ const bundledPath = path40.join(
5785
6332
  packageRoot(),
5786
6333
  "library",
5787
6334
  "global",
@@ -5830,7 +6377,7 @@ async function runDoctor(options) {
5830
6377
  }
5831
6378
  }
5832
6379
  if (workflowExists) {
5833
- const prettierIgnore = await readText(path39.join(root, ".prettierignore")) ?? "";
6380
+ const prettierIgnore = await readText(path40.join(root, ".prettierignore")) ?? "";
5834
6381
  const lines = prettierIgnore.split("\n");
5835
6382
  const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
5836
6383
  if (missing.length > 0) {
@@ -5870,7 +6417,50 @@ async function runDoctor(options) {
5870
6417
  if (finding.level === "ok") ok(finding.line);
5871
6418
  else suggest(finding.line, finding.sentence, finding.fix);
5872
6419
  }
5873
- 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
+ });
5874
6464
  if (skillRefIssues.length > 0) {
5875
6465
  const prefixBug = skillRefIssues.filter((i) => i.bareInstalled);
5876
6466
  const missingDep = skillRefIssues.filter((i) => !i.bareInstalled);
@@ -5934,7 +6524,7 @@ async function runDoctor(options) {
5934
6524
  }
5935
6525
  }
5936
6526
  }
5937
- const pkgJson = await readJson(path39.join(packageRoot(), "package.json"));
6527
+ const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
5938
6528
  const currentVersion = pkgJson?.version ?? "0.0.0";
5939
6529
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
5940
6530
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -5967,7 +6557,7 @@ async function runDoctor(options) {
5967
6557
  }
5968
6558
 
5969
6559
  // src/commands/update.ts
5970
- import path42 from "node:path";
6560
+ import path43 from "node:path";
5971
6561
 
5972
6562
  // src/claude/refresh-project.ts
5973
6563
  import fs31 from "fs-extra";
@@ -5979,12 +6569,16 @@ async function isHausProject(root) {
5979
6569
  }
5980
6570
  async function refreshProjectApply(root) {
5981
6571
  if (!await isHausProject(root)) return [];
5982
- 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
+ });
5983
6577
  }
5984
6578
 
5985
6579
  // src/install/apply.ts
5986
6580
  import crypto2 from "node:crypto";
5987
- import path40 from "node:path";
6581
+ import path41 from "node:path";
5988
6582
  import fs32 from "fs-extra";
5989
6583
 
5990
6584
  // src/install/header.ts
@@ -6078,7 +6672,7 @@ function destFileMissing(destPath) {
6078
6672
  }
6079
6673
  function sourceVersion() {
6080
6674
  try {
6081
- const pkgPath = path40.join(packageRoot(), "package.json");
6675
+ const pkgPath = path41.join(packageRoot(), "package.json");
6082
6676
  const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf8"));
6083
6677
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
6084
6678
  } catch {
@@ -6086,49 +6680,49 @@ function sourceVersion() {
6086
6680
  }
6087
6681
  }
6088
6682
  function globalSrcDir() {
6089
- return path40.join(packageRoot(), "library", "global");
6683
+ return path41.join(packageRoot(), "library", "global");
6090
6684
  }
6091
6685
  function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
6092
- const skillDir = path40.join(skillsDir, skillName);
6686
+ const skillDir = path41.join(skillsDir, skillName);
6093
6687
  const entries = [];
6094
6688
  for (const rel of fs32.readdirSync(skillDir, { recursive: true })) {
6095
6689
  if (rel === "SKILL.md") continue;
6096
- const abs = path40.join(skillDir, rel);
6690
+ const abs = path41.join(skillDir, rel);
6097
6691
  if (!fs32.statSync(abs).isFile()) continue;
6098
- const posixRel = rel.split(path40.sep).join("/");
6692
+ const posixRel = rel.split(path41.sep).join("/");
6099
6693
  entries.push({
6100
6694
  stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
6101
- srcRelPath: path40.join("library", "global", "skills", skillName, rel),
6102
- destPath: path40.join(claudeDir, "skills", skillName, rel)
6695
+ srcRelPath: path41.join("library", "global", "skills", skillName, rel),
6696
+ destPath: path41.join(claudeDir, "skills", skillName, rel)
6103
6697
  });
6104
6698
  }
6105
6699
  return entries;
6106
6700
  }
6107
6701
  function collectSourceFiles(srcDir, claudeDir) {
6108
6702
  const entries = [];
6109
- const skillsDir = path40.join(srcDir, "skills");
6703
+ const skillsDir = path41.join(srcDir, "skills");
6110
6704
  if (fs32.pathExistsSync(skillsDir)) {
6111
6705
  for (const skillName of fs32.readdirSync(skillsDir)) {
6112
- const skillFile = path40.join(skillsDir, skillName, "SKILL.md");
6706
+ const skillFile = path41.join(skillsDir, skillName, "SKILL.md");
6113
6707
  if (fs32.pathExistsSync(skillFile)) {
6114
6708
  entries.push({
6115
6709
  stableId: `skill.${skillName}`,
6116
- srcRelPath: path40.join("library", "global", "skills", skillName, "SKILL.md"),
6117
- 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")
6118
6712
  });
6119
6713
  entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
6120
6714
  }
6121
6715
  }
6122
6716
  }
6123
- const commandsDir = path40.join(srcDir, "commands");
6717
+ const commandsDir = path41.join(srcDir, "commands");
6124
6718
  if (fs32.pathExistsSync(commandsDir)) {
6125
6719
  for (const fileName of fs32.readdirSync(commandsDir)) {
6126
6720
  if (!fileName.endsWith(".md")) continue;
6127
6721
  const commandName = fileName.slice(0, -".md".length);
6128
6722
  entries.push({
6129
6723
  stableId: `command.${commandName}`,
6130
- srcRelPath: path40.join("library", "global", "commands", fileName),
6131
- destPath: path40.join(claudeDir, "commands", fileName)
6724
+ srcRelPath: path41.join("library", "global", "commands", fileName),
6725
+ destPath: path41.join(claudeDir, "commands", fileName)
6132
6726
  });
6133
6727
  }
6134
6728
  }
@@ -6154,7 +6748,7 @@ async function applyInstall(options = {}) {
6154
6748
  };
6155
6749
  const manifestFiles = [];
6156
6750
  for (const entry of sourceFiles) {
6157
- const srcPath = path40.join(packageRoot(), entry.srcRelPath);
6751
+ const srcPath = path41.join(packageRoot(), entry.srcRelPath);
6158
6752
  const rawContent = await readText(srcPath);
6159
6753
  if (rawContent === void 0) {
6160
6754
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -6215,7 +6809,7 @@ async function applyInstall(options = {}) {
6215
6809
  }
6216
6810
  let mergedSettings;
6217
6811
  if (!check) {
6218
- const fragmentPath = path40.join(srcDir, "settings-fragments", "hooks.json");
6812
+ const fragmentPath = path41.join(srcDir, "settings-fragments", "hooks.json");
6219
6813
  const fragments = await loadHooksFragment(fragmentPath);
6220
6814
  const settings = await readSettings();
6221
6815
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -6245,9 +6839,12 @@ async function applyInstall(options = {}) {
6245
6839
  }
6246
6840
  if (!dryRun && !check && mergedSettings) {
6247
6841
  await writeSettings(mergedSettings);
6248
- const manifest = buildManifest(source, manifestFiles, [
6249
- .../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])
6250
- ]);
6842
+ const manifest = buildManifest(
6843
+ source,
6844
+ manifestFiles,
6845
+ [.../* @__PURE__ */ new Set([...existingManifest?.hooks ?? [], ...result.hookIds])],
6846
+ existingManifest?.plugins
6847
+ );
6251
6848
  await writeManifest(manifest);
6252
6849
  }
6253
6850
  return result;
@@ -6304,7 +6901,7 @@ function summarizeLockDiff(before, after) {
6304
6901
 
6305
6902
  // src/update/lockfile.ts
6306
6903
  import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
6307
- import path41 from "node:path";
6904
+ import path42 from "node:path";
6308
6905
  async function readLockSummary(root) {
6309
6906
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
6310
6907
  const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
@@ -6342,7 +6939,7 @@ async function applyLock(root) {
6342
6939
  try {
6343
6940
  const backupDir = hausPath(root, "backups");
6344
6941
  await mkdir(backupDir, { recursive: true });
6345
- await copyFile(lockPath, path41.join(backupDir, `haus.lock.${Date.now()}.json`));
6942
+ await copyFile(lockPath, path42.join(backupDir, `haus.lock.${Date.now()}.json`));
6346
6943
  } catch {
6347
6944
  }
6348
6945
  const enriched = await Promise.all(
@@ -6364,7 +6961,7 @@ function diffLock(before, after) {
6364
6961
  }
6365
6962
  async function hasLocalOverrides(root) {
6366
6963
  try {
6367
- await readFile2(path41.join(root, ".claude", "settings.json"), "utf8");
6964
+ await readFile2(path42.join(root, ".claude", "settings.json"), "utf8");
6368
6965
  return true;
6369
6966
  } catch {
6370
6967
  return false;
@@ -6380,15 +6977,26 @@ async function runUpdate(options) {
6380
6977
  return;
6381
6978
  }
6382
6979
  if (options.check) {
6383
- const pkgJson2 = await readJson(path42.join(packageRoot(), "package.json"));
6980
+ const pkgJson2 = await readJson(path43.join(packageRoot(), "package.json"));
6384
6981
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
6385
- 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([
6386
6991
  options.fast ? fastLockStatus(root) : checkLock(root),
6387
6992
  fetchNpmVersionStatus(currentVersion2),
6388
6993
  fetchLatestCatalogTag(),
6389
6994
  options.fast ? Promise.resolve(null) : detectGlobalInstallDrift(),
6390
6995
  loadCatalog(root),
6391
- 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)
6392
7000
  ]);
6393
7001
  const formerIdMigrations = findFormerIdMigrations(lockItems ?? [], catalogItems);
6394
7002
  const installedRef = options.fast ? status.catalogRef : status.catalogRef ?? "main";
@@ -6403,6 +7011,7 @@ async function runUpdate(options) {
6403
7011
  catalogRefBehind,
6404
7012
  formerIdMigrations,
6405
7013
  globalInstallDrift,
7014
+ globalPluginDrift,
6406
7015
  localOverrides: await hasLocalOverrides(root),
6407
7016
  summary: diffGeneratedFiles(),
6408
7017
  npmVersion
@@ -6412,12 +7021,12 @@ async function runUpdate(options) {
6412
7021
  )
6413
7022
  );
6414
7023
  const hasHashDrift = !options.fast && status.count > 0 && "ok" in status && !status.ok;
6415
- if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true) {
7024
+ if (hasHashDrift || formerIdMigrations.length > 0 || globalInstallDrift === true || globalPluginDrift === true) {
6416
7025
  process.exitCode = 1;
6417
7026
  }
6418
7027
  return;
6419
7028
  }
6420
- const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
7029
+ const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
6421
7030
  const currentVersion = pkgJson?.version ?? "0.0.0";
6422
7031
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
6423
7032
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -6450,6 +7059,7 @@ async function runUpdate(options) {
6450
7059
  warn(formatGithubRateLimitMessage(sync.rateLimit));
6451
7060
  }
6452
7061
  await refreshGlobalInstall();
7062
+ await ensureCatalogPinnedPlugins(root);
6453
7063
  await refreshProjectFiles(root);
6454
7064
  const { after } = await applyLock(root);
6455
7065
  log(diffLock(lockBefore, after));
@@ -6460,7 +7070,7 @@ async function runFromHookCheck(root) {
6460
7070
  try {
6461
7071
  const summary = await readLockSummary(root);
6462
7072
  if (summary.count === 0) return;
6463
- const pkgJson = await readJson(path42.join(packageRoot(), "package.json"));
7073
+ const pkgJson = await readJson(path43.join(packageRoot(), "package.json"));
6464
7074
  const currentVersion = pkgJson?.version ?? "0.0.0";
6465
7075
  const [npmVersion, latestCatalogTag] = await Promise.all([
6466
7076
  fetchNpmVersionStatus(currentVersion),
@@ -6518,6 +7128,26 @@ async function refreshGlobalInstall() {
6518
7128
  warn(`Could not refresh ~/.claude/: ${err instanceof Error ? err.message : String(err)}`);
6519
7129
  }
6520
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
+ }
6521
7151
  async function detectGlobalInstallDrift() {
6522
7152
  try {
6523
7153
  const result = await applyInstall({ check: true });
@@ -6611,7 +7241,7 @@ async function runCiGate(options = {}) {
6611
7241
  init_exec();
6612
7242
  import { existsSync as existsSync2 } from "node:fs";
6613
7243
  import { readdir } from "node:fs/promises";
6614
- import path43 from "node:path";
7244
+ import path44 from "node:path";
6615
7245
  var GIT_LOCATION_VARS = [
6616
7246
  "GIT_DIR",
6617
7247
  "GIT_WORK_TREE",
@@ -6671,11 +7301,11 @@ async function runClone(url, opts = {}) {
6671
7301
  process.exitCode = 1;
6672
7302
  return;
6673
7303
  }
6674
- const target = path43.resolve(opts.dir?.trim() || repoNameFromUrl(url));
7304
+ const target = path44.resolve(opts.dir?.trim() || repoNameFromUrl(url));
6675
7305
  if (existsSync2(target) && !await isEmptyDirectory(target)) {
6676
7306
  const check = await checkExistingTarget(target, url);
6677
7307
  if (check.kind === "matches") {
6678
- 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`);
6679
7309
  return;
6680
7310
  }
6681
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].`;
@@ -6784,7 +7414,7 @@ async function runDecisionsGuard(options = {}) {
6784
7414
  }
6785
7415
 
6786
7416
  // src/commands/decisions.ts
6787
- import path44 from "node:path";
7417
+ import path45 from "node:path";
6788
7418
 
6789
7419
  // src/decisions/next-number.ts
6790
7420
  async function nextDecisionNumber(root) {
@@ -6894,7 +7524,7 @@ async function runDecisions(sub, arg, options = {}) {
6894
7524
  return;
6895
7525
  }
6896
7526
  if (sub === "validate") {
6897
- const target = arg ? path44.resolve(root, arg) : void 0;
7527
+ const target = arg ? path45.resolve(root, arg) : void 0;
6898
7528
  if (!target) {
6899
7529
  error("usage: haus decisions validate <path-to-decision.md>");
6900
7530
  process.exitCode = 1;
@@ -7163,17 +7793,34 @@ async function runGuard(kind, _options) {
7163
7793
  }
7164
7794
 
7165
7795
  // src/commands/init.ts
7166
- import path46 from "node:path";
7796
+ import path47 from "node:path";
7167
7797
  import fs33 from "fs-extra";
7168
7798
 
7169
7799
  // src/recommender/git-signal.ts
7170
7800
  init_exec();
7171
7801
  async function runGitLines(root, args) {
7802
+ const started = Date.now();
7172
7803
  try {
7173
7804
  const result = await runGit(args, { cwd: root, timeout: 3e3 });
7805
+ if (process.env.HAUS_DEBUG_GITSIG) {
7806
+ const { appendFileSync } = await import("node:fs");
7807
+ appendFileSync(
7808
+ process.env.HAUS_DEBUG_GITSIG,
7809
+ `GITSIG ok git ${args.join(" ")} cwd=${root} ms=${Date.now() - started} exit=${result.exitCode} stdout=${JSON.stringify(result.stdout.slice(0, 200))} stderr=${JSON.stringify(result.stderr.slice(0, 200))}
7810
+ `
7811
+ );
7812
+ }
7174
7813
  if (result.exitCode !== 0) return [];
7175
7814
  return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean);
7176
- } catch {
7815
+ } catch (err) {
7816
+ if (process.env.HAUS_DEBUG_GITSIG) {
7817
+ const { appendFileSync } = await import("node:fs");
7818
+ appendFileSync(
7819
+ process.env.HAUS_DEBUG_GITSIG,
7820
+ `GITSIG threw git ${args.join(" ")} cwd=${root} ms=${Date.now() - started} err=${JSON.stringify(String(err).slice(0, 300))}
7821
+ `
7822
+ );
7823
+ }
7177
7824
  return [];
7178
7825
  }
7179
7826
  }
@@ -7189,6 +7836,7 @@ async function readChangedFiles(root) {
7189
7836
 
7190
7837
  // src/recommender/recommend.ts
7191
7838
  async function recommend(root, context, opts = {}) {
7839
+ const activePluginIds = opts.activePluginIds ?? [];
7192
7840
  const items = await loadCatalog(root);
7193
7841
  const sources = buildSourcesReport(items);
7194
7842
  const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
@@ -7246,6 +7894,15 @@ async function recommend(root, context, opts = {}) {
7246
7894
  "Catalog item is deprecated",
7247
7895
  "reviewStatus:deprecated"
7248
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
+ }
7249
7906
  if (normSource === "curated") {
7250
7907
  const rs = item.reviewStatus;
7251
7908
  add(
@@ -7585,7 +8242,7 @@ function buildOptInEligible(items, skipped) {
7585
8242
  }
7586
8243
 
7587
8244
  // src/commands/workspace/discover.ts
7588
- import path45 from "node:path";
8245
+ import path46 from "node:path";
7589
8246
  import fg5 from "fast-glob";
7590
8247
  import YAML2 from "yaml";
7591
8248
  var DEFAULT_MAX_DEPTH = 3;
@@ -7626,10 +8283,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7626
8283
  const gitDirs = /* @__PURE__ */ new Set();
7627
8284
  const manifestDirs = /* @__PURE__ */ new Set();
7628
8285
  for (const match of gitMatches) {
7629
- gitDirs.add(path45.posix.dirname(match));
8286
+ gitDirs.add(path46.posix.dirname(match));
7630
8287
  }
7631
8288
  for (const match of manifestMatches) {
7632
- manifestDirs.add(path45.posix.dirname(match));
8289
+ manifestDirs.add(path46.posix.dirname(match));
7633
8290
  }
7634
8291
  const repoRoots = [...gitDirs];
7635
8292
  const manifestSorted = [...manifestDirs].sort(
@@ -7646,9 +8303,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7646
8303
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7647
8304
  const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
7648
8305
  return mapWithConcurrency(repoRoots, async (relDir) => {
7649
- const absDir = path45.resolve(workspaceRoot, relDir);
7650
- const pkg = await readJson(path45.join(absDir, "package.json"));
7651
- 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);
7652
8309
  let role = "auto";
7653
8310
  try {
7654
8311
  const scan = await scanProject(absDir);
@@ -7691,7 +8348,7 @@ function renderWorkspaceYaml(config) {
7691
8348
  });
7692
8349
  }
7693
8350
  async function runDiscover(workspaceRoot, opts = {}) {
7694
- const yamlPath = path45.join(workspaceRoot, "haus.workspace.yaml");
8351
+ const yamlPath = path46.join(workspaceRoot, "haus.workspace.yaml");
7695
8352
  const existingText = await readText(yamlPath);
7696
8353
  const existing = parseWorkspaceConfig(existingText);
7697
8354
  if (existingText && !existing) {
@@ -7758,7 +8415,11 @@ async function runSetupCore(root, opts) {
7758
8415
  const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
7759
8416
  const context = await readContextOrScan(root);
7760
8417
  const priorManualIncludes = await readPriorManualIncludes(root);
7761
- 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
+ });
7762
8423
  if (recommendation.recommended.length === 0 && !force) {
7763
8424
  const warningLines2 = [
7764
8425
  .../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
@@ -7772,7 +8433,10 @@ async function runSetupCore(root, opts) {
7772
8433
  say(`Repo: ${context.repoName}`);
7773
8434
  for (const warning of warningLines2) say(`- WARN: ${warning}`);
7774
8435
  }
7775
- 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.";
7776
8440
  warn(message);
7777
8441
  process.exitCode = 1;
7778
8442
  return {
@@ -7838,7 +8502,10 @@ async function runSetupCore(root, opts) {
7838
8502
  warn(formatGithubRateLimitMessage(sync.rateLimit));
7839
8503
  }
7840
8504
  }
7841
- 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
+ });
7842
8509
  say("Applied files:");
7843
8510
  files.forEach((f) => say(`- ${displayPath(root, f)}`));
7844
8511
  const hooksAfter = await verifyProjectSettingsHooksContract(root);
@@ -7869,7 +8536,7 @@ async function runSetupProject(options) {
7869
8536
  // src/commands/init.ts
7870
8537
  async function runInit(options) {
7871
8538
  const root = process.cwd();
7872
- const hausDir = path46.join(root, ".haus-workflow");
8539
+ const hausDir = path47.join(root, ".haus-workflow");
7873
8540
  const alreadyInit = await fs33.pathExists(hausDir);
7874
8541
  if (alreadyInit) {
7875
8542
  log("Haus AI already initialized in this project.");
@@ -7934,7 +8601,19 @@ function printCompanionToolSuggestions(deps = {}) {
7934
8601
  }
7935
8602
 
7936
8603
  // src/commands/install.ts
7937
- 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;
7938
8617
  try {
7939
8618
  const result = await applyInstall({
7940
8619
  dryRun: options.dryRun,
@@ -7942,9 +8621,23 @@ async function runInstall(options) {
7942
8621
  check: options.check
7943
8622
  });
7944
8623
  if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
7945
- if (options.check && result.drift) {
7946
- process.exitCode = 1;
7947
- } 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) {
7948
8641
  printCompanionToolSuggestions();
7949
8642
  const total = result.created.length + result.updated.length;
7950
8643
  if (options.postinstall) {
@@ -7956,6 +8649,9 @@ async function runInstall(options) {
7956
8649
  parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
7957
8650
  );
7958
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
+ }
7959
8655
  log("Undo any time with: haus uninstall");
7960
8656
  log("Disable this on install: HAUS_NO_POSTINSTALL=1");
7961
8657
  } else {
@@ -7975,7 +8671,8 @@ async function runRecommend(options) {
7975
8671
  const root = process.cwd();
7976
8672
  const context = await readContextOrScan(root);
7977
8673
  const include = parseIdList(options.include);
7978
- const result = await recommend(root, context, { include });
8674
+ const activePluginIds = await activeGlobalPluginIds(root);
8675
+ const result = await recommend(root, context, { include, activePluginIds });
7979
8676
  await writeJson(hausPath(root, "recommendation.json"), result);
7980
8677
  if (options.json) {
7981
8678
  log(JSON.stringify(result, null, 2));
@@ -7996,7 +8693,14 @@ async function runRecommend(options) {
7996
8693
  async function runRefresh() {
7997
8694
  const root = process.cwd();
7998
8695
  const context = await scanProject(root);
7999
- 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
+ });
8000
8704
  await writeJson(hausPath(root, "recommendation.json"), recommendation);
8001
8705
  log("Haus refresh complete");
8002
8706
  log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
@@ -8005,7 +8709,7 @@ async function runRefresh() {
8005
8709
  }
8006
8710
 
8007
8711
  // src/install/scaffold.ts
8008
- import path47 from "node:path";
8712
+ import path48 from "node:path";
8009
8713
  import fs34 from "fs-extra";
8010
8714
  async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8011
8715
  const scaffolded = [];
@@ -8022,9 +8726,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8022
8726
  const realRoot = await fs34.realpath(catalogRoot).catch(() => null);
8023
8727
  for (const item of items) {
8024
8728
  if (item.type !== "config") continue;
8025
- const sourcePath = path47.resolve(catalogRoot, item.path);
8026
- const rel = path47.relative(catalogRoot, sourcePath);
8027
- 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 === "..");
8028
8732
  if (escapes) {
8029
8733
  warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
8030
8734
  continue;
@@ -8034,8 +8738,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8034
8738
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
8035
8739
  continue;
8036
8740
  }
8037
- const realRel = path47.relative(realRoot, realSource);
8038
- if (realRel !== "" && (path47.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
8741
+ const realRel = path48.relative(realRoot, realSource);
8742
+ if (realRel !== "" && (path48.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
8039
8743
  warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
8040
8744
  continue;
8041
8745
  }
@@ -8049,14 +8753,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8049
8753
  continue;
8050
8754
  }
8051
8755
  if (stat.isFile()) {
8052
- const filename = path47.basename(sourcePath);
8053
- const dest = path47.join(projectRoot, filename);
8756
+ const filename = path48.basename(sourcePath);
8757
+ const dest = path48.join(projectRoot, filename);
8054
8758
  record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
8055
8759
  } else if (stat.isDirectory()) {
8056
8760
  const entries = await fs34.readdir(sourcePath);
8057
8761
  for (const entry of entries) {
8058
- const src = path47.join(sourcePath, entry);
8059
- const dest = path47.join(projectRoot, entry);
8762
+ const src = path48.join(sourcePath, entry);
8763
+ const dest = path48.join(projectRoot, entry);
8060
8764
  record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
8061
8765
  }
8062
8766
  }
@@ -8064,14 +8768,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
8064
8768
  return { scaffolded, skipped, skippedExisting };
8065
8769
  }
8066
8770
  async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
8067
- const destDisplay = path47.relative(projectRoot, dest) || path47.basename(dest);
8771
+ const destDisplay = path48.relative(projectRoot, dest) || path48.basename(dest);
8068
8772
  const srcStat = await fs34.lstat(src).catch(() => null);
8069
8773
  if (!srcStat) {
8070
- warn(`Skipping ${path47.basename(src)}: source not found`);
8774
+ warn(`Skipping ${path48.basename(src)}: source not found`);
8071
8775
  return "skipped-refused";
8072
8776
  }
8073
8777
  if (srcStat.isSymbolicLink()) {
8074
- warn(`Skipping ${path47.basename(src)}: source is a symlink`);
8778
+ warn(`Skipping ${path48.basename(src)}: source is a symlink`);
8075
8779
  return "skipped-refused";
8076
8780
  }
8077
8781
  const exists = await fs34.pathExists(dest);
@@ -8080,13 +8784,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
8080
8784
  return "skipped-exists";
8081
8785
  }
8082
8786
  if (opts.dryRun) {
8083
- log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path47.basename(dest)} (${itemId})`);
8787
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path48.basename(dest)} (${itemId})`);
8084
8788
  return "scaffolded";
8085
8789
  }
8086
8790
  if (exists && opts.force) {
8087
8791
  await fs34.remove(dest);
8088
8792
  }
8089
- await fs34.ensureDir(path47.dirname(dest));
8793
+ await fs34.ensureDir(path48.dirname(dest));
8090
8794
  await fs34.copy(src, dest, {
8091
8795
  overwrite: true,
8092
8796
  dereference: false,
@@ -8099,7 +8803,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
8099
8803
  }
8100
8804
  }
8101
8805
  });
8102
- log(`${exists ? "Overwrote" : "Created"} ${path47.basename(dest)} (${itemId})`);
8806
+ log(`${exists ? "Overwrote" : "Created"} ${path48.basename(dest)} (${itemId})`);
8103
8807
  return "scaffolded";
8104
8808
  }
8105
8809
 
@@ -8164,7 +8868,7 @@ async function runScan(options) {
8164
8868
  }
8165
8869
 
8166
8870
  // src/commands/undo.ts
8167
- import path48 from "node:path";
8871
+ import path49 from "node:path";
8168
8872
  import fs35 from "fs-extra";
8169
8873
 
8170
8874
  // src/claude/managed-paths.ts
@@ -8192,18 +8896,18 @@ async function collectLockTrackedPaths(root) {
8192
8896
  const relPaths = row.paths ?? [];
8193
8897
  const existingRel = [];
8194
8898
  for (const rel of relPaths) {
8195
- if (await fs35.pathExists(path48.resolve(root, rel))) existingRel.push(rel);
8899
+ if (await fs35.pathExists(path49.resolve(root, rel))) existingRel.push(rel);
8196
8900
  }
8197
8901
  if (existingRel.length === 0) continue;
8198
8902
  if (row.hash === void 0) {
8199
- removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
8903
+ removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
8200
8904
  continue;
8201
8905
  }
8202
8906
  const currentHash = await hashInstalledPaths(root, existingRel);
8203
8907
  if (currentHash === row.hash) {
8204
- removable.push(...existingRel.map((rel) => path48.resolve(root, rel)));
8908
+ removable.push(...existingRel.map((rel) => path49.resolve(root, rel)));
8205
8909
  } else {
8206
- preserved.push(...existingRel.map((rel) => path48.resolve(root, rel)));
8910
+ preserved.push(...existingRel.map((rel) => path49.resolve(root, rel)));
8207
8911
  log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
8208
8912
  }
8209
8913
  }
@@ -8216,7 +8920,7 @@ async function settingsHasHausContent(root) {
8216
8920
  return settings._haus != null;
8217
8921
  }
8218
8922
  async function claudeMdHasHausBlock(root) {
8219
- const filePath = path48.join(root, "CLAUDE.md");
8923
+ const filePath = path49.join(root, "CLAUDE.md");
8220
8924
  if (!await fs35.pathExists(filePath)) return false;
8221
8925
  const text = await fs35.readFile(filePath, "utf8");
8222
8926
  return text.includes(BLOCK_BEGIN);
@@ -8229,15 +8933,15 @@ async function stripProjectSettings(root) {
8229
8933
  const hasContent = Object.keys(settings).length > 0;
8230
8934
  if (hasContent) {
8231
8935
  await writeProjectSettings(root, settings);
8232
- 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).`);
8233
8937
  return true;
8234
8938
  }
8235
8939
  await fs35.remove(settingsPath);
8236
- log(`Removed ${path48.relative(root, settingsPath)} (no user-owned settings remained).`);
8940
+ log(`Removed ${path49.relative(root, settingsPath)} (no user-owned settings remained).`);
8237
8941
  return true;
8238
8942
  }
8239
8943
  async function stripRootClaudeMd(root) {
8240
- const filePath = path48.join(root, "CLAUDE.md");
8944
+ const filePath = path49.join(root, "CLAUDE.md");
8241
8945
  if (!await fs35.pathExists(filePath)) return false;
8242
8946
  const prev = await fs35.readFile(filePath, "utf8");
8243
8947
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -8265,12 +8969,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
8265
8969
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
8266
8970
  for (const abs of managedAbsPaths) {
8267
8971
  if (!await fs35.pathExists(abs)) continue;
8268
- const rel = path48.relative(root, abs);
8269
- const backupPath = path48.join(backupRoot, rel);
8270
- 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));
8271
8975
  await fs35.copy(abs, backupPath);
8272
8976
  }
8273
- 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)}.`);
8274
8978
  }
8275
8979
  async function runUndo(options) {
8276
8980
  const root = options.root ?? process.cwd();
@@ -8283,7 +8987,7 @@ async function runUndo(options) {
8283
8987
  log("Nothing to remove: no haus-managed files found in this directory.");
8284
8988
  return;
8285
8989
  }
8286
- const relTargets = managed.map((p) => path48.relative(root, p));
8990
+ const relTargets = managed.map((p) => path49.relative(root, p));
8287
8991
  const summaryParts = [...relTargets];
8288
8992
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
8289
8993
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -8307,7 +9011,7 @@ User-owned .claude/ files will be preserved.`
8307
9011
  for (const abs of managed) {
8308
9012
  if (!await fs35.pathExists(abs)) continue;
8309
9013
  await fs35.remove(abs);
8310
- log(`Removed ${path48.relative(root, abs)}`);
9014
+ log(`Removed ${path49.relative(root, abs)}`);
8311
9015
  }
8312
9016
  if (stripSettings) await stripProjectSettings(root);
8313
9017
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -8318,16 +9022,43 @@ User-owned .claude/ files will be preserved.`
8318
9022
 
8319
9023
  // src/install/uninstall.ts
8320
9024
  import crypto3 from "node:crypto";
8321
- import path49 from "node:path";
9025
+ import path50 from "node:path";
8322
9026
  import fs36 from "fs-extra";
8323
9027
  async function runUninstall(options = {}) {
8324
- const { force = false } = options;
9028
+ const { force = false, removePlugins = removeGlobalPlugins } = options;
8325
9029
  const manifest = await readManifest();
8326
- const result = { deleted: [], skipped: [], hooksStripped: false };
9030
+ const result = {
9031
+ deleted: [],
9032
+ skipped: [],
9033
+ hooksStripped: false,
9034
+ pluginsRemoved: []
9035
+ };
8327
9036
  if (!manifest) {
8328
9037
  warn("No install manifest found \u2014 nothing to uninstall.");
8329
9038
  return result;
8330
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
+ }
8331
9062
  for (const entry of manifest.files) {
8332
9063
  const exists = fs36.pathExistsSync(entry.destPath);
8333
9064
  if (!exists) continue;
@@ -8348,14 +9079,14 @@ async function runUninstall(options = {}) {
8348
9079
  continue;
8349
9080
  }
8350
9081
  await fs36.remove(entry.destPath);
8351
- await pruneEmptyDir(path49.dirname(entry.destPath));
9082
+ await pruneEmptyDir(path50.dirname(entry.destPath));
8352
9083
  result.deleted.push(entry.destPath);
8353
9084
  }
8354
9085
  const settings = await readSettings();
8355
9086
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
8356
9087
  await writeSettings(stripped);
8357
9088
  result.hooksStripped = true;
8358
- const hausDir = path49.join(globalClaudeDir(), "haus");
9089
+ const hausDir = path50.join(globalClaudeDir(), "haus");
8359
9090
  const manifestPath2 = hausManifestPath();
8360
9091
  if (fs36.pathExistsSync(manifestPath2)) {
8361
9092
  await fs36.remove(manifestPath2);
@@ -8381,6 +9112,10 @@ function printUninstallResult(result) {
8381
9112
  if (result.hooksStripped) {
8382
9113
  log("Haus hook entries removed from ~/.claude/settings.json");
8383
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
+ }
8384
9119
  }
8385
9120
 
8386
9121
  // src/commands/uninstall.ts
@@ -8409,11 +9144,11 @@ async function runUninstallCommand(options) {
8409
9144
  }
8410
9145
 
8411
9146
  // src/commands/validate-catalog.ts
8412
- import path51 from "node:path";
9147
+ import path52 from "node:path";
8413
9148
 
8414
9149
  // src/catalog/validate-core.ts
8415
9150
  import fs37 from "node:fs";
8416
- import path50 from "node:path";
9151
+ import path51 from "node:path";
8417
9152
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
8418
9153
  var ITEM_ID_PATTERN = /^haus\./;
8419
9154
  function auditForbiddenStacks(items) {
@@ -8562,7 +9297,7 @@ function readForAudit(absPath, label, failures) {
8562
9297
  }
8563
9298
  }
8564
9299
  function auditTemplateContent(manifestDir, absPath, itemId) {
8565
- const rel = path50.relative(manifestDir, absPath);
9300
+ const rel = path51.relative(manifestDir, absPath);
8566
9301
  const failures = [];
8567
9302
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
8568
9303
  if (text === null) return failures;
@@ -8584,25 +9319,25 @@ function auditShippedFiles(manifestDir, items) {
8584
9319
  const failures = [];
8585
9320
  for (const item of items) {
8586
9321
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
8587
- const absPath = path50.join(manifestDir, item.path);
9322
+ const absPath = path51.join(manifestDir, item.path);
8588
9323
  if (item.type === "skill") {
8589
- const skillMd = path50.join(absPath, "SKILL.md");
9324
+ const skillMd = path51.join(absPath, "SKILL.md");
8590
9325
  if (!fs37.existsSync(skillMd)) {
8591
- failures.push(`${item.id}: missing ${path50.relative(manifestDir, skillMd)}`);
9326
+ failures.push(`${item.id}: missing ${path51.relative(manifestDir, skillMd)}`);
8592
9327
  continue;
8593
9328
  }
8594
9329
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
8595
9330
  if (text === null) continue;
8596
9331
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
8597
9332
  failures.push(
8598
- ...auditForbiddenTagsInText(text, `${item.id}: ${path50.relative(manifestDir, skillMd)}`)
9333
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path51.relative(manifestDir, skillMd)}`)
8599
9334
  );
8600
9335
  } else if (item.type === "agent") {
8601
9336
  if (!fs37.existsSync(absPath)) {
8602
9337
  failures.push(`${item.id}: missing agent file ${item.path}`);
8603
9338
  continue;
8604
9339
  }
8605
- const rel = path50.relative(manifestDir, absPath);
9340
+ const rel = path51.relative(manifestDir, absPath);
8606
9341
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8607
9342
  if (text === null) continue;
8608
9343
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8618,7 +9353,7 @@ function auditShippedFiles(manifestDir, items) {
8618
9353
  failures.push(`${item.id}: missing command file ${item.path}`);
8619
9354
  continue;
8620
9355
  }
8621
- const rel = path50.relative(manifestDir, absPath);
9356
+ const rel = path51.relative(manifestDir, absPath);
8622
9357
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8623
9358
  if (text === null) continue;
8624
9359
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8650,7 +9385,7 @@ function walkAllFiles(dir, fn, failures) {
8650
9385
  }
8651
9386
  for (const entry of entries) {
8652
9387
  if (entry.name === ".DS_Store") continue;
8653
- const full = path50.join(dir, entry.name);
9388
+ const full = path51.join(dir, entry.name);
8654
9389
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
8655
9390
  else fn(full);
8656
9391
  }
@@ -8668,12 +9403,12 @@ function auditDiskOrphans(manifestDir, items) {
8668
9403
  const failures = [];
8669
9404
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
8670
9405
  for (const dir of CONTENT_ROOTS) {
8671
- const abs = path50.join(manifestDir, dir);
9406
+ const abs = path51.join(manifestDir, dir);
8672
9407
  if (!fs37.existsSync(abs)) continue;
8673
9408
  walkAllFiles(
8674
9409
  abs,
8675
9410
  (file) => {
8676
- const relPath = path50.relative(manifestDir, file).replace(/\\/g, "/");
9411
+ const relPath = path51.relative(manifestDir, file).replace(/\\/g, "/");
8677
9412
  if (!isPathClaimed(relPath, claimedPaths)) {
8678
9413
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
8679
9414
  }
@@ -8694,7 +9429,7 @@ function walkMd(dir, fn, failures) {
8694
9429
  return;
8695
9430
  }
8696
9431
  for (const entry of entries) {
8697
- const full = path50.join(dir, entry.name);
9432
+ const full = path51.join(dir, entry.name);
8698
9433
  if (entry.isDirectory()) walkMd(full, fn, failures);
8699
9434
  else if (entry.name.endsWith(".md")) fn(full);
8700
9435
  }
@@ -8703,13 +9438,13 @@ function auditMarkdownContent(manifestDir, items) {
8703
9438
  const failures = [];
8704
9439
  const pathSourceMap = buildItemPathSourceMap(items);
8705
9440
  for (const dir of ["skills", "agents", "templates", "commands"]) {
8706
- const abs = path50.join(manifestDir, dir);
9441
+ const abs = path51.join(manifestDir, dir);
8707
9442
  if (!fs37.existsSync(abs)) continue;
8708
9443
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
8709
9444
  walkMd(
8710
9445
  abs,
8711
9446
  (file) => {
8712
- const rel = path50.relative(manifestDir, file);
9447
+ const rel = path51.relative(manifestDir, file);
8713
9448
  const text = readForAudit(file, rel, failures);
8714
9449
  if (text === null) return;
8715
9450
  const norm = rel.replace(/\\/g, "/");
@@ -8754,8 +9489,8 @@ async function runValidateCatalog(manifestPath2) {
8754
9489
  process.exitCode = 1;
8755
9490
  return;
8756
9491
  }
8757
- const abs = path51.resolve(process.cwd(), manifestPath2);
8758
- const manifestDir = path51.dirname(abs);
9492
+ const abs = path52.resolve(process.cwd(), manifestPath2);
9493
+ const manifestDir = path52.dirname(abs);
8759
9494
  const data = await readJson(abs);
8760
9495
  if (!data?.items) {
8761
9496
  error(`Could not read catalog manifest at ${abs}`);
@@ -8772,7 +9507,7 @@ async function runValidateCatalog(manifestPath2) {
8772
9507
  }
8773
9508
 
8774
9509
  // src/workspace/worktree/add.ts
8775
- import path58 from "node:path";
9510
+ import path59 from "node:path";
8776
9511
  import fs41 from "fs-extra";
8777
9512
 
8778
9513
  // src/workspace/worktree/git-worktree.ts
@@ -8892,12 +9627,12 @@ async function hasUnpushedWork(cwd) {
8892
9627
  }
8893
9628
 
8894
9629
  // src/workspace/worktree/hydrate.ts
8895
- import path54 from "node:path";
9630
+ import path55 from "node:path";
8896
9631
  import fs39 from "fs-extra";
8897
9632
 
8898
9633
  // src/workspace/worktree/cow-copy.ts
8899
9634
  init_exec();
8900
- import path52 from "node:path";
9635
+ import path53 from "node:path";
8901
9636
  import fs38 from "fs-extra";
8902
9637
  async function getMacFilesystemType(dir) {
8903
9638
  const df = await runCommand("df", ["-P", dir]);
@@ -8932,11 +9667,11 @@ async function cowCopyDir(src, dest) {
8932
9667
  const srcExists = await fs38.pathExists(src);
8933
9668
  if (!srcExists)
8934
9669
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
8935
- const strategy = await detectCowStrategy(path52.dirname(dest));
9670
+ const strategy = await detectCowStrategy(path53.dirname(dest));
8936
9671
  if (strategy === "unsupported" || strategy === "unknown-platform") {
8937
9672
  return { strategy, attempted: false, ok: false };
8938
9673
  }
8939
- await fs38.ensureDir(path52.dirname(dest));
9674
+ await fs38.ensureDir(path53.dirname(dest));
8940
9675
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
8941
9676
  const result = await runCommand("cp", args);
8942
9677
  return {
@@ -8950,7 +9685,7 @@ async function cowCopyDir(src, dest) {
8950
9685
  // src/workspace/worktree/install.ts
8951
9686
  init_exec();
8952
9687
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8953
- import path53 from "node:path";
9688
+ import path54 from "node:path";
8954
9689
  import fg6 from "fast-glob";
8955
9690
  function detectInstallPlan(signals) {
8956
9691
  if (signals.hasYarnLock) {
@@ -8971,10 +9706,10 @@ function detectInstallPlan(signals) {
8971
9706
  return { manager: "none" };
8972
9707
  }
8973
9708
  async function computeLockfileSignals(dir) {
8974
- const hasYarnLock = existsSync3(path53.join(dir, "yarn.lock"));
8975
- const hasPnpmLock = existsSync3(path53.join(dir, "pnpm-lock.yaml"));
8976
- const hasPackageLock = existsSync3(path53.join(dir, "package-lock.json"));
8977
- 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"));
8978
9713
  const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
8979
9714
  cwd: dir,
8980
9715
  onlyFiles: true,
@@ -8988,7 +9723,7 @@ async function computeLockfileSignals(dir) {
8988
9723
  });
8989
9724
  const hasDotnetProject = dotnetFiles.length > 0;
8990
9725
  let hasPackageManagerField = false;
8991
- const pkgPath = path53.join(dir, "package.json");
9726
+ const pkgPath = path54.join(dir, "package.json");
8992
9727
  if (existsSync3(pkgPath)) {
8993
9728
  try {
8994
9729
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -9029,8 +9764,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
9029
9764
  const start = Date.now();
9030
9765
  const targets = [];
9031
9766
  for (const target of HYDRATION_TARGETS) {
9032
- const src = path54.join(mainMemberPath, target);
9033
- const dest = path54.join(worktreeMemberPath, target);
9767
+ const src = path55.join(mainMemberPath, target);
9768
+ const dest = path55.join(worktreeMemberPath, target);
9034
9769
  if (!await fs39.pathExists(src)) continue;
9035
9770
  const destExists = await fs39.pathExists(dest);
9036
9771
  if (!opts.force && destExists) continue;
@@ -9048,11 +9783,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
9048
9783
 
9049
9784
  // src/workspace/worktree/local-files.ts
9050
9785
  init_exec();
9051
- import path55 from "node:path";
9786
+ import path56 from "node:path";
9052
9787
  import fs40 from "fs-extra";
9053
9788
  var MACHINE_LOCAL_FILES = [
9054
9789
  "docker-compose.local.yml",
9055
- path55.join(".claude", "settings.local.json")
9790
+ path56.join(".claude", "settings.local.json")
9056
9791
  ];
9057
9792
  async function isTracked(repoRoot, relFile) {
9058
9793
  const gitPath = relFile.replace(/\\/g, "/");
@@ -9062,11 +9797,11 @@ async function isTracked(repoRoot, relFile) {
9062
9797
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
9063
9798
  const copied = [];
9064
9799
  for (const rel of MACHINE_LOCAL_FILES) {
9065
- const src = path55.join(mainMemberPath, rel);
9800
+ const src = path56.join(mainMemberPath, rel);
9066
9801
  if (!await fs40.pathExists(src)) continue;
9067
9802
  if (await isTracked(mainMemberPath, rel)) continue;
9068
- const dest = path55.join(worktreeMemberPath, rel);
9069
- await fs40.ensureDir(path55.dirname(dest));
9803
+ const dest = path56.join(worktreeMemberPath, rel);
9804
+ await fs40.ensureDir(path56.dirname(dest));
9070
9805
  await fs40.copy(src, dest, { overwrite: true });
9071
9806
  copied.push(rel);
9072
9807
  }
@@ -9075,15 +9810,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
9075
9810
 
9076
9811
  // src/workspace/worktree/root.ts
9077
9812
  import { existsSync as existsSync4 } from "node:fs";
9078
- import path56 from "node:path";
9813
+ import path57 from "node:path";
9079
9814
  async function resolveWorkspaceForWorktree(start) {
9080
9815
  const rootInfo = await resolveRoots(start);
9081
9816
  if (!rootInfo.isGitRepo) {
9082
9817
  return { ok: false, reason: "Not inside a git repository." };
9083
9818
  }
9084
9819
  const workspaceRoot = rootInfo.mainRoot;
9085
- const hasYaml = existsSync4(path56.join(workspaceRoot, WORKSPACE_FILE));
9086
- 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));
9087
9822
  if (!hasYaml && !hasManifest) {
9088
9823
  return {
9089
9824
  ok: false,
@@ -9108,10 +9843,10 @@ function selectMembers(members, only) {
9108
9843
  }
9109
9844
 
9110
9845
  // src/workspace/worktree/state.ts
9111
- import path57 from "node:path";
9846
+ import path58 from "node:path";
9112
9847
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
9113
9848
  function worktreesDir(mainRoot) {
9114
- return path57.join(mainRoot, ".claude", "worktrees");
9849
+ return path58.join(mainRoot, ".claude", "worktrees");
9115
9850
  }
9116
9851
  function isValidWorktreeSlug(slug) {
9117
9852
  if (slug === "" || slug === "." || slug === "..") return false;
@@ -9124,15 +9859,15 @@ function worktreePath(mainRoot, slug) {
9124
9859
  `Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
9125
9860
  );
9126
9861
  }
9127
- return path57.join(worktreesDir(mainRoot), slug);
9862
+ return path58.join(worktreesDir(mainRoot), slug);
9128
9863
  }
9129
9864
  async function readWorktreeState(worktreeRoot) {
9130
- const state = await readJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE));
9865
+ const state = await readJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE));
9131
9866
  if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
9132
9867
  return state;
9133
9868
  }
9134
9869
  async function writeWorktreeState(worktreeRoot, state) {
9135
- await writeJson(path57.join(worktreeRoot, WORKTREE_STATE_FILE), state);
9870
+ await writeJson(path58.join(worktreeRoot, WORKTREE_STATE_FILE), state);
9136
9871
  }
9137
9872
 
9138
9873
  // src/workspace/worktree/add.ts
@@ -9166,7 +9901,7 @@ async function runAdd(opts) {
9166
9901
  const memberResults = [];
9167
9902
  for (const member of selected) {
9168
9903
  const start = Date.now();
9169
- const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
9904
+ const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
9170
9905
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
9171
9906
  preferDefaultBranchFrom: true
9172
9907
  });
@@ -9196,7 +9931,7 @@ async function runAdd(opts) {
9196
9931
  await mapWithConcurrency(okResults, async (result) => {
9197
9932
  const member = selected.find((m) => m.id === result.member);
9198
9933
  if (!member) return;
9199
- const memberWorktreePath = path58.join(wsWorktreePath, member.folder);
9934
+ const memberWorktreePath = path59.join(wsWorktreePath, member.folder);
9200
9935
  const hydrationStart = Date.now();
9201
9936
  if (opts.hydrate !== false) {
9202
9937
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -9263,10 +9998,10 @@ async function runDryRun(args) {
9263
9998
 
9264
9999
  // src/workspace/worktree/doctor.ts
9265
10000
  import { existsSync as existsSync5 } from "node:fs";
9266
- import path59 from "node:path";
10001
+ import path60 from "node:path";
9267
10002
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
9268
10003
  function looksHydrated(memberPath) {
9269
- return HYDRATION_MARKERS.some((marker) => existsSync5(path59.join(memberPath, marker)));
10004
+ return HYDRATION_MARKERS.some((marker) => existsSync5(path60.join(memberPath, marker)));
9270
10005
  }
9271
10006
  async function runDoctor2() {
9272
10007
  const rootInfo = await resolveRoots();
@@ -9280,7 +10015,7 @@ async function runDoctor2() {
9280
10015
  };
9281
10016
  }
9282
10017
  const workspaceRoot = rootInfo.mainRoot;
9283
- 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));
9284
10019
  if (!isWorkspace) {
9285
10020
  return {
9286
10021
  isWorkspace: false,
@@ -9293,7 +10028,7 @@ async function runDoctor2() {
9293
10028
  };
9294
10029
  }
9295
10030
  const wsWorktreesDir = worktreesDir(workspaceRoot);
9296
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path59.sep}`);
10031
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path60.sep}`);
9297
10032
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
9298
10033
  const members = await readMembers(rootInfo);
9299
10034
  const problems = [];
@@ -9301,10 +10036,10 @@ async function runDoctor2() {
9301
10036
  let slug;
9302
10037
  if (inWorkspaceWorktree) {
9303
10038
  const wtPath = rootInfo.repoRoot;
9304
- slug = rootInfo.worktreeName ?? path59.basename(wtPath);
10039
+ slug = rootInfo.worktreeName ?? path60.basename(wtPath);
9305
10040
  const state = await readWorktreeState(wtPath);
9306
10041
  for (const member of members) {
9307
- const memberPath = path59.join(wtPath, member.folder);
10042
+ const memberPath = path60.join(wtPath, member.folder);
9308
10043
  const materialized = existsSync5(memberPath);
9309
10044
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
9310
10045
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -9335,16 +10070,16 @@ async function runDoctor2() {
9335
10070
  for (const member of members) {
9336
10071
  const entries = await listWorktrees(member.absPath);
9337
10072
  for (const entry of entries) {
9338
- const entryPath = path59.normalize(entry.path);
9339
- 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}`;
9340
10075
  const idx = entryPath.indexOf(marker);
9341
10076
  if (idx === -1) continue;
9342
10077
  const rest = entryPath.slice(idx + marker.length);
9343
- const slugName = rest.split(path59.sep)[0];
10078
+ const slugName = rest.split(path60.sep)[0];
9344
10079
  if (!slugName) continue;
9345
- if (!existsSync5(path59.join(wsWorktreesDir, slugName))) {
10080
+ if (!existsSync5(path60.join(wsWorktreesDir, slugName))) {
9346
10081
  orphans.push(
9347
- `${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\`.`
9348
10083
  );
9349
10084
  }
9350
10085
  }
@@ -9355,7 +10090,7 @@ async function runDoctor2() {
9355
10090
 
9356
10091
  // src/workspace/worktree/hydrate-command.ts
9357
10092
  import { existsSync as existsSync6 } from "node:fs";
9358
- import path60 from "node:path";
10093
+ import path61 from "node:path";
9359
10094
  async function runHydrateWorktree(opts) {
9360
10095
  const rootInfo = await resolveRoots();
9361
10096
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -9366,7 +10101,7 @@ async function runHydrateWorktree(opts) {
9366
10101
  }
9367
10102
  const workspaceRoot = rootInfo.mainRoot;
9368
10103
  const wtPath = rootInfo.repoRoot;
9369
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path60.sep}`)) {
10104
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path61.sep}`)) {
9370
10105
  return {
9371
10106
  ok: false,
9372
10107
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -9378,13 +10113,13 @@ async function runHydrateWorktree(opts) {
9378
10113
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
9379
10114
  }
9380
10115
  const state = await readWorktreeState(wtPath);
9381
- const slug = state?.slug ?? path60.basename(wtPath);
10116
+ const slug = state?.slug ?? path61.basename(wtPath);
9382
10117
  const detectedBranch = await currentBranch(wtPath);
9383
10118
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
9384
10119
  if (opts.dryRun) {
9385
10120
  const results2 = await Promise.all(
9386
10121
  selected.map(async (member) => {
9387
- const memberPath = path60.join(wtPath, member.folder);
10122
+ const memberPath = path61.join(wtPath, member.folder);
9388
10123
  if (existsSync6(memberPath)) {
9389
10124
  return { member: member.id, folder: member.folder, status: "planned" };
9390
10125
  }
@@ -9405,7 +10140,7 @@ async function runHydrateWorktree(opts) {
9405
10140
  const results = await mapWithConcurrency(
9406
10141
  selected,
9407
10142
  async (member) => {
9408
- const memberPath = path60.join(wtPath, member.folder);
10143
+ const memberPath = path61.join(wtPath, member.folder);
9409
10144
  let branchAction;
9410
10145
  if (!existsSync6(memberPath)) {
9411
10146
  if (!opts.materialize) {
@@ -9468,7 +10203,7 @@ async function runHydrateWorktree(opts) {
9468
10203
  }
9469
10204
 
9470
10205
  // src/workspace/worktree/list.ts
9471
- import path61 from "node:path";
10206
+ import path62 from "node:path";
9472
10207
  import fs42 from "fs-extra";
9473
10208
  async function runList2() {
9474
10209
  const resolved = await resolveWorkspaceForWorktree();
@@ -9481,12 +10216,12 @@ async function runList2() {
9481
10216
  const configuredMembers = await readMembers(rootInfo);
9482
10217
  const worktrees = [];
9483
10218
  for (const slug of slugs) {
9484
- const wtPath = path61.join(dir, slug);
10219
+ const wtPath = path62.join(dir, slug);
9485
10220
  const state = await readWorktreeState(wtPath);
9486
10221
  const members = [];
9487
10222
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
9488
10223
  for (const m of source) {
9489
- const memberPath = path61.join(wtPath, m.folder);
10224
+ const memberPath = path62.join(wtPath, m.folder);
9490
10225
  const materialized = await fs42.pathExists(memberPath);
9491
10226
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
9492
10227
  members.push({
@@ -9508,7 +10243,7 @@ async function runList2() {
9508
10243
  }
9509
10244
 
9510
10245
  // src/workspace/worktree/remove.ts
9511
- import path62 from "node:path";
10246
+ import path63 from "node:path";
9512
10247
  import fs43 from "fs-extra";
9513
10248
  async function runRemove(opts) {
9514
10249
  const resolved = await resolveWorkspaceForWorktree();
@@ -9535,7 +10270,7 @@ async function runRemove(opts) {
9535
10270
  targets.push({
9536
10271
  repo: configMember?.id ?? stateMember.id,
9537
10272
  ownerRepo,
9538
- path: path62.join(wsWorktreePath, stateMember.folder)
10273
+ path: path63.join(wsWorktreePath, stateMember.folder)
9539
10274
  });
9540
10275
  }
9541
10276
  const existingTargets = [];
@@ -9603,12 +10338,6 @@ async function runRemove(opts) {
9603
10338
  }
9604
10339
 
9605
10340
  // src/commands/workspace/worktree.ts
9606
- function normalizeOnly(only) {
9607
- if (!only) return void 0;
9608
- const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
9609
- const cleaned = list.map((s) => s.trim()).filter(Boolean);
9610
- return cleaned.length > 0 ? cleaned : void 0;
9611
- }
9612
10341
  function describeInstall(hydration) {
9613
10342
  if (hydration.install.plan.manager === "none") return "no install step (no recognized lockfile)";
9614
10343
  if (!hydration.install.ran)
@@ -9812,7 +10541,7 @@ async function runWorktree(action, opts = {}) {
9812
10541
 
9813
10542
  // src/commands/workspace.ts
9814
10543
  import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
9815
- import path71 from "node:path";
10544
+ import path72 from "node:path";
9816
10545
 
9817
10546
  // src/commands/workspace/aggregate.ts
9818
10547
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -9867,18 +10596,18 @@ ${summaries.map(
9867
10596
 
9868
10597
  // src/commands/workspace/doctor.ts
9869
10598
  import { existsSync as existsSync7 } from "node:fs";
9870
- import path64 from "node:path";
10599
+ import path65 from "node:path";
9871
10600
 
9872
10601
  // src/commands/workspace/manifest.ts
9873
10602
  import { readFileSync as readFileSync4 } from "node:fs";
9874
- import path63 from "node:path";
10603
+ import path64 from "node:path";
9875
10604
  var MANIFEST_FILE = "workspace.manifest.json";
9876
10605
  function manifestPath(workspaceRoot) {
9877
10606
  return hausPath(workspaceRoot, MANIFEST_FILE);
9878
10607
  }
9879
10608
  function hausVersion() {
9880
10609
  try {
9881
- const pkg = JSON.parse(readFileSync4(path63.join(packageRoot(), "package.json"), "utf8"));
10610
+ const pkg = JSON.parse(readFileSync4(path64.join(packageRoot(), "package.json"), "utf8"));
9882
10611
  return pkg.version ?? "0.0.0";
9883
10612
  } catch {
9884
10613
  return "0.0.0";
@@ -9970,7 +10699,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
9970
10699
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
9971
10700
  const catalogRefByRepo = [];
9972
10701
  for (const repo of config.repos) {
9973
- const repoRoot = path64.resolve(workspaceRoot, repo.path);
10702
+ const repoRoot = path65.resolve(workspaceRoot, repo.path);
9974
10703
  const entry = manifestByName.get(repo.name);
9975
10704
  if (!entry) {
9976
10705
  flag({
@@ -10043,7 +10772,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
10043
10772
  });
10044
10773
  continue;
10045
10774
  }
10046
- if (!existsSync7(path64.join(workspaceRoot, entry.path))) {
10775
+ if (!existsSync7(path65.join(workspaceRoot, entry.path))) {
10047
10776
  flag({
10048
10777
  repo: entry.repo,
10049
10778
  kind: "missing-linked-context-copy",
@@ -10086,15 +10815,15 @@ function emit(args) {
10086
10815
  }
10087
10816
 
10088
10817
  // src/workspace/link-context/link.ts
10089
- import path67 from "node:path";
10818
+ import path68 from "node:path";
10090
10819
  import fs46 from "fs-extra";
10091
10820
 
10092
10821
  // src/workspace/link-context/plan.ts
10093
- import path66 from "node:path";
10822
+ import path67 from "node:path";
10094
10823
  import fs45 from "fs-extra";
10095
10824
 
10096
10825
  // src/workspace/link-context/scan-source.ts
10097
- import path65 from "node:path";
10826
+ import path66 from "node:path";
10098
10827
  import fs44 from "fs-extra";
10099
10828
  var ASSET_SUBDIR = {
10100
10829
  skill: "skills",
@@ -10103,36 +10832,36 @@ var ASSET_SUBDIR = {
10103
10832
  };
10104
10833
  async function hasLinkableContext(member) {
10105
10834
  for (const subdir of Object.values(ASSET_SUBDIR)) {
10106
- 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;
10107
10836
  }
10108
10837
  return false;
10109
10838
  }
10110
10839
  async function collectSkills(member) {
10111
- const dir = path65.join(member.absPath, ".claude", "skills");
10840
+ const dir = path66.join(member.absPath, ".claude", "skills");
10112
10841
  if (!await fs44.pathExists(dir)) return [];
10113
10842
  const names = await fs44.readdir(dir);
10114
10843
  const out = [];
10115
10844
  for (const name of [...names].sort()) {
10116
- const abs = path65.join(dir, name);
10845
+ const abs = path66.join(dir, name);
10117
10846
  const stat = await fs44.lstat(abs);
10118
10847
  if (!stat.isDirectory()) continue;
10119
- if (!await fs44.pathExists(path65.join(abs, "SKILL.md"))) continue;
10120
- 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) });
10121
10850
  }
10122
10851
  return out;
10123
10852
  }
10124
10853
  async function collectFlatMarkdown(member, type) {
10125
10854
  const subdir = ASSET_SUBDIR[type];
10126
- const dir = path65.join(member.absPath, ".claude", subdir);
10855
+ const dir = path66.join(member.absPath, ".claude", subdir);
10127
10856
  if (!await fs44.pathExists(dir)) return [];
10128
10857
  const names = await fs44.readdir(dir);
10129
10858
  const out = [];
10130
10859
  for (const fileName of [...names].sort()) {
10131
10860
  if (!fileName.endsWith(".md")) continue;
10132
- const abs = path65.join(dir, fileName);
10861
+ const abs = path66.join(dir, fileName);
10133
10862
  if (!(await fs44.lstat(abs)).isFile()) continue;
10134
10863
  const name = fileName.slice(0, -".md".length);
10135
- out.push({ type, name, sourceRelPath: path65.posix.join(".claude", subdir, fileName) });
10864
+ out.push({ type, name, sourceRelPath: path66.posix.join(".claude", subdir, fileName) });
10136
10865
  }
10137
10866
  return out;
10138
10867
  }
@@ -10147,9 +10876,9 @@ async function collectSourceAssets(member) {
10147
10876
 
10148
10877
  // src/workspace/link-context/plan.ts
10149
10878
  function repoFolderPrefix(member) {
10150
- const base = path66.posix.basename(member.folder);
10879
+ const base = path67.posix.basename(member.folder);
10151
10880
  if (base && base !== "." && base !== "/") return base;
10152
- const fromAbs = path66.basename(member.absPath);
10881
+ const fromAbs = path67.basename(member.absPath);
10153
10882
  return fromAbs || member.id;
10154
10883
  }
10155
10884
  function assetKey(type, destKey) {
@@ -10206,7 +10935,7 @@ async function buildLinkPlan(members) {
10206
10935
  name: asset.name,
10207
10936
  destKey,
10208
10937
  sourceRelPath: asset.sourceRelPath,
10209
- sourceAbsPath: path66.join(member.absPath, asset.sourceRelPath),
10938
+ sourceAbsPath: path67.join(member.absPath, asset.sourceRelPath),
10210
10939
  sourceHash
10211
10940
  });
10212
10941
  }
@@ -10217,7 +10946,7 @@ async function buildLinkPlan(members) {
10217
10946
  function destRelPathFor(type, destKey) {
10218
10947
  const subdir = `${type}s`;
10219
10948
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
10220
- return path67.posix.join(".claude", subdir, leaf);
10949
+ return path68.posix.join(".claude", subdir, leaf);
10221
10950
  }
10222
10951
  async function runLinkContext(workspaceRoot, opts = {}) {
10223
10952
  const write = opts.write ?? false;
@@ -10243,9 +10972,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
10243
10972
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
10244
10973
  for (const entry of plan.entries) {
10245
10974
  const destRel = destRelPathFor(entry.type, entry.destKey);
10246
- const destAbs = path67.join(workspaceRoot, destRel);
10975
+ const destAbs = path68.join(workspaceRoot, destRel);
10247
10976
  if (write) {
10248
- await fs46.ensureDir(path67.dirname(destAbs));
10977
+ await fs46.ensureDir(path68.dirname(destAbs));
10249
10978
  if (await fs46.pathExists(destAbs)) await fs46.remove(destAbs);
10250
10979
  await fs46.copy(entry.sourceAbsPath, destAbs, {
10251
10980
  overwrite: true,
@@ -10334,10 +11063,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
10334
11063
 
10335
11064
  // src/commands/workspace/setup.ts
10336
11065
  import { existsSync as existsSync8, statSync } from "node:fs";
10337
- import path69 from "node:path";
11066
+ import path70 from "node:path";
10338
11067
 
10339
11068
  // src/claude/write-workspace-claude-md.ts
10340
- import path68 from "node:path";
11069
+ import path69 from "node:path";
10341
11070
  import fs47 from "fs-extra";
10342
11071
  var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
10343
11072
  function buildWorkspaceImportBlock(client, members) {
@@ -10357,7 +11086,7 @@ ${WORKSPACE_BLOCK_END}`;
10357
11086
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
10358
11087
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
10359
11088
  const dryRun = opts.dryRun ?? false;
10360
- 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");
10361
11090
  const say = opts.quiet ? () => {
10362
11091
  } : log;
10363
11092
  const existed = await fs47.pathExists(filePath);
@@ -10388,20 +11117,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
10388
11117
 
10389
11118
  // src/commands/workspace/setup.ts
10390
11119
  function resolveWorkspaceRoot(start = process.cwd()) {
10391
- let dir = path69.resolve(start);
11120
+ let dir = path70.resolve(start);
10392
11121
  for (; ; ) {
10393
- if (existsSync8(path69.join(dir, WORKSPACE_FILE))) return dir;
10394
- const parent = path69.dirname(dir);
10395
- 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);
10396
11125
  dir = parent;
10397
11126
  }
10398
11127
  }
10399
11128
  function isRootRepo(workspaceRoot, repoPath) {
10400
- return path69.resolve(workspaceRoot, repoPath) === path69.resolve(workspaceRoot);
11129
+ return path70.resolve(workspaceRoot, repoPath) === path70.resolve(workspaceRoot);
10401
11130
  }
10402
11131
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
10403
11132
  const apply = options.write ?? false;
10404
- const configText = await readText(path69.join(workspaceRoot, WORKSPACE_FILE));
11133
+ const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
10405
11134
  if (!configText) {
10406
11135
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
10407
11136
  process.exitCode = 1;
@@ -10428,7 +11157,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10428
11157
  const statuses = [];
10429
11158
  const aggregateInputs = [];
10430
11159
  for (const repo of repos) {
10431
- const repoRoot = path69.resolve(workspaceRoot, repo.path);
11160
+ const repoRoot = path70.resolve(workspaceRoot, repo.path);
10432
11161
  say(`
10433
11162
  \u2192 ${repo.name} (${repo.path})`);
10434
11163
  try {
@@ -10468,7 +11197,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10468
11197
  process.exitCode = 1;
10469
11198
  return result2;
10470
11199
  }
10471
- throw err;
11200
+ throw new Error(`Setup failed for ${repo.name}: ${message}`, { cause: err });
10472
11201
  }
10473
11202
  error(`Setup failed for ${repo.name}: ${message}`);
10474
11203
  }
@@ -10518,7 +11247,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10518
11247
  const status = statusByName.get(repo.name);
10519
11248
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
10520
11249
  if (status?.status === "ok") {
10521
- const lock = await checkLock(path69.resolve(workspaceRoot, repo.path));
11250
+ const lock = await checkLock(path70.resolve(workspaceRoot, repo.path));
10522
11251
  manifestRepos.push({
10523
11252
  name: repo.name,
10524
11253
  path: repo.path,
@@ -10574,7 +11303,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10574
11303
  try {
10575
11304
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
10576
11305
  if (linkResult.ok) {
10577
- written.push(...linkResult.added.map((rel) => path69.join(workspaceRoot, rel)));
11306
+ written.push(...linkResult.added.map((rel) => path70.join(workspaceRoot, rel)));
10578
11307
  if (!options.json) {
10579
11308
  say(
10580
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` : ""}.`
@@ -10606,7 +11335,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
10606
11335
 
10607
11336
  // src/commands/workspace/undo.ts
10608
11337
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
10609
- import path70 from "node:path";
11338
+ import path71 from "node:path";
10610
11339
  import fs48 from "fs-extra";
10611
11340
  var WORKSPACE_AGGREGATE_FILES = [
10612
11341
  "workspace-summary.json",
@@ -10643,7 +11372,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10643
11372
  }
10644
11373
  let anyRepoFailed = false;
10645
11374
  for (const repo of config.repos) {
10646
- const repoRoot = path70.resolve(workspaceRoot, repo.path);
11375
+ const repoRoot = path71.resolve(workspaceRoot, repo.path);
10647
11376
  log(`
10648
11377
  \u2192 ${repo.name} (${repo.path})`);
10649
11378
  try {
@@ -10672,20 +11401,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10672
11401
  const abs = hausPath(workspaceRoot, rel);
10673
11402
  if (await fs48.pathExists(abs)) {
10674
11403
  await fs48.remove(abs);
10675
- log(`Removed ${path70.relative(workspaceRoot, abs)}`);
11404
+ log(`Removed ${path71.relative(workspaceRoot, abs)}`);
10676
11405
  }
10677
11406
  }
10678
11407
  const manifestFile = manifestPath(workspaceRoot);
10679
11408
  if (await fs48.pathExists(manifestFile)) {
10680
11409
  await fs48.remove(manifestFile);
10681
- log(`Removed ${path70.relative(workspaceRoot, manifestFile)}`);
11410
+ log(`Removed ${path71.relative(workspaceRoot, manifestFile)}`);
10682
11411
  }
10683
11412
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
10684
11413
  if (await fs48.pathExists(workspaceMdPath)) {
10685
11414
  await fs48.remove(workspaceMdPath);
10686
- log(`Removed ${path70.relative(workspaceRoot, workspaceMdPath)}`);
11415
+ log(`Removed ${path71.relative(workspaceRoot, workspaceMdPath)}`);
10687
11416
  } else {
10688
- const claudeMdPath = path70.join(workspaceRoot, "CLAUDE.md");
11417
+ const claudeMdPath = path71.join(workspaceRoot, "CLAUDE.md");
10689
11418
  if (existsSync9(claudeMdPath)) {
10690
11419
  const prev = await readText(claudeMdPath);
10691
11420
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
@@ -10724,12 +11453,6 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10724
11453
  }
10725
11454
 
10726
11455
  // src/commands/workspace.ts
10727
- function normalizeOnly2(only) {
10728
- if (!only) return void 0;
10729
- const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
10730
- const cleaned = list.map((s) => s.trim()).filter(Boolean);
10731
- return cleaned.length > 0 ? cleaned : void 0;
10732
- }
10733
11456
  function normalizeMaxDepth(maxDepth) {
10734
11457
  if (maxDepth === void 0) return void 0;
10735
11458
  const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
@@ -10760,7 +11483,7 @@ relationships: []
10760
11483
  log("Workspace initialized.");
10761
11484
  }
10762
11485
  async function scanWorkspace(workspaceRoot, opts) {
10763
- const configText = await readText(path71.join(workspaceRoot, WORKSPACE_FILE));
11486
+ const configText = await readText(path72.join(workspaceRoot, WORKSPACE_FILE));
10764
11487
  if (!configText) {
10765
11488
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
10766
11489
  process.exitCode = 1;
@@ -10782,7 +11505,7 @@ async function scanWorkspace(workspaceRoot, opts) {
10782
11505
  const inputs = [];
10783
11506
  const failures = [];
10784
11507
  for (const repo of config.repos) {
10785
- const repoRoot = path71.resolve(workspaceRoot, repo.path);
11508
+ const repoRoot = path72.resolve(workspaceRoot, repo.path);
10786
11509
  try {
10787
11510
  if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
10788
11511
  throw new Error("path is not a directory");
@@ -10830,7 +11553,7 @@ async function runWorkspace(action, options = {}) {
10830
11553
  dryRun: options.dryRun,
10831
11554
  json: options.json,
10832
11555
  continueOnError: options.continueOnError,
10833
- only: normalizeOnly2(options.only)
11556
+ only: normalizeOnly(options.only)
10834
11557
  });
10835
11558
  return;
10836
11559
  case "doctor":
@@ -10848,7 +11571,7 @@ async function runWorkspace(action, options = {}) {
10848
11571
  // src/cli.ts
10849
11572
  function cliVersion() {
10850
11573
  try {
10851
- const pkgPath = path72.join(packageRoot(), "package.json");
11574
+ const pkgPath = path73.join(packageRoot(), "package.json");
10852
11575
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10853
11576
  return pkg.version ?? "0.0.0";
10854
11577
  } catch {
@@ -10858,7 +11581,7 @@ function cliVersion() {
10858
11581
  var program = new Command();
10859
11582
  function validateRuntimeNodeVersion() {
10860
11583
  try {
10861
- const pkgPath = path72.join(packageRoot(), "package.json");
11584
+ const pkgPath = path73.join(packageRoot(), "package.json");
10862
11585
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10863
11586
  const requiredRange = pkg.engines?.node;
10864
11587
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -10872,18 +11595,22 @@ function validateRuntimeNodeVersion() {
10872
11595
  }
10873
11596
  validateRuntimeNodeVersion();
10874
11597
  program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
10875
- program.command("scan").option("--json").action(runScan);
10876
- 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(
10877
11602
  "--include <ids...>",
10878
11603
  "Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
10879
11604
  ).action(runRecommend);
10880
- 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(
10881
11606
  "--force",
10882
11607
  "Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
10883
11608
  ).action(runSetupProject);
10884
- 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);
10885
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);
10886
- 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(
10887
11614
  "--ids <ids...>",
10888
11615
  "Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
10889
11616
  ).option(
@@ -10896,33 +11623,33 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
10896
11623
  "--prune",
10897
11624
  "Remove lock-tracked catalog items no longer in the current recommendation (hash-gated, backed up first)"
10898
11625
  ).action(runApply);
10899
- program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
10900
- 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/");
10901
11628
  backups.command("list").description("List backups under .haus-workflow/backups/").action(() => runBackups("list"));
10902
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 }));
10903
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(
10904
11631
  (opts) => runBackups("prune", { olderThan: opts.olderThan, keep: opts.keep, yes: opts.yes })
10905
11632
  );
10906
- program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
10907
- 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);
10908
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(
10909
11636
  (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
10910
11637
  );
10911
- program.command("refresh").action(runRefresh);
10912
- program.command("catalog-audit").action(runCatalogAudit);
10913
- program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
10914
- 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);
10915
11642
  program.command("ci-gate").description(
10916
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."
10917
11644
  ).option("--json", "Emit machine-readable {doctor, decisions, update, ok}").action((opts) => runCiGate(opts));
10918
- 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);
10919
11646
  program.command("scaffold [ids...]").description(
10920
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."
10921
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);
10922
- program.command("uninstall").option("--force").option("-y, --yes", "Skip confirmation").action(runUninstallCommand);
10923
- var guard = program.command("guard");
10924
- guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
10925
- 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));
10926
11653
  function registerDecisionsCommand(name) {
10927
11654
  const cmd = program.command(name).description("Architecture decision records (ADR) gate and drafts");
10928
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));
@@ -10935,7 +11662,7 @@ function registerDecisionsCommand(name) {
10935
11662
  }
10936
11663
  registerDecisionsCommand("decisions");
10937
11664
  registerDecisionsCommand("adr");
10938
- var workspace = program.command("workspace");
11665
+ var workspace = program.command("workspace").description("Multi-repo workspace commands: discover, setup, link-context, worktree, doctor");
10939
11666
  workspace.command("init").option("-y, --yes", "Skip confirmation when haus.workspace.yaml already exists").action((opts) => runWorkspace("init", opts));
10940
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));
10941
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));