@haus-tech/haus-workflow 0.8.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,21 @@
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
+
13
+ ## [0.9.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.8.0...v0.9.0) (2026-05-28)
14
+
15
+ ### Features
16
+
17
+ * **scanner:** detect stripe, qliro, supabase (T26-T28) ([b2585b0](https://github.com/WeAreHausTech/haus-workflow/commit/b2585b027621be1442533d12255523f8361d967b))
18
+
3
19
  ## [0.8.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.7.0...v0.8.0) (2026-05-28)
4
20
 
5
21
  ### 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);
@@ -1490,6 +1518,13 @@ async function detectStacks(root, deps, files, packageManager) {
1490
1518
  if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
1491
1519
  if (!deps.includes("prettier")) add("tooling", "missing-prettier");
1492
1520
  if (!deps.includes("eslint")) add("tooling", "missing-eslint");
1521
+ if (deps.includes("@stripe/stripe-js") || deps.includes("@stripe/react-stripe-js")) {
1522
+ add("tooling", "stripe");
1523
+ }
1524
+ if (deps.includes("@haus-tech/qliro-plugin")) add("tooling", "qliro");
1525
+ if (deps.includes("@supabase/supabase-js") || deps.some((d) => d.startsWith("@supabase/"))) {
1526
+ add("databases", "supabase");
1527
+ }
1493
1528
  if (deps.includes("@vendure/core")) add("backend", "vendure3");
1494
1529
  if (deps.includes("@nestjs/core")) add("backend", "nestjs");
1495
1530
  if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
@@ -1541,7 +1576,7 @@ async function hasNeedle(root, files, needle) {
1541
1576
  );
1542
1577
  for (const rel of candidates.slice(0, 300)) {
1543
1578
  try {
1544
- const content = await readFile(path13.join(root, rel), "utf8");
1579
+ const content = await readFile(path14.join(root, rel), "utf8");
1545
1580
  if (content.includes(needle)) return true;
1546
1581
  } catch {
1547
1582
  continue;
@@ -1634,7 +1669,7 @@ async function runContext(options) {
1634
1669
  }
1635
1670
 
1636
1671
  // src/commands/doctor.ts
1637
- import path14 from "path";
1672
+ import path15 from "path";
1638
1673
  import fs10 from "fs-extra";
1639
1674
 
1640
1675
  // src/update/npm-version.ts
@@ -1702,7 +1737,7 @@ async function runDoctor(options) {
1702
1737
  const enabled = await isHookEnabled(root, key);
1703
1738
  log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
1704
1739
  }
1705
- const rootClaudeMdPath = path14.join(root, "CLAUDE.md");
1740
+ const rootClaudeMdPath = path15.join(root, "CLAUDE.md");
1706
1741
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
1707
1742
  if (!rootClaudeMdContent) {
1708
1743
  warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
@@ -1722,7 +1757,7 @@ async function runDoctor(options) {
1722
1757
  warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
1723
1758
  } else {
1724
1759
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
1725
- 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");
1726
1761
  const templateContent = await readText(templatePath);
1727
1762
  if (storedHashMatch && templateContent) {
1728
1763
  const currentHash = hashText(templateContent);
@@ -1760,7 +1795,7 @@ async function runDoctor(options) {
1760
1795
  log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
1761
1796
  }
1762
1797
  }
1763
- const pkgJson = await readJson(path14.join(packageRoot(), "package.json"));
1798
+ const pkgJson = await readJson(path15.join(packageRoot(), "package.json"));
1764
1799
  const currentVersion = pkgJson?.version ?? "0.0.0";
1765
1800
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
1766
1801
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -1925,7 +1960,7 @@ async function runGuard(kind, _options) {
1925
1960
  }
1926
1961
 
1927
1962
  // src/commands/init.ts
1928
- import path15 from "path";
1963
+ import path16 from "path";
1929
1964
  import fs11 from "fs-extra";
1930
1965
 
1931
1966
  // src/utils/exec.ts
@@ -1934,6 +1969,7 @@ async function runCommand(command, args = [], options = {}) {
1934
1969
  try {
1935
1970
  const result = await execa(command, args, {
1936
1971
  reject: false,
1972
+ // non-zero exits are returned, not thrown
1937
1973
  ...options
1938
1974
  });
1939
1975
  return {
@@ -2447,7 +2483,7 @@ async function runSetupProject(options) {
2447
2483
  // src/commands/init.ts
2448
2484
  async function runInit(options) {
2449
2485
  const root = process.cwd();
2450
- const hausDir = path15.join(root, ".haus-workflow");
2486
+ const hausDir = path16.join(root, ".haus-workflow");
2451
2487
  const alreadyInit = await fs11.pathExists(hausDir);
2452
2488
  if (alreadyInit) {
2453
2489
  log("Haus AI already initialized in this project.");
@@ -2460,7 +2496,7 @@ async function runInit(options) {
2460
2496
 
2461
2497
  // src/install/apply.ts
2462
2498
  import crypto2 from "crypto";
2463
- import path18 from "path";
2499
+ import path19 from "path";
2464
2500
  import fs13 from "fs-extra";
2465
2501
 
2466
2502
  // src/install/header.ts
@@ -2495,13 +2531,13 @@ ${content}`;
2495
2531
 
2496
2532
  // src/install/manifest.ts
2497
2533
  import os5 from "os";
2498
- import path16 from "path";
2534
+ import path17 from "path";
2499
2535
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
2500
2536
  function globalClaudeDir() {
2501
- return path16.join(os5.homedir(), ".claude");
2537
+ return path17.join(os5.homedir(), ".claude");
2502
2538
  }
2503
2539
  function hausManifestPath() {
2504
- return path16.join(globalClaudeDir(), "haus", "install-manifest.json");
2540
+ return path17.join(globalClaudeDir(), "haus", "install-manifest.json");
2505
2541
  }
2506
2542
  async function readManifest() {
2507
2543
  return readJson(hausManifestPath());
@@ -2520,10 +2556,10 @@ function buildManifest(source, files, hooks) {
2520
2556
  }
2521
2557
 
2522
2558
  // src/install/settings-merge.ts
2523
- import path17 from "path";
2559
+ import path18 from "path";
2524
2560
  import fs12 from "fs-extra";
2525
2561
  function settingsJsonPath() {
2526
- return path17.join(globalClaudeDir(), "settings.json");
2562
+ return path18.join(globalClaudeDir(), "settings.json");
2527
2563
  }
2528
2564
  async function readSettings() {
2529
2565
  const parsed = await readJson(settingsJsonPath());
@@ -2594,7 +2630,7 @@ function hashContent(content) {
2594
2630
  }
2595
2631
  function sourceVersion() {
2596
2632
  try {
2597
- const pkgPath = path18.join(packageRoot(), "package.json");
2633
+ const pkgPath = path19.join(packageRoot(), "package.json");
2598
2634
  const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
2599
2635
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
2600
2636
  } catch {
@@ -2602,32 +2638,32 @@ function sourceVersion() {
2602
2638
  }
2603
2639
  }
2604
2640
  function globalSrcDir() {
2605
- return path18.join(packageRoot(), "library", "global");
2641
+ return path19.join(packageRoot(), "library", "global");
2606
2642
  }
2607
2643
  function collectSourceFiles(srcDir, claudeDir) {
2608
2644
  const entries = [];
2609
- const skillsDir = path18.join(srcDir, "skills");
2645
+ const skillsDir = path19.join(srcDir, "skills");
2610
2646
  if (fs13.pathExistsSync(skillsDir)) {
2611
2647
  for (const skillName of fs13.readdirSync(skillsDir)) {
2612
- const skillFile = path18.join(skillsDir, skillName, "SKILL.md");
2648
+ const skillFile = path19.join(skillsDir, skillName, "SKILL.md");
2613
2649
  if (fs13.pathExistsSync(skillFile)) {
2614
2650
  entries.push({
2615
2651
  stableId: `skill.${skillName}`,
2616
- srcRelPath: path18.join("library", "global", "skills", skillName, "SKILL.md"),
2617
- 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")
2618
2654
  });
2619
2655
  }
2620
2656
  }
2621
2657
  }
2622
- const agentsDir = path18.join(srcDir, "agents");
2658
+ const agentsDir = path19.join(srcDir, "agents");
2623
2659
  if (fs13.pathExistsSync(agentsDir)) {
2624
2660
  for (const agentFile of fs13.readdirSync(agentsDir)) {
2625
2661
  if (!agentFile.endsWith(".md")) continue;
2626
2662
  const agentName = agentFile.replace(/\.md$/, "");
2627
2663
  entries.push({
2628
2664
  stableId: `agent.${agentName}`,
2629
- srcRelPath: path18.join("library", "global", "agents", agentFile),
2630
- destPath: path18.join(claudeDir, "agents", agentFile)
2665
+ srcRelPath: path19.join("library", "global", "agents", agentFile),
2666
+ destPath: path19.join(claudeDir, "agents", agentFile)
2631
2667
  });
2632
2668
  }
2633
2669
  }
@@ -2651,7 +2687,7 @@ async function applyInstall(options = {}) {
2651
2687
  };
2652
2688
  const manifestFiles = [];
2653
2689
  for (const entry of sourceFiles) {
2654
- const srcPath = path18.join(packageRoot(), entry.srcRelPath);
2690
+ const srcPath = path19.join(packageRoot(), entry.srcRelPath);
2655
2691
  const rawContent = await readText(srcPath);
2656
2692
  if (rawContent === void 0) {
2657
2693
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -2707,7 +2743,7 @@ async function applyInstall(options = {}) {
2707
2743
  schemaVersion: SCHEMA_VERSION3
2708
2744
  });
2709
2745
  }
2710
- const fragmentPath = path18.join(srcDir, "settings-fragments", "hooks.json");
2746
+ const fragmentPath = path19.join(srcDir, "settings-fragments", "hooks.json");
2711
2747
  const fragments = await loadHooksFragment(fragmentPath);
2712
2748
  const settings = await readSettings();
2713
2749
  const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
@@ -2892,12 +2928,12 @@ async function runScan(options) {
2892
2928
  }
2893
2929
 
2894
2930
  // src/commands/undo.ts
2895
- import path19 from "path";
2931
+ import path20 from "path";
2896
2932
  import fs14 from "fs-extra";
2897
2933
  var CLAUDE_DIR = ".claude";
2898
2934
  async function runUndo(options) {
2899
2935
  const root = process.cwd();
2900
- 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)];
2901
2937
  const existing = targets.filter((p) => fs14.existsSync(p));
2902
2938
  if (existing.length === 0) {
2903
2939
  log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
@@ -2905,7 +2941,7 @@ async function runUndo(options) {
2905
2941
  }
2906
2942
  if (!options.yes) {
2907
2943
  const ok = await confirm(
2908
- `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.`
2909
2945
  );
2910
2946
  if (!ok) {
2911
2947
  log("Cancelled.");
@@ -2914,13 +2950,13 @@ async function runUndo(options) {
2914
2950
  }
2915
2951
  for (const p of existing) {
2916
2952
  await fs14.remove(p);
2917
- log(`Removed ${path19.relative(root, p)}`);
2953
+ log(`Removed ${path20.relative(root, p)}`);
2918
2954
  }
2919
2955
  }
2920
2956
 
2921
2957
  // src/install/uninstall.ts
2922
2958
  import crypto3 from "crypto";
2923
- import path20 from "path";
2959
+ import path21 from "path";
2924
2960
  import fs15 from "fs-extra";
2925
2961
  async function runUninstall(options = {}) {
2926
2962
  const { force = false } = options;
@@ -2948,14 +2984,14 @@ async function runUninstall(options = {}) {
2948
2984
  continue;
2949
2985
  }
2950
2986
  await fs15.remove(entry.destPath);
2951
- await pruneEmptyDir(path20.dirname(entry.destPath));
2987
+ await pruneEmptyDir(path21.dirname(entry.destPath));
2952
2988
  result.deleted.push(entry.destPath);
2953
2989
  }
2954
2990
  const settings = await readSettings();
2955
2991
  const stripped = stripHausHooks(settings);
2956
2992
  await writeSettings(stripped);
2957
2993
  result.hooksStripped = true;
2958
- const hausDir = path20.join(globalClaudeDir(), "haus");
2994
+ const hausDir = path21.join(globalClaudeDir(), "haus");
2959
2995
  const manifestPath = hausManifestPath();
2960
2996
  if (fs15.pathExistsSync(manifestPath)) {
2961
2997
  await fs15.remove(manifestPath);
@@ -3000,7 +3036,7 @@ async function runUninstallCommand(options) {
3000
3036
  }
3001
3037
 
3002
3038
  // src/commands/update.ts
3003
- import path22 from "path";
3039
+ import path23 from "path";
3004
3040
 
3005
3041
  // src/update/diff-generated-files.ts
3006
3042
  function diffGeneratedFiles() {
@@ -3027,7 +3063,7 @@ function summarizeLockDiff(before, after) {
3027
3063
 
3028
3064
  // src/update/lockfile.ts
3029
3065
  import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
3030
- import path21 from "path";
3066
+ import path22 from "path";
3031
3067
  async function checkLock(root) {
3032
3068
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
3033
3069
  const hasValidVersions = lock.every((item) => !item.version || normalizeVersion(item.version) !== null);
@@ -3046,7 +3082,7 @@ async function applyLock(root) {
3046
3082
  try {
3047
3083
  const backupDir = hausPath(root, "backups");
3048
3084
  await mkdir(backupDir, { recursive: true });
3049
- await copyFile(lockPath, path21.join(backupDir, `haus.lock.${Date.now()}.json`));
3085
+ await copyFile(lockPath, path22.join(backupDir, `haus.lock.${Date.now()}.json`));
3050
3086
  } catch {
3051
3087
  }
3052
3088
  const enriched = await Promise.all(
@@ -3068,7 +3104,7 @@ function diffLock(before, after) {
3068
3104
  }
3069
3105
  async function hasLocalOverrides(root) {
3070
3106
  try {
3071
- await readFile2(path21.join(root, ".claude", "settings.json"), "utf8");
3107
+ await readFile2(path22.join(root, ".claude", "settings.json"), "utf8");
3072
3108
  return true;
3073
3109
  } catch {
3074
3110
  return false;
@@ -3080,7 +3116,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
3080
3116
  async function runUpdate(options) {
3081
3117
  const root = process.cwd();
3082
3118
  if (options.check) {
3083
- const pkgJson2 = await readJson(path22.join(packageRoot(), "package.json"));
3119
+ const pkgJson2 = await readJson(path23.join(packageRoot(), "package.json"));
3084
3120
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
3085
3121
  const [status, npmVersion, latestCatalogTag] = await Promise.all([
3086
3122
  checkLock(root),
@@ -3107,7 +3143,7 @@ async function runUpdate(options) {
3107
3143
  if (!status.ok) process.exitCode = 1;
3108
3144
  return;
3109
3145
  }
3110
- const pkgJson = await readJson(path22.join(packageRoot(), "package.json"));
3146
+ const pkgJson = await readJson(path23.join(packageRoot(), "package.json"));
3111
3147
  const currentVersion = pkgJson?.version ?? "0.0.0";
3112
3148
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3113
3149
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3138,12 +3174,12 @@ async function runUpdate(options) {
3138
3174
 
3139
3175
  // src/commands/validate-catalog.ts
3140
3176
  import fs16 from "fs";
3141
- import path24 from "path";
3177
+ import path25 from "path";
3142
3178
 
3143
3179
  // src/catalog/allowed-stacks.ts
3144
- import path23 from "path";
3180
+ import path24 from "path";
3145
3181
  async function readAllowedStacks(root) {
3146
- 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"));
3147
3183
  return data?.stacks ?? [];
3148
3184
  }
3149
3185
 
@@ -3242,11 +3278,11 @@ function auditShippedFiles(manifestDir, items) {
3242
3278
  const failures = [];
3243
3279
  for (const item of items) {
3244
3280
  if (!item.path) continue;
3245
- const absPath = path24.join(manifestDir, item.path);
3281
+ const absPath = path25.join(manifestDir, item.path);
3246
3282
  if (item.type === "skill") {
3247
- const skillMd = path24.join(absPath, "SKILL.md");
3283
+ const skillMd = path25.join(absPath, "SKILL.md");
3248
3284
  if (!fs16.existsSync(skillMd)) {
3249
- failures.push(`${item.id}: missing ${path24.relative(manifestDir, skillMd)}`);
3285
+ failures.push(`${item.id}: missing ${path25.relative(manifestDir, skillMd)}`);
3250
3286
  continue;
3251
3287
  }
3252
3288
  const text = fs16.readFileSync(skillMd, "utf8");
@@ -3279,11 +3315,11 @@ function auditMarkdownContent(manifestDir) {
3279
3315
  const failures = [];
3280
3316
  const dirs = ["skills", "agents"];
3281
3317
  for (const dir of dirs) {
3282
- const abs = path24.join(manifestDir, dir);
3318
+ const abs = path25.join(manifestDir, dir);
3283
3319
  if (!fs16.existsSync(abs)) continue;
3284
3320
  walkMd(abs, (file) => {
3285
3321
  const text = fs16.readFileSync(file, "utf8");
3286
- const rel = path24.relative(manifestDir, file);
3322
+ const rel = path25.relative(manifestDir, file);
3287
3323
  const lines = text.split(/\r?\n/);
3288
3324
  for (let i = 0; i < lines.length; i++) {
3289
3325
  const line = lines[i] ?? "";
@@ -3303,7 +3339,7 @@ function auditMarkdownContent(manifestDir) {
3303
3339
  }
3304
3340
  function walkMd(dir, fn) {
3305
3341
  for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
3306
- const full = path24.join(dir, entry.name);
3342
+ const full = path25.join(dir, entry.name);
3307
3343
  if (entry.isDirectory()) walkMd(full, fn);
3308
3344
  else if (entry.name.endsWith(".md")) fn(full);
3309
3345
  }
@@ -3314,8 +3350,8 @@ async function runValidateCatalog(manifestPath) {
3314
3350
  process.exitCode = 1;
3315
3351
  return;
3316
3352
  }
3317
- const abs = path24.resolve(process.cwd(), manifestPath);
3318
- const manifestDir = path24.dirname(abs);
3353
+ const abs = path25.resolve(process.cwd(), manifestPath);
3354
+ const manifestDir = path25.dirname(abs);
3319
3355
  const data = await readJson(abs);
3320
3356
  if (!data?.items) {
3321
3357
  error(`Could not read catalog manifest at ${abs}`);
@@ -3348,7 +3384,7 @@ async function runValidateCatalog(manifestPath) {
3348
3384
  }
3349
3385
 
3350
3386
  // src/commands/workspace.ts
3351
- import path25 from "path";
3387
+ import path26 from "path";
3352
3388
  import YAML from "yaml";
3353
3389
  async function runWorkspace(action) {
3354
3390
  if (action === "init") {
@@ -3371,8 +3407,8 @@ relationships: []
3371
3407
  process.exitCode = 1;
3372
3408
  return;
3373
3409
  }
3374
- const config = YAML.parse(configText);
3375
- const repos = config.repos ?? [];
3410
+ const config2 = YAML.parse(configText);
3411
+ const repos = config2.repos ?? [];
3376
3412
  if (repos.length === 0) {
3377
3413
  error("No repos configured in haus.workspace.yaml.");
3378
3414
  process.exitCode = 1;
@@ -3381,7 +3417,7 @@ relationships: []
3381
3417
  const summaries = [];
3382
3418
  const ownership = {};
3383
3419
  for (const repo of repos) {
3384
- const repoRoot = path25.resolve(process.cwd(), repo.path);
3420
+ const repoRoot = path26.resolve(process.cwd(), repo.path);
3385
3421
  const result = await scanProject(repoRoot, "fast");
3386
3422
  summaries.push({
3387
3423
  name: repo.name,
@@ -3417,7 +3453,7 @@ ${summaries.map(
3417
3453
  // src/cli.ts
3418
3454
  function cliVersion() {
3419
3455
  try {
3420
- const pkgPath = path26.join(packageRoot(), "package.json");
3456
+ const pkgPath = path27.join(packageRoot(), "package.json");
3421
3457
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3422
3458
  return pkg.version ?? "0.0.0";
3423
3459
  } catch {
@@ -3427,7 +3463,7 @@ function cliVersion() {
3427
3463
  var program = new Command();
3428
3464
  function validateRuntimeNodeVersion() {
3429
3465
  try {
3430
- const pkgPath = path26.join(packageRoot(), "package.json");
3466
+ const pkgPath = path27.join(packageRoot(), "package.json");
3431
3467
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3432
3468
  const requiredRange = pkg.engines?.node;
3433
3469
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -3464,6 +3500,10 @@ memory.command("promote").action(() => runMemory("promote", {}));
3464
3500
  var guard = program.command("guard");
3465
3501
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
3466
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"));
3467
3507
  var workspace = program.command("workspace");
3468
3508
  workspace.command("init").action(() => runWorkspace("init"));
3469
3509
  workspace.command("scan").action(() => runWorkspace("scan"));
@@ -97,6 +97,10 @@
97
97
  "missing-eslint",
98
98
  "docker",
99
99
  "pm2",
100
- "deployer-php"
100
+ "deployer-php",
101
+ "stripe",
102
+ "qliro",
103
+ "supabase",
104
+ "payments"
101
105
  ]
102
106
  }
@@ -467,6 +467,127 @@
467
467
  "tokenEstimate": 1000,
468
468
  "installMode": "copy-selected"
469
469
  },
470
+ {
471
+ "id": "haus.stripe-patterns",
472
+ "version": "1.0.0",
473
+ "source": "haus",
474
+ "type": "skill",
475
+ "path": "skills/stripe-patterns",
476
+ "title": "Haus Stripe patterns",
477
+ "purpose": "Guide Stripe Elements, Checkout, webhook handling, and PCI-safe integration changes.",
478
+ "whenToUse": "Use for `@stripe/stripe-js` / `@stripe/react-stripe-js` integrations, payment intents, webhooks, and Checkout.",
479
+ "whenNotToUse": "Do not use for non-Stripe payment providers (Qliro, Klarna, etc.).",
480
+ "references": [
481
+ "references/conventions.md",
482
+ "references/scope.md",
483
+ "references/workflow.md",
484
+ "https://docs.stripe.com/"
485
+ ],
486
+ "tokenBudget": 1200,
487
+ "tags": [
488
+ "stripe",
489
+ "payments",
490
+ "tooling"
491
+ ],
492
+ "repoRoles": [
493
+ "next-app",
494
+ "react-app",
495
+ "vendure-plugin"
496
+ ],
497
+ "requiresAny": [
498
+ {
499
+ "stack": "stripe"
500
+ },
501
+ {
502
+ "dependency": "@stripe/stripe-js"
503
+ },
504
+ {
505
+ "dependency": "@stripe/react-stripe-js"
506
+ }
507
+ ],
508
+ "ecosystem": "payments",
509
+ "tokenEstimate": 2000,
510
+ "installMode": "copy-selected"
511
+ },
512
+ {
513
+ "id": "haus.qliro-patterns",
514
+ "version": "1.0.0",
515
+ "source": "haus",
516
+ "type": "skill",
517
+ "path": "skills/qliro-patterns",
518
+ "title": "Haus Qliro patterns",
519
+ "purpose": "Guide Qliro Checkout integration via @haus-tech/qliro-plugin in Vendure storefronts.",
520
+ "whenToUse": "Use for Qliro Checkout flow, order callbacks, refund handling, and merchant API config.",
521
+ "whenNotToUse": "Do not use for non-Nordic payment providers or Stripe-only flows.",
522
+ "references": [
523
+ "references/conventions.md",
524
+ "references/scope.md",
525
+ "references/workflow.md"
526
+ ],
527
+ "tokenBudget": 1200,
528
+ "tags": [
529
+ "qliro",
530
+ "payments",
531
+ "tooling"
532
+ ],
533
+ "repoRoles": [
534
+ "vendure-app",
535
+ "vendure-plugin",
536
+ "next-app"
537
+ ],
538
+ "requiresAny": [
539
+ {
540
+ "stack": "qliro"
541
+ },
542
+ {
543
+ "dependency": "@haus-tech/qliro-plugin"
544
+ }
545
+ ],
546
+ "ecosystem": "payments",
547
+ "tokenEstimate": 1800,
548
+ "installMode": "copy-selected"
549
+ },
550
+ {
551
+ "id": "haus.supabase-patterns",
552
+ "version": "1.0.0",
553
+ "source": "haus",
554
+ "type": "skill",
555
+ "path": "skills/supabase-patterns",
556
+ "title": "Haus Supabase patterns",
557
+ "purpose": "Guide Supabase client wiring, Row-Level Security, edge functions, and auth integration.",
558
+ "whenToUse": "Use for `@supabase/supabase-js` queries, RLS policies, edge functions, and Supabase Auth flows.",
559
+ "whenNotToUse": "Do not use for non-Supabase BaaS (Firebase, AWS Amplify) or self-hosted Postgres without Supabase.",
560
+ "references": [
561
+ "references/conventions.md",
562
+ "references/scope.md",
563
+ "references/workflow.md",
564
+ "https://supabase.com/docs"
565
+ ],
566
+ "tokenBudget": 1200,
567
+ "tags": [
568
+ "supabase",
569
+ "database",
570
+ "backend"
571
+ ],
572
+ "repoRoles": [
573
+ "next-app",
574
+ "react-app"
575
+ ],
576
+ "requiresAny": [
577
+ {
578
+ "stack": "supabase"
579
+ },
580
+ {
581
+ "dependency": "@supabase/supabase-js"
582
+ },
583
+ {
584
+ "packageNamePattern": "@supabase/*"
585
+ }
586
+ ],
587
+ "ecosystem": "database",
588
+ "tokenEstimate": 2000,
589
+ "installMode": "copy-selected"
590
+ },
470
591
  {
471
592
  "id": "haus.sanity-patterns",
472
593
  "version": "1.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.8.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",
@@ -258,6 +258,71 @@
258
258
  "tokenEstimate": 1000,
259
259
  "installMode": "copy-selected"
260
260
  },
261
+ {
262
+ "id": "haus.stripe-patterns",
263
+ "source": "haus",
264
+ "type": "skill",
265
+ "path": "skills/stripe-patterns",
266
+ "title": "Haus Stripe patterns",
267
+ "purpose": "Guide Stripe Elements, Checkout, webhook handling, and PCI-safe integration changes.",
268
+ "whenToUse": "Use for `@stripe/stripe-js` / `@stripe/react-stripe-js` integrations, payment intents, webhooks, and Checkout.",
269
+ "whenNotToUse": "Do not use for non-Stripe payment providers (Qliro, Klarna, etc.).",
270
+ "references": ["references/scope.md", "references/workflow.md", "https://docs.stripe.com/"],
271
+ "tokenBudget": 1200,
272
+ "tags": ["stripe", "payments", "tooling"],
273
+ "repoRoles": ["next-app", "react-app", "vendure-plugin"],
274
+ "requiresAny": [
275
+ { "stack": "stripe" },
276
+ { "dependency": "@stripe/stripe-js" },
277
+ { "dependency": "@stripe/react-stripe-js" }
278
+ ],
279
+ "ecosystem": "payments",
280
+ "tokenEstimate": 2000,
281
+ "installMode": "copy-selected"
282
+ },
283
+ {
284
+ "id": "haus.qliro-patterns",
285
+ "source": "haus",
286
+ "type": "skill",
287
+ "path": "skills/qliro-patterns",
288
+ "title": "Haus Qliro patterns",
289
+ "purpose": "Guide Qliro Checkout integration via @haus-tech/qliro-plugin in Vendure storefronts.",
290
+ "whenToUse": "Use for Qliro Checkout flow, order callbacks, refund handling, and merchant API config.",
291
+ "whenNotToUse": "Do not use for non-Nordic payment providers or Stripe-only flows.",
292
+ "references": ["references/scope.md", "references/workflow.md"],
293
+ "tokenBudget": 1200,
294
+ "tags": ["qliro", "payments", "tooling"],
295
+ "repoRoles": ["vendure-app", "vendure-plugin", "next-app"],
296
+ "requiresAny": [
297
+ { "stack": "qliro" },
298
+ { "dependency": "@haus-tech/qliro-plugin" }
299
+ ],
300
+ "ecosystem": "payments",
301
+ "tokenEstimate": 1800,
302
+ "installMode": "copy-selected"
303
+ },
304
+ {
305
+ "id": "haus.supabase-patterns",
306
+ "source": "haus",
307
+ "type": "skill",
308
+ "path": "skills/supabase-patterns",
309
+ "title": "Haus Supabase patterns",
310
+ "purpose": "Guide Supabase client wiring, Row-Level Security, edge functions, and auth integration.",
311
+ "whenToUse": "Use for `@supabase/supabase-js` queries, RLS policies, edge functions, and Supabase Auth flows.",
312
+ "whenNotToUse": "Do not use for non-Supabase BaaS (Firebase, AWS Amplify) or self-hosted Postgres without Supabase.",
313
+ "references": ["references/scope.md", "references/workflow.md", "https://supabase.com/docs"],
314
+ "tokenBudget": 1200,
315
+ "tags": ["supabase", "database", "backend"],
316
+ "repoRoles": ["next-app", "react-app"],
317
+ "requiresAny": [
318
+ { "stack": "supabase" },
319
+ { "dependency": "@supabase/supabase-js" },
320
+ { "packageNamePattern": "@supabase/*" }
321
+ ],
322
+ "ecosystem": "database",
323
+ "tokenEstimate": 2000,
324
+ "installMode": "copy-selected"
325
+ },
261
326
  {
262
327
  "id": "haus.sanity-patterns",
263
328
  "source": "haus",
@@ -0,0 +1,9 @@
1
+ <!-- Fixture stub — minimal valid SKILL.md for CLI tests. -->
2
+
3
+ ## Use when
4
+
5
+ Use this skill when working with the relevant technology.
6
+
7
+ ## Do not use when
8
+
9
+ Do not use for unrelated tasks.
@@ -0,0 +1,9 @@
1
+ <!-- Fixture stub — minimal valid SKILL.md for CLI tests. -->
2
+
3
+ ## Use when
4
+
5
+ Use this skill when working with the relevant technology.
6
+
7
+ ## Do not use when
8
+
9
+ Do not use for unrelated tasks.
@@ -0,0 +1,9 @@
1
+ <!-- Fixture stub — minimal valid SKILL.md for CLI tests. -->
2
+
3
+ ## Use when
4
+
5
+ Use this skill when working with the relevant technology.
6
+
7
+ ## Do not use when
8
+
9
+ Do not use for unrelated tasks.
@@ -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.