@haus-tech/haus-workflow 0.9.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.9.0...v0.10.0) (2026-05-29)
4
+
5
+ ### Features
6
+
7
+ * Add config command to manage hooks ([1f2c7b4](https://github.com/WeAreHausTech/haus-workflow/commit/1f2c7b4ced3437ef4468759ec7bfc4d8adbf3efc))
8
+
9
+ ### Bug Fixes
10
+
11
+ * address PR review — remove stale module refs, fix plugin-era wording in docs and commands ([6520321](https://github.com/WeAreHausTech/haus-workflow/commit/6520321e1790a304036984c387fdf59d4febe3dd))
12
+
3
13
  ## [0.9.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.8.0...v0.9.0) (2026-05-28)
4
14
 
5
15
  ### Features
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # haus
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.
3
+ CLI that scans a project, recommends AI context assets for the stack, and writes controlled outputs into `.claude/` and `.haus-workflow/`.
4
4
 
5
- > **Internal Haus tool.** Open-source but unsupported for external use.
5
+ > **Internal Haus tool.**
6
6
 
7
7
  ---
8
8
 
@@ -27,7 +27,7 @@ Run once inside each project:
27
27
  haus init
28
28
  ```
29
29
 
30
- This scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
30
+ Scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
31
31
 
32
32
  ---
33
33
 
@@ -35,35 +35,32 @@ This scans the repo, recommends context assets, and writes `.claude/` and `.haus
35
35
 
36
36
  ```bash
37
37
  haus init # first-run setup (scan → recommend → apply)
38
- haus setup-project # re-run setup on an existing project
38
+ haus setup-project # re-run setup on existing project
39
+ haus scan # scan repo and write context-map
40
+ haus recommend # score and recommend catalog items
39
41
  haus apply --dry-run # preview what would be written
40
42
  haus apply --write # write .claude/ files
41
43
  haus update # sync remote catalog + refresh lockfile
42
44
  haus update --check # check for updates without applying
43
45
  haus doctor # health check: hooks, CLAUDE.md, catalog cache
46
+ haus config # manage hook configuration
47
+ haus memory # view project memory store
48
+ haus guard # test bash/file-access guards
44
49
  haus uninstall # remove Haus-managed files from ~/.claude/
45
50
  ```
46
51
 
47
52
  ---
48
53
 
49
- ## Contributing
54
+ ## Development
50
55
 
51
56
  ```bash
52
57
  yarn install
53
58
  yarn verify # typecheck + lint + build + test
59
+ yarn dev <cmd> # run CLI without building (tsx)
54
60
  ```
55
61
 
56
- See [docs/contributing.md](docs/contributing.md).
62
+ ### Internal docs
57
63
 
58
- ---
59
-
60
- ## Docs
61
-
62
- - [User guide](docs/user-guide.md)
63
64
  - [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)
65
+ - [CLI reference](docs/cli.md)
68
66
  - [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
@@ -903,6 +903,34 @@ async function runCatalogAudit() {
903
903
  log("Catalog audit passed.");
904
904
  }
905
905
 
906
+ // src/commands/config.ts
907
+ import path12 from "path";
908
+ var CONFIG_PATH2 = ".haus-workflow/config.json";
909
+ var HOOK_ALIASES = {
910
+ "hook.context": "context",
911
+ "hook.memory": "memoryInject"
912
+ };
913
+ async function runConfig(key, action) {
914
+ const hookKey = HOOK_ALIASES[key];
915
+ if (!hookKey) {
916
+ throw new Error(`Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`);
917
+ }
918
+ const root = process.cwd();
919
+ const configPath = path12.join(root, CONFIG_PATH2);
920
+ const existing = await readJson(configPath);
921
+ const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
922
+ cfg.hooks ??= {};
923
+ cfg.hooks[hookKey] ??= {};
924
+ if (action === "status") {
925
+ const enabled = cfg.hooks[hookKey]?.enabled === true;
926
+ log(`${key}: ${enabled ? "enabled" : "disabled"}`);
927
+ return;
928
+ }
929
+ cfg.hooks[hookKey].enabled = action === "enable";
930
+ await writeJson(configPath, cfg);
931
+ log(`${key} ${action}d`);
932
+ }
933
+
906
934
  // src/recommender/explain-recommendation.ts
907
935
  function normalizeRecommendation(input2) {
908
936
  const recommended = (input2.recommended ?? []).map((item) => {
@@ -1241,7 +1269,7 @@ function computeRuleIntents(rule) {
1241
1269
 
1242
1270
  // src/scanner/scan-project.ts
1243
1271
  import { readFile } from "fs/promises";
1244
- import path13 from "path";
1272
+ import path14 from "path";
1245
1273
 
1246
1274
  // src/utils/audit-checks.ts
1247
1275
  function isRecord(v) {
@@ -1268,7 +1296,7 @@ function compareVersions(a, b) {
1268
1296
  }
1269
1297
 
1270
1298
  // src/scanner/detect-package-manager.ts
1271
- import path12 from "path";
1299
+ import path13 from "path";
1272
1300
  import fs9 from "fs-extra";
1273
1301
  function detectPackageManager(root, packageManagerField) {
1274
1302
  const field = String(packageManagerField ?? "").trim();
@@ -1287,9 +1315,9 @@ function detectPackageManager(root, packageManagerField) {
1287
1315
  if (satisfiesVersion(version, ">=9")) return "npm";
1288
1316
  return "unknown";
1289
1317
  }
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";
1318
+ if (fs9.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
1319
+ if (fs9.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
1320
+ if (fs9.existsSync(path13.join(root, "package-lock.json"))) return "npm";
1293
1321
  return "unknown";
1294
1322
  }
1295
1323
 
@@ -1350,8 +1378,8 @@ function blocked(rel) {
1350
1378
  return SENSITIVE.some((x) => x.test(rel));
1351
1379
  }
1352
1380
  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"));
1381
+ const pkg = await readJson(path14.join(root, "package.json"));
1382
+ const composer = await readJson(path14.join(root, "composer.json"));
1355
1383
  const files = await listFiles(root, SAFE_FILES);
1356
1384
  const safeFiles = files.filter((f) => !blocked(f));
1357
1385
  const deps = dependencySet(pkg, composer);
@@ -1377,7 +1405,7 @@ async function scanProject(root, mode = "fast") {
1377
1405
  mode,
1378
1406
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1379
1407
  root,
1380
- repoName: String(pkg?.name ?? path13.basename(root)),
1408
+ repoName: String(pkg?.name ?? path14.basename(root)),
1381
1409
  packageManager,
1382
1410
  repoRoles: roles,
1383
1411
  confidence: computeConfidence(roles, stacks),
@@ -1392,7 +1420,7 @@ async function scanProject(root, mode = "fast") {
1392
1420
  composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
1393
1421
  };
1394
1422
  const scanHashes = Object.fromEntries(
1395
- await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(path13.join(root, f), "utf8"))]))
1423
+ await Promise.all(safeFiles.map(async (f) => [f, hashText(await readFile(path14.join(root, f), "utf8"))]))
1396
1424
  );
1397
1425
  const repoSummary = renderSummary(context);
1398
1426
  await writeJson(hausPath(root, "context-map.json"), context);
@@ -1548,7 +1576,7 @@ async function hasNeedle(root, files, needle) {
1548
1576
  );
1549
1577
  for (const rel of candidates.slice(0, 300)) {
1550
1578
  try {
1551
- const content = await readFile(path13.join(root, rel), "utf8");
1579
+ const content = await readFile(path14.join(root, rel), "utf8");
1552
1580
  if (content.includes(needle)) return true;
1553
1581
  } catch {
1554
1582
  continue;
@@ -1641,7 +1669,7 @@ async function runContext(options) {
1641
1669
  }
1642
1670
 
1643
1671
  // src/commands/doctor.ts
1644
- import path14 from "path";
1672
+ import path15 from "path";
1645
1673
  import fs10 from "fs-extra";
1646
1674
 
1647
1675
  // src/update/npm-version.ts
@@ -1709,7 +1737,7 @@ async function runDoctor(options) {
1709
1737
  const enabled = await isHookEnabled(root, key);
1710
1738
  log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
1711
1739
  }
1712
- const rootClaudeMdPath = path14.join(root, "CLAUDE.md");
1740
+ const rootClaudeMdPath = path15.join(root, "CLAUDE.md");
1713
1741
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
1714
1742
  if (!rootClaudeMdContent) {
1715
1743
  warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
@@ -1729,7 +1757,7 @@ async function runDoctor(options) {
1729
1757
  warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
1730
1758
  } else {
1731
1759
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
1732
- const templatePath = path14.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
1760
+ const templatePath = path15.join(packageRoot(), "library", "global", "templates", "haus-way-of-work.md");
1733
1761
  const templateContent = await readText(templatePath);
1734
1762
  if (storedHashMatch && templateContent) {
1735
1763
  const currentHash = hashText(templateContent);
@@ -1767,7 +1795,7 @@ async function runDoctor(options) {
1767
1795
  log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
1768
1796
  }
1769
1797
  }
1770
- const pkgJson = await readJson(path14.join(packageRoot(), "package.json"));
1798
+ const pkgJson = await readJson(path15.join(packageRoot(), "package.json"));
1771
1799
  const currentVersion = pkgJson?.version ?? "0.0.0";
1772
1800
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
1773
1801
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -1932,7 +1960,7 @@ async function runGuard(kind, _options) {
1932
1960
  }
1933
1961
 
1934
1962
  // src/commands/init.ts
1935
- import path15 from "path";
1963
+ import path16 from "path";
1936
1964
  import fs11 from "fs-extra";
1937
1965
 
1938
1966
  // src/utils/exec.ts
@@ -1941,6 +1969,7 @@ async function runCommand(command, args = [], options = {}) {
1941
1969
  try {
1942
1970
  const result = await execa(command, args, {
1943
1971
  reject: false,
1972
+ // non-zero exits are returned, not thrown
1944
1973
  ...options
1945
1974
  });
1946
1975
  return {
@@ -2454,7 +2483,7 @@ async function runSetupProject(options) {
2454
2483
  // src/commands/init.ts
2455
2484
  async function runInit(options) {
2456
2485
  const root = process.cwd();
2457
- const hausDir = path15.join(root, ".haus-workflow");
2486
+ const hausDir = path16.join(root, ".haus-workflow");
2458
2487
  const alreadyInit = await fs11.pathExists(hausDir);
2459
2488
  if (alreadyInit) {
2460
2489
  log("Haus AI already initialized in this project.");
@@ -2467,7 +2496,7 @@ async function runInit(options) {
2467
2496
 
2468
2497
  // src/install/apply.ts
2469
2498
  import crypto2 from "crypto";
2470
- import path18 from "path";
2499
+ import path19 from "path";
2471
2500
  import fs13 from "fs-extra";
2472
2501
 
2473
2502
  // src/install/header.ts
@@ -2502,13 +2531,13 @@ ${content}`;
2502
2531
 
2503
2532
  // src/install/manifest.ts
2504
2533
  import os5 from "os";
2505
- import path16 from "path";
2534
+ import path17 from "path";
2506
2535
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
2507
2536
  function globalClaudeDir() {
2508
- return path16.join(os5.homedir(), ".claude");
2537
+ return path17.join(os5.homedir(), ".claude");
2509
2538
  }
2510
2539
  function hausManifestPath() {
2511
- return path16.join(globalClaudeDir(), "haus", "install-manifest.json");
2540
+ return path17.join(globalClaudeDir(), "haus", "install-manifest.json");
2512
2541
  }
2513
2542
  async function readManifest() {
2514
2543
  return readJson(hausManifestPath());
@@ -2527,10 +2556,10 @@ function buildManifest(source, files, hooks) {
2527
2556
  }
2528
2557
 
2529
2558
  // src/install/settings-merge.ts
2530
- import path17 from "path";
2559
+ import path18 from "path";
2531
2560
  import fs12 from "fs-extra";
2532
2561
  function settingsJsonPath() {
2533
- return path17.join(globalClaudeDir(), "settings.json");
2562
+ return path18.join(globalClaudeDir(), "settings.json");
2534
2563
  }
2535
2564
  async function readSettings() {
2536
2565
  const parsed = await readJson(settingsJsonPath());
@@ -2601,7 +2630,7 @@ function hashContent(content) {
2601
2630
  }
2602
2631
  function sourceVersion() {
2603
2632
  try {
2604
- const pkgPath = path18.join(packageRoot(), "package.json");
2633
+ const pkgPath = path19.join(packageRoot(), "package.json");
2605
2634
  const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
2606
2635
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
2607
2636
  } catch {
@@ -2609,32 +2638,32 @@ function sourceVersion() {
2609
2638
  }
2610
2639
  }
2611
2640
  function globalSrcDir() {
2612
- return path18.join(packageRoot(), "library", "global");
2641
+ return path19.join(packageRoot(), "library", "global");
2613
2642
  }
2614
2643
  function collectSourceFiles(srcDir, claudeDir) {
2615
2644
  const entries = [];
2616
- const skillsDir = path18.join(srcDir, "skills");
2645
+ const skillsDir = path19.join(srcDir, "skills");
2617
2646
  if (fs13.pathExistsSync(skillsDir)) {
2618
2647
  for (const skillName of fs13.readdirSync(skillsDir)) {
2619
- const skillFile = path18.join(skillsDir, skillName, "SKILL.md");
2648
+ const skillFile = path19.join(skillsDir, skillName, "SKILL.md");
2620
2649
  if (fs13.pathExistsSync(skillFile)) {
2621
2650
  entries.push({
2622
2651
  stableId: `skill.${skillName}`,
2623
- srcRelPath: path18.join("library", "global", "skills", skillName, "SKILL.md"),
2624
- destPath: path18.join(claudeDir, "skills", skillName, "SKILL.md")
2652
+ srcRelPath: path19.join("library", "global", "skills", skillName, "SKILL.md"),
2653
+ destPath: path19.join(claudeDir, "skills", skillName, "SKILL.md")
2625
2654
  });
2626
2655
  }
2627
2656
  }
2628
2657
  }
2629
- const agentsDir = path18.join(srcDir, "agents");
2658
+ const agentsDir = path19.join(srcDir, "agents");
2630
2659
  if (fs13.pathExistsSync(agentsDir)) {
2631
2660
  for (const agentFile of fs13.readdirSync(agentsDir)) {
2632
2661
  if (!agentFile.endsWith(".md")) continue;
2633
2662
  const agentName = agentFile.replace(/\.md$/, "");
2634
2663
  entries.push({
2635
2664
  stableId: `agent.${agentName}`,
2636
- srcRelPath: path18.join("library", "global", "agents", agentFile),
2637
- destPath: path18.join(claudeDir, "agents", agentFile)
2665
+ srcRelPath: path19.join("library", "global", "agents", agentFile),
2666
+ destPath: path19.join(claudeDir, "agents", agentFile)
2638
2667
  });
2639
2668
  }
2640
2669
  }
@@ -2658,7 +2687,7 @@ async function applyInstall(options = {}) {
2658
2687
  };
2659
2688
  const manifestFiles = [];
2660
2689
  for (const entry of sourceFiles) {
2661
- const srcPath = path18.join(packageRoot(), entry.srcRelPath);
2690
+ const srcPath = path19.join(packageRoot(), entry.srcRelPath);
2662
2691
  const rawContent = await readText(srcPath);
2663
2692
  if (rawContent === void 0) {
2664
2693
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -2714,7 +2743,7 @@ async function applyInstall(options = {}) {
2714
2743
  schemaVersion: SCHEMA_VERSION3
2715
2744
  });
2716
2745
  }
2717
- const fragmentPath = path18.join(srcDir, "settings-fragments", "hooks.json");
2746
+ const fragmentPath = path19.join(srcDir, "settings-fragments", "hooks.json");
2718
2747
  const fragments = await loadHooksFragment(fragmentPath);
2719
2748
  const settings = await readSettings();
2720
2749
  const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
@@ -2899,12 +2928,12 @@ async function runScan(options) {
2899
2928
  }
2900
2929
 
2901
2930
  // src/commands/undo.ts
2902
- import path19 from "path";
2931
+ import path20 from "path";
2903
2932
  import fs14 from "fs-extra";
2904
2933
  var CLAUDE_DIR = ".claude";
2905
2934
  async function runUndo(options) {
2906
2935
  const root = process.cwd();
2907
- const targets = [path19.join(root, CLAUDE_DIR), path19.join(root, HAUS_DIR)];
2936
+ const targets = [path20.join(root, CLAUDE_DIR), path20.join(root, HAUS_DIR)];
2908
2937
  const existing = targets.filter((p) => fs14.existsSync(p));
2909
2938
  if (existing.length === 0) {
2910
2939
  log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
@@ -2912,7 +2941,7 @@ async function runUndo(options) {
2912
2941
  }
2913
2942
  if (!options.yes) {
2914
2943
  const ok = await confirm(
2915
- `Remove ${existing.map((p) => path19.relative(root, p)).join(" and ")}? This cannot be undone.`
2944
+ `Remove ${existing.map((p) => path20.relative(root, p)).join(" and ")}? This cannot be undone.`
2916
2945
  );
2917
2946
  if (!ok) {
2918
2947
  log("Cancelled.");
@@ -2921,13 +2950,13 @@ async function runUndo(options) {
2921
2950
  }
2922
2951
  for (const p of existing) {
2923
2952
  await fs14.remove(p);
2924
- log(`Removed ${path19.relative(root, p)}`);
2953
+ log(`Removed ${path20.relative(root, p)}`);
2925
2954
  }
2926
2955
  }
2927
2956
 
2928
2957
  // src/install/uninstall.ts
2929
2958
  import crypto3 from "crypto";
2930
- import path20 from "path";
2959
+ import path21 from "path";
2931
2960
  import fs15 from "fs-extra";
2932
2961
  async function runUninstall(options = {}) {
2933
2962
  const { force = false } = options;
@@ -2955,14 +2984,14 @@ async function runUninstall(options = {}) {
2955
2984
  continue;
2956
2985
  }
2957
2986
  await fs15.remove(entry.destPath);
2958
- await pruneEmptyDir(path20.dirname(entry.destPath));
2987
+ await pruneEmptyDir(path21.dirname(entry.destPath));
2959
2988
  result.deleted.push(entry.destPath);
2960
2989
  }
2961
2990
  const settings = await readSettings();
2962
2991
  const stripped = stripHausHooks(settings);
2963
2992
  await writeSettings(stripped);
2964
2993
  result.hooksStripped = true;
2965
- const hausDir = path20.join(globalClaudeDir(), "haus");
2994
+ const hausDir = path21.join(globalClaudeDir(), "haus");
2966
2995
  const manifestPath = hausManifestPath();
2967
2996
  if (fs15.pathExistsSync(manifestPath)) {
2968
2997
  await fs15.remove(manifestPath);
@@ -3007,7 +3036,7 @@ async function runUninstallCommand(options) {
3007
3036
  }
3008
3037
 
3009
3038
  // src/commands/update.ts
3010
- import path22 from "path";
3039
+ import path23 from "path";
3011
3040
 
3012
3041
  // src/update/diff-generated-files.ts
3013
3042
  function diffGeneratedFiles() {
@@ -3034,7 +3063,7 @@ function summarizeLockDiff(before, after) {
3034
3063
 
3035
3064
  // src/update/lockfile.ts
3036
3065
  import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
3037
- import path21 from "path";
3066
+ import path22 from "path";
3038
3067
  async function checkLock(root) {
3039
3068
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
3040
3069
  const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
@@ -3053,7 +3082,7 @@ async function applyLock(root) {
3053
3082
  try {
3054
3083
  const backupDir = hausPath(root, "backups");
3055
3084
  await mkdir(backupDir, { recursive: true });
3056
- await copyFile(lockPath, path21.join(backupDir, `haus.lock.${Date.now()}.json`));
3085
+ await copyFile(lockPath, path22.join(backupDir, `haus.lock.${Date.now()}.json`));
3057
3086
  } catch {
3058
3087
  }
3059
3088
  const enriched = await Promise.all(
@@ -3075,7 +3104,7 @@ function diffLock(before, after) {
3075
3104
  }
3076
3105
  async function hasLocalOverrides(root) {
3077
3106
  try {
3078
- await readFile2(path21.join(root, ".claude", "settings.json"), "utf8");
3107
+ await readFile2(path22.join(root, ".claude", "settings.json"), "utf8");
3079
3108
  return true;
3080
3109
  } catch {
3081
3110
  return false;
@@ -3087,7 +3116,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
3087
3116
  async function runUpdate(options) {
3088
3117
  const root = process.cwd();
3089
3118
  if (options.check) {
3090
- const pkgJson2 = await readJson(path22.join(packageRoot(), "package.json"));
3119
+ const pkgJson2 = await readJson(path23.join(packageRoot(), "package.json"));
3091
3120
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
3092
3121
  const [status, npmVersion, latestCatalogTag] = await Promise.all([
3093
3122
  checkLock(root),
@@ -3114,7 +3143,7 @@ async function runUpdate(options) {
3114
3143
  if (!status.ok) process.exitCode = 1;
3115
3144
  return;
3116
3145
  }
3117
- const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
3146
+ const pkgJson = await readJson(path23.join(packageRoot(), "package.json"));
3118
3147
  const currentVersion = pkgJson?.version ?? "0.0.0";
3119
3148
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3120
3149
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3145,12 +3174,12 @@ async function runUpdate(options) {
3145
3174
 
3146
3175
  // src/commands/validate-catalog.ts
3147
3176
  import fs16 from "fs";
3148
- import path24 from "path";
3177
+ import path25 from "path";
3149
3178
 
3150
3179
  // src/catalog/allowed-stacks.ts
3151
- import path23 from "path";
3180
+ import path24 from "path";
3152
3181
  async function readAllowedStacks(root) {
3153
- const data = await readJson(path23.join(root, "library", "catalog", "allowed-stacks.json"));
3182
+ const data = await readJson(path24.join(root, "library", "catalog", "allowed-stacks.json"));
3154
3183
  return data?.stacks ?? [];
3155
3184
  }
3156
3185
 
@@ -3249,11 +3278,11 @@ function auditShippedFiles(manifestDir, items) {
3249
3278
  const failures = [];
3250
3279
  for (const item of items) {
3251
3280
  if (!item.path) continue;
3252
- const absPath = path24.join(manifestDir, item.path);
3281
+ const absPath = path25.join(manifestDir, item.path);
3253
3282
  if (item.type === "skill") {
3254
- const skillMd = path24.join(absPath, "SKILL.md");
3283
+ const skillMd = path25.join(absPath, "SKILL.md");
3255
3284
  if (!fs16.existsSync(skillMd)) {
3256
- failures.push(`${item.id}: missing ${path24.relative(manifestDir, skillMd)}`);
3285
+ failures.push(`${item.id}: missing ${path25.relative(manifestDir, skillMd)}`);
3257
3286
  continue;
3258
3287
  }
3259
3288
  const text = fs16.readFileSync(skillMd, "utf8");
@@ -3286,11 +3315,11 @@ function auditMarkdownContent(manifestDir) {
3286
3315
  const failures = [];
3287
3316
  const dirs = ["skills", "agents"];
3288
3317
  for (const dir of dirs) {
3289
- const abs = path24.join(manifestDir, dir);
3318
+ const abs = path25.join(manifestDir, dir);
3290
3319
  if (!fs16.existsSync(abs)) continue;
3291
3320
  walkMd(abs, (file) => {
3292
3321
  const text = fs16.readFileSync(file, "utf8");
3293
- const rel = path24.relative(manifestDir, file);
3322
+ const rel = path25.relative(manifestDir, file);
3294
3323
  const lines = text.split(/\r?\n/);
3295
3324
  for (let i = 0; i < lines.length; i++) {
3296
3325
  const line = lines[i] ?? "";
@@ -3310,7 +3339,7 @@ function auditMarkdownContent(manifestDir) {
3310
3339
  }
3311
3340
  function walkMd(dir, fn) {
3312
3341
  for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
3313
- const full = path24.join(dir, entry.name);
3342
+ const full = path25.join(dir, entry.name);
3314
3343
  if (entry.isDirectory()) walkMd(full, fn);
3315
3344
  else if (entry.name.endsWith(".md")) fn(full);
3316
3345
  }
@@ -3321,8 +3350,8 @@ async function runValidateCatalog(manifestPath) {
3321
3350
  process.exitCode = 1;
3322
3351
  return;
3323
3352
  }
3324
- const abs = path24.resolve(process.cwd(), manifestPath);
3325
- const manifestDir = path24.dirname(abs);
3353
+ const abs = path25.resolve(process.cwd(), manifestPath);
3354
+ const manifestDir = path25.dirname(abs);
3326
3355
  const data = await readJson(abs);
3327
3356
  if (!data?.items) {
3328
3357
  error(`Could not read catalog manifest at ${abs}`);
@@ -3355,7 +3384,7 @@ async function runValidateCatalog(manifestPath) {
3355
3384
  }
3356
3385
 
3357
3386
  // src/commands/workspace.ts
3358
- import path25 from "path";
3387
+ import path26 from "path";
3359
3388
  import YAML from "yaml";
3360
3389
  async function runWorkspace(action) {
3361
3390
  if (action === "init") {
@@ -3378,8 +3407,8 @@ relationships: []
3378
3407
  process.exitCode = 1;
3379
3408
  return;
3380
3409
  }
3381
- const config = YAML.parse(configText);
3382
- const repos = config.repos ?? [];
3410
+ const config2 = YAML.parse(configText);
3411
+ const repos = config2.repos ?? [];
3383
3412
  if (repos.length === 0) {
3384
3413
  error("No repos configured in haus.workspace.yaml.");
3385
3414
  process.exitCode = 1;
@@ -3388,7 +3417,7 @@ relationships: []
3388
3417
  const summaries = [];
3389
3418
  const ownership = {};
3390
3419
  for (const repo of repos) {
3391
- const repoRoot = path25.resolve(process.cwd(), repo.path);
3420
+ const repoRoot = path26.resolve(process.cwd(), repo.path);
3392
3421
  const result = await scanProject(repoRoot, "fast");
3393
3422
  summaries.push({
3394
3423
  name: repo.name,
@@ -3424,7 +3453,7 @@ ${summaries.map(
3424
3453
  // src/cli.ts
3425
3454
  function cliVersion() {
3426
3455
  try {
3427
- const pkgPath = path26.join(packageRoot(), "package.json");
3456
+ const pkgPath = path27.join(packageRoot(), "package.json");
3428
3457
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3429
3458
  return pkg.version ?? "0.0.0";
3430
3459
  } catch {
@@ -3434,7 +3463,7 @@ function cliVersion() {
3434
3463
  var program = new Command();
3435
3464
  function validateRuntimeNodeVersion() {
3436
3465
  try {
3437
- const pkgPath = path26.join(packageRoot(), "package.json");
3466
+ const pkgPath = path27.join(packageRoot(), "package.json");
3438
3467
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3439
3468
  const requiredRange = pkg.engines?.node;
3440
3469
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -3471,6 +3500,10 @@ memory.command("promote").action(() => runMemory("promote", {}));
3471
3500
  var guard = program.command("guard");
3472
3501
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
3473
3502
  guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
3503
+ var config = program.command("config");
3504
+ config.command("enable <key>").description("Enable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "enable"));
3505
+ config.command("disable <key>").description("Disable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "disable"));
3506
+ config.command("status <key>").description("Show current state of a hook (hook.context, hook.memory)").action((key) => runConfig(key, "status"));
3474
3507
  var workspace = program.command("workspace");
3475
3508
  workspace.command("init").action(() => runWorkspace("init"));
3476
3509
  workspace.command("scan").action(() => runWorkspace("scan"));
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.0",
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",
@@ -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.