@haus-tech/haus-workflow 0.24.0 → 0.25.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.24.1...v0.25.0) (2026-06-12)
4
+
5
+ ### Features
6
+
7
+ - **cloneandsetup:** deterministic env phase, prereq gate, standalone needs, clone fallback ([#105](https://github.com/WeAreHausTech/haus-workflow/issues/105)) ([c6f5858](https://github.com/WeAreHausTech/haus-workflow/commit/c6f585850591eba48d08f7f6e2d8b1a2aa50c355))
8
+
9
+ ## [0.24.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.24.0...v0.24.1) (2026-06-12)
10
+
11
+ ### Bug Fixes
12
+
13
+ - **settings:** reconcile haus permission rules on update/apply ([#104](https://github.com/WeAreHausTech/haus-workflow/issues/104)) ([8ab070c](https://github.com/WeAreHausTech/haus-workflow/commit/8ab070c006943f600b6dccb7582b8330d452d3c1))
14
+
3
15
  ## [0.24.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.23.1...v0.24.0) (2026-06-12)
4
16
 
5
17
  ### Features
package/README.md CHANGED
@@ -8,21 +8,24 @@
8
8
 
9
9
  Requires Node 22+.
10
10
 
11
+ **Terminal:**
12
+
11
13
  ```bash
12
14
  npm install -g @haus-tech/haus-workflow
13
15
  ```
14
16
 
17
+ **Or paste this into Claude Code:**
18
+
19
+ ```
20
+ Install the haus-workflow CLI globally by running `npm install -g @haus-tech/haus-workflow`.
21
+ ```
22
+
15
23
  A **global** install auto-runs `haus install` via a postinstall hook — it seeds
16
24
  `~/.claude/` with Haus-managed skills, global slash commands, and hooks, merges
17
25
  security rules into `~/.claude/settings.json`, and prints a notice of what changed.
18
26
  It is non-fatal, idempotent, and global-only. Skip it with `HAUS_NO_POSTINSTALL=1`;
19
27
  re-run or repair any time with `haus install`. Undo with `haus uninstall`.
20
28
 
21
- **Driving it from Claude Code (no terminal):** once installed, every project's `/`
22
- menu has `/haus-setup`, `/haus-doctor`, and `/haus-fix`. Run `/haus-setup` (or just
23
- ask "set up my project") and the agent scans, asks a few plain-language questions,
24
- and configures the repo for you.
25
-
26
29
  ---
27
30
 
28
31
  ## haus-workflow skill
@@ -30,7 +33,7 @@ and configures the repo for you.
30
33
  Once installed, Claude Code gains a `/haus-workflow` slash command.
31
34
 
32
35
  The `project:*` tasks act on the current repo. The unprefixed verbs (`update`,
33
- `catalog`, `install`, `uninstall`) manage the haus tool itself on your machine
36
+ `install`, `uninstall`) manage the haus tool itself on your machine
34
37
  (`~/.claude` + npm), like `npm install -g`. The short legacy names still work.
35
38
 
36
39
  ```
@@ -41,25 +44,12 @@ The `project:*` tasks act on the current repo. The unprefixed verbs (`update`,
41
44
  /haus-workflow project:refresh # [project] refresh .claude/ and regenerate CLAUDE.md imports
42
45
  /haus-workflow project:doctor # [project] health check for drift
43
46
  /haus-workflow update # [global] update npm package + catalog + ~/.claude/
44
- /haus-workflow catalog # [global] fetch only the latest catalog
45
47
  ```
46
48
 
47
49
  Without an argument, the skill presents a menu so you can pick the task. With an argument, it runs immediately.
48
50
 
49
51
  ---
50
52
 
51
- ## Per-project setup
52
-
53
- Run once inside each project:
54
-
55
- ```bash
56
- haus init
57
- ```
58
-
59
- Scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
60
-
61
- ---
62
-
63
53
  ## Commands
64
54
 
65
55
  ```bash
@@ -73,12 +63,13 @@ haus apply --write # write .claude/ files (skills, agents, commands, templat
73
63
  haus apply --select # interactively choose which recommended items to install
74
64
  haus apply --refill-config # fill still-blank workflow-config.md fields, keep edits
75
65
  haus context --task "<task>" # select context rules for a task (token-budgeted)
76
- haus update # sync remote catalog + re-apply project files
66
+ haus update # check npm for new CLI + sync catalog + refresh ~/.claude/ and this project
77
67
  haus update --check # check for updates without applying
78
68
  haus undo # remove haus-managed project files (lock-tracked paths)
79
69
  haus doctor # health check: hooks, CLAUDE.md, imports, catalog cache
80
70
  haus config # manage hook configuration
81
- haus guard # test bash/file-access guards
71
+ haus guard # security guard hook (bash + file-access); invoked by PreToolUse
72
+ haus workspace # multi-repo ops: discover, scan, setup, doctor across a workspace
82
73
  haus uninstall # remove Haus-managed files from ~/.claude/
83
74
  ```
84
75
 
@@ -87,24 +78,16 @@ haus uninstall # remove Haus-managed files from ~/.claude/
87
78
 
88
79
  ---
89
80
 
90
- ## Development
91
-
92
- ```bash
93
- yarn install
94
- yarn verify # typecheck + lint + build + test
95
- yarn dev <cmd> # run CLI without building (tsx)
96
- ```
97
-
98
- ### Catalog
81
+ ## Catalog
99
82
 
100
83
  Content lives in [`haus-workflow-catalog`](https://github.com/WeAreHausTech/haus-workflow-catalog)
101
- (71 items; version pinned in `library/catalog/manifest.json`). Fetched at runtime from `main` (override with `HAUS_CATALOG_REF`).
84
+ (version pinned in `library/catalog/manifest.json`). Fetched at runtime from `main` (override with `HAUS_CATALOG_REF`).
102
85
  Validation rules sync from catalog → `library/catalog/validation-rules.json` (ADR-0001).
103
86
 
104
87
  On `haus apply` / `haus update`, items **removed from the catalog** are pruned from the
105
88
  project when their on-disk copy still matches the lock hash; user-edited copies are kept.
106
89
 
107
- ### Internal docs
90
+ ## Internal docs
108
91
 
109
92
  - [Architecture](docs/architecture.md)
110
93
  - [CLI reference](docs/cli.md)
package/dist/cli.js CHANGED
@@ -936,53 +936,63 @@ function mergeHooks(settings, fragments) {
936
936
  };
937
937
  return { settings: updated, addedIds };
938
938
  }
939
+ function reconcileManagedRules(existing, prevTracked, newRules) {
940
+ const prevTrackedSet = new Set(prevTracked);
941
+ const userRules = [];
942
+ const userSet = /* @__PURE__ */ new Set();
943
+ for (const rule of existing) {
944
+ if (prevTrackedSet.has(rule) || userSet.has(rule)) continue;
945
+ userSet.add(rule);
946
+ userRules.push(rule);
947
+ }
948
+ const tracked = [];
949
+ const trackedSet = /* @__PURE__ */ new Set();
950
+ for (const rule of newRules) {
951
+ if (userSet.has(rule) || trackedSet.has(rule)) continue;
952
+ trackedSet.add(rule);
953
+ tracked.push(rule);
954
+ }
955
+ const existingSet = new Set(existing);
956
+ const added = tracked.filter((rule) => !existingSet.has(rule));
957
+ const newSet = /* @__PURE__ */ new Set([...userSet, ...trackedSet]);
958
+ const removed = existing.filter((rule) => !newSet.has(rule));
959
+ return { rules: [...userRules, ...tracked], tracked, added, removed };
960
+ }
939
961
  function mergeDenyRules(settings, rules) {
940
962
  const existingDeny = settings.permissions?.deny ?? [];
941
- const seen = new Set(existingDeny);
942
963
  const trackedDeny = settings._haus?.denyRules ?? [];
943
- const addedRules = [];
944
- for (const rule of rules) {
945
- if (seen.has(rule)) continue;
946
- seen.add(rule);
947
- addedRules.push(rule);
948
- }
964
+ const { rules: deny2, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, rules);
949
965
  const updated = { ...settings };
950
966
  updated.permissions = {
951
967
  ...settings.permissions ?? {},
952
- deny: [...existingDeny, ...addedRules]
968
+ deny: deny2
953
969
  };
954
970
  updated._haus = {
955
971
  hooks: settings._haus?.hooks ?? [],
956
972
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
957
- denyRules: [...trackedDeny, ...addedRules],
973
+ denyRules: tracked,
958
974
  ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
959
975
  ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
960
976
  };
961
- return { settings: updated, addedRules };
977
+ return { settings: updated, addedRules: added };
962
978
  }
963
979
  function mergeAllowRules(settings, rules) {
964
980
  const existingAllow = settings.permissions?.allow ?? [];
965
- const seen = new Set(existingAllow);
966
981
  const trackedAllow = settings._haus?.allowRules ?? [];
967
- const addedRules = [];
968
- for (const rule of rules) {
969
- if (seen.has(rule)) continue;
970
- seen.add(rule);
971
- addedRules.push(rule);
972
- }
982
+ const { rules: allow, tracked, added } = reconcileManagedRules(existingAllow, trackedAllow, rules);
973
983
  const updated = { ...settings };
974
984
  updated.permissions = {
975
985
  ...settings.permissions ?? {},
976
- allow: [...existingAllow, ...addedRules]
986
+ allow
977
987
  };
978
988
  updated._haus = {
979
989
  hooks: settings._haus?.hooks ?? [],
980
990
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
981
991
  ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
982
- allowRules: [...trackedAllow, ...addedRules],
992
+ allowRules: tracked,
983
993
  ...settings._haus?.askRules ? { askRules: settings._haus.askRules } : {}
984
994
  };
985
- return { settings: updated, addedRules };
995
+ return { settings: updated, addedRules: added };
986
996
  }
987
997
  function stripHausAllow(settings) {
988
998
  const prevHaus = settings._haus;
@@ -1039,27 +1049,21 @@ function stripHausHooks(settings) {
1039
1049
  }
1040
1050
  function mergeAskRules(settings, rules) {
1041
1051
  const existingAsk = settings.permissions?.ask ?? [];
1042
- const seen = new Set(existingAsk);
1043
1052
  const trackedAsk = settings._haus?.askRules ?? [];
1044
- const addedRules = [];
1045
- for (const rule of rules) {
1046
- if (seen.has(rule)) continue;
1047
- seen.add(rule);
1048
- addedRules.push(rule);
1049
- }
1053
+ const { rules: ask2, tracked, added } = reconcileManagedRules(existingAsk, trackedAsk, rules);
1050
1054
  const updated = { ...settings };
1051
1055
  updated.permissions = {
1052
1056
  ...settings.permissions ?? {},
1053
- ask: [...existingAsk, ...addedRules]
1057
+ ask: ask2
1054
1058
  };
1055
1059
  updated._haus = {
1056
1060
  hooks: settings._haus?.hooks ?? [],
1057
1061
  ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
1058
1062
  ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
1059
1063
  ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {},
1060
- askRules: [...trackedAsk, ...addedRules]
1064
+ askRules: tracked
1061
1065
  };
1062
- return { settings: updated, addedRules };
1066
+ return { settings: updated, addedRules: added };
1063
1067
  }
1064
1068
  function stripHausAsk(settings) {
1065
1069
  const prevHaus = settings._haus;
@@ -1523,31 +1527,9 @@ var CANONICAL_HOOKS = {
1523
1527
  ]
1524
1528
  }
1525
1529
  };
1526
- var STABLE_HOOK_IDS = {
1527
- "haus context --from-hook": "haus.context-hook",
1528
- "haus guard file-access --from-hook": "haus.guard-file",
1529
- "haus guard bash --from-hook": "haus.guard-bash"
1530
- };
1531
1530
  async function loadClaudeHooksSettings() {
1532
1531
  return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
1533
1532
  }
1534
- function flattenRecommendedHooks(settings) {
1535
- const out = [];
1536
- let generic = 0;
1537
- for (const block of settings.hooks.UserPromptSubmit) {
1538
- for (const h of block.hooks) {
1539
- const id = STABLE_HOOK_IDS[h.command] ?? `haus.hook.user-${generic++}`;
1540
- out.push({ id, command: h.command });
1541
- }
1542
- }
1543
- for (const block of settings.hooks.PreToolUse) {
1544
- for (const h of block.hooks) {
1545
- const id = STABLE_HOOK_IDS[h.command] ?? `haus.hook.pre-${generic++}`;
1546
- out.push({ id, command: h.command });
1547
- }
1548
- }
1549
- return out;
1550
- }
1551
1533
 
1552
1534
  // src/claude/verify-hooks-contract.ts
1553
1535
  function collectHookCommands(settings) {
@@ -1748,7 +1730,6 @@ function refillContent(existing, v) {
1748
1730
  }).join("\n");
1749
1731
  }
1750
1732
  var FALLBACK_CONTEXT = {
1751
- mode: "fast",
1752
1733
  generatedAt: "",
1753
1734
  root: "",
1754
1735
  repoName: "",
@@ -1871,7 +1852,6 @@ function targetDirForType(type) {
1871
1852
  }
1872
1853
  async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1873
1854
  const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
1874
- mode: "fast",
1875
1855
  recommended: [],
1876
1856
  skipped: [],
1877
1857
  warnings: [],
@@ -1885,7 +1865,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1885
1865
  const coreFiles = [
1886
1866
  claudePath(root, "settings.json"),
1887
1867
  claudePath(root, "rules", "haus.md"),
1888
- claudePath(root, "rules", "security.md"),
1889
1868
  claudePath(root, "commands", "haus-doctor.md")
1890
1869
  ];
1891
1870
  const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
@@ -1898,12 +1877,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1898
1877
  ...workflowPath ? [workflowPath] : [],
1899
1878
  ...workflowConfigPath ? [workflowConfigPath] : []
1900
1879
  ];
1901
- const files = dryRun ? [...coreFiles, ...p6Files] : [
1902
- ...coreFiles,
1903
- ...p6Files,
1904
- hausPath(root, "selected-context.json"),
1905
- hausPath(root, "haus.lock.json")
1906
- ];
1880
+ const files = dryRun ? [...coreFiles, ...p6Files] : [...coreFiles, ...p6Files, hausPath(root, "haus.lock.json")];
1907
1881
  if (dryRun) {
1908
1882
  const mergedSettings = await mergeProjectSettings(root);
1909
1883
  await writeManagedJson(root, claudePath(root, "settings.json"), mergedSettings, true);
@@ -1938,15 +1912,58 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1938
1912
  await writeManagedText(
1939
1913
  root,
1940
1914
  claudePath(root, "rules", "haus.md"),
1941
- "- Keep context minimal.\n- Follow project conventions.\n\n## Driving haus\nWhen the user asks to set up, configure, check, or fix the project, run `haus setup-project` or `haus doctor` and narrate results in plain language \u2014 never make them use a terminal or read JSON. The `/haus-setup`, `/haus-doctor`, and `/haus-fix` commands do the same.\n",
1942
- dryRun
1943
- );
1944
- await writeManagedText(
1945
- root,
1946
- claudePath(root, "rules", "security.md"),
1947
- "- Never read secrets.\n- Block dangerous shell commands.\n",
1915
+ [
1916
+ "- Keep context minimal.",
1917
+ "- Follow project conventions.",
1918
+ "- Never read secrets.",
1919
+ "- Block dangerous shell commands.",
1920
+ "- NEVER hand-edit haus-managed blocks (`<!-- HAUS:BEGIN \u2026 -->` \u2026 `<!-- HAUS:END \u2026 -->`)",
1921
+ " or haus-owned files under `.claude/` / `.haus-workflow/` \u2014 regenerate via `haus apply`.",
1922
+ " Hand-edits are silently overwritten or flagged as drift.",
1923
+ "",
1924
+ "## Driving haus",
1925
+ "haus owns `.claude/` and `.haus-workflow/`. When the user asks to set up, configure,",
1926
+ "check, fix, refresh, or update the project, run the matching `haus` command and narrate",
1927
+ "results in plain language \u2014 never make them use a terminal or read JSON.",
1928
+ "- Set up / configure / fix / check \u2192 `haus setup-project`, `haus apply --write`, `haus doctor`",
1929
+ "- Update package + catalog \u2192 `haus update`",
1930
+ "- The `/haus-workflow`, `/haus-setup`, `/haus-doctor`, and `/haus-fix` commands do the same.",
1931
+ ""
1932
+ ].join("\n"),
1948
1933
  dryRun
1949
1934
  );
1935
+ const legacySecurityPath = claudePath(root, "rules", "security.md");
1936
+ if (await fs12.pathExists(legacySecurityPath)) {
1937
+ const content2 = await fs12.readFile(legacySecurityPath, "utf8");
1938
+ const stub = "- Never read secrets.\n- Block dangerous shell commands.";
1939
+ if (content2 === stub || content2 === `${stub}
1940
+ ` || content2 === `${stub}\r
1941
+ `) {
1942
+ if (dryRun) {
1943
+ log(`[dry-run] would remove stale ${displayPath(root, legacySecurityPath)}`);
1944
+ } else {
1945
+ await fs12.remove(legacySecurityPath);
1946
+ }
1947
+ }
1948
+ }
1949
+ const LEGACY_PRUNED_ARTIFACTS = [
1950
+ "selected-context.json",
1951
+ "dependency-map.json",
1952
+ "scan-hashes.json",
1953
+ "recommended-hooks.json",
1954
+ "recommended-rules.json",
1955
+ "repo-summary.md"
1956
+ ];
1957
+ for (const rel of LEGACY_PRUNED_ARTIFACTS) {
1958
+ const legacyPath = hausPath(root, rel);
1959
+ if (await fs12.pathExists(legacyPath)) {
1960
+ if (dryRun) {
1961
+ log(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
1962
+ } else {
1963
+ await fs12.remove(legacyPath);
1964
+ }
1965
+ }
1966
+ }
1950
1967
  const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
1951
1968
  const manifestById = new Map(manifestItems.map((item) => [item.id, item]));
1952
1969
  const installedPathsByItem = /* @__PURE__ */ new Map();
@@ -2028,17 +2045,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
2028
2045
  (prevLock ?? []).filter((e) => e.id && e.catalogRef).map((e) => [e.id, e.catalogRef])
2029
2046
  );
2030
2047
  const lockCatalogRef = (itemId) => isCatalogRefResolved() ? getResolvedCatalogRef() : prevRefById.get(itemId) ?? getResolvedCatalogRef();
2031
- await writeManagedJson(
2032
- root,
2033
- hausPath(root, "selected-context.json"),
2034
- installedItems.map((r) => ({
2035
- id: r.id,
2036
- type: r.type,
2037
- reason: r.reason,
2038
- selectionMode: r.selectionMode
2039
- })),
2040
- false
2041
- );
2042
2048
  const lock = await Promise.all(
2043
2049
  installedItems.map(async (r) => {
2044
2050
  const relPaths = installedPathsByItem.get(r.id) ?? [];
@@ -2354,7 +2360,6 @@ function normalizeRecommendation(input2) {
2354
2360
  })) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
2355
2361
  }));
2356
2362
  return {
2357
- mode: input2.mode === "guided" ? "guided" : "fast",
2358
2363
  recommended,
2359
2364
  skipped,
2360
2365
  warnings: input2.warnings ?? [],
@@ -2677,7 +2682,6 @@ function selectRules(recommended, task, taskIntents) {
2677
2682
  }
2678
2683
 
2679
2684
  // src/scanner/scan-project.ts
2680
- import { readFile as readFile2 } from "fs/promises";
2681
2685
  import path20 from "path";
2682
2686
 
2683
2687
  // src/utils/audit-checks.ts
@@ -3143,7 +3147,7 @@ var SAFE_FILES = [
3143
3147
  "build.gradle.kts",
3144
3148
  "Gemfile"
3145
3149
  ];
3146
- async function scanProject(root, mode = "fast") {
3150
+ async function scanProject(root) {
3147
3151
  const pkg = await readJson(path20.join(root, "package.json"));
3148
3152
  const composer = await readJson(path20.join(root, "composer.json"));
3149
3153
  const files = await listFiles(root, SAFE_FILES);
@@ -3159,9 +3163,6 @@ async function scanProject(root, mode = "fast") {
3159
3163
  const warnings = [];
3160
3164
  const securityRisks = [];
3161
3165
  const crossRepoHints = [];
3162
- if (!safeFiles.some((f) => f.endsWith(".env.example"))) warnings.push("No .env.example found");
3163
- if (!(pkg && isRecord(pkg.scripts) && String(pkg.scripts.test ?? "").length > 0))
3164
- warnings.push("No package.json test script found");
3165
3166
  const nodeEngine = isRecord(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
3166
3167
  if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
3167
3168
  warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
@@ -3170,13 +3171,11 @@ async function scanProject(root, mode = "fast") {
3170
3171
  crossRepoHints.push("Containerized services detected");
3171
3172
  if (safeFiles.some((f) => f.includes("turbo.json") || f.includes("nx.json")))
3172
3173
  crossRepoHints.push("Monorepo orchestration detected");
3173
- if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
3174
3174
  if (safeFiles.some((f) => f.includes("wp-content/uploads")))
3175
3175
  securityRisks.push("Uploads directory present");
3176
3176
  const unsupportedSignals = collectUnsupportedSignals(safeFiles);
3177
3177
  const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
3178
3178
  const context = {
3179
- mode,
3180
3179
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3181
3180
  root,
3182
3181
  repoName: String(pkg?.name ?? path20.basename(root)),
@@ -3190,30 +3189,16 @@ async function scanProject(root, mode = "fast") {
3190
3189
  detectionStatus,
3191
3190
  unsupportedSignals
3192
3191
  };
3193
- const dependencyMap = {
3194
- node: deps.filter((d) => !d.includes("/")),
3195
- composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
3196
- };
3197
- const scanHashes = Object.fromEntries(
3198
- await mapWithConcurrency(
3199
- safeFiles,
3200
- async (f) => [f, hashText(await readFile2(path20.join(root, f), "utf8"))]
3201
- )
3202
- );
3203
- const repoSummary = renderSummary(context);
3204
3192
  await writeJson(hausPath(root, "context-map.json"), context);
3205
- await writeJson(hausPath(root, "dependency-map.json"), dependencyMap);
3206
- await writeJson(hausPath(root, "scan-hashes.json"), scanHashes);
3207
- await writeText(hausPath(root, "repo-summary.md"), repoSummary);
3208
3193
  await writeSourcesReport(root);
3209
- return { ...context, dependencyMap, scanHashes, repoSummary };
3194
+ return context;
3210
3195
  }
3211
3196
 
3212
3197
  // src/scanner/read-context.ts
3213
3198
  async function readContextOrScan(root) {
3214
3199
  const context = await readJson(hausPath(root, "context-map.json"));
3215
3200
  if (context) return context;
3216
- const scan = await scanProject(root, "fast");
3201
+ const scan = await scanProject(root);
3217
3202
  return scan;
3218
3203
  }
3219
3204
 
@@ -3236,7 +3221,7 @@ async function runContext(options) {
3236
3221
  return;
3237
3222
  }
3238
3223
  const context = await readContextOrScan(root);
3239
- const summary = await readText(hausPath(root, "repo-summary.md")) ?? "";
3224
+ const summary = renderSummary(context);
3240
3225
  const recommendationRaw = await readJson(hausPath(root, "recommendation.json"));
3241
3226
  const recommendation = recommendationRaw ? normalizeRecommendation(recommendationRaw) : void 0;
3242
3227
  const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
@@ -3529,7 +3514,6 @@ function formatReasonWithSignal(reason) {
3529
3514
  function formatRecommendationHuman(rec) {
3530
3515
  const lines = [];
3531
3516
  lines.push("Recommendation explanation");
3532
- lines.push(` mode: ${rec.mode}`);
3533
3517
  lines.push(
3534
3518
  ` selected: ${rec.selectedRules} | skipped: ${rec.skippedRules} | estimated token reduction: ${rec.estimatedTokenReductionPct}%`
3535
3519
  );
@@ -3725,7 +3709,6 @@ function mergeRecommendationWarnings(context) {
3725
3709
  // src/recommender/recommend.ts
3726
3710
  async function recommend(root, context) {
3727
3711
  const items = await loadCatalog(root);
3728
- const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
3729
3712
  const sources = await readJson(
3730
3713
  hausPath(root, "sources-report.json")
3731
3714
  ) ?? {};
@@ -3746,7 +3729,6 @@ async function recommend(root, context) {
3746
3729
  const depSet = scannerDeps;
3747
3730
  const recommended = [];
3748
3731
  const skipped = [];
3749
- const goals = Object.values(setupAnswers).join(" ").toLowerCase();
3750
3732
  const sourceTrust = new Map((sources.items ?? []).map((x) => [x.id, x.status ?? "candidate"]));
3751
3733
  const changedFiles = await readChangedFiles(root);
3752
3734
  const skip = (id, code, message, signal) => {
@@ -3820,10 +3802,6 @@ async function recommend(root, context) {
3820
3802
  if (roleMatch) push("repo-role-match", "repo role match", roleSignal(roleMatch));
3821
3803
  const tagMatch = item.tags.find((t) => stackSet.has(t.toLowerCase()));
3822
3804
  if (tagMatch) push("stack-match", "stack/dependency match", stackSignal(tagMatch));
3823
- const goalMatch = item.tags.find(
3824
- (t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
3825
- );
3826
- if (goalMatch) push("goal-match", "guided goal match", `goal:${goalMatch}`);
3827
3805
  const pm = context.packageManager;
3828
3806
  const pmVersionedMatch = pm === "yarn" || pm === "pnpm" ? item.tags.includes(pm) || item.tags.includes(`${pm}4`) || item.tags.includes(`${pm}89`) : item.tags.includes(pm);
3829
3807
  if (pmVersionedMatch) {
@@ -3882,7 +3860,6 @@ async function recommend(root, context) {
3882
3860
  const estimatedContextTokens = estimateContextTokens(selectedRules);
3883
3861
  const estimatedTokenReductionPct = tokenReductionPct(selectedRules, skippedRules);
3884
3862
  return {
3885
- mode: context.mode,
3886
3863
  recommended,
3887
3864
  skipped,
3888
3865
  warnings: mergeRecommendationWarnings(context),
@@ -3902,8 +3879,8 @@ function buildStackSet(context) {
3902
3879
 
3903
3880
  // src/commands/setup-core.ts
3904
3881
  async function runSetupCore(root, opts) {
3905
- const { mode, json, apply, dryRun, confirm: confirm2 } = opts;
3906
- const scanResult = await scanProject(root, mode);
3882
+ const { json, apply, dryRun, confirm: confirm2 } = opts;
3883
+ const scanResult = await scanProject(root);
3907
3884
  if (json) {
3908
3885
  log(JSON.stringify(scanResult, null, 2));
3909
3886
  } else {
@@ -3914,12 +3891,6 @@ async function runSetupCore(root, opts) {
3914
3891
  const context = await readContextOrScan(root);
3915
3892
  const recommendation = await recommend(root, context);
3916
3893
  await writeJson(hausPath(root, "recommendation.json"), recommendation);
3917
- const hookSettings = await loadClaudeHooksSettings();
3918
- await writeJson(hausPath(root, "recommended-hooks.json"), flattenRecommendedHooks(hookSettings));
3919
- await writeJson(hausPath(root, "recommended-rules.json"), [
3920
- { id: "haus.rule.context-minimal", enabled: true },
3921
- { id: "haus.rule.security", enabled: true }
3922
- ]);
3923
3894
  if (json) {
3924
3895
  log(JSON.stringify(recommendation, null, 2));
3925
3896
  } else {
@@ -3987,7 +3958,6 @@ async function runSetupCore(root, opts) {
3987
3958
  async function runSetupProject(options) {
3988
3959
  const root = process.cwd();
3989
3960
  await runSetupCore(root, {
3990
- mode: "fast",
3991
3961
  json: options.json,
3992
3962
  apply: !options.json,
3993
3963
  dryRun: false,
@@ -4317,12 +4287,6 @@ async function runRecommend(options) {
4317
4287
  const context = await readContextOrScan(root);
4318
4288
  const result = await recommend(root, context);
4319
4289
  await writeJson(hausPath(root, "recommendation.json"), result);
4320
- const hookSettings = await loadClaudeHooksSettings();
4321
- await writeJson(hausPath(root, "recommended-hooks.json"), flattenRecommendedHooks(hookSettings));
4322
- await writeJson(hausPath(root, "recommended-rules.json"), [
4323
- { id: "haus.rule.context-minimal", enabled: true },
4324
- { id: "haus.rule.security", enabled: true }
4325
- ]);
4326
4290
  if (options.json) {
4327
4291
  log(JSON.stringify(result, null, 2));
4328
4292
  return;
@@ -4335,7 +4299,7 @@ async function runRecommend(options) {
4335
4299
  // src/commands/refresh.ts
4336
4300
  async function runRefresh() {
4337
4301
  const root = process.cwd();
4338
- const context = await scanProject(root, "fast");
4302
+ const context = await scanProject(root);
4339
4303
  const recommendation = await recommend(root, context);
4340
4304
  await writeJson(hausPath(root, "recommendation.json"), recommendation);
4341
4305
  log("Haus refresh complete");
@@ -4346,8 +4310,7 @@ async function runRefresh() {
4346
4310
 
4347
4311
  // src/commands/scan.ts
4348
4312
  async function runScan(options) {
4349
- const mode = options.mode ?? "fast";
4350
- const result = await scanProject(process.cwd(), mode);
4313
+ const result = await scanProject(process.cwd());
4351
4314
  if (options.json) {
4352
4315
  log(JSON.stringify(result, null, 2));
4353
4316
  return;
@@ -4362,16 +4325,8 @@ import path24 from "path";
4362
4325
  import fs18 from "fs-extra";
4363
4326
 
4364
4327
  // src/claude/managed-paths.ts
4365
- var PROJECT_MANAGED_CLAUDE_REL = [
4366
- "rules/haus.md",
4367
- "rules/security.md",
4368
- "commands/haus-doctor.md"
4369
- ];
4370
- var PROJECT_MANAGED_HAUS_REL = [
4371
- "selected-context.json",
4372
- "haus.lock.json",
4373
- "config.json"
4374
- ];
4328
+ var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md", "commands/haus-doctor.md"];
4329
+ var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json", "config.json"];
4375
4330
  function coreManagedAbsolutePaths(root) {
4376
4331
  const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
4377
4332
  const haus = PROJECT_MANAGED_HAUS_REL.map((rel) => hausPath(root, rel));
@@ -4579,7 +4534,7 @@ function summarizeLockDiff(before, after) {
4579
4534
  }
4580
4535
 
4581
4536
  // src/update/lockfile.ts
4582
- import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
4537
+ import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
4583
4538
  import path26 from "path";
4584
4539
  async function checkLock(root) {
4585
4540
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
@@ -4603,7 +4558,7 @@ async function applyLock(root) {
4603
4558
  const lockPath = hausPath(root, "haus.lock.json");
4604
4559
  let before = "[]";
4605
4560
  try {
4606
- before = await readFile3(lockPath, "utf8");
4561
+ before = await readFile2(lockPath, "utf8");
4607
4562
  } catch {
4608
4563
  before = "[]";
4609
4564
  }
@@ -4633,7 +4588,7 @@ function diffLock(before, after) {
4633
4588
  }
4634
4589
  async function hasLocalOverrides(root) {
4635
4590
  try {
4636
- await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
4591
+ await readFile2(path26.join(root, ".claude", "settings.json"), "utf8");
4637
4592
  return true;
4638
4593
  } catch {
4639
4594
  return false;
@@ -5091,7 +5046,7 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
5091
5046
  const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path30.basename(relDir === "." ? workspaceRoot : absDir);
5092
5047
  let role = "auto";
5093
5048
  try {
5094
- const scan = await scanProject(absDir, "fast");
5049
+ const scan = await scanProject(absDir);
5095
5050
  if (scan.repoRoles[0]) role = scan.repoRoles[0];
5096
5051
  } catch {
5097
5052
  }
@@ -5376,7 +5331,6 @@ function isRootRepo(workspaceRoot, repoPath) {
5376
5331
  return path34.resolve(workspaceRoot, repoPath) === path34.resolve(workspaceRoot);
5377
5332
  }
5378
5333
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
5379
- const mode = options.mode ?? "fast";
5380
5334
  const apply = options.write ?? false;
5381
5335
  const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
5382
5336
  if (!configText) {
@@ -5403,7 +5357,6 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
5403
5357
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5404
5358
  }
5405
5359
  const res = await runSetupCore(repoRoot, {
5406
- mode,
5407
5360
  json: options.json,
5408
5361
  apply,
5409
5362
  dryRun: options.dryRun
@@ -5569,7 +5522,7 @@ async function scanWorkspace(workspaceRoot, opts) {
5569
5522
  if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5570
5523
  throw new Error(`Repo path is not a directory: ${repo.path}`);
5571
5524
  }
5572
- const result = await scanProject(repoRoot, "fast");
5525
+ const result = await scanProject(repoRoot);
5573
5526
  inputs.push({ name: repo.name, path: repo.path, context: result });
5574
5527
  }
5575
5528
  const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config2.relationships);
@@ -5599,7 +5552,6 @@ async function runWorkspace(action, options = {}) {
5599
5552
  return;
5600
5553
  case "setup":
5601
5554
  await runWorkspaceSetup(workspaceRoot, {
5602
- mode: options.guided ? "guided" : "fast",
5603
5555
  write: options.write,
5604
5556
  dryRun: options.dryRun,
5605
5557
  json: options.json,
@@ -5675,7 +5627,7 @@ var workspace = program.command("workspace");
5675
5627
  workspace.command("init").action(() => runWorkspace("init"));
5676
5628
  workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
5677
5629
  workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
5678
- workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--fast", "Skip interactive prompts (default)").option("--guided", "Enable guided Q&A per repo").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
5630
+ workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
5679
5631
  workspace.command("doctor").description("Report workspace drift against the manifest").option("--json", "Output the manifest and drift array as JSON").action((opts) => runWorkspace("doctor", opts));
5680
5632
  program.parseAsync(process.argv).catch((err) => {
5681
5633
  const message = err instanceof Error ? err.message : String(err);
@@ -33,4 +33,7 @@ Clone a whole **workspace** from its manifest. Workspace-only (a `repos.manifest
33
33
  - **Cancel** — do nothing.
34
34
  5. Show the concrete plan before touching anything: list which repos will be cloned (and into which `folder`) and which will be reused/skipped. Get a final go-ahead.
35
35
  6. For each repo to clone, run (quoting it first): `haus clone <repo-url> <folder>` from the workspace root. Offer `--dry-run` first if the user wants a preview. If one repo fails, report it and continue to the next.
36
+
37
+ **Transport fallback.** Manifest URLs may be SSH (`git@github.com:org/repo.git` or `ssh://…`). Probe SSH connectivity once up front (`ssh -T git@github.com`); if it fails, **auto-fall back to the HTTPS URL** (`https://github.com/org/repo.git`) using your `gh auth` credentials, clone over HTTPS, and **report that you switched transport** so the user knows their SSH is down. Don't halt the run for an SSH outage when HTTPS works.
38
+
36
39
  7. After the loop, report which repos were cloned, reused (local), skipped (already present), and failed. Remind the user that installing dependencies and configuring each repo (`.env`, services) is still a manual step for now.
@@ -6,16 +6,23 @@ Clone a project's repos **and** set each one up for local development — node v
6
6
 
7
7
  Run the full `project:clone` flow by following `~/.claude/commands/haus-clone.md` end to end (name → one repo; no name → workspace repos from `repos.manifest.json`). Carry the resulting repo list into Step 2.
8
8
 
9
- ## Step 2 — Confirm the setup pass
9
+ ## Step 2 — Prerequisite gate (one consolidated check)
10
10
 
11
- 1. List the repos and what each will run (node, deps, localdev steps). Get a go-ahead. For **reused** local clones, ask whether to re-run setup.
12
- 2. Check `NODE_AUTH_TOKEN` is exported if any repo needs private `@`-scoped packages; if missing, tell the user — those installs fail without it.
11
+ Before any setup work, probe **everything** the workspace will need and surface **all** gaps in a single prompt — never discover them piecemeal mid-flow. For the repos being set up, check:
12
+
13
+ - **Auth tokens** — `NODE_AUTH_TOKEN` exported (private `@`-scoped npm packages fail without it); `~/.composer/auth.json` (or a repo-local `auth.json`) present if any repo has private composer deps.
14
+ - **Docker daemon** — running (`docker info`); required for any repo's `needs:` services.
15
+ - **PHP environment** — `valet`, `herd`, `ddev`, or `php` on PATH, if any repo is a PHP/WordPress site.
16
+ - **WP-CLI** — `wp --version`, if any repo's `seed:` pulls a WordPress DB.
17
+ - **Node versions** — the versions named in each repo's `.nvmrc` / `engines.node` are installable via `nvm`.
18
+
19
+ Then list the repos and what each will run (node, deps, localdev steps), **report every gap at once**, and get a single go-ahead. For **reused** local clones, ask whether to re-run setup. For each gap, name what it blocks; the user decides whether to fix it now (**ask before any global/system install**) or proceed and skip the affected steps. Don't begin per-repo work until this gate is acknowledged.
13
20
 
14
21
  ## Step 3 — Per-repo dependency pass
15
22
 
16
23
  For each repo, in its own directory, detect and install from the repo's own files (read its `docs/setup.md` / `CLAUDE.md` / `README.md` first — they win). Select node from `.nvmrc`/`engines.node` (`nvm install`), enable the pinned package manager (`corepack enable`), install JS deps (`yarn`/`pnpm`/`npm` by lockfile), composer deps if `composer.json` + `composer` present. Run each repo's steps in one login shell so the node version stays active. Per-repo failure is reported and skipped, not fatal.
17
24
 
18
- **Scaffold `.env`** so the env-wiring in Step 4 has a file to write to: if `.env.example` exists and `.env` does not, copy it; otherwise create an empty `.env`. Per decision D5, write `.env`; if the write is blocked, print the values for the user to add. Tell the user real secrets still need filling.
25
+ **Leave `.env` to Step 4 "Env".** The dependency pass installs only; the env phase writes each repo's `.env` deterministically once services, links, and values are in place.
19
26
 
20
27
  ## Step 4 — Local-dev orchestration
21
28
 
@@ -27,7 +34,7 @@ Specify the least. Infer the rest from the repo's stack + these conventions, and
27
34
 
28
35
  - **IMPORTANT — never install anything without asking first; global/system tools especially** (Homebrew, Docker, Laravel Herd, global `npm`/`composer` packages). Detect what's already present; if something required is missing, name it, say why it's needed, and get an explicit **yes** before installing. Prefer the least-invasive option, and don't switch or override tools the dev already has.
29
36
  - **PHP / WordPress sites are served by the developer's own PHP environment.** If they already have one — detect `valet`, `herd`, `ddev`, or `php` on PATH — **use it**: just satisfy the env contract (docroot → the repo's `web/`, HTTPS) and report the URL; never override what they already run. **Only when no local PHP environment exists** (a completely fresh machine) suggest installing **[Laravel Herd](https://herd.laravel.com)** as the default (asking first) and point its docroot at `web/` + `herd secure`.
30
- - **Databases and other services (a repo's `needs:`) run in Docker.** **If Docker isn't installed**, it's a prerequisite for these tell the user and **ask before installing it** (it's a global install). Once Docker is available, provision services the simplest way (a one-off `docker run`, or the repo's own compose if it ships one), then wire the matching env vars to point at them. Confirm before creating/overwriting data.
37
+ - **Databases and other services (a repo's `needs:`) run in Docker as standalone containers.** Bring each up with a **clean `docker run`** — image, host port, and env from the house convention for that service (e.g. `mysql` → `mysql:8` on `127.0.0.1:3306`, root password from generated secrets; `postgres` → `postgres:16` on `5432`). **Do not provision a `needs:` service from the repo's own `docker-compose.yml` when that compose bind-mounts repo-relative init files** (e.g. `./seed.sql`, `./docker-entrypoint-initdb.d/`): those mounts need files the guard won't let us create, and they conflate bring-up with seeding (which is the separate `seed:` step). **If Docker isn't installed** it's a prerequisite (surfaced in the Step 2 gate) **ask before installing it** (global install). **Port-conflict caveat:** if the conventional host port is already taken, pick the next free port, use it, and **record the chosen port in the workspace `localdev.yml` `env` map** so sink repos point at the right place. A `needs:` service comes up **empty** — populating it is always the separate `seed:` step.
31
38
  - **Dependencies** install from the lockfile (Step 3).
32
39
  - A repo's `localdev.yml` carries **only what can't be inferred** — its `needs`, repo-specific `build`/`seed` commands, env keys, and URL. Detect the stack (e.g. Bedrock = `composer.json` + `web/` docroot + `wp-cli.yml`; Vendure/Node = `docker-compose.yml` + `@vendure/*`) and apply the matching convention.
33
40
 
@@ -57,6 +64,8 @@ steps: # optional escape hatch: explicit ordered shell steps when intent isn't e
57
64
 
58
65
  **Workspace — `<workspace>/.haus-workflow/localdev.yml`** (the glue BETWEEN repos):
59
66
 
67
+ The workspace `env` map is the **single source of truth for cross-repo values** — DB names, ports, host URLs are chosen once and recorded here as literals. The env phase reads values **from here** and writes them into each repo's `.env`. Recording them here (not only in a repo's `.env`) means a later `seed:` / `db:pull` step can find them without depending on env-file load order.
68
+
60
69
  ```yaml
61
70
  order: [repo-a, repo-b] # setup/startup order, by manifest id
62
71
  links:
@@ -64,9 +73,10 @@ links:
64
73
  - { type: composer-path, in: <repo>, dep: <sibling-repo> }
65
74
  - { type: yarn-link, in: [<repo>, ...], dep: <sibling-package-repo> }
66
75
  env:
67
- - source: { repo: <repo>, provides: '<value>' }
76
+ - value: 'app_local' # a chosen literal — the recorded source of truth, OR …
77
+ # source: { repo: <repo>, provides: '<value>' } # … a value produced by another repo
68
78
  sinks:
69
- - { repo: <repo>, key: ENV_KEY }
79
+ - { repo: <repo>, key: DB_NAME } # written under `key` into each sink's .env
70
80
  ```
71
81
 
72
82
  ### Run order
@@ -74,23 +84,46 @@ env:
74
84
  1. **Discover** `.haus-workflow/localdev.yml` in the workspace root and each repo.
75
85
  2. **Resolve order** from the workspace `order` (repos not listed run last, in manifest order). No workspace file → manifest order.
76
86
  3. **Per repo, in order**, apply intent via the conventions:
77
- - **`needs`** → provision each service in Docker if not already running, and wire its env vars. Confirm before creating/overwriting data.
87
+ - **`needs`** → bring up each service as a **standalone `docker run`** (image/port/env from conventions; **not** the repo's compose when it bind-mounts repo-relative init files) if not already running. The service comes up **empty**; record its values in the workspace `localdev.yml` `env` map. No data is created here, so no overwrite prompt at this step — data lands in `seed:`.
78
88
  - **`build`** → run it (honor any node version the repo's docs note).
79
89
  - **`serve`** → for `via: herd` / PHP envs, install nothing — verify the dev's environment serves `web/`, and report the URL.
80
- - **`seed`** → run it; **confirm first** if it's remote (SSH) or destructive (overwrites data).
90
+ - **`seed`** → populate the empty datastore. **Always a distinct, confirm-gated step, separate from `needs:` bring-up** (don't conflate "DB up" with "DB has data"). Before running, **check its prerequisites and report any gap instead of running blind** — e.g. WP-CLI present (for `wp` / `db:pull` seeds), the target repo's **`.env` written** (the seed reads connection values from it — done in the Env step above), the **SSH alias resolves** (for remote pulls like `dep db:pull staging-oderland`). **Confirm first** for every remote (SSH) or destructive (overwrites data) seed — every run, even on re-run. Missing prerequisite → skip with a clear message, don't guess.
81
91
  - **`steps`** (escape hatch) → run in order, selecting `node:` per step, `optional:` failures continue, **confirming before any `remote:`/`destructive:` step** — every run, even on re-run.
82
92
  4. **Links** (workspace-owned, performed generically — do NOT call a repo's own `setup-dev-mode.sh`, which is deprecated):
83
93
  - `symlink` → `ln -s <from> <to>`; replace an existing symlink, but never clobber a real directory without confirmation.
84
94
  - `composer-path` → in `in`'s `composer.json`, set the `dep`'s require to `{ "type": "path", "url": "../<dep-folder>", "options": { "symlink": true } }`, then `composer update <vendor/dep>`.
85
95
  - `yarn-link` → `yarn link` in the `dep` repo, then `yarn link <pkg-name>` in each `in` repo (read `<pkg-name>` from the dep's `package.json`).
86
- 5. **Env:** for each workspace `env` entry, confirm the `source` is satisfiable, then upsert the value into each sink repo's `.env` under its `key`**creating `.env` if it does not exist** (Step 3 scaffolds it, but don't assume). **If the write is blocked or fails, print the exact `KEY=value` lines for the user to paste** (decision D5). Real secrets (DB passwords, tokens) remain the user's to fill.
96
+ 5. **Env (deterministic):** write each repo's `.env` from known valuesno improvising.
97
+ 1. **Compute** each repo's values, in this precedence: the workspace `localdev.yml` `env` map (chosen literals + cross-repo `source` values — the single source of truth) → generated secrets (DB passwords/tokens minted this run) → the **dev-defaults table** below for anything still unset.
98
+ 2. **Write** them into each repo's `.env` (create it if absent; **upsert** keys, never clobbering a value the user already set). These are local dev files — write them directly.
99
+ 3. **Real secrets the generator can't mint** (third-party API keys, prod credentials) go in as clearly-marked `KEY=` blanks for the user to fill; report exactly which keys are still blank.
100
+
101
+ **Dev-defaults table** — used only when neither `localdev.yml` nor a generated secret supplies the value:
102
+
103
+ | Key | Default |
104
+ | -------------------------------- | ------------------------------------------------------------------- |
105
+ | `DB_HOST` | `127.0.0.1` |
106
+ | `DB_PORT` | `3306` (mysql) / `5432` (postgres) — or the port chosen on conflict |
107
+ | `DB_USER` / `DB_PASSWORD` | `root` / a generated secret |
108
+ | `WP_HOME`, `WP_SITEURL`, `*_URL` | the repo's `serve.url` |
109
+ | `WP_ENV` / `APP_ENV` | `development` |
110
+
87
111
  6. **Report, then offer to start.** Give the per-repo summary, then **ask the user whether to start everything now.**
88
112
  - **Yes** → start each repo in the workspace `order` (its `serve.start`, e.g. `yarn dev`; bring up any remaining foreground services), run any follow-ups (e.g. `wp sync-products sync`), then **print the live URLs** (each repo's `serve.url`).
89
113
  - **No** → just print the ordered start commands + follow-ups as next steps; start nothing.
90
114
 
91
- **Default is "ready to run" (D2):** the preparation — datastores up (`docker compose up -d`), DBs pulled, links, builds, env wired — always happens. Starting the **foreground** dev servers and the initial product sync happens **only if the user says yes** to the start prompt above; otherwise they're printed, not run.
115
+ **Default is "ready to run" (D2):** the preparation — datastores up (standalone `docker run`), seeds applied (confirm-gated), links, builds, each repo's `.env` written — always happens. Starting the **foreground** dev servers and the initial product sync happen **only if the user says yes** to the start prompt above; otherwise they're printed, not run.
116
+
117
+ ## Step 5 — Report and define "done"
118
+
119
+ **"Done" is an explicit terminal state**, not "looks set up". The preparation reaches:
120
+
121
+ - datastores up (standalone containers),
122
+ - dependencies installed and builds green,
123
+ - links wired,
124
+ - each repo's `.env` written from known values.
92
125
 
93
- ## Step 5Report
126
+ From there, **live URLs are reachable only if** every required secret is satisfiable **and** the user starts the servers. If a secret can't be minted (a third-party key, a prod credential), say exactly which keys are still blank and what the user must fill don't imply links a missing secret will quietly break.
94
127
 
95
128
  Summarise per repo (node, deps, localdev steps, links, env). Then **ask whether to start everything now**:
96
129
 
@@ -11,19 +11,12 @@ Do this in order:
11
11
  honestly ("I couldn't fully recognise this stack, so I'll apply the general
12
12
  workflow and security guidance").
13
13
 
14
- 2. **Ask the guided questions as chat.** Ask the project's guided questions one
15
- or two at a time, in plain language. Collect the answers.
16
-
17
- 3. **Record answers.** Write the answers to `.haus-workflow/setup-answers.json`
18
- as a flat `{ "question": "answer" }` object (the exact question strings as
19
- keys). This is what lets setup proceed without re-prompting.
20
-
21
- 4. **Apply the basics.** Run `haus apply --write`. Read the result. This installs
14
+ 2. **Apply the basics.** Run `haus apply --write`. Read the result. This installs
22
15
  the core guardrails and helpers — including the documentation skill haus uses
23
16
  in the next step.
24
17
 
25
- 5. **Write the project docs.** Open and follow the instructions in
26
- `.claude/skills/writing-documentation/SKILL.md`, which step 4 just installed.
18
+ 3. **Write the project docs.** Open and follow the instructions in
19
+ `.claude/skills/writing-documentation/SKILL.md`, which step 2 just installed.
27
20
  Following it, do a deep read of the project and:
28
21
  - write the project documentation (the `CLAUDE.md` body and `docs/` files).
29
22
  NEVER alter the `<!-- HAUS:BEGIN haus-imports … -->` … `<!-- HAUS:END … -->`
@@ -31,17 +24,17 @@ Do this in order:
31
24
  - write `.haus-workflow/deep-context.json` describing what the deep read found
32
25
  (roles, stacks, patterns the quick scan in step 1 could not see).
33
26
  If this step can't be completed for any reason, say so plainly and skip to
34
- step 8 — setup still finishes correctly with the basics from step 4.
27
+ step 6 — setup still finishes correctly with the basics from step 2.
35
28
 
36
- 6. **Re-check recommendations with the new understanding.** Run `haus recommend`.
29
+ 4. **Re-check recommendations with the new understanding.** Run `haus recommend`.
37
30
  It re-reads `deep-context.json` and may surface extra helpers matching what the
38
31
  deep read discovered. You MUST run this before the next apply — `haus apply`
39
32
  does not re-calculate recommendations on its own.
40
33
 
41
- 7. **Apply the rest.** Run `haus apply --write` again. It only writes what changed,
42
- so this just adds the newly-matched helpers from step 6.
34
+ 5. **Apply the rest.** Run `haus apply --write` again. It only writes what changed,
35
+ so this just adds the newly-matched helpers from step 4.
43
36
 
44
- 8. **Confirm.** End with one plain-language line, for example:
37
+ 6. **Confirm.** End with one plain-language line, for example:
45
38
  "✅ Your project is configured — I wrote your project docs, added N guardrails
46
39
  and M coding helpers (K matched after reading your code in depth). Run
47
40
  `/haus-doctor` any time to re-check." Fill the numbers from the apply output.
@@ -17,7 +17,7 @@ All-in-one entry point for the Haus AI workflow.
17
17
 
18
18
  Task names use an asymmetric scope convention. The **project:** namespace marks tasks that
19
19
  act on **this repo** (`./.claude`, `./.haus-workflow`) — type `project:` to see them all.
20
- The unprefixed verbs (`update`, `catalog`, `install`, `uninstall`) act on **this machine's
20
+ The unprefixed verbs (`update`, `install`, `uninstall`) act on **this machine's
21
21
  haus install** (`~/.claude`, npm) — they manage the haus tool itself, like `npm install -g`.
22
22
  The short legacy aliases still work but the names below are canonical.
23
23
 
@@ -29,7 +29,6 @@ The short legacy aliases still work but the names below are canonical.
29
29
  | `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
30
30
  | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
31
31
  | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
32
- | `catalog` | `haus update` | global | Fetch latest catalog (same command as update) |
33
32
  | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
34
33
  | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
35
34
 
@@ -48,11 +47,9 @@ Options:
48
47
  (haus apply --write — re-runs setup, regenerates CLAUDE.md imports)
49
48
  3. [global] update — update haus package + catalog + global files
50
49
  (haus update — checks npm for new version, fetches catalog, refreshes ~/.claude/)
51
- 4. [global] catalogfetch catalog updates only
52
- (haus update — same command; pulls latest workflow templates and lockfile)
53
- 5. [project] project:clone [name] — clone repos
50
+ 4. [project] project:clone [name] clone repos
54
51
  (no name: clone a workspace from repos.manifest.json; with a name: find & clone one repo by name from GitHub)
55
- 6. [project] project:cloneandsetup [name] — clone repos, then set them up for local dev
52
+ 5. [project] project:cloneandsetup [name] — clone repos, then set them up for local dev
56
53
  (project:clone, then per-repo deps + databases + cross-repo links + env from localdev.yml)
57
54
  ```
58
55
 
@@ -74,7 +71,7 @@ After the command completes, follow the relevant post-run steps below.
74
71
 
75
72
  ### Setup (`project:init`)
76
73
 
77
- 1. Open and follow `~/.claude/commands/haus-setup.md` — the installed `haus-setup` command (in some projects also `.claude/commands/haus-setup.md`). Run every step in order. It detects the stack, asks the guided questions, runs `haus apply --write` (scaffolding, skills, commands, rules, docs skill), writes the **project docs** (`CLAUDE.md` body + `docs/`) and `.haus-workflow/deep-context.json`, runs `haus recommend`, applies the newly-matched helpers, and confirms.
74
+ 1. Open and follow `~/.claude/commands/haus-setup.md` — the installed `haus-setup` command (in some projects also `.claude/commands/haus-setup.md`). Run every step in order. It detects the stack, runs `haus apply --write` (scaffolding, skills, commands, rules, docs skill), writes the **project docs** (`CLAUDE.md` body + `docs/`) and `.haus-workflow/deep-context.json`, runs `haus recommend`, applies the newly-matched helpers, and confirms.
78
75
  2. Then fill `.haus-workflow/workflow-config.md` — replace every placeholder (`TODO`, `n/a`, empty): test/lint/typecheck/build commands (check `package.json`), docs paths, validation library, pre-commit tool, highest-stakes logic (ask if unclear). Leave none.
79
76
 
80
77
  ### Clone (`project:clone`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {