@haus-tech/haus-workflow 0.23.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.24.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.24.0...v0.24.1) (2026-06-12)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **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))
8
+
9
+ ## [0.24.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.23.1...v0.24.0) (2026-06-12)
10
+
11
+ ### Features
12
+
13
+ - **cloneandsetup:** local-dev orchestration via localdev.yml ([#97](https://github.com/WeAreHausTech/haus-workflow/issues/97)) ([8a2b5d3](https://github.com/WeAreHausTech/haus-workflow/commit/8a2b5d3e9a767bfe92152189b37921d58878818b))
14
+
3
15
  ## [0.23.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.23.0...v0.23.1) (2026-06-12)
4
16
 
5
17
  ## [0.23.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.22.1...v0.23.0) (2026-06-12)
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);
@@ -1,51 +1,100 @@
1
- Clone a project's repos **and** set each one up locally — node version, dependencies, env scaffold. This is `project:clone` followed by a per-repo setup pass.
1
+ Clone a project's repos **and** set each one up for local development — node version, dependencies, databases, cross-repo links, and env. This is `project:clone` followed by a per-repo setup pass and a localdev orchestration pass.
2
2
 
3
- **Always ask before doing work — never assume.** Cloning and setup both run things that take time, hit the network, and need auth; confirm with the user before each phase, and respect repos they already have.
3
+ **Always ask before doing work — never assume.** Cloning and setup hit the network, need auth, and can touch databases; confirm before each phase, and respect repos the user already has.
4
4
 
5
5
  ## Step 1 — Clone
6
6
 
7
- Run the full `project:clone` flow first by following `~/.claude/commands/haus-clone.md` end to end whichever mode applies:
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
- - A **name** was given it finds and clones that one repo from GitHub.
10
- - **No name** → it clones the workspace's repos from `repos.manifest.json` (asking clean-clone vs reuse-local first).
9
+ ## Step 2 Confirm the setup pass
11
10
 
12
- When that finishes you have a set of repos on disk (freshly cloned and/or reused-local). Carry that list into Step 2.
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.
13
13
 
14
- ## Step 2Confirm the setup pass
14
+ ## Step 3Per-repo dependency pass
15
15
 
16
- Before running any setup:
16
+ 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
17
 
18
- 1. List the repos you're about to set up and what each will run (node version select, dependency install, etc.). Get a go-ahead. For repos that were **reused** from an existing local clone, ask whether to (re)run setup there too they may already be set up.
19
- 2. Check `NODE_AUTH_TOKEN` is exported if any repo depends on private `@`-scoped packages (e.g. `@haus-storefront-*`, `@haus-tech/*`). If it's missing, tell the user to set it first — those installs will fail without it. Let them decide whether to continue or stop.
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.
20
19
 
21
- ## Step 3Set up each repo
20
+ ## Step 4Local-dev orchestration
22
21
 
23
- For each repo directory, run its setup **in that directory**, detecting what's needed from the repo's own files don't assume a stack. Run a repo's steps in a single login shell so the selected node version stays active for the install. A robust pattern:
22
+ This is the phase that takes the workspace from "installed" to "ready to run." It is driven by `localdev.yml` files; a repo with none is set up by Step 3 only.
24
23
 
25
- ```
26
- bash -lc 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; cd "<repo>" && nvm install && corepack enable && yarn install'
24
+ ### House conventions (the "how", so repos specify only the "what")
25
+
26
+ Specify the least. Infer the rest from the repo's stack + these conventions, and work out the concrete commands at run time:
27
+
28
+ - **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
+ - **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.
31
+ - **Dependencies** install from the lockfile (Step 3).
32
+ - 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
+
34
+ ### The `localdev.yml` format
35
+
36
+ **Per-repo — `<repo>/.haus-workflow/localdev.yml`** (how to set up THAT repo alone; sibling-agnostic):
37
+
38
+ All fields optional. **Prefer intent (`needs`/`build`/`seed`/`serve`) over explicit `steps`** —
39
+ the conventions above turn intent into commands.
40
+
41
+ ```yaml
42
+ needs: [mysql] # services this repo requires; provisioned in Docker, env wired to them
43
+ needsEnv: [WP_HOME, DB_NAME] # env keys that must be present to run/serve
44
+ build: 'yarn build' # optional: the build command (run after deps)
45
+ seed: 'dep db:pull staging-oderland' # optional: how to load data; remote/destructive → confirm first
46
+ serve: # optional: how it runs (printed as a next-step, never auto-started)
47
+ via: herd # herd | valet | docker | command — for PHP envs, bring-your-own (don't install)
48
+ url: 'https://example.test'
49
+ start: 'yarn dev'
50
+ steps: # optional escape hatch: explicit ordered shell steps when intent isn't enough
51
+ - run: 'composer install'
52
+ node: 10 # nvm version for THIS step
53
+ remote: true # SSH → confirm first
54
+ destructive: true # overwrites data → confirm first
55
+ optional: true # failure is non-fatal, continue
27
56
  ```
28
57
 
29
- **Read the repo's own setup docs first they win.** Before applying the file-heuristics below, look for the repo's canonical setup instructions: `docs/setup.md`, `CLAUDE.md`, `README.md` (or follow `docs/SUMMARY.md` to the setup page). If present, **follow them as authoritative** — they capture nested or non-standard builds a root file-scan can't see. Use the heuristics below only to fill gaps, or when the repo ships no setup doc. Example: a WordPress/Bedrock repo has **no root `package.json`** — its JS/theme build lives under `web/app/themes/<theme>` and is only described in the docs, so a root-only scan wrongly reports "no JS". When the docs point at a nested build, scan subdirectories for the relevant `package.json` / build script and run it.
58
+ **Workspace`<workspace>/.haus-workflow/localdev.yml`** (the glue BETWEEN repos):
59
+
60
+ ```yaml
61
+ order: [repo-a, repo-b] # setup/startup order, by manifest id
62
+ links:
63
+ - { type: symlink, from: <repo-folder>, to: <repo-folder>/path/to/link }
64
+ - { type: composer-path, in: <repo>, dep: <sibling-repo> }
65
+ - { type: yarn-link, in: [<repo>, ...], dep: <sibling-package-repo> }
66
+ env:
67
+ - source: { repo: <repo>, provides: '<value>' }
68
+ sinks:
69
+ - { repo: <repo>, key: ENV_KEY }
70
+ ```
30
71
 
31
- Adjust per repo (gap-fill, or when no setup doc exists):
72
+ ### Run order
32
73
 
33
- 1. **Node version.** If `.nvmrc` (or `engines.node` in `package.json`) is present, select it with `nvm install` (reads `.nvmrc`, installs the version if missing, then switches to it). If the user uses `fnm` instead, `fnm use --install-if-missing`. If neither is available, tell the user the required version and continue on the current node.
34
- 2. **JS dependencies.** Enable the pinned package manager with `corepack enable`, then install based on what's present:
35
- - `yarn.lock` or `packageManager: "yarn@…"` `yarn install`
36
- - `pnpm-lock.yaml``pnpm install`
37
- - `package-lock.json``npm install`
38
- - no JS manifest skip
39
- 3. **PHP dependencies.** If `composer.json` is present and `composer` is installed `composer install`. If composer is missing, note it and skip.
40
- 4. **Env scaffold.** If `.env.example` exists and `.env` does not copy `.env.example` to `.env` (never overwrite an existing `.env`). Tell the user the real values still need filling.
74
+ 1. **Discover** `.haus-workflow/localdev.yml` in the workspace root and each repo.
75
+ 2. **Resolve order** from the workspace `order` (repos not listed run last, in manifest order). No workspace file → manifest order.
76
+ 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.
78
+ - **`build`**run it (honor any node version the repo's docs note).
79
+ - **`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).
81
+ - **`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
+ 4. **Links** (workspace-owned, performed generically — do NOT call a repo's own `setup-dev-mode.sh`, which is deprecated):
83
+ - `symlink` → `ln -s <from> <to>`; replace an existing symlink, but never clobber a real directory without confirmation.
84
+ - `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
+ - `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.
87
+ 6. **Report, then offer to start.** Give the per-repo summary, then **ask the user whether to start everything now.**
88
+ - **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
+ - **No** → just print the ordered start commands + follow-ups as next steps; start nothing.
41
90
 
42
- If a repo's setup fails, report the error and **continue to the next repo** don't abort the whole run.
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.
43
92
 
44
- ## Step 4 — Report
93
+ ## Step 5 — Report
45
94
 
46
- Summarise per repo: node version used, dependency install result, composer (if any), env seeded. Then list what's still manual:
95
+ Summarise per repo (node, deps, localdev steps, links, env). Then **ask whether to start everything now**:
47
96
 
48
- - Fill in each `.env` with real values (cross-repo values must match see the workspace's environment docs).
49
- - Start Docker services and dev servers in dependency order see the workspace's local-development docs.
97
+ - **Yes** → start the dev servers in workspace `order`, run any follow-ups, and **print the live URLs** so the user can open the running app.
98
+ - **No** print the ordered start commands + follow-ups instead, and start nothing.
50
99
 
51
- **Do not** start servers or run `docker compose up` / `yarn dev` — this command only prepares the repos.
100
+ Never start the app without that explicit yes.
@@ -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,21 +17,20 @@ 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
 
24
- | Task name (legacy aliases) | Command | Scope | What it does |
25
- | ----------------------------------------------------------------- | ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
26
- | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
- | `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
28
- | `project:cloneandsetup [name]` (`cloneandsetup`) | _Clone & setup procedure below_ | project | Run `project:clone`, then set up each repo locally (node version, deps, `.env`) |
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
- | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
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
- | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
34
- | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
24
+ | Task name (legacy aliases) | Command | Scope | What it does |
25
+ | ----------------------------------------------------------------- | ------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
+ | `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
28
+ | `project:cloneandsetup [name]` (`cloneandsetup`) | _Clone & setup procedure below_ | project | Run `project:clone`, then set up each repo for local dev deps, databases, cross-repo links, and env — via each repo's `.haus-workflow/localdev.yml` (+ the workspace's order/links/env) |
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
+ | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
31
+ | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
32
+ | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
33
+ | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
35
34
 
36
35
  ## Step 1 — Determine the task
37
36
 
@@ -48,12 +47,10 @@ 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
56
- (project:clone, then per-repo node version + dependency install + .env scaffold)
52
+ 5. [project] project:cloneandsetup [name] — clone repos, then set them up for local dev
53
+ (project:clone, then per-repo deps + databases + cross-repo links + env from localdev.yml)
57
54
  ```
58
55
 
59
56
  Map the user's selection to the command from the alias table, then continue to Step 2.
@@ -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.23.1",
3
+ "version": "0.24.1",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {