@haus-tech/haus-workflow 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,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
 
@@ -119,6 +119,7 @@ var validation_rules_default = {
119
119
  ],
120
120
  allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
121
121
  anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
122
+ npxTsxOnlyExemptTypes: ["agent"],
122
123
  httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
123
124
  placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
124
125
  allowedStacks: [
@@ -239,6 +240,7 @@ var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatt
239
240
  var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
240
241
  var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
241
242
  var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
243
+ var NPX_TSX_ONLY_EXEMPT_TYPES = validation_rules_default.npxTsxOnlyExemptTypes ?? [];
242
244
  var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
243
245
  var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
244
246
  var ALLOWED_STACKS = validation_rules_default.allowedStacks;
@@ -317,13 +319,14 @@ ${extractUseWhenSection(text)}`;
317
319
  // src/catalog/ingest-catalog.ts
318
320
  function validateCatalogItem(item, content2) {
319
321
  const label = item.id;
322
+ const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(item.type);
320
323
  const lines = content2.split(/\r?\n/);
321
324
  for (let i = 0; i < lines.length; i++) {
322
325
  const line2 = lines[i] ?? "";
323
326
  if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
324
327
  return { ok: false, reason: `${label}: risky install pattern at line ${i + 1}` };
325
328
  }
326
- if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
329
+ if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
327
330
  return { ok: false, reason: `${label}: disallowed npx at line ${i + 1}` };
328
331
  }
329
332
  }
@@ -839,14 +842,7 @@ async function getCacheManifestAge() {
839
842
  }
840
843
 
841
844
  // src/install/allow-rules.ts
842
- var ALLOWED_SUBCOMMANDS = [
843
- "setup-project",
844
- "apply",
845
- "doctor",
846
- "scan",
847
- "context",
848
- "recommend"
849
- ];
845
+ var ALLOWED_SUBCOMMANDS = ["setup-project", "apply", "doctor", "scan", "recommend"];
850
846
  function buildAllowRules() {
851
847
  return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
852
848
  }
@@ -901,6 +897,44 @@ function collectEventHookCommands(entries) {
901
897
  }
902
898
  return cmds;
903
899
  }
900
+ var RETIRED_HAUS_HOOK_COMMANDS = ["haus context --from-hook"];
901
+ var RETIRED_HAUS_HOOK_IDS = ["hook.context", "haus.context-hook"];
902
+ function reconcileRetiredHausHooks(updated, fragments, priorHaus, addedIds, addedCommands) {
903
+ const canonicalCommands = new Set(
904
+ fragments.filter((f) => f.gate === "keep").map((f) => f.command)
905
+ );
906
+ const canonicalIds = new Set(fragments.filter((f) => f.gate === "keep").map((f) => f.id));
907
+ const retiredCommands = new Set(RETIRED_HAUS_HOOK_COMMANDS);
908
+ for (const cmd of [...priorHaus?.hookCommands ?? [], ...addedCommands]) {
909
+ if (!canonicalCommands.has(cmd)) retiredCommands.add(cmd);
910
+ }
911
+ const prunedHooks = {};
912
+ for (const [event, entries] of Object.entries(updated.hooks ?? {})) {
913
+ const kept = entries.filter((entry) => {
914
+ const cmds = (entry.hooks ?? []).map((h) => h.command).filter(Boolean);
915
+ return cmds.length === 0 || !cmds.some((cmd) => retiredCommands.has(cmd));
916
+ });
917
+ if (kept.length > 0) prunedHooks[event] = kept;
918
+ }
919
+ updated.hooks = prunedHooks;
920
+ const retiredIdSet = new Set(RETIRED_HAUS_HOOK_IDS);
921
+ for (const id of priorHaus?.hooks ?? []) {
922
+ if (!canonicalIds.has(id)) retiredIdSet.add(id);
923
+ }
924
+ const survivingIds = [.../* @__PURE__ */ new Set([...priorHaus?.hooks ?? [], ...addedIds])].filter(
925
+ (id) => canonicalIds.has(id) && !retiredIdSet.has(id)
926
+ );
927
+ const survivingCommands = [
928
+ .../* @__PURE__ */ new Set([...priorHaus?.hookCommands ?? [], ...addedCommands])
929
+ ].filter((cmd) => canonicalCommands.has(cmd));
930
+ updated._haus = {
931
+ hooks: survivingIds,
932
+ ...survivingCommands.length > 0 ? { hookCommands: survivingCommands } : {},
933
+ ...priorHaus?.denyRules ? { denyRules: priorHaus.denyRules } : {},
934
+ ...priorHaus?.allowRules ? { allowRules: priorHaus.allowRules } : {},
935
+ ...priorHaus?.askRules ? { askRules: priorHaus.askRules } : {}
936
+ };
937
+ }
904
938
  function mergeHooks(settings, fragments) {
905
939
  const existing = settings._haus?.hooks ?? [];
906
940
  const existingCommands = settings._haus?.hookCommands ?? [];
@@ -926,14 +960,7 @@ function mergeHooks(settings, fragments) {
926
960
  if (!existing.includes(fragment.id)) addedIds.push(fragment.id);
927
961
  if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
928
962
  }
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
- };
963
+ reconcileRetiredHausHooks(updated, fragments, settings._haus, addedIds, addedCommands);
937
964
  return { settings: updated, addedIds };
938
965
  }
939
966
  function reconcileManagedRules(existing, prevTracked, newRules) {
@@ -1185,8 +1212,8 @@ function buildDenyRules() {
1185
1212
  for (const command of DENY_COMMANDS) {
1186
1213
  rules.push(`Bash(${command}:*)`);
1187
1214
  }
1188
- for (const path37 of DENY_PATHS) {
1189
- const pattern = DENY_DIRS.has(path37) ? `${path37}/**` : path37;
1215
+ for (const path35 of DENY_PATHS) {
1216
+ const pattern = DENY_DIRS.has(path35) ? `${path35}/**` : path35;
1190
1217
  for (const tool of FILE_TOOLS) {
1191
1218
  rules.push(`${tool}(${pattern})`);
1192
1219
  }
@@ -1242,12 +1269,6 @@ function packageRoot() {
1242
1269
 
1243
1270
  // src/claude/merge-project-settings.ts
1244
1271
  var PROJECT_HOOK_FRAGMENTS = [
1245
- {
1246
- id: "haus.context-hook",
1247
- gate: "keep",
1248
- event: "UserPromptSubmit",
1249
- command: "haus context --from-hook"
1250
- },
1251
1272
  {
1252
1273
  id: "haus.guard-file",
1253
1274
  gate: "keep",
@@ -1285,7 +1306,7 @@ async function applyProjectSettingsMerge(root) {
1285
1306
  }
1286
1307
 
1287
1308
  // src/claude/write-claude-files.ts
1288
- import path13 from "path";
1309
+ import path12 from "path";
1289
1310
  import fs12 from "fs-extra";
1290
1311
 
1291
1312
  // src/catalog/load-catalog.ts
@@ -1388,19 +1409,6 @@ async function hashInstalledPaths(root, relPaths) {
1388
1409
  return hashText(fileDigests.map((f) => `${f.rel}=${f.digest}`).join("|"));
1389
1410
  }
1390
1411
 
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
1412
  // src/claude/managed-write.ts
1405
1413
  import fs5 from "fs-extra";
1406
1414
 
@@ -1454,7 +1462,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
1454
1462
  }
1455
1463
 
1456
1464
  // src/claude/superpowers-install.ts
1457
- import path9 from "path";
1465
+ import path8 from "path";
1458
1466
  import fg3 from "fast-glob";
1459
1467
  import fs6 from "fs-extra";
1460
1468
  var SUPERPOWERS_ORIGIN_SOURCE_ID = "superpowers-pcvelz";
@@ -1471,7 +1479,7 @@ function rewriteSuperpowersMarkdown(text) {
1471
1479
  async function rewriteMarkdownTree(dir) {
1472
1480
  const files = await fg3("**/*.md", { cwd: dir, onlyFiles: true, dot: true });
1473
1481
  for (const rel of files) {
1474
- const abs = path9.join(dir, rel);
1482
+ const abs = path8.join(dir, rel);
1475
1483
  const text = await fs6.readFile(abs, "utf8");
1476
1484
  const rewritten = rewriteSuperpowersMarkdown(text);
1477
1485
  if (rewritten !== text) {
@@ -1481,7 +1489,7 @@ async function rewriteMarkdownTree(dir) {
1481
1489
  }
1482
1490
  async function installCatalogSkill(sourcePath, destination, opts) {
1483
1491
  if (opts.dryRun) return;
1484
- await fs6.ensureDir(path9.dirname(destination));
1492
+ await fs6.ensureDir(path8.dirname(destination));
1485
1493
  if (await fs6.pathExists(destination)) {
1486
1494
  await fs6.remove(destination);
1487
1495
  }
@@ -1491,17 +1499,17 @@ async function installCatalogSkill(sourcePath, destination, opts) {
1491
1499
  }
1492
1500
  }
1493
1501
  async function installSuperpowersShared(contentRoot, projectRoot, dryRun) {
1494
- const source = path9.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
1502
+ const source = path8.join(contentRoot, SUPERPOWERS_SHARED_CATALOG_REL);
1495
1503
  if (!await fs6.pathExists(source)) return null;
1496
1504
  const destination = claudePath(projectRoot, "skills", "shared");
1497
- if (dryRun) return path9.relative(projectRoot, destination);
1498
- await fs6.ensureDir(path9.dirname(destination));
1505
+ if (dryRun) return path8.relative(projectRoot, destination);
1506
+ await fs6.ensureDir(path8.dirname(destination));
1499
1507
  if (await fs6.pathExists(destination)) {
1500
1508
  await fs6.remove(destination);
1501
1509
  }
1502
1510
  await fs6.copy(source, destination, { overwrite: true, errorOnExist: false });
1503
1511
  await rewriteMarkdownTree(destination);
1504
- return path9.relative(projectRoot, destination);
1512
+ return path8.relative(projectRoot, destination);
1505
1513
  }
1506
1514
 
1507
1515
  // src/claude/verify-hooks-contract.ts
@@ -1510,11 +1518,6 @@ import fs7 from "fs-extra";
1510
1518
  // src/claude/load-hooks.ts
1511
1519
  var CANONICAL_HOOKS = {
1512
1520
  hooks: {
1513
- UserPromptSubmit: [
1514
- {
1515
- hooks: [{ type: "command", command: "haus context --from-hook" }]
1516
- }
1517
- ],
1518
1521
  PreToolUse: [
1519
1522
  {
1520
1523
  matcher: "Read|Edit|Write",
@@ -1545,11 +1548,6 @@ function collectHookCommands(settings) {
1545
1548
  }
1546
1549
  function hausHookContractSatisfied(project, canonical) {
1547
1550
  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
1551
  for (const block of canonical.hooks.PreToolUse) {
1554
1552
  for (const h of block.hooks) {
1555
1553
  if (!present.has(h.command)) return false;
@@ -1604,7 +1602,7 @@ async function verifyProjectSettingsHooksContract(root) {
1604
1602
  }
1605
1603
 
1606
1604
  // src/claude/write-root-claude-md.ts
1607
- import path10 from "path";
1605
+ import path9 from "path";
1608
1606
  import fs8 from "fs-extra";
1609
1607
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
1610
1608
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
@@ -1645,7 +1643,7 @@ ${block}
1645
1643
  `;
1646
1644
  }
1647
1645
  async function writeRootClaudeMd(root, dryRun) {
1648
- const filePath = path10.join(root, "CLAUDE.md");
1646
+ const filePath = path9.join(root, "CLAUDE.md");
1649
1647
  const block = buildImportBlock();
1650
1648
  const prev = await fs8.pathExists(filePath) ? await fs8.readFile(filePath, "utf8") : "";
1651
1649
  const next = injectHausBlock(prev, block);
@@ -1654,11 +1652,11 @@ async function writeRootClaudeMd(root, dryRun) {
1654
1652
  }
1655
1653
 
1656
1654
  // src/claude/write-workflow-config.ts
1657
- import path12 from "path";
1655
+ import path11 from "path";
1658
1656
  import fs10 from "fs-extra";
1659
1657
 
1660
1658
  // src/claude/derive-workflow-config.ts
1661
- import path11 from "path";
1659
+ import path10 from "path";
1662
1660
  import fs9 from "fs-extra";
1663
1661
  function binCmd(pm, bin, args) {
1664
1662
  const tail = args ? ` ${args}` : "";
@@ -1668,7 +1666,7 @@ function binCmd(pm, bin, args) {
1668
1666
  }
1669
1667
  async function deriveWorkflowConfig(root, ctx) {
1670
1668
  const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
1671
- const pkg = await readJson(path11.join(root, "package.json"));
1669
+ const pkg = await readJson(path10.join(root, "package.json"));
1672
1670
  const scripts = pkg?.scripts ?? {};
1673
1671
  const deps = new Set(ctx.dependencies);
1674
1672
  const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
@@ -1678,7 +1676,7 @@ async function deriveWorkflowConfig(root, ctx) {
1678
1676
  return null;
1679
1677
  };
1680
1678
  const hasDep = (name) => deps.has(name);
1681
- const exists = (rel) => fs9.pathExistsSync(path11.join(root, rel));
1679
+ const exists = (rel) => fs9.pathExistsSync(path10.join(root, rel));
1682
1680
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
1683
1681
  const hasCypress = hasDep("cypress");
1684
1682
  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 +1752,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1754
1752
  const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
1755
1753
  ...FALLBACK_CONTEXT,
1756
1754
  root,
1757
- repoName: path12.basename(root)
1755
+ repoName: path11.basename(root)
1758
1756
  };
1759
1757
  const values = await deriveWorkflowConfig(root, ctx);
1760
1758
  if (exists) {
@@ -1861,7 +1859,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1861
1859
  estimatedTokenReductionPct: 0
1862
1860
  };
1863
1861
  const pkgRoot = packageRoot();
1864
- const hausVersion2 = (await readJson(path13.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1862
+ const hausVersion2 = (await readJson(path12.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1865
1863
  const coreFiles = [
1866
1864
  claudePath(root, "settings.json"),
1867
1865
  claudePath(root, "rules", "haus.md"),
@@ -1885,10 +1883,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1885
1883
  await applyProjectSettingsMerge(root);
1886
1884
  await assertPostApplySettingsHausContract(root);
1887
1885
  }
1888
- const configPath = hausPath(root, "config.json");
1889
- if (!await fs12.pathExists(configPath)) {
1890
- await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
1891
- }
1892
1886
  await writeManagedText(
1893
1887
  root,
1894
1888
  claudePath(root, "commands", "haus-doctor.md"),
@@ -1947,6 +1941,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1947
1941
  }
1948
1942
  }
1949
1943
  const LEGACY_PRUNED_ARTIFACTS = [
1944
+ "config.json",
1950
1945
  "selected-context.json",
1951
1946
  "dependency-map.json",
1952
1947
  "scan-hashes.json",
@@ -1997,7 +1992,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1997
1992
  );
1998
1993
  continue;
1999
1994
  }
2000
- const destination = claudePath(root, target, path13.basename(sourcePath));
1995
+ const destination = claudePath(root, target, path12.basename(sourcePath));
2001
1996
  if (await fs12.pathExists(sourcePath)) {
2002
1997
  if (dryRun) {
2003
1998
  const exists = await fs12.pathExists(destination);
@@ -2010,17 +2005,17 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
2010
2005
  dryRun: false
2011
2006
  });
2012
2007
  } else {
2013
- await fs12.ensureDir(path13.dirname(destination));
2008
+ await fs12.ensureDir(path12.dirname(destination));
2014
2009
  await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
2015
2010
  }
2016
2011
  files.push(destination);
2017
- const relPaths = [path13.relative(root, destination)];
2012
+ const relPaths = [path12.relative(root, destination)];
2018
2013
  if (!superpowersSharedInstalled && manifestItem.originSourceId === SUPERPOWERS_ORIGIN_SOURCE_ID && item.type === "skill") {
2019
2014
  const sharedRel = await installSuperpowersShared(contentRoot, root, dryRun);
2020
2015
  if (sharedRel) {
2021
2016
  superpowersSharedInstalled = true;
2022
2017
  relPaths.push(sharedRel);
2023
- files.push(path13.join(root, sharedRel));
2018
+ files.push(path12.join(root, sharedRel));
2024
2019
  }
2025
2020
  }
2026
2021
  const current = installedPathsByItem.get(item.id) ?? [];
@@ -2083,7 +2078,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
2083
2078
  if (relPaths.length === 0) continue;
2084
2079
  const existing = [];
2085
2080
  for (const rel of relPaths) {
2086
- if (await fs12.pathExists(path13.join(root, rel))) existing.push(rel);
2081
+ if (await fs12.pathExists(path12.join(root, rel))) existing.push(rel);
2087
2082
  }
2088
2083
  if (existing.length === 0) continue;
2089
2084
  if (entry.hash === void 0) {
@@ -2100,13 +2095,13 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
2100
2095
  continue;
2101
2096
  }
2102
2097
  for (const rel of existing) {
2103
- const abs = path13.join(root, rel);
2098
+ const abs = path12.join(root, rel);
2104
2099
  if (dryRun) {
2105
2100
  log(`[dry-run] would remove stale ${displayPath(root, abs)} (${entry.id})`);
2106
2101
  continue;
2107
2102
  }
2108
2103
  await fs12.remove(abs);
2109
- await pruneEmptyDir(path13.dirname(abs));
2104
+ await pruneEmptyDir(path12.dirname(abs));
2110
2105
  log(`Removed stale ${displayPath(root, abs)} (${entry.id})`);
2111
2106
  }
2112
2107
  }
@@ -2114,7 +2109,7 @@ async function cleanupStaleCatalogItems(root, knownIds, dryRun) {
2114
2109
 
2115
2110
  // src/commands/apply.ts
2116
2111
  async function cacheHasItems() {
2117
- const data = await readJson(path14.join(getCacheDir(), "manifest.json"));
2112
+ const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
2118
2113
  return Array.isArray(data?.items) && data.items.length > 0;
2119
2114
  }
2120
2115
  async function runApply(options) {
@@ -2219,7 +2214,7 @@ async function runCatalogAudit() {
2219
2214
 
2220
2215
  // src/commands/clone.ts
2221
2216
  import { existsSync as existsSync2 } from "fs";
2222
- import path15 from "path";
2217
+ import path14 from "path";
2223
2218
 
2224
2219
  // src/utils/exec.ts
2225
2220
  import { execa } from "execa";
@@ -2272,9 +2267,9 @@ async function runClone(url, opts = {}) {
2272
2267
  process.exitCode = 1;
2273
2268
  return;
2274
2269
  }
2275
- const target = path15.resolve(opts.dir?.trim() || repoNameFromUrl(url));
2270
+ const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
2276
2271
  if (existsSync2(target)) {
2277
- log(`\u2022 ${path15.basename(target)} already present at ${target} \u2014 skipped`);
2272
+ log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
2278
2273
  return;
2279
2274
  }
2280
2275
  if (opts.dryRun) {
@@ -2290,399 +2285,12 @@ async function runClone(url, opts = {}) {
2290
2285
  log(`\u2713 cloned ${url} \u2192 ${target}`);
2291
2286
  }
2292
2287
 
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
- }
2288
+ // src/commands/doctor.ts
2289
+ import path19 from "path";
2290
+ import fs15 from "fs-extra";
2683
2291
 
2684
2292
  // src/scanner/scan-project.ts
2685
- import path20 from "path";
2293
+ import path18 from "path";
2686
2294
 
2687
2295
  // src/utils/audit-checks.ts
2688
2296
  function isRecord(v) {
@@ -2709,7 +2317,7 @@ function compareVersions(a, b) {
2709
2317
  }
2710
2318
 
2711
2319
  // src/scanner/detect-package-manager.ts
2712
- import path17 from "path";
2320
+ import path15 from "path";
2713
2321
  import fs14 from "fs-extra";
2714
2322
  function detectPackageManager(root, packageManagerField) {
2715
2323
  const field = String(packageManagerField ?? "").trim();
@@ -2728,9 +2336,9 @@ function detectPackageManager(root, packageManagerField) {
2728
2336
  if (satisfiesVersion(version, ">=9")) return "npm";
2729
2337
  return "unknown";
2730
2338
  }
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";
2339
+ if (fs14.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2340
+ if (fs14.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2341
+ if (fs14.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2734
2342
  return "unknown";
2735
2343
  }
2736
2344
 
@@ -2866,6 +2474,11 @@ var STACK_RULES = [
2866
2474
  stack: ["backend", "graphql"],
2867
2475
  any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")]
2868
2476
  },
2477
+ // PHP (Composer-managed) — broad language signal so php-tooling catalog items
2478
+ // (e.g. the php-reviewer agent gated on { stack: 'php' }) are recommendable on any
2479
+ // Composer project. Placed before laravel/wordpress so 'php' precedes the
2480
+ // framework-specific stacks in the backend bucket.
2481
+ { stack: ["backend", "php"], any: [fileEndsWith("composer.json")] },
2869
2482
  { stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
2870
2483
  { stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
2871
2484
  { stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
@@ -2927,7 +2540,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2927
2540
  }
2928
2541
 
2929
2542
  // src/scanner/detection.ts
2930
- import path18 from "path";
2543
+ import path16 from "path";
2931
2544
  var UNSUPPORTED_MARKERS = {
2932
2545
  "requirements.txt": "python",
2933
2546
  "pyproject.toml": "python",
@@ -2981,68 +2594,14 @@ function finalizeRoles(registryRoles, deps, files) {
2981
2594
  function collectUnsupportedSignals(files) {
2982
2595
  return [
2983
2596
  ...new Set(
2984
- files.map((f) => UNSUPPORTED_MARKERS[path18.basename(f)]).filter((s) => Boolean(s))
2597
+ files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
2985
2598
  )
2986
2599
  ].sort();
2987
2600
  }
2988
2601
 
2989
2602
  // src/scanner/render.ts
2990
2603
  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
2604
+ import path17 from "path";
3046
2605
  async function buildContentBlob(root, files) {
3047
2606
  const candidates = files.filter(
3048
2607
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -3050,24 +2609,13 @@ async function buildContentBlob(root, files) {
3050
2609
  const slice = candidates.slice(0, 300);
3051
2610
  const parts = await mapWithConcurrency(slice, async (rel) => {
3052
2611
  try {
3053
- return await readFile(path19.join(root, rel), "utf8");
2612
+ return await readFile(path17.join(root, rel), "utf8");
3054
2613
  } catch {
3055
2614
  return "";
3056
2615
  }
3057
2616
  });
3058
2617
  return parts.join("\n");
3059
2618
  }
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
2619
 
3072
2620
  // src/scanner/write-sources-report.ts
3073
2621
  function buildSourcesReport(items) {
@@ -3148,8 +2696,8 @@ var SAFE_FILES = [
3148
2696
  "Gemfile"
3149
2697
  ];
3150
2698
  async function scanProject(root) {
3151
- const pkg = await readJson(path20.join(root, "package.json"));
3152
- const composer = await readJson(path20.join(root, "composer.json"));
2699
+ const pkg = await readJson(path18.join(root, "package.json"));
2700
+ const composer = await readJson(path18.join(root, "composer.json"));
3153
2701
  const files = await listFiles(root, SAFE_FILES);
3154
2702
  const safeFiles = files.filter((f) => !blocked(f));
3155
2703
  const deps = dependencySet(pkg, composer);
@@ -3178,7 +2726,7 @@ async function scanProject(root) {
3178
2726
  const context = {
3179
2727
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3180
2728
  root,
3181
- repoName: String(pkg?.name ?? path20.basename(root)),
2729
+ repoName: String(pkg?.name ?? path18.basename(root)),
3182
2730
  packageManager,
3183
2731
  repoRoles: roles,
3184
2732
  detectedStacks: stacks,
@@ -3202,74 +2750,6 @@ async function readContextOrScan(root) {
3202
2750
  return scan;
3203
2751
  }
3204
2752
 
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
2753
  // src/update/npm-version.ts
3274
2754
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
3275
2755
  async function fetchNpmVersionStatus(currentVersion) {
@@ -3343,12 +2823,7 @@ async function runDoctor(options) {
3343
2823
  } else {
3344
2824
  ok(`- HOOKS OK: ${hooks.message}`);
3345
2825
  }
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");
2826
+ const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
3352
2827
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
3353
2828
  if (!rootClaudeMdContent) {
3354
2829
  flag(
@@ -3410,8 +2885,8 @@ async function runDoctor(options) {
3410
2885
  "haus apply --write --force"
3411
2886
  );
3412
2887
  } else {
3413
- const cachePath = path21.join(getCacheDir(), "templates/agentic-workflow-standard.md");
3414
- const bundledPath = path21.join(
2888
+ const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2889
+ const bundledPath = path19.join(
3415
2890
  packageRoot(),
3416
2891
  "library",
3417
2892
  "global",
@@ -3477,7 +2952,7 @@ async function runDoctor(options) {
3477
2952
  ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
3478
2953
  }
3479
2954
  }
3480
- const pkgJson = await readJson(path21.join(packageRoot(), "package.json"));
2955
+ const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
3481
2956
  const currentVersion = pkgJson?.version ?? "0.0.0";
3482
2957
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3483
2958
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3541,6 +3016,80 @@ function formatRecommendationHuman(rec) {
3541
3016
  return lines.join("\n");
3542
3017
  }
3543
3018
 
3019
+ // src/recommender/token-estimate.ts
3020
+ var TOKENS_PER_ITEM = 320;
3021
+ function estimateContextTokens(selectedCount) {
3022
+ return selectedCount * TOKENS_PER_ITEM;
3023
+ }
3024
+ function tokenReductionPct(selected, skipped) {
3025
+ const total = selected + skipped;
3026
+ if (total === 0) return 0;
3027
+ return Math.max(0, Math.round(skipped / total * 100));
3028
+ }
3029
+
3030
+ // src/recommender/explain-recommendation.ts
3031
+ function normalizeRecommendation(input2) {
3032
+ const recommended = (input2.recommended ?? []).map((item) => {
3033
+ const normalizedReasons = item.reasons?.map((reason) => ({
3034
+ code: reason.code ?? "legacy-reason",
3035
+ message: reason.message ?? item.reason ?? "legacy recommendation reason",
3036
+ ...reason.signal ? { signal: reason.signal } : {}
3037
+ })) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
3038
+ return {
3039
+ id: item.id,
3040
+ type: item.type ?? "skill",
3041
+ reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
3042
+ reasons: normalizedReasons,
3043
+ selectionMode: item.selectionMode ?? "matched",
3044
+ install: item.install ?? true,
3045
+ tags: item.tags,
3046
+ ecosystem: item.ecosystem,
3047
+ tokenEstimate: item.tokenEstimate
3048
+ };
3049
+ });
3050
+ const skipped = (input2.skipped ?? []).map((item) => ({
3051
+ id: item.id,
3052
+ reason: item.reason ?? "legacy skipped reason",
3053
+ skipReasons: item.skipReasons?.map((reason) => ({
3054
+ code: reason.code ?? "legacy-skip-reason",
3055
+ message: reason.message ?? item.reason ?? "legacy skipped reason",
3056
+ ...reason.signal ? { signal: reason.signal } : {}
3057
+ })) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
3058
+ }));
3059
+ return {
3060
+ recommended,
3061
+ skipped,
3062
+ warnings: input2.warnings ?? [],
3063
+ estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
3064
+ selectedRules: input2.selectedRules ?? recommended.length,
3065
+ skippedRules: input2.skippedRules ?? skipped.length,
3066
+ estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
3067
+ };
3068
+ }
3069
+ function buildRecommendationExplanation(recommendation) {
3070
+ return {
3071
+ selected: recommendation.recommended.map((item) => ({
3072
+ id: item.id,
3073
+ selectionMode: item.selectionMode,
3074
+ reasons: item.reasons.map((reason) => reason.message)
3075
+ })),
3076
+ skipped: recommendation.skipped.map((item) => ({
3077
+ id: item.id,
3078
+ reasons: item.skipReasons.map((reason) => reason.message),
3079
+ reasonDetails: item.skipReasons.map((reason) => ({
3080
+ code: reason.code,
3081
+ message: reason.message,
3082
+ ...reason.signal ? { signal: reason.signal } : {}
3083
+ }))
3084
+ })),
3085
+ stats: {
3086
+ selectedRules: recommendation.selectedRules,
3087
+ skippedRules: recommendation.skippedRules,
3088
+ estimatedTokenReductionPct: recommendation.estimatedTokenReductionPct
3089
+ }
3090
+ };
3091
+ }
3092
+
3544
3093
  // src/commands/explain-recommendation.ts
3545
3094
  async function runExplainRecommendation(options) {
3546
3095
  const root = process.cwd();
@@ -3620,7 +3169,7 @@ async function runGuard(kind, _options) {
3620
3169
  }
3621
3170
 
3622
3171
  // src/commands/init.ts
3623
- import path22 from "path";
3172
+ import path20 from "path";
3624
3173
  import fs16 from "fs-extra";
3625
3174
 
3626
3175
  // src/utils/prompts.ts
@@ -3968,7 +3517,7 @@ async function runSetupProject(options) {
3968
3517
  // src/commands/init.ts
3969
3518
  async function runInit(options) {
3970
3519
  const root = process.cwd();
3971
- const hausDir = path22.join(root, ".haus-workflow");
3520
+ const hausDir = path20.join(root, ".haus-workflow");
3972
3521
  const alreadyInit = await fs16.pathExists(hausDir);
3973
3522
  if (alreadyInit) {
3974
3523
  log("Haus AI already initialized in this project.");
@@ -3981,7 +3530,7 @@ async function runInit(options) {
3981
3530
 
3982
3531
  // src/install/apply.ts
3983
3532
  import crypto2 from "crypto";
3984
- import path23 from "path";
3533
+ import path21 from "path";
3985
3534
  import fs17 from "fs-extra";
3986
3535
 
3987
3536
  // src/install/header.ts
@@ -4070,7 +3619,7 @@ function hashContent(content2) {
4070
3619
  }
4071
3620
  function sourceVersion() {
4072
3621
  try {
4073
- const pkgPath = path23.join(packageRoot(), "package.json");
3622
+ const pkgPath = path21.join(packageRoot(), "package.json");
4074
3623
  const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
4075
3624
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
4076
3625
  } catch {
@@ -4078,32 +3627,32 @@ function sourceVersion() {
4078
3627
  }
4079
3628
  }
4080
3629
  function globalSrcDir() {
4081
- return path23.join(packageRoot(), "library", "global");
3630
+ return path21.join(packageRoot(), "library", "global");
4082
3631
  }
4083
3632
  function collectSourceFiles(srcDir, claudeDir) {
4084
3633
  const entries = [];
4085
- const skillsDir = path23.join(srcDir, "skills");
3634
+ const skillsDir = path21.join(srcDir, "skills");
4086
3635
  if (fs17.pathExistsSync(skillsDir)) {
4087
3636
  for (const skillName of fs17.readdirSync(skillsDir)) {
4088
- const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
3637
+ const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
4089
3638
  if (fs17.pathExistsSync(skillFile)) {
4090
3639
  entries.push({
4091
3640
  stableId: `skill.${skillName}`,
4092
- srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
4093
- destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
3641
+ srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
3642
+ destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
4094
3643
  });
4095
3644
  }
4096
3645
  }
4097
3646
  }
4098
- const commandsDir = path23.join(srcDir, "commands");
3647
+ const commandsDir = path21.join(srcDir, "commands");
4099
3648
  if (fs17.pathExistsSync(commandsDir)) {
4100
3649
  for (const fileName of fs17.readdirSync(commandsDir)) {
4101
3650
  if (!fileName.endsWith(".md")) continue;
4102
3651
  const commandName = fileName.slice(0, -".md".length);
4103
3652
  entries.push({
4104
3653
  stableId: `command.${commandName}`,
4105
- srcRelPath: path23.join("library", "global", "commands", fileName),
4106
- destPath: path23.join(claudeDir, "commands", fileName)
3654
+ srcRelPath: path21.join("library", "global", "commands", fileName),
3655
+ destPath: path21.join(claudeDir, "commands", fileName)
4107
3656
  });
4108
3657
  }
4109
3658
  }
@@ -4127,7 +3676,7 @@ async function applyInstall(options = {}) {
4127
3676
  };
4128
3677
  const manifestFiles = [];
4129
3678
  for (const entry of sourceFiles) {
4130
- const srcPath = path23.join(packageRoot(), entry.srcRelPath);
3679
+ const srcPath = path21.join(packageRoot(), entry.srcRelPath);
4131
3680
  const rawContent = await readText(srcPath);
4132
3681
  if (rawContent === void 0) {
4133
3682
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -4183,7 +3732,7 @@ async function applyInstall(options = {}) {
4183
3732
  schemaVersion: SCHEMA_VERSION2
4184
3733
  });
4185
3734
  }
4186
- const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
3735
+ const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
4187
3736
  const fragments = await loadHooksFragment(fragmentPath);
4188
3737
  const settings = await readSettings();
4189
3738
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -4321,12 +3870,12 @@ async function runScan(options) {
4321
3870
  }
4322
3871
 
4323
3872
  // src/commands/undo.ts
4324
- import path24 from "path";
3873
+ import path22 from "path";
4325
3874
  import fs18 from "fs-extra";
4326
3875
 
4327
3876
  // src/claude/managed-paths.ts
4328
3877
  var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
4329
- var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json", "config.json"];
3878
+ var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"];
4330
3879
  function coreManagedAbsolutePaths(root) {
4331
3880
  const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
4332
3881
  const haus = PROJECT_MANAGED_HAUS_REL.map((rel) => hausPath(root, rel));
@@ -4339,7 +3888,7 @@ async function collectManagedPaths(root) {
4339
3888
  const lock = await readJson(hausPath(root, "haus.lock.json"));
4340
3889
  for (const row of lock ?? []) {
4341
3890
  for (const rel of row.paths ?? []) {
4342
- paths.add(path24.resolve(root, rel));
3891
+ paths.add(path22.resolve(root, rel));
4343
3892
  }
4344
3893
  }
4345
3894
  const existing = [];
@@ -4355,7 +3904,7 @@ async function settingsHasHausContent(root) {
4355
3904
  return settings._haus != null;
4356
3905
  }
4357
3906
  async function claudeMdHasHausBlock(root) {
4358
- const filePath = path24.join(root, "CLAUDE.md");
3907
+ const filePath = path22.join(root, "CLAUDE.md");
4359
3908
  if (!await fs18.pathExists(filePath)) return false;
4360
3909
  const text = await fs18.readFile(filePath, "utf8");
4361
3910
  return text.includes(BLOCK_BEGIN);
@@ -4368,15 +3917,15 @@ async function stripProjectSettings(root) {
4368
3917
  const hasContent = Object.keys(settings).length > 0;
4369
3918
  if (hasContent) {
4370
3919
  await writeProjectSettings(root, settings);
4371
- log(`Stripped haus rules from ${path24.relative(root, settingsPath)} (user settings preserved).`);
3920
+ log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
4372
3921
  return true;
4373
3922
  }
4374
3923
  await fs18.remove(settingsPath);
4375
- log(`Removed ${path24.relative(root, settingsPath)} (no user-owned settings remained).`);
3924
+ log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
4376
3925
  return true;
4377
3926
  }
4378
3927
  async function stripRootClaudeMd(root) {
4379
- const filePath = path24.join(root, "CLAUDE.md");
3928
+ const filePath = path22.join(root, "CLAUDE.md");
4380
3929
  if (!await fs18.pathExists(filePath)) return false;
4381
3930
  const prev = await fs18.readFile(filePath, "utf8");
4382
3931
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -4404,7 +3953,7 @@ async function runUndo(options) {
4404
3953
  log("Nothing to remove: no haus-managed files found in this directory.");
4405
3954
  return;
4406
3955
  }
4407
- const relTargets = managed.map((p) => path24.relative(root, p));
3956
+ const relTargets = managed.map((p) => path22.relative(root, p));
4408
3957
  const summaryParts = [...relTargets];
4409
3958
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
4410
3959
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -4422,7 +3971,7 @@ User-owned .claude/ files will be preserved.`
4422
3971
  for (const abs of managed) {
4423
3972
  if (!await fs18.pathExists(abs)) continue;
4424
3973
  await fs18.remove(abs);
4425
- log(`Removed ${path24.relative(root, abs)}`);
3974
+ log(`Removed ${path22.relative(root, abs)}`);
4426
3975
  }
4427
3976
  if (stripSettings) await stripProjectSettings(root);
4428
3977
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -4433,7 +3982,7 @@ User-owned .claude/ files will be preserved.`
4433
3982
 
4434
3983
  // src/install/uninstall.ts
4435
3984
  import crypto3 from "crypto";
4436
- import path25 from "path";
3985
+ import path23 from "path";
4437
3986
  import fs19 from "fs-extra";
4438
3987
  async function runUninstall(options = {}) {
4439
3988
  const { force = false } = options;
@@ -4463,14 +4012,14 @@ async function runUninstall(options = {}) {
4463
4012
  continue;
4464
4013
  }
4465
4014
  await fs19.remove(entry.destPath);
4466
- await pruneEmptyDir(path25.dirname(entry.destPath));
4015
+ await pruneEmptyDir(path23.dirname(entry.destPath));
4467
4016
  result.deleted.push(entry.destPath);
4468
4017
  }
4469
4018
  const settings = await readSettings();
4470
4019
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
4471
4020
  await writeSettings(stripped);
4472
4021
  result.hooksStripped = true;
4473
- const hausDir = path25.join(globalClaudeDir(), "haus");
4022
+ const hausDir = path23.join(globalClaudeDir(), "haus");
4474
4023
  const manifestPath2 = hausManifestPath();
4475
4024
  if (fs19.pathExistsSync(manifestPath2)) {
4476
4025
  await fs19.remove(manifestPath2);
@@ -4508,7 +4057,7 @@ async function runUninstallCommand(options) {
4508
4057
  }
4509
4058
 
4510
4059
  // src/commands/update.ts
4511
- import path27 from "path";
4060
+ import path25 from "path";
4512
4061
 
4513
4062
  // src/update/diff-generated-files.ts
4514
4063
  function diffGeneratedFiles() {
@@ -4535,7 +4084,7 @@ function summarizeLockDiff(before, after) {
4535
4084
 
4536
4085
  // src/update/lockfile.ts
4537
4086
  import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
4538
- import path26 from "path";
4087
+ import path24 from "path";
4539
4088
  async function checkLock(root) {
4540
4089
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
4541
4090
  const hasValidVersions = lock.every(
@@ -4566,7 +4115,7 @@ async function applyLock(root) {
4566
4115
  try {
4567
4116
  const backupDir = hausPath(root, "backups");
4568
4117
  await mkdir(backupDir, { recursive: true });
4569
- await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
4118
+ await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
4570
4119
  } catch {
4571
4120
  }
4572
4121
  const enriched = await Promise.all(
@@ -4588,7 +4137,7 @@ function diffLock(before, after) {
4588
4137
  }
4589
4138
  async function hasLocalOverrides(root) {
4590
4139
  try {
4591
- await readFile2(path26.join(root, ".claude", "settings.json"), "utf8");
4140
+ await readFile2(path24.join(root, ".claude", "settings.json"), "utf8");
4592
4141
  return true;
4593
4142
  } catch {
4594
4143
  return false;
@@ -4600,7 +4149,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
4600
4149
  async function runUpdate(options) {
4601
4150
  const root = process.cwd();
4602
4151
  if (options.check) {
4603
- const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
4152
+ const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
4604
4153
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
4605
4154
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
4606
4155
  checkLock(root),
@@ -4630,7 +4179,7 @@ async function runUpdate(options) {
4630
4179
  if (status.driftCount > 0) process.exitCode = 1;
4631
4180
  return;
4632
4181
  }
4633
- const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
4182
+ const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
4634
4183
  const currentVersion = pkgJson?.version ?? "0.0.0";
4635
4184
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4636
4185
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4708,7 +4257,7 @@ async function detectGlobalInstallDrift() {
4708
4257
 
4709
4258
  // src/commands/validate-catalog.ts
4710
4259
  import fs20 from "fs";
4711
- import path28 from "path";
4260
+ import path26 from "path";
4712
4261
  function auditForbiddenStacks(items) {
4713
4262
  const failures = [];
4714
4263
  for (const item of items) {
@@ -4785,17 +4334,17 @@ function auditShippedFiles(manifestDir, items) {
4785
4334
  const failures = [];
4786
4335
  for (const item of items) {
4787
4336
  if (!item.path) continue;
4788
- const absPath = path28.join(manifestDir, item.path);
4337
+ const absPath = path26.join(manifestDir, item.path);
4789
4338
  if (item.type === "skill") {
4790
- const skillMd = path28.join(absPath, "SKILL.md");
4339
+ const skillMd = path26.join(absPath, "SKILL.md");
4791
4340
  if (!fs20.existsSync(skillMd)) {
4792
- failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
4341
+ failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4793
4342
  continue;
4794
4343
  }
4795
4344
  const text = fs20.readFileSync(skillMd, "utf8");
4796
4345
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4797
4346
  failures.push(
4798
- ...auditForbiddenTagsInText(text, `${item.id}: ${path28.relative(manifestDir, skillMd)}`)
4347
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4799
4348
  );
4800
4349
  } else if (item.type === "agent") {
4801
4350
  if (!fs20.existsSync(absPath)) {
@@ -4803,7 +4352,7 @@ function auditShippedFiles(manifestDir, items) {
4803
4352
  continue;
4804
4353
  }
4805
4354
  const text = fs20.readFileSync(absPath, "utf8");
4806
- const rel = path28.relative(manifestDir, absPath);
4355
+ const rel = path26.relative(manifestDir, absPath);
4807
4356
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4808
4357
  failures.push(...auditForbiddenTagsInText(text, `${item.id}: ${rel}`));
4809
4358
  } else if (item.type === "template") {
@@ -4818,7 +4367,7 @@ function auditShippedFiles(manifestDir, items) {
4818
4367
  continue;
4819
4368
  }
4820
4369
  const text = fs20.readFileSync(absPath, "utf8");
4821
- const rel = path28.relative(manifestDir, absPath);
4370
+ const rel = path26.relative(manifestDir, absPath);
4822
4371
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4823
4372
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4824
4373
  }
@@ -4826,7 +4375,7 @@ function auditShippedFiles(manifestDir, items) {
4826
4375
  return failures;
4827
4376
  }
4828
4377
  function auditTemplateContent(manifestDir, absPath, itemId) {
4829
- const rel = path28.relative(manifestDir, absPath);
4378
+ const rel = path26.relative(manifestDir, absPath);
4830
4379
  const text = fs20.readFileSync(absPath, "utf8");
4831
4380
  const failures = [];
4832
4381
  const lines = text.split(/\r?\n/);
@@ -4845,22 +4394,29 @@ function auditTemplateContent(manifestDir, absPath, itemId) {
4845
4394
  failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
4846
4395
  return failures;
4847
4396
  }
4397
+ var DIR_ITEM_TYPE = {
4398
+ skills: "skill",
4399
+ agents: "agent",
4400
+ templates: "template",
4401
+ commands: "command"
4402
+ };
4848
4403
  function auditMarkdownContent(manifestDir) {
4849
4404
  const failures = [];
4850
4405
  const dirs = ["skills", "agents", "templates", "commands"];
4851
4406
  for (const dir of dirs) {
4852
- const abs = path28.join(manifestDir, dir);
4407
+ const abs = path26.join(manifestDir, dir);
4853
4408
  if (!fs20.existsSync(abs)) continue;
4409
+ const checkNonTsxNpx = !NPX_TSX_ONLY_EXEMPT_TYPES.includes(DIR_ITEM_TYPE[dir] ?? "");
4854
4410
  walkMd(abs, (file) => {
4855
4411
  const text = fs20.readFileSync(file, "utf8");
4856
- const rel = path28.relative(manifestDir, file);
4412
+ const rel = path26.relative(manifestDir, file);
4857
4413
  const lines = text.split(/\r?\n/);
4858
4414
  for (let i = 0; i < lines.length; i++) {
4859
4415
  const line2 = lines[i] ?? "";
4860
4416
  if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
4861
4417
  failures.push(`${rel}:${i + 1}: risky install pattern`);
4862
4418
  }
4863
- if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
4419
+ if (checkNonTsxNpx && ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
4864
4420
  failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
4865
4421
  }
4866
4422
  }
@@ -4873,7 +4429,7 @@ function auditMarkdownContent(manifestDir) {
4873
4429
  }
4874
4430
  function walkMd(dir, fn) {
4875
4431
  for (const entry of fs20.readdirSync(dir, { withFileTypes: true })) {
4876
- const full = path28.join(dir, entry.name);
4432
+ const full = path26.join(dir, entry.name);
4877
4433
  if (entry.isDirectory()) walkMd(full, fn);
4878
4434
  else if (entry.name.endsWith(".md")) fn(full);
4879
4435
  }
@@ -4884,8 +4440,8 @@ async function runValidateCatalog(manifestPath2) {
4884
4440
  process.exitCode = 1;
4885
4441
  return;
4886
4442
  }
4887
- const abs = path28.resolve(process.cwd(), manifestPath2);
4888
- const manifestDir = path28.dirname(abs);
4443
+ const abs = path26.resolve(process.cwd(), manifestPath2);
4444
+ const manifestDir = path26.dirname(abs);
4889
4445
  const data = await readJson(abs);
4890
4446
  if (!data?.items) {
4891
4447
  error(`Could not read catalog manifest at ${abs}`);
@@ -4915,7 +4471,7 @@ async function runValidateCatalog(manifestPath2) {
4915
4471
 
4916
4472
  // src/commands/workspace.ts
4917
4473
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4918
- import path35 from "path";
4474
+ import path33 from "path";
4919
4475
 
4920
4476
  // src/commands/workspace/aggregate.ts
4921
4477
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4969,7 +4525,7 @@ ${summaries.map(
4969
4525
  }
4970
4526
 
4971
4527
  // src/commands/workspace/config.ts
4972
- import path29 from "path";
4528
+ import path27 from "path";
4973
4529
  import YAML from "yaml";
4974
4530
  var WORKSPACE_FILE = "haus.workspace.yaml";
4975
4531
  function parseWorkspaceConfig(text) {
@@ -4992,11 +4548,11 @@ function parseWorkspaceConfig(text) {
4992
4548
  };
4993
4549
  }
4994
4550
  async function readWorkspaceConfig(workspaceRoot) {
4995
- return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4551
+ return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
4996
4552
  }
4997
4553
 
4998
4554
  // src/commands/workspace/discover.ts
4999
- import path30 from "path";
4555
+ import path28 from "path";
5000
4556
  import fg4 from "fast-glob";
5001
4557
  import YAML2 from "yaml";
5002
4558
  var DEFAULT_MAX_DEPTH = 3;
@@ -5024,8 +4580,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
5024
4580
  const gitDirs = /* @__PURE__ */ new Set();
5025
4581
  const manifestDirs = /* @__PURE__ */ new Set();
5026
4582
  for (const match of matches) {
5027
- const base = path30.posix.basename(match);
5028
- const dir = path30.posix.dirname(match);
4583
+ const base = path28.posix.basename(match);
4584
+ const dir = path28.posix.dirname(match);
5029
4585
  const owner = dir === "." ? "." : dir;
5030
4586
  if (base === ".git") gitDirs.add(owner);
5031
4587
  else manifestDirs.add(owner);
@@ -5041,9 +4597,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
5041
4597
  }
5042
4598
  repoRoots.sort((a, b) => a.localeCompare(b));
5043
4599
  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);
4600
+ const absDir = path28.resolve(workspaceRoot, relDir);
4601
+ const pkg = await readJson(path28.join(absDir, "package.json"));
4602
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
5047
4603
  let role = "auto";
5048
4604
  try {
5049
4605
  const scan = await scanProject(absDir);
@@ -5078,15 +4634,15 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
5078
4634
  relationships: existing?.relationships ?? []
5079
4635
  };
5080
4636
  }
5081
- function renderWorkspaceYaml(config2) {
4637
+ function renderWorkspaceYaml(config) {
5082
4638
  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
4639
+ client: config.client,
4640
+ repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
4641
+ relationships: config.relationships
5086
4642
  });
5087
4643
  }
5088
4644
  async function runDiscover(workspaceRoot, opts = {}) {
5089
- const yamlPath = path30.join(workspaceRoot, "haus.workspace.yaml");
4645
+ const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
5090
4646
  const existingText = await readText(yamlPath);
5091
4647
  const existing = parseWorkspaceConfig(existingText);
5092
4648
  if (existingText && !existing) {
@@ -5120,18 +4676,18 @@ async function runDiscover(workspaceRoot, opts = {}) {
5120
4676
 
5121
4677
  // src/commands/workspace/doctor.ts
5122
4678
  import { existsSync as existsSync3 } from "fs";
5123
- import path32 from "path";
4679
+ import path30 from "path";
5124
4680
 
5125
4681
  // src/commands/workspace/manifest.ts
5126
4682
  import { readFileSync as readFileSync3 } from "fs";
5127
- import path31 from "path";
4683
+ import path29 from "path";
5128
4684
  var MANIFEST_FILE = "workspace.manifest.json";
5129
4685
  function manifestPath(workspaceRoot) {
5130
4686
  return hausPath(workspaceRoot, MANIFEST_FILE);
5131
4687
  }
5132
4688
  function hausVersion() {
5133
4689
  try {
5134
- const pkg = JSON.parse(readFileSync3(path31.join(packageRoot(), "package.json"), "utf8"));
4690
+ const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
5135
4691
  return pkg.version ?? "0.0.0";
5136
4692
  } catch {
5137
4693
  return "0.0.0";
@@ -5169,7 +4725,7 @@ async function writeWorkspaceManifest(workspaceRoot, manifest) {
5169
4725
 
5170
4726
  // src/commands/workspace/doctor.ts
5171
4727
  async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5172
- const config2 = await readWorkspaceConfig(workspaceRoot);
4728
+ const config = await readWorkspaceConfig(workspaceRoot);
5173
4729
  const manifest = await readManifest2(workspaceRoot);
5174
4730
  const currentVersion = hausVersion();
5175
4731
  const drift = [];
@@ -5179,7 +4735,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5179
4735
  drift.push(item);
5180
4736
  detail.push({ stream: "warn", text: `- ${item.repo}: ${item.detail}` });
5181
4737
  };
5182
- if (!config2) {
4738
+ if (!config) {
5183
4739
  flag({
5184
4740
  repo: "(workspace)",
5185
4741
  kind: "no-config",
@@ -5196,8 +4752,8 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
5196
4752
  return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
5197
4753
  }
5198
4754
  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);
4755
+ for (const repo of config.repos) {
4756
+ const repoRoot = path30.resolve(workspaceRoot, repo.path);
5201
4757
  const entry = manifestByName.get(repo.name);
5202
4758
  if (!entry) {
5203
4759
  flag({
@@ -5271,10 +4827,10 @@ function emit(args) {
5271
4827
 
5272
4828
  // src/commands/workspace/setup.ts
5273
4829
  import { existsSync as existsSync4, statSync } from "fs";
5274
- import path34 from "path";
4830
+ import path32 from "path";
5275
4831
 
5276
4832
  // src/claude/write-workspace-claude-md.ts
5277
- import path33 from "path";
4833
+ import path31 from "path";
5278
4834
  import fs21 from "fs-extra";
5279
4835
  function buildWorkspaceImportBlock(client, members) {
5280
4836
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -5293,7 +4849,7 @@ ${BLOCK_END}`;
5293
4849
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5294
4850
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
5295
4851
  const dryRun = opts.dryRun ?? false;
5296
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path33.join(workspaceRoot, "CLAUDE.md");
4852
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
5297
4853
  const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
5298
4854
  const next = opts.collision ? `${block}
5299
4855
  ` : injectHausBlock(prev, block);
@@ -5319,37 +4875,37 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
5319
4875
 
5320
4876
  // src/commands/workspace/setup.ts
5321
4877
  function resolveWorkspaceRoot(start = process.cwd()) {
5322
- let dir = path34.resolve(start);
4878
+ let dir = path32.resolve(start);
5323
4879
  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);
4880
+ if (existsSync4(path32.join(dir, WORKSPACE_FILE))) return dir;
4881
+ const parent = path32.dirname(dir);
4882
+ if (parent === dir) return path32.resolve(start);
5327
4883
  dir = parent;
5328
4884
  }
5329
4885
  }
5330
4886
  function isRootRepo(workspaceRoot, repoPath) {
5331
- return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
4887
+ return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
5332
4888
  }
5333
4889
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5334
4890
  const apply = options.write ?? false;
5335
- const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
4891
+ const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
5336
4892
  if (!configText) {
5337
4893
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5338
4894
  process.exitCode = 1;
5339
4895
  return { workspaceRoot, statuses: [], written: [] };
5340
4896
  }
5341
- const config2 = parseWorkspaceConfig(configText);
5342
- if (!config2 || config2.repos.length === 0) {
4897
+ const config = parseWorkspaceConfig(configText);
4898
+ if (!config || config.repos.length === 0) {
5343
4899
  error(`No repos configured in ${WORKSPACE_FILE}.`);
5344
4900
  process.exitCode = 1;
5345
4901
  return { workspaceRoot, statuses: [], written: [] };
5346
4902
  }
5347
4903
  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;
4904
+ const repos = onlySet ? config.repos.filter((r) => onlySet.has(r.name)) : config.repos;
5349
4905
  const statuses = [];
5350
4906
  const aggregateInputs = [];
5351
4907
  for (const repo of repos) {
5352
- const repoRoot = path34.resolve(workspaceRoot, repo.path);
4908
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
5353
4909
  log(`
5354
4910
  \u2192 ${repo.name} (${repo.path})`);
5355
4911
  try {
@@ -5386,18 +4942,18 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5386
4942
  }
5387
4943
  const written = [];
5388
4944
  if (apply && aggregateInputs.length > 0) {
5389
- const collision = config2.repos.some((r) => isRootRepo(workspaceRoot, r.path));
4945
+ const collision = config.repos.some((r) => isRootRepo(workspaceRoot, r.path));
5390
4946
  if (!options.dryRun) {
5391
4947
  const artifacts = await writeWorkspaceArtifacts(
5392
4948
  workspaceRoot,
5393
4949
  aggregateInputs,
5394
- config2.relationships
4950
+ config.relationships
5395
4951
  );
5396
4952
  written.push(...artifacts);
5397
4953
  }
5398
4954
  const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
5399
- client: config2.client,
5400
- members: config2.repos.map((r) => ({ name: r.name, path: r.path })),
4955
+ client: config.client,
4956
+ members: config.repos.map((r) => ({ name: r.name, path: r.path })),
5401
4957
  collision,
5402
4958
  dryRun: options.dryRun
5403
4959
  });
@@ -5413,11 +4969,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5413
4969
  }
5414
4970
  const priorByName = new Map((prior?.repos ?? []).map((r) => [r.name, r]));
5415
4971
  const manifestRepos = [];
5416
- for (const repo of config2.repos) {
4972
+ for (const repo of config.repos) {
5417
4973
  const status = statusByName.get(repo.name);
5418
4974
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
5419
4975
  if (status?.status === "ok") {
5420
- const lock = await checkLock(path34.resolve(workspaceRoot, repo.path));
4976
+ const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
5421
4977
  manifestRepos.push({
5422
4978
  name: repo.name,
5423
4979
  path: repo.path,
@@ -5460,7 +5016,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5460
5016
  );
5461
5017
  }
5462
5018
  }
5463
- const manifest = buildManifest2({ client: config2.client, repos: manifestRepos });
5019
+ const manifest = buildManifest2({ client: config.client, repos: manifestRepos });
5464
5020
  const manifestFile = await writeWorkspaceManifest(workspaceRoot, manifest);
5465
5021
  written.push(manifestFile);
5466
5022
  }
@@ -5497,35 +5053,35 @@ relationships: []
5497
5053
  log("Workspace initialized.");
5498
5054
  }
5499
5055
  async function scanWorkspace(workspaceRoot, opts) {
5500
- const configText = await readText(path35.join(workspaceRoot, WORKSPACE_FILE));
5056
+ const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
5501
5057
  if (!configText) {
5502
5058
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
5503
5059
  process.exitCode = 1;
5504
5060
  return;
5505
5061
  }
5506
- const config2 = parseWorkspaceConfig(configText);
5507
- if (!config2) {
5062
+ const config = parseWorkspaceConfig(configText);
5063
+ if (!config) {
5508
5064
  error(
5509
5065
  `Malformed ${WORKSPACE_FILE}. Fix the YAML or re-run \`haus workspace discover --write\`.`
5510
5066
  );
5511
5067
  process.exitCode = 1;
5512
5068
  return;
5513
5069
  }
5514
- if (config2.repos.length === 0) {
5070
+ if (config.repos.length === 0) {
5515
5071
  error(`No repos configured in ${WORKSPACE_FILE}.`);
5516
5072
  process.exitCode = 1;
5517
5073
  return;
5518
5074
  }
5519
5075
  const inputs = [];
5520
- for (const repo of config2.repos) {
5521
- const repoRoot = path35.resolve(workspaceRoot, repo.path);
5076
+ for (const repo of config.repos) {
5077
+ const repoRoot = path33.resolve(workspaceRoot, repo.path);
5522
5078
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5523
5079
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5524
5080
  }
5525
5081
  const result = await scanProject(repoRoot);
5526
5082
  inputs.push({ name: repo.name, path: repo.path, context: result });
5527
5083
  }
5528
- const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config2.relationships);
5084
+ const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config.relationships);
5529
5085
  if (opts.json) {
5530
5086
  log(JSON.stringify({ written }, null, 2));
5531
5087
  } else {
@@ -5568,7 +5124,7 @@ async function runWorkspace(action, options = {}) {
5568
5124
  // src/cli.ts
5569
5125
  function cliVersion() {
5570
5126
  try {
5571
- const pkgPath = path36.join(packageRoot(), "package.json");
5127
+ const pkgPath = path34.join(packageRoot(), "package.json");
5572
5128
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5573
5129
  return pkg.version ?? "0.0.0";
5574
5130
  } catch {
@@ -5578,7 +5134,7 @@ function cliVersion() {
5578
5134
  var program = new Command();
5579
5135
  function validateRuntimeNodeVersion() {
5580
5136
  try {
5581
- const pkgPath = path36.join(packageRoot(), "package.json");
5137
+ const pkgPath = path34.join(packageRoot(), "package.json");
5582
5138
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5583
5139
  const requiredRange = pkg.engines?.node;
5584
5140
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -5605,7 +5161,6 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
5605
5161
  ).option("--force", "Overwrite user-modified managed workflow files").action(runApply);
5606
5162
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
5607
5163
  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
5164
  program.command("init").option("--json").action(runInit);
5610
5165
  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
5166
  (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
@@ -5619,10 +5174,6 @@ program.command("uninstall").option("--force").action(runUninstallCommand);
5619
5174
  var guard = program.command("guard");
5620
5175
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
5621
5176
  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
5177
  var workspace = program.command("workspace");
5627
5178
  workspace.command("init").action(() => runWorkspace("init"));
5628
5179
  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));