@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 +12 -0
- package/README.md +15 -32
- package/dist/cli.js +101 -149
- package/library/global/commands/haus-clone.md +3 -0
- package/library/global/commands/haus-cloneandsetup.md +45 -12
- package/library/global/commands/haus-setup.md +8 -15
- package/library/global/skills/haus-workflow/SKILL.md +4 -7
- package/package.json +1 -1
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
|
-
`
|
|
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
|
|
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 #
|
|
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
|
-
##
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 {
|
|
3906
|
-
const scanResult = await scanProject(root
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("--
|
|
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 —
|
|
9
|
+
## Step 2 — Prerequisite gate (one consolidated check)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
**
|
|
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
|
|
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
|
-
-
|
|
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:
|
|
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`** →
|
|
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`** →
|
|
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:**
|
|
96
|
+
5. **Env (deterministic):** write each repo's `.env` from known values — no 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
|
|
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
|
-
|
|
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. **
|
|
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
|
-
|
|
26
|
-
`.claude/skills/writing-documentation/SKILL.md`, which step
|
|
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
|
|
27
|
+
step 6 — setup still finishes correctly with the basics from step 2.
|
|
35
28
|
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
so this just adds the newly-matched helpers from step
|
|
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
|
-
|
|
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`, `
|
|
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. [
|
|
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
|
-
|
|
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,
|
|
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`)
|