@haus-tech/haus-workflow 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
- import path36 from "path";
5
+ import path34 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path14 from "path";
9
+ import path13 from "path";
10
10
  import checkbox from "@inquirer/checkbox";
11
11
  import fs13 from "fs-extra";
12
12
 
@@ -839,14 +839,7 @@ async function getCacheManifestAge() {
839
839
  }
840
840
 
841
841
  // src/install/allow-rules.ts
842
- var ALLOWED_SUBCOMMANDS = [
843
- "setup-project",
844
- "apply",
845
- "doctor",
846
- "scan",
847
- "context",
848
- "recommend"
849
- ];
842
+ var ALLOWED_SUBCOMMANDS = ["setup-project", "apply", "doctor", "scan", "recommend"];
850
843
  function buildAllowRules() {
851
844
  return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
852
845
  }
@@ -901,6 +894,44 @@ function collectEventHookCommands(entries) {
901
894
  }
902
895
  return cmds;
903
896
  }
897
+ var RETIRED_HAUS_HOOK_COMMANDS = ["haus context --from-hook"];
898
+ var RETIRED_HAUS_HOOK_IDS = ["hook.context", "haus.context-hook"];
899
+ function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, addedCommands) {
900
+ const canonicalCommands = new Set(
901
+ fragments.filter((f) => f.gate === "keep").map((f) => f.command)
902
+ );
903
+ const canonicalIds = new Set(fragments.filter((f) => f.gate === "keep").map((f) => f.id));
904
+ const retiredCommands = new Set(RETIRED_HAUS_HOOK_COMMANDS);
905
+ for (const cmd of [...priorHaus?.hookCommands ?? [], ...addedCommands]) {
906
+ if (!canonicalCommands.has(cmd)) retiredCommands.add(cmd);
907
+ }
908
+ const prunedHooks = {};
909
+ for (const [event, entries] of Object.entries(updated.hooks ?? {})) {
910
+ const kept = entries.filter((entry) => {
911
+ const cmds = (entry.hooks ?? []).map((h) => h.command).filter(Boolean);
912
+ return cmds.length === 0 || !cmds.some((cmd) => retiredCommands.has(cmd));
913
+ });
914
+ if (kept.length > 0) prunedHooks[event] = kept;
915
+ }
916
+ updated.hooks = prunedHooks;
917
+ const retiredIdSet = new Set(RETIRED_HAUS_HOOK_IDS);
918
+ for (const id of priorHaus?.hooks ?? []) {
919
+ if (!canonicalIds.has(id)) retiredIdSet.add(id);
920
+ }
921
+ const survivingIds = [.../* @__PURE__ */ new Set([...priorHaus?.hooks ?? [], ...addedIds])].filter(
922
+ (id) => canonicalIds.has(id) && !retiredIdSet.has(id)
923
+ );
924
+ const survivingCommands = [
925
+ .../* @__PURE__ */ new Set([...priorHaus?.hookCommands ?? [], ...addedCommands])
926
+ ].filter((cmd) => canonicalCommands.has(cmd));
927
+ updated._haus = {
928
+ hooks: survivingIds,
929
+ ...survivingCommands.length > 0 ? { hookCommands: survivingCommands } : {},
930
+ ...priorHaus?.denyRules ? { denyRules: priorHaus.denyRules } : {},
931
+ ...priorHaus?.allowRules ? { allowRules: priorHaus.allowRules } : {},
932
+ ...priorHaus?.askRules ? { askRules: priorHaus.askRules } : {}
933
+ };
934
+ }
904
935
  function mergeHooks(settings, fragments) {
905
936
  const existing = settings._haus?.hooks ?? [];
906
937
  const existingCommands = settings._haus?.hookCommands ?? [];
@@ -926,14 +957,7 @@ function mergeHooks(settings, fragments) {
926
957
  if (!existing.includes(fragment.id)) addedIds.push(fragment.id);
927
958
  if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
928
959
  }
929
- updated._haus = {
930
- hooks: [...existing, ...addedIds],
931
- hookCommands: [...existingCommands, ...addedCommands],
932
- // Preserve deny/allow/ask tracking so hook, deny, allow, and ask merges are order-independent.
933
- ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
934
- ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
935
- ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
936
- };
960
+ reconcileRetiredHausHooks(updated, fragments, settings._haus, addedIds, addedCommands);
937
961
  return { settings: updated, addedIds };
938
962
  }
939
963
  function reconcileManagedRules(existing, prevTracked, newRules) {
@@ -1185,8 +1209,8 @@ function buildDenyRules() {
1185
1209
  for (const command of DENY_COMMANDS) {
1186
1210
  rules.push(`Bash(${command}:*)`);
1187
1211
  }
1188
- for (const path37 of DENY_PATHS) {
1189
- const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
1212
+ for (const path35 of DENY_PATHS) {
1213
+ const pattern = DENY_DIRS.has(path35) ? `${path35}/**` : path35;
1190
1214
  for (const tool of FILE_TOOLS) {
1191
1215
  rules.push(`${tool}(${pattern})`);
1192
1216
  }
@@ -1242,12 +1266,6 @@ function packageRoot() {
1242
1266
 
1243
1267
  // src/claude/merge-project-settings.ts
1244
1268
  var PROJECT_HOOK_FRAGMENTS = [
1245
- {
1246
- id: "haus.context-hook",
1247
- gate: "keep",
1248
- event: "UserPromptSubmit",
1249
- command: "haus context --from-hook"
1250
- },
1251
1269
  {
1252
1270
  id: "haus.guard-file",
1253
1271
  gate: "keep",
@@ -1285,7 +1303,7 @@ async function applyProjectSettingsMerge(root) {
1285
1303
  }
1286
1304
 
1287
1305
  // src/claude/write-claude-files.ts
1288
- import path13 from "path";
1306
+ import path12 from "path";
1289
1307
  import fs12 from "fs-extra";
1290
1308
 
1291
1309
  // src/catalog/load-catalog.ts
@@ -1388,19 +1406,6 @@ async function hashInstalledPaths(root, relPaths) {
1388
1406
  return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
1389
1407
  }
1390
1408
 
1391
- // src/claude/load-hooks-config.ts
1392
- import path8 from "path";
1393
- var CONFIG_PATH = ".haus-workflow/config.json";
1394
- var DEFAULT_HOOKS_CONFIG = {
1395
- hooks: {
1396
- context: { enabled: false }
1397
- }
1398
- };
1399
- async function isHookEnabled(root, key) {
1400
- const cfg = await readJson(path8.join(root, CONFIG_PATH));
1401
- return cfg?.hooks?.[key]?.enabled === true;
1402
- }
1403
-
1404
1409
  // src/claude/managed-write.ts
1405
1410
  import fs5 from "fs-extra";
1406
1411
 
@@ -1454,7 +1459,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
1454
1459
  }
1455
1460
 
1456
1461
  // src/claude/superpowers-install.ts
1457
- import path9 from "path";
1462
+ import path8 from "path";
1458
1463
  import fg3 from "fast-glob";
1459
1464
  import fs6 from "fs-extra";
1460
1465
  var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
@@ -1471,7 +1476,7 @@ function rewriteSuperpowersMarkdown(text) {
1471
1476
  async function rewriteMarkdownTree(dir) {
1472
1477
  const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
1473
1478
  for (const rel of files) {
1474
- const abs = path9.join(dir, rel);
1479
+ const abs = path8.join(dir, rel);
1475
1480
  const text = await fs6.readFile(abs, "utf8");
1476
1481
  const rewritten = rewriteSuperpowersMarkdown(text);
1477
1482
  if (rewritten !== text) {
@@ -1481,7 +1486,7 @@ async function rewriteMarkdownTree(dir) {
1481
1486
  }
1482
1487
  async function installCatalogSkill(sourcePath, destination, opts) {
1483
1488
  if (opts.dryRun) return;
1484
- await fs6.ensureDir(path9.dirname(destination));
1489
+ await fs6.ensureDir(path8.dirname(destination));
1485
1490
  if (await fs6.pathExists(destination)) {
1486
1491
  await fs6.remove(destination);
1487
1492
  }
@@ -1491,17 +1496,17 @@ async function installCatalogSkill(sourcePath, destination, opts) {
1491
1496
  }
1492
1497
  }
1493
1498
  async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
1494
- const source = path9.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
1499
+ const source = path8.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
1495
1500
  if (!await fs6.pathExists(source)) return null;
1496
1501
  const destination = claudePath(projectRoot, "skills", "shared");
1497
- if (dryRun) return path9.relative(projectRoot, destination);
1498
- await fs6.ensureDir(path9.dirname(destination));
1502
+ if (dryRun) return path8.relative(projectRoot, destination);
1503
+ await fs6.ensureDir(path8.dirname(destination));
1499
1504
  if (await fs6.pathExists(destination)) {
1500
1505
  await fs6.remove(destination);
1501
1506
  }
1502
1507
  await fs6.copy(source, destination, { overwrite: true, errorOnExist: false });
1503
1508
  await rewriteMarkdownTree(destination);
1504
- return path9.relative(projectRoot, destination);
1509
+ return path8.relative(projectRoot, destination);
1505
1510
  }
1506
1511
 
1507
1512
  // src/claude/verify-hooks-contract.ts
@@ -1510,11 +1515,6 @@ import fs7 from "fs-extra";
1510
1515
  // src/claude/load-hooks.ts
1511
1516
  var CANONICAL_HOOKS = {
1512
1517
  hooks: {
1513
- UserPromptSubmit: [
1514
- {
1515
- hooks: [{ type: "command", command: "haus context --from-hook" }]
1516
- }
1517
- ],
1518
1518
  PreToolUse: [
1519
1519
  {
1520
1520
  matcher: "Read|Edit|Write",
@@ -1545,11 +1545,6 @@ function collectHookCommands(settings) {
1545
1545
  }
1546
1546
  function hausHookContractSatisfied(project, canonical) {
1547
1547
  const present = new Set(collectHookCommands(project));
1548
- for (const block of canonical.hooks.UserPromptSubmit) {
1549
- for (const h of block.hooks) {
1550
- if (!present.has(h.command)) return false;
1551
- }
1552
- }
1553
1548
  for (const block of canonical.hooks.PreToolUse) {
1554
1549
  for (const h of block.hooks) {
1555
1550
  if (!present.has(h.command)) return false;
@@ -1604,7 +1599,7 @@ async function verifyProjectSettingsHooksContract(root) {
1604
1599
  }
1605
1600
 
1606
1601
  // src/claude/write-root-claude-md.ts
1607
- import path10 from "path";
1602
+ import path9 from "path";
1608
1603
  import fs8 from "fs-extra";
1609
1604
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
1610
1605
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
@@ -1645,7 +1640,7 @@ ${block}
1645
1640
  `;
1646
1641
  }
1647
1642
  async function writeRootClaudeMd(root, dryRun) {
1648
- const filePath = path10.join(root, "CLAUDE.md");
1643
+ const filePath = path9.join(root, "CLAUDE.md");
1649
1644
  const block = buildImportBlock();
1650
1645
  const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
1651
1646
  const next = injectHausBlock(prev, block);
@@ -1654,11 +1649,11 @@ async function writeRootClaudeMd(root, dryRun) {
1654
1649
  }
1655
1650
 
1656
1651
  // src/claude/write-workflow-config.ts
1657
- import path12 from "path";
1652
+ import path11 from "path";
1658
1653
  import fs10 from "fs-extra";
1659
1654
 
1660
1655
  // src/claude/derive-workflow-config.ts
1661
- import path11 from "path";
1656
+ import path10 from "path";
1662
1657
  import fs9 from "fs-extra";
1663
1658
  function binCmd(pm, bin, args) {
1664
1659
  const tail = args ? ` ${args}` : "";
@@ -1668,7 +1663,7 @@ function binCmd(pm, bin, args) {
1668
1663
  }
1669
1664
  async function deriveWorkflowConfig(root, ctx) {
1670
1665
  const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
1671
- const pkg = await readJson(path11.join(root, "package.json"));
1666
+ const pkg = await readJson(path10.join(root, "package.json"));
1672
1667
  const scripts = pkg?.scripts ?? {};
1673
1668
  const deps = new Set(ctx.dependencies);
1674
1669
  const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
@@ -1678,7 +1673,7 @@ async function deriveWorkflowConfig(root, ctx) {
1678
1673
  return null;
1679
1674
  };
1680
1675
  const hasDep = (name) => deps.has(name);
1681
- const exists = (rel) => fs9.pathExistsSync(path11.join(root, rel));
1676
+ const exists = (rel) => fs9.pathExistsSync(path10.join(root, rel));
1682
1677
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
1683
1678
  const hasCypress = hasDep("cypress");
1684
1679
  const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
@@ -1754,7 +1749,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1754
1749
  const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
1755
1750
  ...FALLBACK_CONTEXT,
1756
1751
  root,
1757
- repoName: path12.basename(root)
1752
+ repoName: path11.basename(root)
1758
1753
  };
1759
1754
  const values = await deriveWorkflowConfig(root, ctx);
1760
1755
  if (exists) {
@@ -1861,7 +1856,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1861
1856
  estimatedTokenReductionPct: 0
1862
1857
  };
1863
1858
  const pkgRoot = packageRoot();
1864
- const hausVersion2 = (await readJson(path13.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1859
+ const hausVersion2 = (await readJson(path12.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1865
1860
  const coreFiles = [
1866
1861
  claudePath(root, "settings.json"),
1867
1862
  claudePath(root, "rules", "haus.md"),
@@ -1885,10 +1880,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1885
1880
  await applyProjectSettingsMerge(root);
1886
1881
  await assertPostApplySettingsHausContract(root);
1887
1882
  }
1888
- const configPath = hausPath(root, "config.json");
1889
- if (!await fs12.pathExists(configPath)) {
1890
- await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
1891
- }
1892
1883
  await writeManagedText(
1893
1884
  root,
1894
1885
  claudePath(root, "commands", "haus-doctor.md"),
@@ -1947,6 +1938,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1947
1938
  }
1948
1939
  }
1949
1940
  const LEGACY_PRUNED_ARTIFACTS = [
1941
+ "config.json",
1950
1942
  "selected-context.json",
1951
1943
  "dependency-map.json",
1952
1944
  "scan-hashes.json",
@@ -1997,7 +1989,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1997
1989
  );
1998
1990
  continue;
1999
1991
  }
2000
- const destination = claudePath(root, target, path13.basename(sourcePath));
1992
+ const destination = claudePath(root, target, path12.basename(sourcePath));
2001
1993
  if (await fs12.pathExists(sourcePath)) {
2002
1994
  if (dryRun) {
2003
1995
  const exists = await fs12.pathExists(destination);
@@ -2010,17 +2002,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
2010
2002
  dryRun: false
2011
2003
  });
2012
2004
  } else {
2013
- await fs12.ensureDir(path13.dirname(destination));
2005
+ await fs12.ensureDir(path12.dirname(destination));
2014
2006
  await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
2015
2007
  }
2016
2008
  files.push(destination);
2017
- const relPaths = [path13.relative(root, destination)];
2009
+ const relPaths = [path12.relative(root, destination)];
2018
2010
  if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
2019
2011
  const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
2020
2012
  if (sharedRel) {
2021
2013
  superpowersSharedInstalled = true;
2022
2014
  relPaths.push(sharedRel);
2023
- files.push(path13.join(root, sharedRel));
2015
+ files.push(path12.join(root, sharedRel));
2024
2016
  }
2025
2017
  }
2026
2018
  const current = installedPathsByItem.get(item.id) ?? [];
@@ -2083,7 +2075,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
2083
2075
  if (relPaths.length === 0) continue;
2084
2076
  const existing = [];
2085
2077
  for (const rel of relPaths) {
2086
- if (await fs12.pathExists(path13.join(root, rel))) existing.push(rel);
2078
+ if (await fs12.pathExists(path12.join(root, rel))) existing.push(rel);
2087
2079
  }
2088
2080
  if (existing.length === 0) continue;
2089
2081
  if (entry.hash === void 0) {
@@ -2100,13 +2092,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
2100
2092
  continue;
2101
2093
  }
2102
2094
  for (const rel of existing) {
2103
- const abs = path13.join(root, rel);
2095
+ const abs = path12.join(root, rel);
2104
2096
  if (dryRun) {
2105
2097
  log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
2106
2098
  continue;
2107
2099
  }
2108
2100
  await fs12.remove(abs);
2109
- await pruneEmptyDir(path13.dirname(abs));
2101
+ await pruneEmptyDir(path12.dirname(abs));
2110
2102
  log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
2111
2103
  }
2112
2104
  }
@@ -2114,7 +2106,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
2114
2106
 
2115
2107
  // src/commands/apply.ts
2116
2108
  async function cacheHasItems() {
2117
- const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
2109
+ const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
2118
2110
  return Array.isArray(data?.items) && data.items.length > 0;
2119
2111
  }
2120
2112
  async function runApply(options) {
@@ -2219,7 +2211,7 @@ async function runCatalogAudit() {
2219
2211
 
2220
2212
  // src/commands/clone.ts
2221
2213
  import { existsSync as existsSync2 } from "fs";
2222
- import path15 from "path";
2214
+ import path14 from "path";
2223
2215
 
2224
2216
  // src/utils/exec.ts
2225
2217
  import { execa } from "execa";
@@ -2272,9 +2264,9 @@ async function runClone(url, opts = {}) {
2272
2264
  process.exitCode = 1;
2273
2265
  return;
2274
2266
  }
2275
- const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
2267
+ const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
2276
2268
  if (existsSync2(target)) {
2277
- log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
2269
+ log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
2278
2270
  return;
2279
2271
  }
2280
2272
  if (opts.dryRun) {
@@ -2290,399 +2282,12 @@ async function runClone(url, opts = {}) {
2290
2282
  log(`\u2713 cloned ${url} \u2192 ${target}`);
2291
2283
  }
2292
2284
 
2293
- // src/commands/config.ts
2294
- import path16 from "path";
2295
- var CONFIG_PATH2 = ".haus-workflow/config.json";
2296
- var HOOK_ALIASES = {
2297
- "hook.context": "context"
2298
- };
2299
- async function runConfig(key, action) {
2300
- const hookKey = HOOK_ALIASES[key];
2301
- if (!hookKey) {
2302
- throw new Error(
2303
- `Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`
2304
- );
2305
- }
2306
- const root = process.cwd();
2307
- const configPath = path16.join(root, CONFIG_PATH2);
2308
- const existing = await readJson(configPath);
2309
- const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
2310
- cfg.hooks ??= {};
2311
- cfg.hooks[hookKey] ??= {};
2312
- if (action === "status") {
2313
- const enabled = cfg.hooks[hookKey]?.enabled === true;
2314
- log(`${key}: ${enabled ? "enabled" : "disabled"}`);
2315
- return;
2316
- }
2317
- cfg.hooks[hookKey].enabled = action === "enable";
2318
- await writeJson(configPath, cfg);
2319
- log(`${key} ${action}d`);
2320
- }
2321
-
2322
- // src/recommender/token-estimate.ts
2323
- var TOKENS_PER_ITEM = 320;
2324
- function estimateContextTokens(selectedCount) {
2325
- return selectedCount * TOKENS_PER_ITEM;
2326
- }
2327
- function tokenReductionPct(selected, skipped) {
2328
- const total = selected + skipped;
2329
- if (total === 0) return 0;
2330
- return Math.max(0, Math.round(skipped / total * 100));
2331
- }
2332
-
2333
- // src/recommender/explain-recommendation.ts
2334
- function normalizeRecommendation(input2) {
2335
- const recommended = (input2.recommended ?? []).map((item) => {
2336
- const normalizedReasons = item.reasons?.map((reason) => ({
2337
- code: reason.code ?? "legacy-reason",
2338
- message: reason.message ?? item.reason ?? "legacy recommendation reason",
2339
- ...reason.signal ? { signal: reason.signal } : {}
2340
- })) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
2341
- return {
2342
- id: item.id,
2343
- type: item.type ?? "skill",
2344
- reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
2345
- reasons: normalizedReasons,
2346
- selectionMode: item.selectionMode ?? "matched",
2347
- install: item.install ?? true,
2348
- tags: item.tags,
2349
- ecosystem: item.ecosystem,
2350
- tokenEstimate: item.tokenEstimate
2351
- };
2352
- });
2353
- const skipped = (input2.skipped ?? []).map((item) => ({
2354
- id: item.id,
2355
- reason: item.reason ?? "legacy skipped reason",
2356
- skipReasons: item.skipReasons?.map((reason) => ({
2357
- code: reason.code ?? "legacy-skip-reason",
2358
- message: reason.message ?? item.reason ?? "legacy skipped reason",
2359
- ...reason.signal ? { signal: reason.signal } : {}
2360
- })) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
2361
- }));
2362
- return {
2363
- recommended,
2364
- skipped,
2365
- warnings: input2.warnings ?? [],
2366
- estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
2367
- selectedRules: input2.selectedRules ?? recommended.length,
2368
- skippedRules: input2.skippedRules ?? skipped.length,
2369
- estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
2370
- };
2371
- }
2372
- function buildRecommendationExplanation(recommendation) {
2373
- return {
2374
- selected: recommendation.recommended.map((item) => ({
2375
- id: item.id,
2376
- selectionMode: item.selectionMode,
2377
- reasons: item.reasons.map((reason) => reason.message)
2378
- })),
2379
- skipped: recommendation.skipped.map((item) => ({
2380
- id: item.id,
2381
- reasons: item.skipReasons.map((reason) => reason.message),
2382
- reasonDetails: item.skipReasons.map((reason) => ({
2383
- code: reason.code,
2384
- message: reason.message,
2385
- ...reason.signal ? { signal: reason.signal } : {}
2386
- }))
2387
- })),
2388
- stats: {
2389
- selectedRules: recommendation.selectedRules,
2390
- skippedRules: recommendation.skippedRules,
2391
- estimatedTokenReductionPct: recommendation.estimatedTokenReductionPct
2392
- }
2393
- };
2394
- }
2395
-
2396
- // src/recommender/task-classification.ts
2397
- var ALL_INTENTS = [
2398
- "backend",
2399
- "frontend",
2400
- "admin-ui",
2401
- "storefront",
2402
- "graphql",
2403
- "database",
2404
- "auth",
2405
- "testing",
2406
- "docs",
2407
- "monorepo"
2408
- ];
2409
- var TASK_INTENT_KEYWORDS = {
2410
- backend: [
2411
- "api",
2412
- "endpoint",
2413
- "controller",
2414
- "service",
2415
- "queue",
2416
- "job",
2417
- "worker",
2418
- "cron",
2419
- "middleware",
2420
- "resolver",
2421
- "migration",
2422
- "seeder",
2423
- "model",
2424
- "repository",
2425
- "handler",
2426
- "plugin",
2427
- "webhook",
2428
- "schedule",
2429
- "background",
2430
- "consumer",
2431
- "producer",
2432
- "command",
2433
- "nova resource",
2434
- "api mutation",
2435
- "api subscription"
2436
- ],
2437
- frontend: [
2438
- "component",
2439
- "page",
2440
- "route",
2441
- "view",
2442
- "layout",
2443
- "form",
2444
- "dashboard",
2445
- "modal",
2446
- "navbar",
2447
- "navigation",
2448
- "sidebar",
2449
- "menu",
2450
- "tailwind",
2451
- "scss",
2452
- "style",
2453
- "theme",
2454
- "tanstack",
2455
- "shadcn",
2456
- "radix",
2457
- "block",
2458
- "client component",
2459
- "server component"
2460
- ],
2461
- "admin-ui": [
2462
- "admin",
2463
- "admin-ui",
2464
- "admin ui",
2465
- "backoffice",
2466
- "back-office",
2467
- "back office",
2468
- "nova",
2469
- "control panel",
2470
- "wp-admin",
2471
- "vendure admin"
2472
- ],
2473
- storefront: [
2474
- "storefront",
2475
- "checkout",
2476
- "cart",
2477
- "product page",
2478
- "product listing",
2479
- "category page",
2480
- "shop",
2481
- "ecommerce",
2482
- "e-commerce",
2483
- "order page"
2484
- ],
2485
- graphql: ["graphql", "resolver", "graphql mutation", "graphql subscription", "schema", "codegen"],
2486
- database: [
2487
- "database",
2488
- "migration",
2489
- "seed",
2490
- "table",
2491
- "index",
2492
- "elasticsearch",
2493
- "postgres",
2494
- "mariadb",
2495
- "mssql",
2496
- "sql query",
2497
- "db query"
2498
- ],
2499
- auth: [
2500
- "auth",
2501
- "login",
2502
- "logout",
2503
- "oauth",
2504
- "oidc",
2505
- "bankid",
2506
- "azure ad",
2507
- "session",
2508
- "jwt",
2509
- "permission",
2510
- "rbac",
2511
- "acl",
2512
- "guard",
2513
- "saml"
2514
- ],
2515
- testing: [
2516
- "test",
2517
- "tests",
2518
- "testing",
2519
- "spec",
2520
- "e2e",
2521
- "unit",
2522
- "story",
2523
- "stories",
2524
- "snapshot",
2525
- "fixture",
2526
- "playwright",
2527
- "cypress",
2528
- "phpunit",
2529
- "vitest"
2530
- ],
2531
- docs: ["doc", "docs", "documentation", "readme", "guide", "tutorial", "changelog"],
2532
- monorepo: [
2533
- "lib",
2534
- "library",
2535
- "package",
2536
- "workspace",
2537
- "shared",
2538
- "monorepo",
2539
- "nx",
2540
- "turbo",
2541
- "pnpm workspace",
2542
- "yarn workspace"
2543
- ]
2544
- };
2545
- function normalizeTaskForMatching(task) {
2546
- return ` ${task.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
2547
- }
2548
- function classifyTaskIntents(task) {
2549
- const t = normalizeTaskForMatching(task);
2550
- const intents = /* @__PURE__ */ new Set();
2551
- for (const intent of ALL_INTENTS) {
2552
- const keywords = TASK_INTENT_KEYWORDS[intent];
2553
- for (const kw of keywords) {
2554
- const needle = ` ${kw} `;
2555
- if (t.includes(needle)) {
2556
- intents.add(intent);
2557
- break;
2558
- }
2559
- }
2560
- }
2561
- return intents;
2562
- }
2563
- function computeRuleIntents(rule) {
2564
- const intents = /* @__PURE__ */ new Set();
2565
- const tags = new Set((rule.tags ?? []).map((t) => t.toLowerCase()));
2566
- const eco = rule.ecosystem;
2567
- if (!eco && tags.size === 0) return intents;
2568
- const isTestingRule = tags.has("playwright") || tags.has("phpunit") || tags.has("testing-library") || tags.has("storybook") || tags.has("testing");
2569
- if (isTestingRule) {
2570
- intents.add("testing");
2571
- return intents;
2572
- }
2573
- if (eco === "laravel" || eco === "nestjs" || eco === "dotnet") {
2574
- intents.add("backend");
2575
- }
2576
- if (eco === "vendure") {
2577
- intents.add("backend");
2578
- intents.add("admin-ui");
2579
- }
2580
- if (eco === "wordpress") {
2581
- intents.add("backend");
2582
- intents.add("frontend");
2583
- intents.add("admin-ui");
2584
- }
2585
- if (eco === "nextjs" || eco === "react" || eco === "vue") {
2586
- intents.add("frontend");
2587
- intents.add("admin-ui");
2588
- intents.add("storefront");
2589
- }
2590
- if (eco === "tailwind" || eco === "vite") {
2591
- intents.add("frontend");
2592
- }
2593
- if (eco === "nx" || eco === "turbo") {
2594
- intents.add("monorepo");
2595
- }
2596
- if (tags.has("backend")) intents.add("backend");
2597
- if (tags.has("frontend")) intents.add("frontend");
2598
- if (tags.has("graphql")) intents.add("graphql");
2599
- if (tags.has("laravel-nova")) intents.add("admin-ui");
2600
- if (tags.has("oidc") || tags.has("azure-ad") || tags.has("bankid")) intents.add("auth");
2601
- if (tags.has("postgresql") || tags.has("mariadb") || tags.has("mssql") || tags.has("elasticsearch")) {
2602
- intents.add("database");
2603
- }
2604
- if (tags.has("nx21") || tags.has("turbo") || tags.has("yarn4") || tags.has("pnpm89")) {
2605
- intents.add("monorepo");
2606
- }
2607
- return intents;
2608
- }
2609
-
2610
- // src/recommender/rule-selection.ts
2611
- function evidenceCount(rule) {
2612
- return rule.reasons.filter((r) => r.code !== "default-baseline").length;
2613
- }
2614
- function isRoleOnly(rule) {
2615
- const codes = rule.reasons.map((r) => r.code).filter((c) => c !== "default-baseline");
2616
- return codes.length > 0 && codes.every((c) => c === "repo-role-match");
2617
- }
2618
- var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
2619
- function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
2620
- const recommended = recommendation?.recommended ?? [];
2621
- return applyTokenBudget(selectRules(recommended, task, taskIntents), opts.tokenBudget);
2622
- }
2623
- function applyTokenBudget(rules, budget) {
2624
- if (!budget || budget <= 0) return rules;
2625
- const total = rules.reduce((sum, r) => sum + (r.tokenEstimate ?? 0), 0);
2626
- if (total <= budget) return rules;
2627
- const keep = /* @__PURE__ */ new Set();
2628
- let used = 0;
2629
- for (const r of rules) {
2630
- if (r.selectionMode === "baseline") {
2631
- keep.add(r.id);
2632
- used += r.tokenEstimate ?? 0;
2633
- }
2634
- }
2635
- const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => evidenceCount(b) - evidenceCount(a) || a.id.localeCompare(b.id));
2636
- for (const r of matched) {
2637
- const est = r.tokenEstimate ?? 0;
2638
- if (used + est <= budget) {
2639
- keep.add(r.id);
2640
- used += est;
2641
- }
2642
- }
2643
- return rules.filter((r) => keep.has(r.id));
2644
- }
2645
- function selectRules(recommended, task, taskIntents) {
2646
- if (!task) return recommended;
2647
- if (taskIntents.size > 0) {
2648
- const intentMatches = recommended.filter((rule) => {
2649
- if (rule.selectionMode === "baseline") return false;
2650
- const ruleIntents = computeRuleIntents(rule);
2651
- if (ruleIntents.size === 0) return false;
2652
- for (const ti of taskIntents) {
2653
- if (ruleIntents.has(ti)) return true;
2654
- }
2655
- return false;
2656
- });
2657
- if (intentMatches.length > 0) return intentMatches;
2658
- }
2659
- const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
2660
- const tokenMatches = recommended.filter((rule) => {
2661
- if (rule.selectionMode === "baseline") return false;
2662
- const corpus = [
2663
- rule.id,
2664
- rule.ecosystem ?? "",
2665
- ...rule.tags ?? [],
2666
- rule.reason ?? "",
2667
- ...rule.reasons.map((r) => r.message)
2668
- ].join(" ").toLowerCase();
2669
- return tokens.some((token) => corpus.includes(token));
2670
- });
2671
- if (tokenMatches.length > 0) return tokenMatches;
2672
- const taskWantsTesting = taskIntents.has("testing");
2673
- const capped = recommended.filter((rule) => {
2674
- if (rule.selectionMode === "baseline") return false;
2675
- if (isRoleOnly(rule)) return false;
2676
- if (taskWantsTesting) return true;
2677
- const ruleIntents = computeRuleIntents(rule);
2678
- const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
2679
- return !isTestingOnly;
2680
- });
2681
- return capped.slice(0, 8);
2682
- }
2285
+ // src/commands/doctor.ts
2286
+ import path19 from "path";
2287
+ import fs15 from "fs-extra";
2683
2288
 
2684
2289
  // src/scanner/scan-project.ts
2685
- import path20 from "path";
2290
+ import path18 from "path";
2686
2291
 
2687
2292
  // src/utils/audit-checks.ts
2688
2293
  function isRecord(v) {
@@ -2709,7 +2314,7 @@ function compareVersions(a, b) {
2709
2314
  }
2710
2315
 
2711
2316
  // src/scanner/detect-package-manager.ts
2712
- import path17 from "path";
2317
+ import path15 from "path";
2713
2318
  import fs14 from "fs-extra";
2714
2319
  function detectPackageManager(root, packageManagerField) {
2715
2320
  const field = String(packageManagerField ?? "").trim();
@@ -2728,9 +2333,9 @@ function detectPackageManager(root, packageManagerField) {
2728
2333
  if (satisfiesVersion(version, ">=9")) return "npm";
2729
2334
  return "unknown";
2730
2335
  }
2731
- if (fs14.existsSync(path17.join(root, "yarn.lock"))) return "yarn";
2732
- if (fs14.existsSync(path17.join(root, "pnpm-lock.yaml"))) return "pnpm";
2733
- if (fs14.existsSync(path17.join(root, "package-lock.json"))) return "npm";
2336
+ if (fs14.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2337
+ if (fs14.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2338
+ if (fs14.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2734
2339
  return "unknown";
2735
2340
  }
2736
2341
 
@@ -2927,7 +2532,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2927
2532
  }
2928
2533
 
2929
2534
  // src/scanner/detection.ts
2930
- import path18 from "path";
2535
+ import path16 from "path";
2931
2536
  var UNSUPPORTED_MARKERS = {
2932
2537
  "requirements.txt": "python",
2933
2538
  "pyproject.toml": "python",
@@ -2981,68 +2586,14 @@ function finalizeRoles(registryRoles, deps, files) {
2981
2586
  function collectUnsupportedSignals(files) {
2982
2587
  return [
2983
2588
  ...new Set(
2984
- files.map((f) => UNSUPPORTED_MARKERS[path18.basename(f)]).filter((s) => Boolean(s))
2589
+ files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
2985
2590
  )
2986
2591
  ].sort();
2987
2592
  }
2988
2593
 
2989
2594
  // src/scanner/render.ts
2990
2595
  import { readFile } from "fs/promises";
2991
- import path19 from "path";
2992
-
2993
- // src/scanner/role-labels.ts
2994
- var ROLE_LABELS = {
2995
- "next-app": "a Next.js app",
2996
- "react-app": "a React app",
2997
- "vite-app": "a Vite app",
2998
- "react-router-app": "a React Router app",
2999
- "sanity-studio": "a Sanity Studio",
3000
- "strapi-app": "a Strapi app",
3001
- "expo-app": "an Expo app",
3002
- "vendure-app": "a Vendure server",
3003
- "vendure-plugin": "a Vendure plugin",
3004
- "nestjs-api": "a NestJS API",
3005
- "graphql-api": "a GraphQL API",
3006
- "nx-monorepo": "an Nx monorepo",
3007
- "turbo-monorepo": "a Turborepo monorepo",
3008
- "laravel-app": "a Laravel app",
3009
- "laravel-nova-app": "a Laravel Nova app",
3010
- "dotnet-service": "a .NET service",
3011
- "express-service": "an Express service",
3012
- "wordpress-bedrock-site": "a WordPress (Bedrock) site",
3013
- "wordpress-vanilla-site": "a WordPress site",
3014
- "wordpress-site": "a WordPress site"
3015
- };
3016
- function article(word) {
3017
- return /^[aeiou]/i.test(word) ? "an" : "a";
3018
- }
3019
- function friendlyRole(role) {
3020
- const known = ROLE_LABELS[role];
3021
- if (known) return known;
3022
- const words = role.replace(/[-_]+/g, " ").trim();
3023
- return words ? `${article(words)} ${words}` : "a project";
3024
- }
3025
- function joinRoles(labels) {
3026
- if (labels.length === 0) return "";
3027
- if (labels.length === 1) return labels[0];
3028
- return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
3029
- }
3030
- function describeRepo(context) {
3031
- const labels = context.repoRoles.map(friendlyRole);
3032
- const roleText = joinRoles(labels);
3033
- if (context.detectionStatus === "unknown") {
3034
- const markers = context.unsupportedSignals.join(", ");
3035
- const detail = markers ? ` (I see ${markers})` : "";
3036
- return `I couldn't fully recognise this stack${detail}, so I'll apply the general workflow and security guidance rather than framework-specific help.`;
3037
- }
3038
- const base = roleText ? `This looks like ${roleText}, using ${context.packageManager}.` : `I recognised this project's tooling (${context.packageManager}) but not a specific framework.`;
3039
- if (context.detectionStatus === "partial" && context.unsupportedSignals.length > 0) {
3040
- return `${base} I also see ${context.unsupportedSignals.join(", ")}, which haus doesn't fully support \u2014 guidance covers the recognised parts.`;
3041
- }
3042
- return base;
3043
- }
3044
-
3045
- // src/scanner/render.ts
2596
+ import path17 from "path";
3046
2597
  async function buildContentBlob(root, files) {
3047
2598
  const candidates = files.filter(
3048
2599
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -3050,24 +2601,13 @@ async function buildContentBlob(root, files) {
3050
2601
  const slice = candidates.slice(0, 300);
3051
2602
  const parts = await mapWithConcurrency(slice, async (rel) => {
3052
2603
  try {
3053
- return await readFile(path19.join(root, rel), "utf8");
2604
+ return await readFile(path17.join(root, rel), "utf8");
3054
2605
  } catch {
3055
2606
  return "";
3056
2607
  }
3057
2608
  });
3058
2609
  return parts.join("\n");
3059
2610
  }
3060
- function renderSummary(context) {
3061
- return `# Repo summary
3062
-
3063
- ${describeRepo(context)}
3064
-
3065
- - Repo: ${context.repoName}
3066
- - Package manager: ${context.packageManager}
3067
- - Roles: ${context.repoRoles.join(", ") || "unknown"}
3068
- - Generated: ${context.generatedAt}
3069
- `;
3070
- }
3071
2611
 
3072
2612
  // src/scanner/write-sources-report.ts
3073
2613
  function buildSourcesReport(items) {
@@ -3148,8 +2688,8 @@ var SAFE_FILES = [
3148
2688
  "Gemfile"
3149
2689
  ];
3150
2690
  async function scanProject(root) {
3151
- const pkg = await readJson(path20.join(root, "package.json"));
3152
- const composer = await readJson(path20.join(root, "composer.json"));
2691
+ const pkg = await readJson(path18.join(root, "package.json"));
2692
+ const composer = await readJson(path18.join(root, "composer.json"));
3153
2693
  const files = await listFiles(root, SAFE_FILES);
3154
2694
  const safeFiles = files.filter((f) => !blocked(f));
3155
2695
  const deps = dependencySet(pkg, composer);
@@ -3178,7 +2718,7 @@ async function scanProject(root) {
3178
2718
  const context = {
3179
2719
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3180
2720
  root,
3181
- repoName: String(pkg?.name ?? path20.basename(root)),
2721
+ repoName: String(pkg?.name ?? path18.basename(root)),
3182
2722
  packageManager,
3183
2723
  repoRoles: roles,
3184
2724
  detectedStacks: stacks,
@@ -3202,74 +2742,6 @@ async function readContextOrScan(root) {
3202
2742
  return scan;
3203
2743
  }
3204
2744
 
3205
- // src/security/secret-patterns.ts
3206
- var SECRET_PATTERNS = [
3207
- /api[_-]?key\s*[:=]\s*\S+/i,
3208
- /token\s*[:=]\s*\S+/i,
3209
- /password\s*[:=]\s*\S+/i
3210
- ];
3211
-
3212
- // src/security/redact-sensitive.ts
3213
- function redactSensitive(input2) {
3214
- return SECRET_PATTERNS.reduce((acc, pattern) => acc.replace(pattern, "[REDACTED]"), input2);
3215
- }
3216
-
3217
- // src/commands/context.ts
3218
- async function runContext(options) {
3219
- const root = process.cwd();
3220
- if (options.fromHook && !await isHookEnabled(root, "context")) {
3221
- return;
3222
- }
3223
- const context = await readContextOrScan(root);
3224
- const summary = renderSummary(context);
3225
- const recommendationRaw = await readJson(hausPath(root, "recommendation.json"));
3226
- const recommendation = recommendationRaw ? normalizeRecommendation(recommendationRaw) : void 0;
3227
- const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
3228
- const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
3229
- tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
3230
- });
3231
- const payload = {
3232
- task: options.task ?? "not provided",
3233
- taskIntents: [...taskIntents].sort(),
3234
- roles: context.repoRoles,
3235
- selectedRules: selected.map((x) => ({
3236
- id: x.id,
3237
- selectionMode: x.selectionMode,
3238
- reasons: x.reasons.map((reason) => reason.message),
3239
- ...options.verbose ? { signals: x.reasons.map((r) => r.signal).filter(Boolean) } : {}
3240
- })),
3241
- skippedCount: recommendation?.skippedRules ?? 0,
3242
- estimatedTokenReductionPct: recommendation?.estimatedTokenReductionPct ?? 0
3243
- };
3244
- if (options.json) {
3245
- log(JSON.stringify(payload, null, 2));
3246
- return;
3247
- }
3248
- const lines = [
3249
- "# Haus Context",
3250
- `Task: ${payload.task}`,
3251
- `Task intents: ${payload.taskIntents.join(", ") || "(none classified)"}`,
3252
- `Roles: ${payload.roles.join(", ") || "unknown"}`,
3253
- `Selected rules: ${payload.selectedRules.length}`,
3254
- `Skipped rules: ${payload.skippedCount}`,
3255
- `Estimated token reduction: ${payload.estimatedTokenReductionPct}%`,
3256
- "Use minimal context.",
3257
- ...payload.selectedRules.flatMap((rule) => {
3258
- const reasonLine = `- ${rule.id}: ${rule.reasons.join(", ")}`;
3259
- if (!options.verbose) return [reasonLine];
3260
- const signals = (rule.signals ?? []).map((s) => ` \u2022 ${s}`);
3261
- return [reasonLine, ...signals];
3262
- }),
3263
- summary
3264
- ];
3265
- const text = redactSensitive(lines.join("\n"));
3266
- log(options.fromHook ? text.slice(0, 3e3) : text);
3267
- }
3268
-
3269
- // src/commands/doctor.ts
3270
- import path21 from "path";
3271
- import fs15 from "fs-extra";
3272
-
3273
2745
  // src/update/npm-version.ts
3274
2746
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
3275
2747
  async function fetchNpmVersionStatus(currentVersion) {
@@ -3343,12 +2815,7 @@ async function runDoctor(options) {
3343
2815
  } else {
3344
2816
  ok(`- HOOKS OK: ${hooks.message}`);
3345
2817
  }
3346
- const gatedHooks = ["context"];
3347
- for (const key of gatedHooks) {
3348
- const enabled = await isHookEnabled(root, key);
3349
- ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
3350
- }
3351
- const rootClaudeMdPath = path21.join(root, "CLAUDE.md");
2818
+ const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
3352
2819
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
3353
2820
  if (!rootClaudeMdContent) {
3354
2821
  flag(
@@ -3410,8 +2877,8 @@ async function runDoctor(options) {
3410
2877
  "haus apply --write --force"
3411
2878
  );
3412
2879
  } else {
3413
- const cachePath = path21.join(getCacheDir(), "templates/agentic-workflow-standard.md");
3414
- const bundledPath = path21.join(
2880
+ const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2881
+ const bundledPath = path19.join(
3415
2882
  packageRoot(),
3416
2883
  "library",
3417
2884
  "global",
@@ -3477,7 +2944,7 @@ async function runDoctor(options) {
3477
2944
  ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
3478
2945
  }
3479
2946
  }
3480
- const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
2947
+ const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
3481
2948
  const currentVersion = pkgJson?.version ?? "0.0.0";
3482
2949
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3483
2950
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3541,6 +3008,80 @@ function formatRecommendationHuman(rec) {
3541
3008
  return lines.join("\n");
3542
3009
  }
3543
3010
 
3011
+ // src/recommender/token-estimate.ts
3012
+ var TOKENS_PER_ITEM = 320;
3013
+ function estimateContextTokens(selectedCount) {
3014
+ return selectedCount * TOKENS_PER_ITEM;
3015
+ }
3016
+ function tokenReductionPct(selected, skipped) {
3017
+ const total = selected + skipped;
3018
+ if (total === 0) return 0;
3019
+ return Math.max(0, Math.round(skipped / total * 100));
3020
+ }
3021
+
3022
+ // src/recommender/explain-recommendation.ts
3023
+ function normalizeRecommendation(input2) {
3024
+ const recommended = (input2.recommended ?? []).map((item) => {
3025
+ const normalizedReasons = item.reasons?.map((reason) => ({
3026
+ code: reason.code ?? "legacy-reason",
3027
+ message: reason.message ?? item.reason ?? "legacy recommendation reason",
3028
+ ...reason.signal ? { signal: reason.signal } : {}
3029
+ })) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
3030
+ return {
3031
+ id: item.id,
3032
+ type: item.type ?? "skill",
3033
+ reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
3034
+ reasons: normalizedReasons,
3035
+ selectionMode: item.selectionMode ?? "matched",
3036
+ install: item.install ?? true,
3037
+ tags: item.tags,
3038
+ ecosystem: item.ecosystem,
3039
+ tokenEstimate: item.tokenEstimate
3040
+ };
3041
+ });
3042
+ const skipped = (input2.skipped ?? []).map((item) => ({
3043
+ id: item.id,
3044
+ reason: item.reason ?? "legacy skipped reason",
3045
+ skipReasons: item.skipReasons?.map((reason) => ({
3046
+ code: reason.code ?? "legacy-skip-reason",
3047
+ message: reason.message ?? item.reason ?? "legacy skipped reason",
3048
+ ...reason.signal ? { signal: reason.signal } : {}
3049
+ })) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
3050
+ }));
3051
+ return {
3052
+ recommended,
3053
+ skipped,
3054
+ warnings: input2.warnings ?? [],
3055
+ estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
3056
+ selectedRules: input2.selectedRules ?? recommended.length,
3057
+ skippedRules: input2.skippedRules ?? skipped.length,
3058
+ estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
3059
+ };
3060
+ }
3061
+ function buildRecommendationExplanation(recommendation) {
3062
+ return {
3063
+ selected: recommendation.recommended.map((item) => ({
3064
+ id: item.id,
3065
+ selectionMode: item.selectionMode,
3066
+ reasons: item.reasons.map((reason) => reason.message)
3067
+ })),
3068
+ skipped: recommendation.skipped.map((item) => ({
3069
+ id: item.id,
3070
+ reasons: item.skipReasons.map((reason) => reason.message),
3071
+ reasonDetails: item.skipReasons.map((reason) => ({
3072
+ code: reason.code,
3073
+ message: reason.message,
3074
+ ...reason.signal ? { signal: reason.signal } : {}
3075
+ }))
3076
+ })),
3077
+ stats: {
3078
+ selectedRules: recommendation.selectedRules,
3079
+ skippedRules: recommendation.skippedRules,
3080
+ estimatedTokenReductionPct: recommendation.estimatedTokenReductionPct
3081
+ }
3082
+ };
3083
+ }
3084
+
3544
3085
  // src/commands/explain-recommendation.ts
3545
3086
  async function runExplainRecommendation(options) {
3546
3087
  const root = process.cwd();
@@ -3620,7 +3161,7 @@ async function runGuard(kind, _options) {
3620
3161
  }
3621
3162
 
3622
3163
  // src/commands/init.ts
3623
- import path22 from "path";
3164
+ import path20 from "path";
3624
3165
  import fs16 from "fs-extra";
3625
3166
 
3626
3167
  // src/utils/prompts.ts
@@ -3968,7 +3509,7 @@ async function runSetupProject(options) {
3968
3509
  // src/commands/init.ts
3969
3510
  async function runInit(options) {
3970
3511
  const root = process.cwd();
3971
- const hausDir = path22.join(root, ".haus-workflow");
3512
+ const hausDir = path20.join(root, ".haus-workflow");
3972
3513
  const alreadyInit = await fs16.pathExists(hausDir);
3973
3514
  if (alreadyInit) {
3974
3515
  log("Haus AI already initialized in this project.");
@@ -3981,7 +3522,7 @@ async function runInit(options) {
3981
3522
 
3982
3523
  // src/install/apply.ts
3983
3524
  import crypto2 from "crypto";
3984
- import path23 from "path";
3525
+ import path21 from "path";
3985
3526
  import fs17 from "fs-extra";
3986
3527
 
3987
3528
  // src/install/header.ts
@@ -4070,7 +3611,7 @@ function hashContent(content2) {
4070
3611
  }
4071
3612
  function sourceVersion() {
4072
3613
  try {
4073
- const pkgPath = path23.join(packageRoot(), "package.json");
3614
+ const pkgPath = path21.join(packageRoot(), "package.json");
4074
3615
  const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
4075
3616
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
4076
3617
  } catch {
@@ -4078,32 +3619,32 @@ function sourceVersion() {
4078
3619
  }
4079
3620
  }
4080
3621
  function globalSrcDir() {
4081
- return path23.join(packageRoot(), "library", "global");
3622
+ return path21.join(packageRoot(), "library", "global");
4082
3623
  }
4083
3624
  function collectSourceFiles(srcDir, claudeDir) {
4084
3625
  const entries = [];
4085
- const skillsDir = path23.join(srcDir, "skills");
3626
+ const skillsDir = path21.join(srcDir, "skills");
4086
3627
  if (fs17.pathExistsSync(skillsDir)) {
4087
3628
  for (const skillName of fs17.readdirSync(skillsDir)) {
4088
- const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
3629
+ const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
4089
3630
  if (fs17.pathExistsSync(skillFile)) {
4090
3631
  entries.push({
4091
3632
  stableId: `skill.${skillName}`,
4092
- srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
4093
- destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
3633
+ srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
3634
+ destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
4094
3635
  });
4095
3636
  }
4096
3637
  }
4097
3638
  }
4098
- const commandsDir = path23.join(srcDir, "commands");
3639
+ const commandsDir = path21.join(srcDir, "commands");
4099
3640
  if (fs17.pathExistsSync(commandsDir)) {
4100
3641
  for (const fileName of fs17.readdirSync(commandsDir)) {
4101
3642
  if (!fileName.endsWith(".md")) continue;
4102
3643
  const commandName = fileName.slice(0, -".md".length);
4103
3644
  entries.push({
4104
3645
  stableId: `command.${commandName}`,
4105
- srcRelPath: path23.join("library", "global", "commands", fileName),
4106
- destPath: path23.join(claudeDir, "commands", fileName)
3646
+ srcRelPath: path21.join("library", "global", "commands", fileName),
3647
+ destPath: path21.join(claudeDir, "commands", fileName)
4107
3648
  });
4108
3649
  }
4109
3650
  }
@@ -4127,7 +3668,7 @@ async function applyInstall(options = {}) {
4127
3668
  };
4128
3669
  const manifestFiles = [];
4129
3670
  for (const entry of sourceFiles) {
4130
- const srcPath = path23.join(packageRoot(), entry.srcRelPath);
3671
+ const srcPath = path21.join(packageRoot(), entry.srcRelPath);
4131
3672
  const rawContent = await readText(srcPath);
4132
3673
  if (rawContent === void 0) {
4133
3674
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -4183,7 +3724,7 @@ async function applyInstall(options = {}) {
4183
3724
  schemaVersion: SCHEMA_VERSION2
4184
3725
  });
4185
3726
  }
4186
- const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
3727
+ const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
4187
3728
  const fragments = await loadHooksFragment(fragmentPath);
4188
3729
  const settings = await readSettings();
4189
3730
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -4321,12 +3862,12 @@ async function runScan(options) {
4321
3862
  }
4322
3863
 
4323
3864
  // src/commands/undo.ts
4324
- import path24 from "path";
3865
+ import path22 from "path";
4325
3866
  import fs18 from "fs-extra";
4326
3867
 
4327
3868
  // src/claude/managed-paths.ts
4328
3869
  var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
4329
- var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json", "config.json"];
3870
+ var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"];
4330
3871
  function coreManagedAbsolutePaths(root) {
4331
3872
  const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
4332
3873
  const haus = PROJECT_MANAGED_HAUS_REL.map((rel) => hausPath(root, rel));
@@ -4339,7 +3880,7 @@ async function collectManagedPaths(root) {
4339
3880
  const lock = await readJson(hausPath(root, "haus.lock.json"));
4340
3881
  for (const row of lock ?? []) {
4341
3882
  for (const rel of row.paths ?? []) {
4342
- paths.add(path24.resolve(root, rel));
3883
+ paths.add(path22.resolve(root, rel));
4343
3884
  }
4344
3885
  }
4345
3886
  const existing = [];
@@ -4355,7 +3896,7 @@ async function settingsHasHausContent(root) {
4355
3896
  return settings._haus != null;
4356
3897
  }
4357
3898
  async function claudeMdHasHausBlock(root) {
4358
- const filePath = path24.join(root, "CLAUDE.md");
3899
+ const filePath = path22.join(root, "CLAUDE.md");
4359
3900
  if (!await fs18.pathExists(filePath)) return false;
4360
3901
  const text = await fs18.readFile(filePath, "utf8");
4361
3902
  return text.includes(BLOCK_BEGIN);
@@ -4368,15 +3909,15 @@ async function stripProjectSettings(root) {
4368
3909
  const hasContent = Object.keys(settings).length > 0;
4369
3910
  if (hasContent) {
4370
3911
  await writeProjectSettings(root, settings);
4371
- log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
3912
+ log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
4372
3913
  return true;
4373
3914
  }
4374
3915
  await fs18.remove(settingsPath);
4375
- log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
3916
+ log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
4376
3917
  return true;
4377
3918
  }
4378
3919
  async function stripRootClaudeMd(root) {
4379
- const filePath = path24.join(root, "CLAUDE.md");
3920
+ const filePath = path22.join(root, "CLAUDE.md");
4380
3921
  if (!await fs18.pathExists(filePath)) return false;
4381
3922
  const prev = await fs18.readFile(filePath, "utf8");
4382
3923
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -4404,7 +3945,7 @@ async function runUndo(options) {
4404
3945
  log("Nothing to remove: no haus-managed files found in this directory.");
4405
3946
  return;
4406
3947
  }
4407
- const relTargets = managed.map((p) => path24.relative(root, p));
3948
+ const relTargets = managed.map((p) => path22.relative(root, p));
4408
3949
  const summaryParts = [...relTargets];
4409
3950
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
4410
3951
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -4422,7 +3963,7 @@ User-owned .claude/ files will be preserved.`
4422
3963
  for (const abs of managed) {
4423
3964
  if (!await fs18.pathExists(abs)) continue;
4424
3965
  await fs18.remove(abs);
4425
- log(`Removed ${path24.relative(root, abs)}`);
3966
+ log(`Removed ${path22.relative(root, abs)}`);
4426
3967
  }
4427
3968
  if (stripSettings) await stripProjectSettings(root);
4428
3969
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -4433,7 +3974,7 @@ User-owned .claude/ files will be preserved.`
4433
3974
 
4434
3975
  // src/install/uninstall.ts
4435
3976
  import crypto3 from "crypto";
4436
- import path25 from "path";
3977
+ import path23 from "path";
4437
3978
  import fs19 from "fs-extra";
4438
3979
  async function runUninstall(options = {}) {
4439
3980
  const { force = false } = options;
@@ -4463,14 +4004,14 @@ async function runUninstall(options = {}) {
4463
4004
  continue;
4464
4005
  }
4465
4006
  await fs19.remove(entry.destPath);
4466
- await pruneEmptyDir(path25.dirname(entry.destPath));
4007
+ await pruneEmptyDir(path23.dirname(entry.destPath));
4467
4008
  result.deleted.push(entry.destPath);
4468
4009
  }
4469
4010
  const settings = await readSettings();
4470
4011
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4471
4012
  await writeSettings(stripped);
4472
4013
  result.hooksStripped = true;
4473
- const hausDir = path25.join(globalClaudeDir(), "haus");
4014
+ const hausDir = path23.join(globalClaudeDir(), "haus");
4474
4015
  const manifestPath2 = hausManifestPath();
4475
4016
  if (fs19.pathExistsSync(manifestPath2)) {
4476
4017
  await fs19.remove(manifestPath2);
@@ -4508,7 +4049,7 @@ async function runUninstallCommand(options) {
4508
4049
  }
4509
4050
 
4510
4051
  // src/commands/update.ts
4511
- import path27 from "path";
4052
+ import path25 from "path";
4512
4053
 
4513
4054
  // src/update/diff-generated-files.ts
4514
4055
  function diffGeneratedFiles() {
@@ -4535,7 +4076,7 @@ function summarizeLockDiff(before, after) {
4535
4076
 
4536
4077
  // src/update/lockfile.ts
4537
4078
  import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
4538
- import path26 from "path";
4079
+ import path24 from "path";
4539
4080
  async function checkLock(root) {
4540
4081
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4541
4082
  const hasValidVersions = lock.every(
@@ -4566,7 +4107,7 @@ async function applyLock(root) {
4566
4107
  try {
4567
4108
  const backupDir = hausPath(root, "backups");
4568
4109
  await mkdir(backupDir, { recursive: true });
4569
- await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
4110
+ await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
4570
4111
  } catch {
4571
4112
  }
4572
4113
  const enriched = await Promise.all(
@@ -4588,7 +4129,7 @@ function diffLock(before, after) {
4588
4129
  }
4589
4130
  async function hasLocalOverrides(root) {
4590
4131
  try {
4591
- await readFile2(path26.join(root, ".claude", "settings.json"), "utf8");
4132
+ await readFile2(path24.join(root, ".claude", "settings.json"), "utf8");
4592
4133
  return true;
4593
4134
  } catch {
4594
4135
  return false;
@@ -4600,7 +4141,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4600
4141
  async function runUpdate(options) {
4601
4142
  const root = process.cwd();
4602
4143
  if (options.check) {
4603
- const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
4144
+ const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
4604
4145
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4605
4146
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4606
4147
  checkLock(root),
@@ -4630,7 +4171,7 @@ async function runUpdate(options) {
4630
4171
  if (status.driftCount > 0) process.exitCode = 1;
4631
4172
  return;
4632
4173
  }
4633
- const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
4174
+ const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
4634
4175
  const currentVersion = pkgJson?.version ?? "0.0.0";
4635
4176
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4636
4177
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4708,7 +4249,7 @@ async function detectGlobalInstallDrift() {
4708
4249
 
4709
4250
  // src/commands/validate-catalog.ts
4710
4251
  import fs20 from "fs";
4711
- import path28 from "path";
4252
+ import path26 from "path";
4712
4253
  function auditForbiddenStacks(items) {
4713
4254
  const failures = [];
4714
4255
  for (const item of items) {
@@ -4785,17 +4326,17 @@ function auditShippedFiles(manifestDir, items) {
4785
4326
  const failures = [];
4786
4327
  for (const item of items) {
4787
4328
  if (!item.path) continue;
4788
- const absPath = path28.join(manifestDir, item.path);
4329
+ const absPath = path26.join(manifestDir, item.path);
4789
4330
  if (item.type === "skill") {
4790
- const skillMd = path28.join(absPath, "SKILL.md");
4331
+ const skillMd = path26.join(absPath, "SKILL.md");
4791
4332
  if (!fs20.existsSync(skillMd)) {
4792
- failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
4333
+ failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4793
4334
  continue;
4794
4335
  }
4795
4336
  const text = fs20.readFileSync(skillMd, "utf8");
4796
4337
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4797
4338
  failures.push(
4798
- ...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
4339
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4799
4340
  );
4800
4341
  } else if (item.type === "agent") {
4801
4342
  if (!fs20.existsSync(absPath)) {
@@ -4803,7 +4344,7 @@ function auditShippedFiles(manifestDir, items) {
4803
4344
  continue;
4804
4345
  }
4805
4346
  const text = fs20.readFileSync(absPath, "utf8");
4806
- const rel = path28.relative(manifestDir, absPath);
4347
+ const rel = path26.relative(manifestDir, absPath);
4807
4348
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4808
4349
  failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
4809
4350
  } else if (item.type === "template") {
@@ -4818,7 +4359,7 @@ function auditShippedFiles(manifestDir, items) {
4818
4359
  continue;
4819
4360
  }
4820
4361
  const text = fs20.readFileSync(absPath, "utf8");
4821
- const rel = path28.relative(manifestDir, absPath);
4362
+ const rel = path26.relative(manifestDir, absPath);
4822
4363
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4823
4364
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4824
4365
  }
@@ -4826,7 +4367,7 @@ function auditShippedFiles(manifestDir, items) {
4826
4367
  return failures;
4827
4368
  }
4828
4369
  function auditTemplateContent(manifestDir, absPath, itemId) {
4829
- const rel = path28.relative(manifestDir, absPath);
4370
+ const rel = path26.relative(manifestDir, absPath);
4830
4371
  const text = fs20.readFileSync(absPath, "utf8");
4831
4372
  const failures = [];
4832
4373
  const lines = text.split(/\r?\n/);
@@ -4849,11 +4390,11 @@ function auditMarkdownContent(manifestDir) {
4849
4390
  const failures = [];
4850
4391
  const dirs = ["skills", "agents", "templates", "commands"];
4851
4392
  for (const dir of dirs) {
4852
- const abs = path28.join(manifestDir, dir);
4393
+ const abs = path26.join(manifestDir, dir);
4853
4394
  if (!fs20.existsSync(abs)) continue;
4854
4395
  walkMd(abs, (file) => {
4855
4396
  const text = fs20.readFileSync(file, "utf8");
4856
- const rel = path28.relative(manifestDir, file);
4397
+ const rel = path26.relative(manifestDir, file);
4857
4398
  const lines = text.split(/\r?\n/);
4858
4399
  for (let i = 0; i < lines.length; i++) {
4859
4400
  const line2 = lines[i] ?? "";
@@ -4873,7 +4414,7 @@ function auditMarkdownContent(manifestDir) {
4873
4414
  }
4874
4415
  function walkMd(dir, fn) {
4875
4416
  for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4876
- const full = path28.join(dir, entry.name);
4417
+ const full = path26.join(dir, entry.name);
4877
4418
  if (entry.isDirectory()) walkMd(full, fn);
4878
4419
  else if (entry.name.endsWith(".md")) fn(full);
4879
4420
  }
@@ -4884,8 +4425,8 @@ async function runValidateCatalog(manifestPath2) {
4884
4425
  process.exitCode = 1;
4885
4426
  return;
4886
4427
  }
4887
- const abs = path28.resolve(process.cwd(), manifestPath2);
4888
- const manifestDir = path28.dirname(abs);
4428
+ const abs = path26.resolve(process.cwd(), manifestPath2);
4429
+ const manifestDir = path26.dirname(abs);
4889
4430
  const data = await readJson(abs);
4890
4431
  if (!data?.items) {
4891
4432
  error(`Could not read catalog manifest at ${abs}`);
@@ -4915,7 +4456,7 @@ async function runValidateCatalog(manifestPath2) {
4915
4456
 
4916
4457
  // src/commands/workspace.ts
4917
4458
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4918
- import path35 from "path";
4459
+ import path33 from "path";
4919
4460
 
4920
4461
  // src/commands/workspace/aggregate.ts
4921
4462
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4969,7 +4510,7 @@ ${summaries.map(
4969
4510
  }
4970
4511
 
4971
4512
  // src/commands/workspace/config.ts
4972
- import path29 from "path";
4513
+ import path27 from "path";
4973
4514
  import YAML from "yaml";
4974
4515
  var WORKSPACE_FILE = "haus.workspace.yaml";
4975
4516
  function parseWorkspaceConfig(text) {
@@ -4992,11 +4533,11 @@ function parseWorkspaceConfig(text) {
4992
4533
  };
4993
4534
  }
4994
4535
  async function readWorkspaceConfig(workspaceRoot) {
4995
- return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4536
+ return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
4996
4537
  }
4997
4538
 
4998
4539
  // src/commands/workspace/discover.ts
4999
- import path30 from "path";
4540
+ import path28 from "path";
5000
4541
  import fg4 from "fast-glob";
5001
4542
  import YAML2 from "yaml";
5002
4543
  var DEFAULT_MAX_DEPTH = 3;
@@ -5024,8 +4565,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
5024
4565
  const gitDirs = /* @__PURE__ */ new Set();
5025
4566
  const manifestDirs = /* @__PURE__ */ new Set();
5026
4567
  for (const match of matches) {
5027
- const base = path30.posix.basename(match);
5028
- const dir = path30.posix.dirname(match);
4568
+ const base = path28.posix.basename(match);
4569
+ const dir = path28.posix.dirname(match);
5029
4570
  const owner = dir === "." ? "." : dir;
5030
4571
  if (base === ".git") gitDirs.add(owner);
5031
4572
  else manifestDirs.add(owner);
@@ -5041,9 +4582,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
5041
4582
  }
5042
4583
  repoRoots.sort((a, b) => a.localeCompare(b));
5043
4584
  return mapWithConcurrency(repoRoots, async (relDir) => {
5044
- const absDir = path30.resolve(workspaceRoot, relDir);
5045
- const pkg = await readJson(path30.join(absDir, "package.json"));
5046
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
4585
+ const absDir = path28.resolve(workspaceRoot, relDir);
4586
+ const pkg = await readJson(path28.join(absDir, "package.json"));
4587
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
5047
4588
  let role = "auto";
5048
4589
  try {
5049
4590
  const scan = await scanProject(absDir);
@@ -5078,15 +4619,15 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
5078
4619
  relationships: existing?.relationships ?? []
5079
4620
  };
5080
4621
  }
5081
- function renderWorkspaceYaml(config2) {
4622
+ function renderWorkspaceYaml(config) {
5082
4623
  return YAML2.stringify({
5083
- client: config2.client,
5084
- repos: config2.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
5085
- relationships: config2.relationships
4624
+ client: config.client,
4625
+ repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
4626
+ relationships: config.relationships
5086
4627
  });
5087
4628
  }
5088
4629
  async function runDiscover(workspaceRoot, opts = {}) {
5089
- const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
4630
+ const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
5090
4631
  const existingText = await readText(yamlPath);
5091
4632
  const existing = parseWorkspaceConfig(existingText);
5092
4633
  if (existingText && !existing) {
@@ -5120,18 +4661,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
5120
4661
 
5121
4662
  // src/commands/workspace/doctor.ts
5122
4663
  import { existsSync as existsSync3 } from "fs";
5123
- import path32 from "path";
4664
+ import path30 from "path";
5124
4665
 
5125
4666
  // src/commands/workspace/manifest.ts
5126
4667
  import { readFileSync as readFileSync3 } from "fs";
5127
- import path31 from "path";
4668
+ import path29 from "path";
5128
4669
  var MANIFEST_FILE = "workspace.manifest.json";
5129
4670
  function manifestPath(workspaceRoot) {
5130
4671
  return hausPath(workspaceRoot, MANIFEST_FILE);
5131
4672
  }
5132
4673
  function hausVersion() {
5133
4674
  try {
5134
- const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
4675
+ const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
5135
4676
  return pkg.version ?? "0.0.0";
5136
4677
  } catch {
5137
4678
  return "0.0.0";
@@ -5169,7 +4710,7 @@ async function writeWorkspaceManifest(workspaceRoot, manifest) {
5169
4710
 
5170
4711
  // src/commands/workspace/doctor.ts
5171
4712
  async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5172
- const config2 = await readWorkspaceConfig(workspaceRoot);
4713
+ const config = await readWorkspaceConfig(workspaceRoot);
5173
4714
  const manifest = await readManifest2(workspaceRoot);
5174
4715
  const currentVersion = hausVersion();
5175
4716
  const drift = [];
@@ -5179,7 +4720,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5179
4720
  drift.push(item);
5180
4721
  detail.push({ stream: "warn", text: `- ${item.repo}: ${item.detail}` });
5181
4722
  };
5182
- if (!config2) {
4723
+ if (!config) {
5183
4724
  flag({
5184
4725
  repo: "(workspace)",
5185
4726
  kind: "no-config",
@@ -5196,8 +4737,8 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5196
4737
  return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
5197
4738
  }
5198
4739
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
5199
- for (const repo of config2.repos) {
5200
- const repoRoot = path32.resolve(workspaceRoot, repo.path);
4740
+ for (const repo of config.repos) {
4741
+ const repoRoot = path30.resolve(workspaceRoot, repo.path);
5201
4742
  const entry = manifestByName.get(repo.name);
5202
4743
  if (!entry) {
5203
4744
  flag({
@@ -5271,10 +4812,10 @@ function emit(args) {
5271
4812
 
5272
4813
  // src/commands/workspace/setup.ts
5273
4814
  import { existsSync as existsSync4, statSync } from "fs";
5274
- import path34 from "path";
4815
+ import path32 from "path";
5275
4816
 
5276
4817
  // src/claude/write-workspace-claude-md.ts
5277
- import path33 from "path";
4818
+ import path31 from "path";
5278
4819
  import fs21 from "fs-extra";
5279
4820
  function buildWorkspaceImportBlock(client, members) {
5280
4821
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -5293,7 +4834,7 @@ ${BLOCK_END}`;
5293
4834
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5294
4835
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
5295
4836
  const dryRun = opts.dryRun ?? false;
5296
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
4837
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
5297
4838
  const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
5298
4839
  const next = opts.collision ? `${block}
5299
4840
  ` : injectHausBlock(prev, block);
@@ -5319,37 +4860,37 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5319
4860
 
5320
4861
  // src/commands/workspace/setup.ts
5321
4862
  function resolveWorkspaceRoot(start = process.cwd()) {
5322
- let dir = path34.resolve(start);
4863
+ let dir = path32.resolve(start);
5323
4864
  for (; ; ) {
5324
- if (existsSync4(path34.join(dir, WORKSPACE_FILE))) return dir;
5325
- const parent = path34.dirname(dir);
5326
- if (parent === dir) return path34.resolve(start);
4865
+ if (existsSync4(path32.join(dir, WORKSPACE_FILE))) return dir;
4866
+ const parent = path32.dirname(dir);
4867
+ if (parent === dir) return path32.resolve(start);
5327
4868
  dir = parent;
5328
4869
  }
5329
4870
  }
5330
4871
  function isRootRepo(workspaceRoot, repoPath) {
5331
- return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
4872
+ return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
5332
4873
  }
5333
4874
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5334
4875
  const apply = options.write ?? false;
5335
- const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
4876
+ const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
5336
4877
  if (!configText) {
5337
4878
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5338
4879
  process.exitCode = 1;
5339
4880
  return { workspaceRoot, statuses: [], written: [] };
5340
4881
  }
5341
- const config2 = parseWorkspaceConfig(configText);
5342
- if (!config2 || config2.repos.length === 0) {
4882
+ const config = parseWorkspaceConfig(configText);
4883
+ if (!config || config.repos.length === 0) {
5343
4884
  error(`No repos configured in ${WORKSPACE_FILE}.`);
5344
4885
  process.exitCode = 1;
5345
4886
  return { workspaceRoot, statuses: [], written: [] };
5346
4887
  }
5347
4888
  const onlySet = options.only && options.only.length > 0 ? new Set(options.only) : void 0;
5348
- const repos = onlySet ? config2.repos.filter((r) => onlySet.has(r.name)) : config2.repos;
4889
+ const repos = onlySet ? config.repos.filter((r) => onlySet.has(r.name)) : config.repos;
5349
4890
  const statuses = [];
5350
4891
  const aggregateInputs = [];
5351
4892
  for (const repo of repos) {
5352
- const repoRoot = path34.resolve(workspaceRoot, repo.path);
4893
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
5353
4894
  log(`
5354
4895
  \u2192 ${repo.name} (${repo.path})`);
5355
4896
  try {
@@ -5386,18 +4927,18 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5386
4927
  }
5387
4928
  const written = [];
5388
4929
  if (apply && aggregateInputs.length > 0) {
5389
- const collision = config2.repos.some((r) => isRootRepo(workspaceRoot, r.path));
4930
+ const collision = config.repos.some((r) => isRootRepo(workspaceRoot, r.path));
5390
4931
  if (!options.dryRun) {
5391
4932
  const artifacts = await writeWorkspaceArtifacts(
5392
4933
  workspaceRoot,
5393
4934
  aggregateInputs,
5394
- config2.relationships
4935
+ config.relationships
5395
4936
  );
5396
4937
  written.push(...artifacts);
5397
4938
  }
5398
4939
  const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
5399
- client: config2.client,
5400
- members: config2.repos.map((r) => ({ name: r.name, path: r.path })),
4940
+ client: config.client,
4941
+ members: config.repos.map((r) => ({ name: r.name, path: r.path })),
5401
4942
  collision,
5402
4943
  dryRun: options.dryRun
5403
4944
  });
@@ -5413,11 +4954,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5413
4954
  }
5414
4955
  const priorByName = new Map((prior?.repos ?? []).map((r) => [r.name, r]));
5415
4956
  const manifestRepos = [];
5416
- for (const repo of config2.repos) {
4957
+ for (const repo of config.repos) {
5417
4958
  const status = statusByName.get(repo.name);
5418
4959
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5419
4960
  if (status?.status === "ok") {
5420
- const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
4961
+ const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
5421
4962
  manifestRepos.push({
5422
4963
  name: repo.name,
5423
4964
  path: repo.path,
@@ -5460,7 +5001,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5460
5001
  );
5461
5002
  }
5462
5003
  }
5463
- const manifest = buildManifest2({ client: config2.client, repos: manifestRepos });
5004
+ const manifest = buildManifest2({ client: config.client, repos: manifestRepos });
5464
5005
  const manifestFile = await writeWorkspaceManifest(workspaceRoot, manifest);
5465
5006
  written.push(manifestFile);
5466
5007
  }
@@ -5497,35 +5038,35 @@ relationships: []
5497
5038
  log("Workspace initialized.");
5498
5039
  }
5499
5040
  async function scanWorkspace(workspaceRoot, opts) {
5500
- const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
5041
+ const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5501
5042
  if (!configText) {
5502
5043
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5503
5044
  process.exitCode = 1;
5504
5045
  return;
5505
5046
  }
5506
- const config2 = parseWorkspaceConfig(configText);
5507
- if (!config2) {
5047
+ const config = parseWorkspaceConfig(configText);
5048
+ if (!config) {
5508
5049
  error(
5509
5050
  `Malformed ${WORKSPACE_FILE}. Fix the YAML or re-run \`haus workspace discover --write\`.`
5510
5051
  );
5511
5052
  process.exitCode = 1;
5512
5053
  return;
5513
5054
  }
5514
- if (config2.repos.length === 0) {
5055
+ if (config.repos.length === 0) {
5515
5056
  error(`No repos configured in ${WORKSPACE_FILE}.`);
5516
5057
  process.exitCode = 1;
5517
5058
  return;
5518
5059
  }
5519
5060
  const inputs = [];
5520
- for (const repo of config2.repos) {
5521
- const repoRoot = path35.resolve(workspaceRoot, repo.path);
5061
+ for (const repo of config.repos) {
5062
+ const repoRoot = path33.resolve(workspaceRoot, repo.path);
5522
5063
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5523
5064
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5524
5065
  }
5525
5066
  const result = await scanProject(repoRoot);
5526
5067
  inputs.push({ name: repo.name, path: repo.path, context: result });
5527
5068
  }
5528
- const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config2.relationships);
5069
+ const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config.relationships);
5529
5070
  if (opts.json) {
5530
5071
  log(JSON.stringify({ written }, null, 2));
5531
5072
  } else {
@@ -5568,7 +5109,7 @@ async function runWorkspace(action, options = {}) {
5568
5109
  // src/cli.ts
5569
5110
  function cliVersion() {
5570
5111
  try {
5571
- const pkgPath = path36.join(packageRoot(), "package.json");
5112
+ const pkgPath = path34.join(packageRoot(), "package.json");
5572
5113
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5573
5114
  return pkg.version ?? "0.0.0";
5574
5115
  } catch {
@@ -5578,7 +5119,7 @@ function cliVersion() {
5578
5119
  var program = new Command();
5579
5120
  function validateRuntimeNodeVersion() {
5580
5121
  try {
5581
- const pkgPath = path36.join(packageRoot(), "package.json");
5122
+ const pkgPath = path34.join(packageRoot(), "package.json");
5582
5123
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5583
5124
  const requiredRange = pkg.engines?.node;
5584
5125
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -5605,7 +5146,6 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
5605
5146
  ).option("--force", "Overwrite user-modified managed workflow files").action(runApply);
5606
5147
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
5607
5148
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
5608
- program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
5609
5149
  program.command("init").option("--json").action(runInit);
5610
5150
  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(
5611
5151
  (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
@@ -5619,10 +5159,6 @@ program.command("uninstall").option("--force").action(runUninstallCommand);
5619
5159
  var guard = program.command("guard");
5620
5160
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
5621
5161
  guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
5622
- var config = program.command("config");
5623
- config.command("enable <key>").description("Enable a hook (hook.context)").action((key) => runConfig(key, "enable"));
5624
- config.command("disable <key>").description("Disable a hook (hook.context)").action((key) => runConfig(key, "disable"));
5625
- config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
5626
5162
  var workspace = program.command("workspace");
5627
5163
  workspace.command("init").action(() => runWorkspace("init"));
5628
5164
  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));