@haus-tech/haus-workflow 0.9.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.10.0...v0.10.1) (2026-05-29)
4
+
5
+ ## [0.10.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.9.0...v0.10.0) (2026-05-29)
6
+
7
+ ### Features
8
+
9
+ * Add config command to manage hooks ([1f2c7b4](https://github.com/WeAreHausTech/haus-workflow/commit/1f2c7b4ced3437ef4468759ec7bfc4d8adbf3efc))
10
+
11
+ ### Bug Fixes
12
+
13
+ * address PR review — remove stale module refs, fix plugin-era wording in docs and commands ([6520321](https://github.com/WeAreHausTech/haus-workflow/commit/6520321e1790a304036984c387fdf59d4febe3dd))
14
+
3
15
  ## [0.9.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.8.0...v0.9.0) (2026-05-28)
4
16
 
5
17
  ### Features
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
- # haus
1
+ # Haus Workflow
2
2
 
3
- Claude Code workflow CLI for Haus projects. Scans a repo, recommends stack-fit context assets, and writes controlled `.claude/` and `.haus-workflow/` outputs so Claude works with safer, stack-aware guidance.
4
-
5
- > **Internal Haus tool.** Open-source but unsupported for external use.
3
+ > Internal Haus tool. Open-source but unsupported for external use. No external issues, PRs, or roadmap commitments accepted.
6
4
 
7
5
  ---
8
6
 
@@ -27,7 +25,7 @@ Run once inside each project:
27
25
  haus init
28
26
  ```
29
27
 
30
- This scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
28
+ Scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
31
29
 
32
30
  ---
33
31
 
@@ -35,35 +33,32 @@ This scans the repo, recommends context assets, and writes `.claude/` and `.haus
35
33
 
36
34
  ```bash
37
35
  haus init # first-run setup (scan → recommend → apply)
38
- haus setup-project # re-run setup on an existing project
36
+ haus setup-project # re-run setup on existing project
37
+ haus scan # scan repo and write context-map
38
+ haus recommend # score and recommend catalog items
39
39
  haus apply --dry-run # preview what would be written
40
40
  haus apply --write # write .claude/ files
41
41
  haus update # sync remote catalog + refresh lockfile
42
42
  haus update --check # check for updates without applying
43
43
  haus doctor # health check: hooks, CLAUDE.md, catalog cache
44
+ haus config # manage hook configuration
45
+ haus memory # view project memory store
46
+ haus guard # test bash/file-access guards
44
47
  haus uninstall # remove Haus-managed files from ~/.claude/
45
48
  ```
46
49
 
47
50
  ---
48
51
 
49
- ## Contributing
52
+ ## Development
50
53
 
51
54
  ```bash
52
55
  yarn install
53
56
  yarn verify # typecheck + lint + build + test
57
+ yarn dev <cmd> # run CLI without building (tsx)
54
58
  ```
55
59
 
56
- See [docs/contributing.md](docs/contributing.md).
57
-
58
- ---
59
-
60
- ## Docs
60
+ ### Internal docs
61
61
 
62
- - [User guide](docs/user-guide.md)
63
62
  - [Architecture](docs/architecture.md)
64
- - [Commands](docs/commands.md)
65
- - [Global install layout](docs/global-install.md)
66
- - [Generated files](docs/generated-files.md)
67
- - [Updates and lockfile](docs/updates.md)
63
+ - [CLI reference](docs/cli.md)
68
64
  - [Security](docs/security.md)
69
- - [Memory](docs/memory.md)
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFileSync as readFileSync3 } from "fs";
5
- import path26 from "path";
5
+ import path27 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
@@ -70,8 +70,12 @@ async function syncRemoteCatalog() {
70
70
  return { newItems: [], unchanged: 0, failed: [] };
71
71
  }
72
72
  await fs.ensureDir(CACHE_DIR);
73
- await fs.writeFile(path.join(CACHE_DIR, "manifest.json"), `${JSON.stringify({ items }, null, 2)}
74
- `, "utf8");
73
+ await fs.writeFile(
74
+ path.join(CACHE_DIR, "manifest.json"),
75
+ `${JSON.stringify({ items }, null, 2)}
76
+ `,
77
+ "utf8"
78
+ );
75
79
  const newItems = [];
76
80
  let unchanged = 0;
77
81
  const failed = [];
@@ -374,10 +378,14 @@ import fs4 from "fs-extra";
374
378
  async function assertPostApplySettingsMatchCanonical(root, canonical) {
375
379
  const written = await readJson(claudePath(root, "settings.json"));
376
380
  if (written == null || typeof written !== "object") {
377
- throw new Error("haus: post-apply self-check failed: .claude/settings.json missing or unreadable");
381
+ throw new Error(
382
+ "haus: post-apply self-check failed: .claude/settings.json missing or unreadable"
383
+ );
378
384
  }
379
385
  if (!isDeepStrictEqual(canonical, written)) {
380
- throw new Error("haus: post-apply self-check failed: .claude/settings.json does not match canonical hook contract");
386
+ throw new Error(
387
+ "haus: post-apply self-check failed: .claude/settings.json does not match canonical hook contract"
388
+ );
381
389
  }
382
390
  }
383
391
  async function verifyProjectSettingsHooksContract(root) {
@@ -561,7 +569,11 @@ import fs7 from "fs-extra";
561
569
  var STABLE_ID2 = "template.way-of-work";
562
570
  var SCHEMA_VERSION2 = "1";
563
571
  var TEMPLATE_REL = "library/global/templates/haus-way-of-work.md";
564
- var CATALOG_CACHE_TEMPLATE = path8.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "templates/haus-way-of-work.md");
572
+ var CATALOG_CACHE_TEMPLATE = path8.join(
573
+ os3.homedir(),
574
+ CATALOG_CACHE_SUBDIR,
575
+ "templates/haus-way-of-work.md"
576
+ );
565
577
  function makeWayOfWorkHeader(pkgVersion, contentHash) {
566
578
  return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
567
579
  }
@@ -648,7 +660,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
648
660
  const wayOfWorkPath = await writeWayOfWork(root, hausVersion, dryRun);
649
661
  const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
650
662
  const p6Files = [rootClaudeMdPath, projectFactsPath, ...wayOfWorkPath ? [wayOfWorkPath] : []];
651
- const files = dryRun ? [...coreFiles, ...p6Files] : [...coreFiles, ...p6Files, hausPath(root, "selected-context.json"), hausPath(root, "haus.lock.json")];
663
+ const files = dryRun ? [...coreFiles, ...p6Files] : [
664
+ ...coreFiles,
665
+ ...p6Files,
666
+ hausPath(root, "selected-context.json"),
667
+ hausPath(root, "haus.lock.json")
668
+ ];
652
669
  const hookSettings = await loadClaudeHooksSettings();
653
670
  await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
654
671
  if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
@@ -656,7 +673,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
656
673
  if (!await fs8.pathExists(configPath)) {
657
674
  await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
658
675
  }
659
- await writeManagedText(root, claudePath(root, "commands", "haus-doctor.md"), "Run `haus doctor`.", dryRun);
676
+ await writeManagedText(
677
+ root,
678
+ claudePath(root, "commands", "haus-doctor.md"),
679
+ "Run `haus doctor`.",
680
+ dryRun
681
+ );
660
682
  await writeManagedText(
661
683
  root,
662
684
  claudePath(root, "commands", "haus-review.md"),
@@ -680,7 +702,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
680
702
  const manifestDir = path9.dirname(manifestPath);
681
703
  const manifest = await readJson(manifestPath) ?? { items: [] };
682
704
  const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
683
- const cacheManifest = await readJson(path9.join(CACHE_DIR, "manifest.json"));
705
+ const cacheManifest = await readJson(
706
+ path9.join(CACHE_DIR, "manifest.json")
707
+ );
684
708
  const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
685
709
  const installedPathsByItem = /* @__PURE__ */ new Map();
686
710
  const installedIds = /* @__PURE__ */ new Set();
@@ -708,7 +732,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
708
732
  if (await fs8.pathExists(sourcePath)) {
709
733
  if (dryRun) {
710
734
  const exists = await fs8.pathExists(destination);
711
- log(`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`);
735
+ log(
736
+ `${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
737
+ );
712
738
  } else {
713
739
  await fs8.ensureDir(path9.dirname(destination));
714
740
  await fs8.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
@@ -718,7 +744,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
718
744
  installedPathsByItem.set(item.id, [...current, path9.relative(root, destination)]);
719
745
  installedIds.add(item.id);
720
746
  } else {
721
- warn(`Skipping ${item.id}: source not found at ${sourcePath} \u2014 run \`haus update\` to populate catalog cache`);
747
+ warn(
748
+ `Skipping ${item.id}: source not found at ${sourcePath} \u2014 run \`haus update\` to populate catalog cache`
749
+ );
722
750
  }
723
751
  }
724
752
  if (dryRun) return [...new Set(files)];
@@ -726,7 +754,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
726
754
  await writeManagedJson(
727
755
  root,
728
756
  hausPath(root, "selected-context.json"),
729
- installedItems.map((r) => ({ id: r.id, type: r.type, reason: r.reason, confidenceLevel: r.confidenceLevel })),
757
+ installedItems.map((r) => ({
758
+ id: r.id,
759
+ type: r.type,
760
+ reason: r.reason,
761
+ confidenceLevel: r.confidenceLevel
762
+ })),
730
763
  false
731
764
  );
732
765
  const lock = await Promise.all(
@@ -831,7 +864,9 @@ async function runApply(options) {
831
864
  const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
832
865
  if (catalogItemCount > 0 && !await cacheHasItems()) {
833
866
  if (isDryRun) {
834
- warn("Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first.");
867
+ warn(
868
+ "Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first."
869
+ );
835
870
  } else {
836
871
  error(
837
872
  "Catalog cache is empty \u2014 cannot install catalog items. Run `haus update` first, or pass --allow-empty-cache to apply core files only."
@@ -893,7 +928,8 @@ async function runCatalogAudit() {
893
928
  const failures = [];
894
929
  for (const item of items) {
895
930
  const text = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
896
- for (const word of FORBIDDEN) if (text.includes(word)) failures.push(`${item.id} has unsupported tag ${word}`);
931
+ for (const word of FORBIDDEN)
932
+ if (text.includes(word)) failures.push(`${item.id} has unsupported tag ${word}`);
897
933
  }
898
934
  if (failures.length) {
899
935
  failures.forEach((f) => error(f));
@@ -903,6 +939,36 @@ async function runCatalogAudit() {
903
939
  log("Catalog audit passed.");
904
940
  }
905
941
 
942
+ // src/commands/config.ts
943
+ import path12 from "path";
944
+ var CONFIG_PATH2 = ".haus-workflow/config.json";
945
+ var HOOK_ALIASES = {
946
+ "hook.context": "context",
947
+ "hook.memory": "memoryInject"
948
+ };
949
+ async function runConfig(key, action) {
950
+ const hookKey = HOOK_ALIASES[key];
951
+ if (!hookKey) {
952
+ throw new Error(
953
+ `Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`
954
+ );
955
+ }
956
+ const root = process.cwd();
957
+ const configPath = path12.join(root, CONFIG_PATH2);
958
+ const existing = await readJson(configPath);
959
+ const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
960
+ cfg.hooks ??= {};
961
+ cfg.hooks[hookKey] ??= {};
962
+ if (action === "status") {
963
+ const enabled = cfg.hooks[hookKey]?.enabled === true;
964
+ log(`${key}: ${enabled ? "enabled" : "disabled"}`);
965
+ return;
966
+ }
967
+ cfg.hooks[hookKey].enabled = action === "enable";
968
+ await writeJson(configPath, cfg);
969
+ log(`${key} ${action}d`);
970
+ }
971
+
906
972
  // src/recommender/explain-recommendation.ts
907
973
  function normalizeRecommendation(input2) {
908
974
  const recommended = (input2.recommended ?? []).map((item) => {
@@ -911,7 +977,9 @@ function normalizeRecommendation(input2) {
911
977
  message: reason.message ?? item.reason ?? "legacy recommendation reason",
912
978
  weight: reason.weight ?? 0,
913
979
  ...reason.signal ? { signal: reason.signal } : {}
914
- })) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }];
980
+ })) ?? [
981
+ { code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }
982
+ ];
915
983
  const confidence = item.confidence ?? 0;
916
984
  return {
917
985
  id: item.id,
@@ -956,7 +1024,10 @@ function normalizeRecommendation(input2) {
956
1024
  estimatedContextTokens: input2.estimatedContextTokens ?? recommended.length * 320,
957
1025
  selectedRules: input2.selectedRules ?? recommended.length,
958
1026
  skippedRules: input2.skippedRules ?? skipped.length,
959
- estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? Math.max(0, Math.round(skipped.length / Math.max(recommended.length + skipped.length, 1) * 100))
1027
+ estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? Math.max(
1028
+ 0,
1029
+ Math.round(skipped.length / Math.max(recommended.length + skipped.length, 1) * 100)
1030
+ )
960
1031
  };
961
1032
  }
962
1033
  function buildRecommendationExplanation(recommendation) {
@@ -1241,7 +1312,7 @@ function computeRuleIntents(rule) {
1241
1312
 
1242
1313
  // src/scanner/scan-project.ts
1243
1314
  import { readFile } from "fs/promises";
1244
- import path13 from "path";
1315
+ import path14 from "path";
1245
1316
 
1246
1317
  // src/utils/audit-checks.ts
1247
1318
  function isRecord(v) {
@@ -1268,7 +1339,7 @@ function compareVersions(a, b) {
1268
1339
  }
1269
1340
 
1270
1341
  // src/scanner/detect-package-manager.ts
1271
- import path12 from "path";
1342
+ import path13 from "path";
1272
1343
  import fs9 from "fs-extra";
1273
1344
  function detectPackageManager(root, packageManagerField) {
1274
1345
  const field = String(packageManagerField ?? "").trim();
@@ -1287,9 +1358,9 @@ function detectPackageManager(root, packageManagerField) {
1287
1358
  if (satisfiesVersion(version, ">=9")) return "npm";
1288
1359
  return "unknown";
1289
1360
  }
1290
- if (fs9.existsSync(path12.join(root, "yarn.lock"))) return "yarn";
1291
- if (fs9.existsSync(path12.join(root, "pnpm-lock.yaml"))) return "pnpm";
1292
- if (fs9.existsSync(path12.join(root, "package-lock.json"))) return "npm";
1361
+ if (fs9.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
1362
+ if (fs9.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
1363
+ if (fs9.existsSync(path13.join(root, "package-lock.json"))) return "npm";
1293
1364
  return "unknown";
1294
1365
  }
1295
1366
 
@@ -1350,8 +1421,8 @@ function blocked(rel) {
1350
1421
  return SENSITIVE.some((x) => x.test(rel));
1351
1422
  }
1352
1423
  async function scanProject(root, mode = "fast") {
1353
- const pkg = await readJson(path13.join(root, "package.json"));
1354
- const composer = await readJson(path13.join(root, "composer.json"));
1424
+ const pkg = await readJson(path14.join(root, "package.json"));
1425
+ const composer = await readJson(path14.join(root, "composer.json"));
1355
1426
  const files = await listFiles(root, SAFE_FILES);
1356
1427
  const safeFiles = files.filter((f) => !blocked(f));
1357
1428
  const deps = dependencySet(pkg, composer);
@@ -1368,16 +1439,18 @@ async function scanProject(root, mode = "fast") {
1368
1439
  if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
1369
1440
  warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
1370
1441
  }
1371
- if (safeFiles.some((f) => f.includes("docker-compose"))) crossRepoHints.push("Containerized services detected");
1442
+ if (safeFiles.some((f) => f.includes("docker-compose")))
1443
+ crossRepoHints.push("Containerized services detected");
1372
1444
  if (safeFiles.some((f) => f.includes("turbo.json") || f.includes("nx.json")))
1373
1445
  crossRepoHints.push("Monorepo orchestration detected");
1374
1446
  if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
1375
- if (safeFiles.some((f) => f.includes("wp-content/uploads"))) securityRisks.push("Uploads directory present");
1447
+ if (safeFiles.some((f) => f.includes("wp-content/uploads")))
1448
+ securityRisks.push("Uploads directory present");
1376
1449
  const context = {
1377
1450
  mode,
1378
1451
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1379
1452
  root,
1380
- repoName: String(pkg?.name ?? path13.basename(root)),
1453
+ repoName: String(pkg?.name ?? path14.basename(root)),
1381
1454
  packageManager,
1382
1455
  repoRoles: roles,
1383
1456
  confidence: computeConfidence(roles, stacks),
@@ -1392,7 +1465,11 @@ async function scanProject(root, mode = "fast") {
1392
1465
  composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
1393
1466
  };
1394
1467
  const scanHashes = Object.fromEntries(
1395
- await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(path13.join(root, f), "utf8"))]))
1468
+ await Promise.all(
1469
+ safeFiles.map(
1470
+ async (f) => [f, hashText(await readFile(path14.join(root, f), "utf8"))]
1471
+ )
1472
+ )
1396
1473
  );
1397
1474
  const repoSummary = renderSummary(context);
1398
1475
  await writeJson(hausPath(root, "context-map.json"), context);
@@ -1418,9 +1495,11 @@ function detectRoles(deps, files) {
1418
1495
  if (deps.includes("next") || files.some((f) => f.includes("next.config."))) roles.add("next-app");
1419
1496
  if (deps.includes("react")) roles.add("react-app");
1420
1497
  if (deps.includes("vite") || files.some((f) => f.includes("vite.config."))) roles.add("vite-app");
1421
- if (deps.includes("react-router") && deps.includes("@react-router/node")) roles.add("react-router-app");
1498
+ if (deps.includes("react-router") && deps.includes("@react-router/node"))
1499
+ roles.add("react-router-app");
1422
1500
  if (deps.includes("sanity")) roles.add("sanity-studio");
1423
- if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/"))) roles.add("strapi-app");
1501
+ if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
1502
+ roles.add("strapi-app");
1424
1503
  if (deps.includes("expo")) roles.add("expo-app");
1425
1504
  if (deps.includes("@vendure/core")) roles.add("vendure-app");
1426
1505
  if (deps.some((d) => d.startsWith("@haus/vendure-")) || files.some((f) => f.includes("vendure-config")))
@@ -1429,7 +1508,8 @@ function detectRoles(deps, files) {
1429
1508
  if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) roles.add("graphql-api");
1430
1509
  if (files.some((f) => f.endsWith("nx.json"))) roles.add("nx-monorepo");
1431
1510
  if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
1432
- if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework")) roles.add("laravel-app");
1511
+ if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
1512
+ roles.add("laravel-app");
1433
1513
  if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
1434
1514
  const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
1435
1515
  const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
@@ -1465,7 +1545,8 @@ async function detectStacks(root, deps, files, packageManager) {
1465
1545
  if (deps.includes("react")) add("frontend", "react19");
1466
1546
  if (deps.includes("vue")) add("frontend", "vue");
1467
1547
  if (deps.includes("vite")) add("frontend", "vite8");
1468
- if (deps.includes("react-router") && deps.includes("@react-router/node")) add("frontend", "react-router-v7");
1548
+ if (deps.includes("react-router") && deps.includes("@react-router/node"))
1549
+ add("frontend", "react-router-v7");
1469
1550
  if (deps.includes("tailwindcss") || files.some((f) => f.includes("tailwind.config."))) {
1470
1551
  add("frontend", "tailwindcss");
1471
1552
  }
@@ -1484,8 +1565,10 @@ async function detectStacks(root, deps, files, packageManager) {
1484
1565
  if (deps.includes("react-native")) add("frontend", "react-native");
1485
1566
  if (deps.includes("i18next") || deps.includes("react-i18next")) add("tooling", "i18next");
1486
1567
  if (deps.includes("bullmq")) add("tooling", "bullmq");
1487
- if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose"))) add("tooling", "docker");
1488
- if (deps.includes("pm2") || files.some((f) => f.includes("ecosystem.config"))) add("tooling", "pm2");
1568
+ if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
1569
+ add("tooling", "docker");
1570
+ if (deps.includes("pm2") || files.some((f) => f.includes("ecosystem.config")))
1571
+ add("tooling", "pm2");
1489
1572
  if (deps.some((d) => d.startsWith("@sentry/"))) add("tooling", "sentry");
1490
1573
  if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
1491
1574
  if (!deps.includes("prettier")) add("tooling", "missing-prettier");
@@ -1502,10 +1585,13 @@ async function detectStacks(root, deps, files, packageManager) {
1502
1585
  if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
1503
1586
  if (await hasNeedle(root, files, "@VendurePlugin")) add("backend", "vendure3");
1504
1587
  if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) add("backend", "graphql");
1505
- if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql"))) add("backend", "graphql");
1588
+ if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
1589
+ add("backend", "graphql");
1506
1590
  if (deps.includes("laravel/framework")) add("backend", "laravel");
1507
- if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/"))) add("backend", "laravel");
1508
- if (files.some((f) => f.endsWith("wp-config.php")) || deps.includes("roots/wordpress")) add("backend", "wordpress");
1591
+ if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
1592
+ add("backend", "laravel");
1593
+ if (files.some((f) => f.endsWith("wp-config.php")) || deps.includes("roots/wordpress"))
1594
+ add("backend", "wordpress");
1509
1595
  if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
1510
1596
  add("backend", "elementor");
1511
1597
  }
@@ -1548,7 +1634,7 @@ async function hasNeedle(root, files, needle) {
1548
1634
  );
1549
1635
  for (const rel of candidates.slice(0, 300)) {
1550
1636
  try {
1551
- const content = await readFile(path13.join(root, rel), "utf8");
1637
+ const content = await readFile(path14.join(root, rel), "utf8");
1552
1638
  if (content.includes(needle)) return true;
1553
1639
  } catch {
1554
1640
  continue;
@@ -1641,16 +1727,19 @@ async function runContext(options) {
1641
1727
  }
1642
1728
 
1643
1729
  // src/commands/doctor.ts
1644
- import path14 from "path";
1730
+ import path15 from "path";
1645
1731
  import fs10 from "fs-extra";
1646
1732
 
1647
1733
  // src/update/npm-version.ts
1648
1734
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
1649
1735
  async function fetchNpmVersionStatus(currentVersion) {
1650
1736
  try {
1651
- const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`, {
1652
- signal: AbortSignal.timeout(8e3)
1653
- });
1737
+ const res = await fetch(
1738
+ `https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`,
1739
+ {
1740
+ signal: AbortSignal.timeout(8e3)
1741
+ }
1742
+ );
1654
1743
  if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
1655
1744
  const data = await res.json();
1656
1745
  const latest = data?.version;
@@ -1709,7 +1798,7 @@ async function runDoctor(options) {
1709
1798
  const enabled = await isHookEnabled(root, key);
1710
1799
  log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
1711
1800
  }
1712
- const rootClaudeMdPath = path14.join(root, "CLAUDE.md");
1801
+ const rootClaudeMdPath = path15.join(root, "CLAUDE.md");
1713
1802
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
1714
1803
  if (!rootClaudeMdContent) {
1715
1804
  warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
@@ -1729,12 +1818,20 @@ async function runDoctor(options) {
1729
1818
  warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
1730
1819
  } else {
1731
1820
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
1732
- const templatePath = path14.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
1821
+ const templatePath = path15.join(
1822
+ packageRoot(),
1823
+ "library",
1824
+ "global",
1825
+ "templates",
1826
+ "haus-way-of-work.md"
1827
+ );
1733
1828
  const templateContent = await readText(templatePath);
1734
1829
  if (storedHashMatch && templateContent) {
1735
1830
  const currentHash = hashText(templateContent);
1736
1831
  if (storedHashMatch[1] !== currentHash) {
1737
- warn("- .haus-workflow/haus-way-of-work.md: stale (template updated \u2014 run `haus apply --write`)");
1832
+ warn(
1833
+ "- .haus-workflow/haus-way-of-work.md: stale (template updated \u2014 run `haus apply --write`)"
1834
+ );
1738
1835
  } else {
1739
1836
  log("- .haus-workflow/haus-way-of-work.md: OK");
1740
1837
  }
@@ -1767,11 +1864,13 @@ async function runDoctor(options) {
1767
1864
  log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
1768
1865
  }
1769
1866
  }
1770
- const pkgJson = await readJson(path14.join(packageRoot(), "package.json"));
1867
+ const pkgJson = await readJson(path15.join(packageRoot(), "package.json"));
1771
1868
  const currentVersion = pkgJson?.version ?? "0.0.0";
1772
1869
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
1773
1870
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
1774
- warn(`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`);
1871
+ warn(
1872
+ `- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`
1873
+ );
1775
1874
  process.exitCode = 1;
1776
1875
  } else if (npmStatus.latest !== null) {
1777
1876
  log(`- CLI: ${currentVersion} (up to date)`);
@@ -1932,7 +2031,7 @@ async function runGuard(kind, _options) {
1932
2031
  }
1933
2032
 
1934
2033
  // src/commands/init.ts
1935
- import path15 from "path";
2034
+ import path16 from "path";
1936
2035
  import fs11 from "fs-extra";
1937
2036
 
1938
2037
  // src/utils/exec.ts
@@ -1941,6 +2040,7 @@ async function runCommand(command, args = [], options = {}) {
1941
2040
  try {
1942
2041
  const result = await execa(command, args, {
1943
2042
  reject: false,
2043
+ // non-zero exits are returned, not thrown
1944
2044
  ...options
1945
2045
  });
1946
2046
  return {
@@ -2001,7 +2101,9 @@ var ECOSYSTEM_COMPATIBLE_BACKENDS = {
2001
2101
  async function recommend(root, context) {
2002
2102
  const items = await loadCatalog(root);
2003
2103
  const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
2004
- const sources = await readJson(hausPath(root, "sources-report.json")) ?? {};
2104
+ const sources = await readJson(
2105
+ hausPath(root, "sources-report.json")
2106
+ ) ?? {};
2005
2107
  const stackSet = buildStackSet(context);
2006
2108
  const depSet = new Set(context.dependencies.map((d) => d.toLowerCase()));
2007
2109
  const roleSet = new Set(context.repoRoles.map((r) => r.toLowerCase()));
@@ -2085,14 +2187,23 @@ async function recommend(root, context) {
2085
2187
  if (tagMatch) {
2086
2188
  pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
2087
2189
  }
2088
- const goalMatch = item.tags.find((t) => goals.includes(t) || goals.includes(t.replace(/-/g, " ")));
2190
+ const goalMatch = item.tags.find(
2191
+ (t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
2192
+ );
2089
2193
  if (goalMatch) {
2090
2194
  pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
2091
2195
  }
2092
2196
  if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
2093
- pushReason("package-manager-match", "package manager match", 10, `packageManager:${context.packageManager}`);
2197
+ pushReason(
2198
+ "package-manager-match",
2199
+ "package manager match",
2200
+ 10,
2201
+ `packageManager:${context.packageManager}`
2202
+ );
2094
2203
  }
2095
- const configSignal = item.tags.find((t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase()));
2204
+ const configSignal = item.tags.find(
2205
+ (t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
2206
+ );
2096
2207
  if (configSignal) {
2097
2208
  pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
2098
2209
  }
@@ -2179,9 +2290,15 @@ async function recommend(root, context) {
2179
2290
  pushSkipReason("source-approval", "Source not approved", 100);
2180
2291
  }
2181
2292
  if (securityRiskCount > 0 && !isDefaultBaseline && (item.tags.includes("security") || item.id.includes("security"))) {
2182
- pushSkipReason("security-risk-penalty", "Security-tagged item penalized by active risk signals", 20);
2293
+ pushSkipReason(
2294
+ "security-risk-penalty",
2295
+ "Security-tagged item penalized by active risk signals",
2296
+ 20
2297
+ );
2183
2298
  }
2184
- const positiveReasonCodes = new Set(reasons.map((r) => r.code).filter((c) => c !== "default-baseline"));
2299
+ const positiveReasonCodes = new Set(
2300
+ reasons.map((r) => r.code).filter((c) => c !== "default-baseline")
2301
+ );
2185
2302
  const hasRoleSignal = positiveReasonCodes.has("repo-role-match");
2186
2303
  const hasDepOrStackSignal = positiveReasonCodes.has("stack-match") || positiveReasonCodes.has("requires-any-match");
2187
2304
  if (hasRoleSignal && !hasDepOrStackSignal && !isDefaultBaseline && requiresAny.length === 0) {
@@ -2252,7 +2369,11 @@ async function recommend(root, context) {
2252
2369
  };
2253
2370
  }
2254
2371
  function buildStackSet(context) {
2255
- return new Set([...context.repoRoles, ...Object.values(context.detectedStacks).flat()].map((x) => x.toLowerCase()));
2372
+ return new Set(
2373
+ [...context.repoRoles, ...Object.values(context.detectedStacks).flat()].map(
2374
+ (x) => x.toLowerCase()
2375
+ )
2376
+ );
2256
2377
  }
2257
2378
  function inferRepoEcosystems(roles) {
2258
2379
  const ecosystems = /* @__PURE__ */ new Set();
@@ -2454,7 +2575,7 @@ async function runSetupProject(options) {
2454
2575
  // src/commands/init.ts
2455
2576
  async function runInit(options) {
2456
2577
  const root = process.cwd();
2457
- const hausDir = path15.join(root, ".haus-workflow");
2578
+ const hausDir = path16.join(root, ".haus-workflow");
2458
2579
  const alreadyInit = await fs11.pathExists(hausDir);
2459
2580
  if (alreadyInit) {
2460
2581
  log("Haus AI already initialized in this project.");
@@ -2467,7 +2588,7 @@ async function runInit(options) {
2467
2588
 
2468
2589
  // src/install/apply.ts
2469
2590
  import crypto2 from "crypto";
2470
- import path18 from "path";
2591
+ import path19 from "path";
2471
2592
  import fs13 from "fs-extra";
2472
2593
 
2473
2594
  // src/install/header.ts
@@ -2502,13 +2623,13 @@ ${content}`;
2502
2623
 
2503
2624
  // src/install/manifest.ts
2504
2625
  import os5 from "os";
2505
- import path16 from "path";
2626
+ import path17 from "path";
2506
2627
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
2507
2628
  function globalClaudeDir() {
2508
- return path16.join(os5.homedir(), ".claude");
2629
+ return path17.join(os5.homedir(), ".claude");
2509
2630
  }
2510
2631
  function hausManifestPath() {
2511
- return path16.join(globalClaudeDir(), "haus", "install-manifest.json");
2632
+ return path17.join(globalClaudeDir(), "haus", "install-manifest.json");
2512
2633
  }
2513
2634
  async function readManifest() {
2514
2635
  return readJson(hausManifestPath());
@@ -2527,10 +2648,10 @@ function buildManifest(source, files, hooks) {
2527
2648
  }
2528
2649
 
2529
2650
  // src/install/settings-merge.ts
2530
- import path17 from "path";
2651
+ import path18 from "path";
2531
2652
  import fs12 from "fs-extra";
2532
2653
  function settingsJsonPath() {
2533
- return path17.join(globalClaudeDir(), "settings.json");
2654
+ return path18.join(globalClaudeDir(), "settings.json");
2534
2655
  }
2535
2656
  async function readSettings() {
2536
2657
  const parsed = await readJson(settingsJsonPath());
@@ -2601,7 +2722,7 @@ function hashContent(content) {
2601
2722
  }
2602
2723
  function sourceVersion() {
2603
2724
  try {
2604
- const pkgPath = path18.join(packageRoot(), "package.json");
2725
+ const pkgPath = path19.join(packageRoot(), "package.json");
2605
2726
  const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
2606
2727
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
2607
2728
  } catch {
@@ -2609,32 +2730,32 @@ function sourceVersion() {
2609
2730
  }
2610
2731
  }
2611
2732
  function globalSrcDir() {
2612
- return path18.join(packageRoot(), "library", "global");
2733
+ return path19.join(packageRoot(), "library", "global");
2613
2734
  }
2614
2735
  function collectSourceFiles(srcDir, claudeDir) {
2615
2736
  const entries = [];
2616
- const skillsDir = path18.join(srcDir, "skills");
2737
+ const skillsDir = path19.join(srcDir, "skills");
2617
2738
  if (fs13.pathExistsSync(skillsDir)) {
2618
2739
  for (const skillName of fs13.readdirSync(skillsDir)) {
2619
- const skillFile = path18.join(skillsDir, skillName, "SKILL.md");
2740
+ const skillFile = path19.join(skillsDir, skillName, "SKILL.md");
2620
2741
  if (fs13.pathExistsSync(skillFile)) {
2621
2742
  entries.push({
2622
2743
  stableId: `skill.${skillName}`,
2623
- srcRelPath: path18.join("library", "global", "skills", skillName, "SKILL.md"),
2624
- destPath: path18.join(claudeDir, "skills", skillName, "SKILL.md")
2744
+ srcRelPath: path19.join("library", "global", "skills", skillName, "SKILL.md"),
2745
+ destPath: path19.join(claudeDir, "skills", skillName, "SKILL.md")
2625
2746
  });
2626
2747
  }
2627
2748
  }
2628
2749
  }
2629
- const agentsDir = path18.join(srcDir, "agents");
2750
+ const agentsDir = path19.join(srcDir, "agents");
2630
2751
  if (fs13.pathExistsSync(agentsDir)) {
2631
2752
  for (const agentFile of fs13.readdirSync(agentsDir)) {
2632
2753
  if (!agentFile.endsWith(".md")) continue;
2633
2754
  const agentName = agentFile.replace(/\.md$/, "");
2634
2755
  entries.push({
2635
2756
  stableId: `agent.${agentName}`,
2636
- srcRelPath: path18.join("library", "global", "agents", agentFile),
2637
- destPath: path18.join(claudeDir, "agents", agentFile)
2757
+ srcRelPath: path19.join("library", "global", "agents", agentFile),
2758
+ destPath: path19.join(claudeDir, "agents", agentFile)
2638
2759
  });
2639
2760
  }
2640
2761
  }
@@ -2658,7 +2779,7 @@ async function applyInstall(options = {}) {
2658
2779
  };
2659
2780
  const manifestFiles = [];
2660
2781
  for (const entry of sourceFiles) {
2661
- const srcPath = path18.join(packageRoot(), entry.srcRelPath);
2782
+ const srcPath = path19.join(packageRoot(), entry.srcRelPath);
2662
2783
  const rawContent = await readText(srcPath);
2663
2784
  if (rawContent === void 0) {
2664
2785
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -2714,7 +2835,7 @@ async function applyInstall(options = {}) {
2714
2835
  schemaVersion: SCHEMA_VERSION3
2715
2836
  });
2716
2837
  }
2717
- const fragmentPath = path18.join(srcDir, "settings-fragments", "hooks.json");
2838
+ const fragmentPath = path19.join(srcDir, "settings-fragments", "hooks.json");
2718
2839
  const fragments = await loadHooksFragment(fragmentPath);
2719
2840
  const settings = await readSettings();
2720
2841
  const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
@@ -2739,7 +2860,10 @@ async function applyInstall(options = {}) {
2739
2860
  }
2740
2861
  if (!dryRun && !check) {
2741
2862
  await writeSettings(mergedSettings);
2742
- const manifest = buildManifest(source, manifestFiles, [...existingManifest?.hooks ?? [], ...addedIds]);
2863
+ const manifest = buildManifest(source, manifestFiles, [
2864
+ ...existingManifest?.hooks ?? [],
2865
+ ...addedIds
2866
+ ]);
2743
2867
  await writeManifest(manifest);
2744
2868
  }
2745
2869
  return result;
@@ -2783,7 +2907,9 @@ async function runInstall(options) {
2783
2907
  process.exitCode = 1;
2784
2908
  } else if (!options.check && !options.dryRun) {
2785
2909
  const total = result.created.length + result.updated.length;
2786
- log(`haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`);
2910
+ log(
2911
+ `haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
2912
+ );
2787
2913
  }
2788
2914
  } catch (err) {
2789
2915
  error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -2792,7 +2918,12 @@ async function runInstall(options) {
2792
2918
  }
2793
2919
 
2794
2920
  // src/memory/memory-store.ts
2795
- var FILES = ["project-learnings.md", "decisions.md", "recurring-issues.md", "client-context.md"];
2921
+ var FILES = [
2922
+ "project-learnings.md",
2923
+ "decisions.md",
2924
+ "recurring-issues.md",
2925
+ "client-context.md"
2926
+ ];
2796
2927
  async function ensureMemory(root) {
2797
2928
  await Promise.all(
2798
2929
  FILES.map(async (name) => {
@@ -2849,7 +2980,10 @@ async function runMemory(subcommand, options) {
2849
2980
  return;
2850
2981
  }
2851
2982
  const compact = `Task: ${options.task ?? "n/a"}
2852
- ${text}`.slice(0, options.fromHook ? 1200 : 4e3);
2983
+ ${text}`.slice(
2984
+ 0,
2985
+ options.fromHook ? 1200 : 4e3
2986
+ );
2853
2987
  log(compact);
2854
2988
  return;
2855
2989
  }
@@ -2899,12 +3033,12 @@ async function runScan(options) {
2899
3033
  }
2900
3034
 
2901
3035
  // src/commands/undo.ts
2902
- import path19 from "path";
3036
+ import path20 from "path";
2903
3037
  import fs14 from "fs-extra";
2904
3038
  var CLAUDE_DIR = ".claude";
2905
3039
  async function runUndo(options) {
2906
3040
  const root = process.cwd();
2907
- const targets = [path19.join(root, CLAUDE_DIR), path19.join(root, HAUS_DIR)];
3041
+ const targets = [path20.join(root, CLAUDE_DIR), path20.join(root, HAUS_DIR)];
2908
3042
  const existing = targets.filter((p) => fs14.existsSync(p));
2909
3043
  if (existing.length === 0) {
2910
3044
  log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
@@ -2912,7 +3046,7 @@ async function runUndo(options) {
2912
3046
  }
2913
3047
  if (!options.yes) {
2914
3048
  const ok = await confirm(
2915
- `Remove ${existing.map((p) => path19.relative(root, p)).join(" and ")}? This cannot be undone.`
3049
+ `Remove ${existing.map((p) => path20.relative(root, p)).join(" and ")}? This cannot be undone.`
2916
3050
  );
2917
3051
  if (!ok) {
2918
3052
  log("Cancelled.");
@@ -2921,13 +3055,13 @@ async function runUndo(options) {
2921
3055
  }
2922
3056
  for (const p of existing) {
2923
3057
  await fs14.remove(p);
2924
- log(`Removed ${path19.relative(root, p)}`);
3058
+ log(`Removed ${path20.relative(root, p)}`);
2925
3059
  }
2926
3060
  }
2927
3061
 
2928
3062
  // src/install/uninstall.ts
2929
3063
  import crypto3 from "crypto";
2930
- import path20 from "path";
3064
+ import path21 from "path";
2931
3065
  import fs15 from "fs-extra";
2932
3066
  async function runUninstall(options = {}) {
2933
3067
  const { force = false } = options;
@@ -2950,19 +3084,21 @@ async function runUninstall(options = {}) {
2950
3084
  }
2951
3085
  const currentHash = `sha256-${crypto3.createHash("sha256").update(content).digest("hex")}`;
2952
3086
  if (currentHash !== entry.hash && !force) {
2953
- warn(`Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`);
3087
+ warn(
3088
+ `Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
3089
+ );
2954
3090
  result.skipped.push(entry.destPath);
2955
3091
  continue;
2956
3092
  }
2957
3093
  await fs15.remove(entry.destPath);
2958
- await pruneEmptyDir(path20.dirname(entry.destPath));
3094
+ await pruneEmptyDir(path21.dirname(entry.destPath));
2959
3095
  result.deleted.push(entry.destPath);
2960
3096
  }
2961
3097
  const settings = await readSettings();
2962
3098
  const stripped = stripHausHooks(settings);
2963
3099
  await writeSettings(stripped);
2964
3100
  result.hooksStripped = true;
2965
- const hausDir = path20.join(globalClaudeDir(), "haus");
3101
+ const hausDir = path21.join(globalClaudeDir(), "haus");
2966
3102
  const manifestPath = hausManifestPath();
2967
3103
  if (fs15.pathExistsSync(manifestPath)) {
2968
3104
  await fs15.remove(manifestPath);
@@ -3007,7 +3143,7 @@ async function runUninstallCommand(options) {
3007
3143
  }
3008
3144
 
3009
3145
  // src/commands/update.ts
3010
- import path22 from "path";
3146
+ import path23 from "path";
3011
3147
 
3012
3148
  // src/update/diff-generated-files.ts
3013
3149
  function diffGeneratedFiles() {
@@ -3034,10 +3170,12 @@ function summarizeLockDiff(before, after) {
3034
3170
 
3035
3171
  // src/update/lockfile.ts
3036
3172
  import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
3037
- import path21 from "path";
3173
+ import path22 from "path";
3038
3174
  async function checkLock(root) {
3039
3175
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
3040
- const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
3176
+ const hasValidVersions = lock.every(
3177
+ (item) => !item.version || normalizeVersion(item.version) !== null
3178
+ );
3041
3179
  const catalogRef = lock[0]?.catalogRef ?? null;
3042
3180
  return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
3043
3181
  }
@@ -3053,7 +3191,7 @@ async function applyLock(root) {
3053
3191
  try {
3054
3192
  const backupDir = hausPath(root, "backups");
3055
3193
  await mkdir(backupDir, { recursive: true });
3056
- await copyFile(lockPath, path21.join(backupDir, `haus.lock.${Date.now()}.json`));
3194
+ await copyFile(lockPath, path22.join(backupDir, `haus.lock.${Date.now()}.json`));
3057
3195
  } catch {
3058
3196
  }
3059
3197
  const enriched = await Promise.all(
@@ -3075,7 +3213,7 @@ function diffLock(before, after) {
3075
3213
  }
3076
3214
  async function hasLocalOverrides(root) {
3077
3215
  try {
3078
- await readFile2(path21.join(root, ".claude", "settings.json"), "utf8");
3216
+ await readFile2(path22.join(root, ".claude", "settings.json"), "utf8");
3079
3217
  return true;
3080
3218
  } catch {
3081
3219
  return false;
@@ -3087,7 +3225,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
3087
3225
  async function runUpdate(options) {
3088
3226
  const root = process.cwd();
3089
3227
  if (options.check) {
3090
- const pkgJson2 = await readJson(path22.join(packageRoot(), "package.json"));
3228
+ const pkgJson2 = await readJson(path23.join(packageRoot(), "package.json"));
3091
3229
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
3092
3230
  const [status, npmVersion, latestCatalogTag] = await Promise.all([
3093
3231
  checkLock(root),
@@ -3114,7 +3252,7 @@ async function runUpdate(options) {
3114
3252
  if (!status.ok) process.exitCode = 1;
3115
3253
  return;
3116
3254
  }
3117
- const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
3255
+ const pkgJson = await readJson(path23.join(packageRoot(), "package.json"));
3118
3256
  const currentVersion = pkgJson?.version ?? "0.0.0";
3119
3257
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3120
3258
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3145,12 +3283,14 @@ async function runUpdate(options) {
3145
3283
 
3146
3284
  // src/commands/validate-catalog.ts
3147
3285
  import fs16 from "fs";
3148
- import path24 from "path";
3286
+ import path25 from "path";
3149
3287
 
3150
3288
  // src/catalog/allowed-stacks.ts
3151
- import path23 from "path";
3289
+ import path24 from "path";
3152
3290
  async function readAllowedStacks(root) {
3153
- const data = await readJson(path23.join(root, "library", "catalog", "allowed-stacks.json"));
3291
+ const data = await readJson(
3292
+ path24.join(root, "library", "catalog", "allowed-stacks.json")
3293
+ );
3154
3294
  return data?.stacks ?? [];
3155
3295
  }
3156
3296
 
@@ -3175,7 +3315,12 @@ var FORBIDDEN_TAGS = [
3175
3315
  var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
3176
3316
  var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
3177
3317
  var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
3178
- var RISKY_INSTALL_PATTERNS = [/\bnpx\s+-y\b/i, /\bnpx\s+--yes\b/i, /\byarn\s+dlx\b/i, /\bpnpm\s+dlx\b/i];
3318
+ var RISKY_INSTALL_PATTERNS = [
3319
+ /\bnpx\s+-y\b/i,
3320
+ /\bnpx\s+--yes\b/i,
3321
+ /\byarn\s+dlx\b/i,
3322
+ /\bpnpm\s+dlx\b/i
3323
+ ];
3179
3324
  var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
3180
3325
  var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
3181
3326
  var HTTP_URL_PATTERN = /^http:\/\//i;
@@ -3249,11 +3394,11 @@ function auditShippedFiles(manifestDir, items) {
3249
3394
  const failures = [];
3250
3395
  for (const item of items) {
3251
3396
  if (!item.path) continue;
3252
- const absPath = path24.join(manifestDir, item.path);
3397
+ const absPath = path25.join(manifestDir, item.path);
3253
3398
  if (item.type === "skill") {
3254
- const skillMd = path24.join(absPath, "SKILL.md");
3399
+ const skillMd = path25.join(absPath, "SKILL.md");
3255
3400
  if (!fs16.existsSync(skillMd)) {
3256
- failures.push(`${item.id}: missing ${path24.relative(manifestDir, skillMd)}`);
3401
+ failures.push(`${item.id}: missing ${path25.relative(manifestDir, skillMd)}`);
3257
3402
  continue;
3258
3403
  }
3259
3404
  const text = fs16.readFileSync(skillMd, "utf8");
@@ -3272,7 +3417,8 @@ function auditShippedFiles(manifestDir, items) {
3272
3417
  }
3273
3418
  const lower = text.toLowerCase();
3274
3419
  for (const phrase of BANNED_AGENT_PHRASES) {
3275
- if (lower.includes(phrase)) failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
3420
+ if (lower.includes(phrase))
3421
+ failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
3276
3422
  }
3277
3423
  } else if (item.type === "template") {
3278
3424
  if (!fs16.existsSync(absPath)) {
@@ -3286,11 +3432,11 @@ function auditMarkdownContent(manifestDir) {
3286
3432
  const failures = [];
3287
3433
  const dirs = ["skills", "agents"];
3288
3434
  for (const dir of dirs) {
3289
- const abs = path24.join(manifestDir, dir);
3435
+ const abs = path25.join(manifestDir, dir);
3290
3436
  if (!fs16.existsSync(abs)) continue;
3291
3437
  walkMd(abs, (file) => {
3292
3438
  const text = fs16.readFileSync(file, "utf8");
3293
- const rel = path24.relative(manifestDir, file);
3439
+ const rel = path25.relative(manifestDir, file);
3294
3440
  const lines = text.split(/\r?\n/);
3295
3441
  for (let i = 0; i < lines.length; i++) {
3296
3442
  const line = lines[i] ?? "";
@@ -3310,7 +3456,7 @@ function auditMarkdownContent(manifestDir) {
3310
3456
  }
3311
3457
  function walkMd(dir, fn) {
3312
3458
  for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
3313
- const full = path24.join(dir, entry.name);
3459
+ const full = path25.join(dir, entry.name);
3314
3460
  if (entry.isDirectory()) walkMd(full, fn);
3315
3461
  else if (entry.name.endsWith(".md")) fn(full);
3316
3462
  }
@@ -3321,8 +3467,8 @@ async function runValidateCatalog(manifestPath) {
3321
3467
  process.exitCode = 1;
3322
3468
  return;
3323
3469
  }
3324
- const abs = path24.resolve(process.cwd(), manifestPath);
3325
- const manifestDir = path24.dirname(abs);
3470
+ const abs = path25.resolve(process.cwd(), manifestPath);
3471
+ const manifestDir = path25.dirname(abs);
3326
3472
  const data = await readJson(abs);
3327
3473
  if (!data?.items) {
3328
3474
  error(`Could not read catalog manifest at ${abs}`);
@@ -3345,7 +3491,13 @@ async function runValidateCatalog(manifestPath) {
3345
3491
  }
3346
3492
  }
3347
3493
  }
3348
- const allFailures = [...structureFailures, ...stackFailures, ...fileFailures, ...contentFailures, ...tagFailures];
3494
+ const allFailures = [
3495
+ ...structureFailures,
3496
+ ...stackFailures,
3497
+ ...fileFailures,
3498
+ ...contentFailures,
3499
+ ...tagFailures
3500
+ ];
3349
3501
  if (allFailures.length) {
3350
3502
  allFailures.forEach((f) => error(f));
3351
3503
  process.exitCode = 1;
@@ -3355,7 +3507,7 @@ async function runValidateCatalog(manifestPath) {
3355
3507
  }
3356
3508
 
3357
3509
  // src/commands/workspace.ts
3358
- import path25 from "path";
3510
+ import path26 from "path";
3359
3511
  import YAML from "yaml";
3360
3512
  async function runWorkspace(action) {
3361
3513
  if (action === "init") {
@@ -3378,8 +3530,8 @@ relationships: []
3378
3530
  process.exitCode = 1;
3379
3531
  return;
3380
3532
  }
3381
- const config = YAML.parse(configText);
3382
- const repos = config.repos ?? [];
3533
+ const config2 = YAML.parse(configText);
3534
+ const repos = config2.repos ?? [];
3383
3535
  if (repos.length === 0) {
3384
3536
  error("No repos configured in haus.workspace.yaml.");
3385
3537
  process.exitCode = 1;
@@ -3388,7 +3540,7 @@ relationships: []
3388
3540
  const summaries = [];
3389
3541
  const ownership = {};
3390
3542
  for (const repo of repos) {
3391
- const repoRoot = path25.resolve(process.cwd(), repo.path);
3543
+ const repoRoot = path26.resolve(process.cwd(), repo.path);
3392
3544
  const result = await scanProject(repoRoot, "fast");
3393
3545
  summaries.push({
3394
3546
  name: repo.name,
@@ -3424,7 +3576,7 @@ ${summaries.map(
3424
3576
  // src/cli.ts
3425
3577
  function cliVersion() {
3426
3578
  try {
3427
- const pkgPath = path26.join(packageRoot(), "package.json");
3579
+ const pkgPath = path27.join(packageRoot(), "package.json");
3428
3580
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3429
3581
  return pkg.version ?? "0.0.0";
3430
3582
  } catch {
@@ -3434,7 +3586,7 @@ function cliVersion() {
3434
3586
  var program = new Command();
3435
3587
  function validateRuntimeNodeVersion() {
3436
3588
  try {
3437
- const pkgPath = path26.join(packageRoot(), "package.json");
3589
+ const pkgPath = path27.join(packageRoot(), "package.json");
3438
3590
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3439
3591
  const requiredRange = pkg.engines?.node;
3440
3592
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -3452,7 +3604,10 @@ program.command("scan").option("--json").action(runScan);
3452
3604
  program.command("recommend").option("--json").action(runRecommend);
3453
3605
  program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
3454
3606
  program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
3455
- program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option("--allow-empty-cache", "Apply core files only when catalog cache is empty (skip catalog items without error)").action(runApply);
3607
+ program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
3608
+ "--allow-empty-cache",
3609
+ "Apply core files only when catalog cache is empty (skip catalog items without error)"
3610
+ ).action(runApply);
3456
3611
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
3457
3612
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
3458
3613
  program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
@@ -3471,6 +3626,10 @@ memory.command("promote").action(() => runMemory("promote", {}));
3471
3626
  var guard = program.command("guard");
3472
3627
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
3473
3628
  guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
3629
+ var config = program.command("config");
3630
+ config.command("enable <key>").description("Enable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "enable"));
3631
+ config.command("disable <key>").description("Disable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "disable"));
3632
+ config.command("status <key>").description("Show current state of a hook (hook.context, hook.memory)").action((key) => runConfig(key, "status"));
3474
3633
  var workspace = program.command("workspace");
3475
3634
  workspace.command("init").action(() => runWorkspace("init"));
3476
3635
  workspace.command("scan").action(() => runWorkspace("scan"));
@@ -407,14 +407,14 @@
407
407
  },
408
408
  {
409
409
  "id": "haus.prettier-setup",
410
- "version": "1.0.0",
410
+ "version": "1.1.0",
411
411
  "source": "haus",
412
412
  "type": "skill",
413
413
  "path": "skills/prettier-setup",
414
414
  "title": "Haus Prettier setup",
415
- "purpose": "Install `@haus-tech/prettier-config` and wire `.prettierrc` to the shared config.",
416
- "whenToUse": "Use when Prettier is missing from a haus repo or when migrating an ad-hoc Prettier config to the shared package.",
417
- "whenNotToUse": "Do not use when Prettier is already configured against `@haus-tech/prettier-config`.",
415
+ "purpose": "Install `@haus-tech/tech-config` and wire `.prettierrc` to the shared prettier config via the `/prettier` subpath export.",
416
+ "whenToUse": "Use when Prettier is missing from a haus repo, when migrating an ad-hoc config, or when migrating from the deprecated `@haus-tech/prettier-config`.",
417
+ "whenNotToUse": "Do not use when Prettier is already configured against `@haus-tech/tech-config/prettier`.",
418
418
  "references": [
419
419
  "references/conventions.md",
420
420
  "references/scope.md",
@@ -438,7 +438,7 @@
438
438
  },
439
439
  {
440
440
  "id": "haus.eslint-setup",
441
- "version": "1.0.0",
441
+ "version": "1.1.0",
442
442
  "source": "haus",
443
443
  "type": "skill",
444
444
  "path": "skills/eslint-setup",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,6 @@
16
16
  "library/global",
17
17
  "library/catalog",
18
18
  "tests/fixtures/catalog",
19
- "docs/user-guide.md",
20
19
  "README.md",
21
20
  "CHANGELOG.md",
22
21
  "LICENSE",
@@ -31,8 +30,6 @@
31
30
  "format:write": "prettier --write src scripts",
32
31
  "typecheck": "tsc --noEmit",
33
32
  "typecheck:scripts": "tsc --noEmit --project tsconfig.scripts.json",
34
- "cleanup:status": "tsx scripts/cleanup-status.ts",
35
- "bench:hooks": "tsx scripts/bench-hooks.ts",
36
33
  "pack:local": "yarn pack",
37
34
  "publish:dry": "npm pack --dry-run",
38
35
  "publish:public": "yarn npm publish --access public",
@@ -1,176 +0,0 @@
1
- # Haus AI User Guide
2
-
3
- This guide shows how to use `haus` in a real project, even if you are not a developer.
4
-
5
- ## What Haus AI does
6
-
7
- Haus AI scans your project, recommends context files/rules, then writes controlled files so Claude works with safer, stack-aware guidance.
8
-
9
- Main output folders:
10
-
11
- - `./.claude` (Claude settings/rules/commands)
12
- - `./.haus-workflow` (scan/recommendation/lock/memory metadata)
13
-
14
- ## Before you start
15
-
16
- You need:
17
-
18
- - a project folder on your machine
19
- - Node.js 22+ (`node --version`)
20
- - terminal access
21
-
22
- Check Node:
23
-
24
- ```bash
25
- node --version
26
- ```
27
-
28
- If version is below 22, install/update Node first.
29
-
30
- ## Install Haus AI
31
-
32
- ```bash
33
- npm install -g @haus-tech/haus-workflow
34
- haus --help
35
- ```
36
-
37
- Seed `~/.claude/` with Haus skills, agents, and hooks (once per machine):
38
-
39
- ```bash
40
- haus install
41
- ```
42
-
43
- ### If you switch Node versions often (nvm, Herd, Volta…)
44
-
45
- `npm install -g` binds to the currently active Node version. Switch Node → `haus` disappears. Two options:
46
-
47
- 1. **Re-install per version.** When you change Node, carry globals forward:
48
- ```bash
49
- nvm install <new-version> --reinstall-packages-from=current
50
- ```
51
-
52
- 2. **Use a shell alias.** No per-version install needed:
53
- ```bash
54
- echo 'alias haus="node $(npm root -g)/@haus-tech/haus-workflow/dist/cli.js"' >> ~/.zshrc
55
- source ~/.zshrc
56
- ```
57
-
58
- ## Use Haus in a project
59
-
60
- Move terminal to project root (folder that contains your app code), then run:
61
-
62
- ```bash
63
- haus setup-project
64
- ```
65
-
66
- Setup modes:
67
-
68
- - guided: asks simple onboarding questions
69
- - fast: minimal prompts, default flow
70
-
71
- ## Typical daily workflow
72
-
73
- ### 1) Scan project
74
-
75
- ```bash
76
- haus scan --json
77
- ```
78
-
79
- Writes project detection outputs to `./.haus-workflow/*`.
80
-
81
- ### 2) Generate recommendations
82
-
83
- ```bash
84
- haus recommend --json
85
- ```
86
-
87
- Creates `./.haus-workflow/recommendation.json` with selected and skipped items, confidence, and reasons.
88
-
89
- ### 3) Preview generated changes
90
-
91
- ```bash
92
- haus apply --dry-run
93
- ```
94
-
95
- Shows planned files without writing.
96
-
97
- ### 4) Apply generated files
98
-
99
- ```bash
100
- haus apply --write
101
- ```
102
-
103
- Writes generated files and reports overwrite summaries with concise diff counts.
104
-
105
- ### 5) Verify setup health
106
-
107
- ```bash
108
- haus doctor
109
- haus doctor --hooks
110
- ```
111
-
112
- `--hooks` checks that project hook settings still match the hook contract.
113
-
114
- ## Update flow
115
-
116
- Check update state:
117
-
118
- ```bash
119
- haus update --check
120
- ```
121
-
122
- Apply lock refresh:
123
-
124
- ```bash
125
- haus update
126
- ```
127
-
128
- Update behavior:
129
-
130
- - preserves local `.claude` overrides
131
- - backs up lockfile under `./.haus-workflow/backups`
132
- - prints unified lockfile diff summary
133
-
134
- ## Memory commands
135
-
136
- ```bash
137
- haus memory status
138
- haus memory add "Use explicit transaction boundaries in checkout service"
139
- haus memory inject --task "review checkout flow"
140
- haus memory promote
141
- ```
142
-
143
- Memory is local-only in `./.haus-workflow/memory`.
144
-
145
- ## Explain/context commands
146
-
147
- Use when you need to understand why rules were selected:
148
-
149
- ```bash
150
- haus explain-recommendation --json
151
- haus context --task "build shipping plugin" --json
152
- ```
153
-
154
- ## Claude slash-command usage
155
-
156
- After `haus apply --write`, command docs are generated in:
157
-
158
- - `./.claude/commands/haus-doctor.md`
159
- - `./.claude/commands/haus-review.md`
160
-
161
- Some environments expose these as slash commands. If not, run the CLI commands directly.
162
-
163
- ## If something fails
164
-
165
- - `haus: command not found` -> run `npm install -g @haus-tech/haus-workflow` or check Node version
166
- - Node engine error -> switch to Node 22+
167
- - hook mismatch in doctor -> run `haus apply --write` again
168
- - wrong project scanned -> `cd` into correct project root, rerun
169
-
170
- ## Remove generated setup
171
-
172
- ```bash
173
- haus undo --yes
174
- ```
175
-
176
- Removes `./.claude` and `./.haus-workflow` in current project.