@haus-tech/haus-workflow 0.32.1 → 1.1.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 +160 -0
- package/dist/cli.js +233 -59
- package/library/catalog/manifest.json +3 -3
- package/library/global/settings-fragments/hooks.json +7 -0
- package/library/global/skills/haus-workflow/SKILL.md +111 -42
- package/library/global/{commands/haus-clone.md → skills/haus-workflow/references/clone.md} +2 -4
- package/library/global/{commands/haus-cloneandsetup.md → skills/haus-workflow/references/cloneandsetup.md} +2 -4
- package/library/global/{commands/haus-setup.md → skills/haus-workflow/references/init.md} +9 -4
- package/package.json +1 -1
- package/library/global/commands/haus-doctor.md +0 -14
- package/library/global/commands/haus-fix.md +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,165 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0](https://github.com/WeAreHausTech/haus-workflow/compare/v1.0.0...v1.1.0) (2026-07-02)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **decisions:** add local pre-PR ADR gate (decisions guard hook) ([#163](https://github.com/WeAreHausTech/haus-workflow/issues/163)) ([2f2d8fb](https://github.com/WeAreHausTech/haus-workflow/commit/2f2d8fb0840389a9b4d1631b686da996064fc8e6))
|
|
8
|
+
|
|
9
|
+
## [1.0.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.32.1...v1.0.0) (2026-07-02)
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
- **workflow:** the standalone /haus-setup, /haus-clone,
|
|
14
|
+
/haus-cloneandsetup, /haus-doctor, and /haus-fix slash commands no longer
|
|
15
|
+
exist. Use /haus-workflow <task> instead (e.g. /haus-workflow project:init,
|
|
16
|
+
/haus-workflow project:doctor). Existing global installs self-clean the
|
|
17
|
+
old files on the next `haus update`/`haus install` via the installer's
|
|
18
|
+
existing orphan-pruning.
|
|
19
|
+
|
|
20
|
+
- docs(decisions): add ADR-0011 for the haus-workflow command consolidation
|
|
21
|
+
|
|
22
|
+
Satisfies the decisions-gate CI check for the breaking-change command
|
|
23
|
+
removal + skill-install mechanism change in the prior commit.
|
|
24
|
+
|
|
25
|
+
- fix(workflow): clarify the Step 1 menu is two sequential AskUserQuestion calls
|
|
26
|
+
|
|
27
|
+
Copilot review flagged ambiguous wording ("split into two grouped
|
|
28
|
+
questions in one call") that could read as cramming both question
|
|
29
|
+
blocks into a single AskUserQuestion invocation. AskUserQuestion is
|
|
30
|
+
not guaranteed multi-question across every environment this skill
|
|
31
|
+
runs in, so make the instruction unambiguous: ask Question 1, read
|
|
32
|
+
the answer, then ask Question 2 as a second, separate call.
|
|
33
|
+
|
|
34
|
+
- feat(workflow): upgrade project:refresh to a full non-destructive sync
|
|
35
|
+
|
|
36
|
+
Previously project:refresh just ran `haus apply --write` (reapply from
|
|
37
|
+
the already-computed recommendation, no package/catalog update, no
|
|
38
|
+
rescan, no docs refresh). Upgrade it to: run `haus update` first (npm
|
|
39
|
+
package + catalog + global install), then re-run the project:init
|
|
40
|
+
pipeline in place (rescan, redo the deep docs read, re-recommend,
|
|
41
|
+
apply) — nothing removed first, contrasting with project:reinit which
|
|
42
|
+
wipes everything before re-running the same pipeline.
|
|
43
|
+
|
|
44
|
+
Moves the root-CLAUDE.md-imports check from a now-orphaned SKILL.md
|
|
45
|
+
section into references/init.md's own apply step, since project:init,
|
|
46
|
+
project:reinit, and project:refresh all funnel through it.
|
|
47
|
+
|
|
48
|
+
- feat(workflow): full project:refresh sync + SessionStart staleness hook
|
|
49
|
+
|
|
50
|
+
project:refresh now runs `haus update` (npm package + catalog) first,
|
|
51
|
+
then re-runs the project:init pipeline in place (rescan, redo docs,
|
|
52
|
+
re-recommend, apply) instead of just `haus apply --write`. The `update`
|
|
53
|
+
task's own post-run steps cascade into the same pipeline when run
|
|
54
|
+
inside an already-set-up project, so its "also refreshes this project"
|
|
55
|
+
scope note is now literally true.
|
|
56
|
+
|
|
57
|
+
Adds a new SessionStart hook (`haus.update-check`, in both
|
|
58
|
+
PROJECT_HOOK_FRAGMENTS and CANONICAL_HOOKS) running `haus update
|
|
59
|
+
--from-hook`: silent when the project is up to date, emits a
|
|
60
|
+
SessionStart hookSpecificOutput note nudging `/haus-workflow
|
|
61
|
+
project:refresh` when the npm package, catalog, or lockfile has
|
|
62
|
+
drifted. Never blocks session start on a network failure.
|
|
63
|
+
|
|
64
|
+
Fixes found by adversarial review before landing:
|
|
65
|
+
|
|
66
|
+
- fetchNpmVersionStatus had no test-mode gate, so the new --from-hook
|
|
67
|
+
tests hit the real npm registry; added HAUS_SKIP_NPM_CHECK (mirrors
|
|
68
|
+
HAUS_CATALOG_REMOTE_BASE's test-only-when-explicit pattern) rather
|
|
69
|
+
than gating on bare HAUS_TEST_MODE, which the whole test run already
|
|
70
|
+
sets and would have broken tests/npm-version.test.js's fetch mocks.
|
|
71
|
+
- checkLock() now runs before the npm/catalog network calls in the
|
|
72
|
+
hook check, so a non-haus directory costs nothing.
|
|
73
|
+
- matcher parity between the two hardcoded hook-fragment lists.
|
|
74
|
+
- project:refresh's step 1 now explicitly inherits the "After `haus
|
|
75
|
+
update`" reactions (version bump, WORKFLOW.md nudge, catalog
|
|
76
|
+
summary), which it was silently skipping despite running the same
|
|
77
|
+
command.
|
|
78
|
+
- runbook entry: doctor --hooks going red right after an upgrade,
|
|
79
|
+
before the next apply, is expected (same as any new required rule)
|
|
80
|
+
— documented with the fix (re-apply once).
|
|
81
|
+
|
|
82
|
+
yarn verify: 627/627 tests green.
|
|
83
|
+
|
|
84
|
+
- fix(workflow): drop expensive lock-hash check from the SessionStart hook, add help to the menu
|
|
85
|
+
|
|
86
|
+
Copilot review, two fixes:
|
|
87
|
+
|
|
88
|
+
- runFromHookCheck() called checkLock(), which hashes the content of
|
|
89
|
+
every installed managed path on every single SessionStart. Replaced
|
|
90
|
+
with a new readLockSummary() (count + catalogRef only, no hashing) —
|
|
91
|
+
the hook only ever needed those two cheap facts. Drops the
|
|
92
|
+
lock-drift reason from the hook's output entirely; doctor/project:fix
|
|
93
|
+
remain the tools for catching local file tamper, which is a
|
|
94
|
+
different concern than "is this project behind the installed
|
|
95
|
+
package/catalog" (what this hook actually checks).
|
|
96
|
+
- The Step 1 no-arg menu didn't offer `help`, even though it's a real
|
|
97
|
+
task in the alias table. Added as Question 2 option 4 (still within
|
|
98
|
+
the 4-option cap).
|
|
99
|
+
|
|
100
|
+
Test changes: replaced the lock-drift `--from-hook` test (no longer
|
|
101
|
+
applicable — the hook no longer looks at hashes) with a "healthy
|
|
102
|
+
project with a real lock stays silent" integration test, plus a new
|
|
103
|
+
direct unit test for the "npm behind" JSON-emission path (mocked
|
|
104
|
+
fetch, same style as tests/npm-version.test.js) — there's no way to
|
|
105
|
+
force fetchLatestCatalogTag/fetchNpmVersionStatus to report "behind"
|
|
106
|
+
via env vars alone, so this exercises the actual behind branch and its
|
|
107
|
+
JSON shape deterministically instead of only ever testing silence.
|
|
108
|
+
|
|
109
|
+
yarn verify: 628/628 tests green.
|
|
110
|
+
|
|
111
|
+
- fix(test): use exact hostname match instead of substring for npm registry mock
|
|
112
|
+
|
|
113
|
+
CodeQL flagged String(url).includes('registry.npmjs.org') as an
|
|
114
|
+
incomplete URL substring sanitization (a host like
|
|
115
|
+
evil.com/registry.npmjs.org or registry.npmjs.org.evil.com would also
|
|
116
|
+
match). Not exploitable here (url comes from our own hardcoded fetch
|
|
117
|
+
call, not untrusted input), but the check should still be precise —
|
|
118
|
+
parse the URL and compare new URL(url).hostname exactly.
|
|
119
|
+
|
|
120
|
+
- fix(workflow): stop false-positive catalog-behind nudges, fix docs
|
|
121
|
+
|
|
122
|
+
Copilot review, four fixes:
|
|
123
|
+
|
|
124
|
+
- docs/runbook.md: a backtick code span was split across a line wrap
|
|
125
|
+
("`/haus-workflow\nproject:refresh`"), rendering broken. Rewrapped.
|
|
126
|
+
- docs/cli.md: --from-hook's description said it checks npm/catalog/
|
|
127
|
+
lock; it never inspected lock content (no hashing) and only reads
|
|
128
|
+
the lock to confirm this is a haus project and get its catalogRef.
|
|
129
|
+
Corrected the wording.
|
|
130
|
+
- readLockSummary() only ever read catalogRef from lock item 0. An
|
|
131
|
+
older/mixed lock could have it missing there while later items carry
|
|
132
|
+
it. Now searches all items for the first defined catalogRef.
|
|
133
|
+
- runFromHookCheck() fell back to 'main' when catalogRef was absent,
|
|
134
|
+
which then almost always compared "behind" against any real release
|
|
135
|
+
tag — a false-positive nudge, and a wrong claim ("installed from
|
|
136
|
+
main") when the ref was actually unknown. Now skips the catalog
|
|
137
|
+
comparison entirely when catalogRef is null.
|
|
138
|
+
|
|
139
|
+
New tests: readLockSummary's cross-item catalogRef fallback (and that
|
|
140
|
+
it never hashes file content, unlike checkLock), plus a regression
|
|
141
|
+
test proving no false "behind" nudge fires when catalogRef is missing
|
|
142
|
+
even if the catalog has newer tags (mocked GitHub tags API).
|
|
143
|
+
|
|
144
|
+
yarn verify: 633/633 tests green.
|
|
145
|
+
|
|
146
|
+
- fix(docs): avoid a prettier-reflowed broken inline code span in cli.md
|
|
147
|
+
|
|
148
|
+
The previous commit's own pre-commit prettier pass reflowed the
|
|
149
|
+
--from-hook bullet and split a `/haus-workflow project:refresh`
|
|
150
|
+
code span across the wrap (the same bug class Copilot flagged in
|
|
151
|
+
runbook.md moments earlier) — verified by re-running prettier on
|
|
152
|
+
docs/cli.md, docs/runbook.md, and SKILL.md directly; all three are
|
|
153
|
+
now stable ("unchanged") under formatting.
|
|
154
|
+
|
|
155
|
+
### Features
|
|
156
|
+
|
|
157
|
+
- **workflow:** consolidate haus-\* commands into /haus-workflow skill ([#161](https://github.com/WeAreHausTech/haus-workflow/issues/161)) ([e5c34f7](https://github.com/WeAreHausTech/haus-workflow/commit/e5c34f73f723520041d54523f6cf384d890d6c99))
|
|
158
|
+
|
|
159
|
+
### Bug Fixes
|
|
160
|
+
|
|
161
|
+
- **apply:** scope llms.txt cache to items actually installed ([#162](https://github.com/WeAreHausTech/haus-workflow/issues/162)) ([f331f08](https://github.com/WeAreHausTech/haus-workflow/commit/f331f08a5c2e58bff2d8beda062f30515661685e))
|
|
162
|
+
|
|
3
163
|
## [0.32.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.32.0...v0.32.1) (2026-06-30)
|
|
4
164
|
|
|
5
165
|
### Bug Fixes
|
package/dist/cli.js
CHANGED
|
@@ -1538,11 +1538,11 @@ function reconcileManagedRules(existing, prevTracked, newRules) {
|
|
|
1538
1538
|
function mergeDenyRules(settings, rules) {
|
|
1539
1539
|
const existingDeny = settings.permissions?.deny ?? [];
|
|
1540
1540
|
const trackedDeny = settings._haus?.denyRules ?? [];
|
|
1541
|
-
const { rules:
|
|
1541
|
+
const { rules: deny, tracked, added } = reconcileManagedRules(existingDeny, trackedDeny, rules);
|
|
1542
1542
|
const updated = { ...settings };
|
|
1543
1543
|
updated.permissions = {
|
|
1544
1544
|
...settings.permissions ?? {},
|
|
1545
|
-
deny
|
|
1545
|
+
deny
|
|
1546
1546
|
};
|
|
1547
1547
|
updated._haus = {
|
|
1548
1548
|
hooks: settings._haus?.hooks ?? [],
|
|
@@ -1805,12 +1805,26 @@ var PROJECT_HOOK_FRAGMENTS = [
|
|
|
1805
1805
|
matcher: "Bash",
|
|
1806
1806
|
command: "haus guard bash --from-hook"
|
|
1807
1807
|
},
|
|
1808
|
+
{
|
|
1809
|
+
id: "haus.decisions-guard",
|
|
1810
|
+
gate: "keep",
|
|
1811
|
+
event: "PreToolUse",
|
|
1812
|
+
matcher: "Bash",
|
|
1813
|
+
command: "haus decisions guard --from-hook"
|
|
1814
|
+
},
|
|
1808
1815
|
{
|
|
1809
1816
|
id: "haus.decisions-suggest",
|
|
1810
1817
|
gate: "keep",
|
|
1811
1818
|
event: "Stop",
|
|
1812
1819
|
matcher: "*",
|
|
1813
1820
|
command: "haus decisions suggest --from-hook"
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
id: "haus.update-check",
|
|
1824
|
+
gate: "keep",
|
|
1825
|
+
event: "SessionStart",
|
|
1826
|
+
matcher: "*",
|
|
1827
|
+
command: "haus update --from-hook"
|
|
1814
1828
|
}
|
|
1815
1829
|
];
|
|
1816
1830
|
async function readProjectSettings(root) {
|
|
@@ -1903,6 +1917,10 @@ var CANONICAL_HOOKS = {
|
|
|
1903
1917
|
{
|
|
1904
1918
|
matcher: "Bash",
|
|
1905
1919
|
hooks: [{ type: "command", command: "haus guard bash --from-hook" }]
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
matcher: "Bash",
|
|
1923
|
+
hooks: [{ type: "command", command: "haus decisions guard --from-hook" }]
|
|
1906
1924
|
}
|
|
1907
1925
|
],
|
|
1908
1926
|
Stop: [
|
|
@@ -1910,6 +1928,12 @@ var CANONICAL_HOOKS = {
|
|
|
1910
1928
|
matcher: "*",
|
|
1911
1929
|
hooks: [{ type: "command", command: "haus decisions suggest --from-hook" }]
|
|
1912
1930
|
}
|
|
1931
|
+
],
|
|
1932
|
+
SessionStart: [
|
|
1933
|
+
{
|
|
1934
|
+
matcher: "*",
|
|
1935
|
+
hooks: [{ type: "command", command: "haus update --from-hook" }]
|
|
1936
|
+
}
|
|
1913
1937
|
]
|
|
1914
1938
|
}
|
|
1915
1939
|
};
|
|
@@ -2414,11 +2438,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2414
2438
|
};
|
|
2415
2439
|
const pkgRoot = packageRoot();
|
|
2416
2440
|
const hausVersion2 = (await readJson(path14.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
2417
|
-
const coreFiles = [
|
|
2418
|
-
claudePath(root, "settings.json"),
|
|
2419
|
-
claudePath(root, "rules", "haus.md"),
|
|
2420
|
-
claudePath(root, "commands", "haus-doctor.md")
|
|
2421
|
-
];
|
|
2441
|
+
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
2422
2442
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
2423
2443
|
const decisionsSeedPath = await writeDecisionsSeed(root, dryRun);
|
|
2424
2444
|
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force);
|
|
@@ -2441,12 +2461,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2441
2461
|
await applyProjectSettingsMerge(root);
|
|
2442
2462
|
await assertPostApplySettingsHausContract(root);
|
|
2443
2463
|
}
|
|
2444
|
-
await writeManagedText(
|
|
2445
|
-
root,
|
|
2446
|
-
claudePath(root, "commands", "haus-doctor.md"),
|
|
2447
|
-
"Run `haus doctor`.",
|
|
2448
|
-
dryRun
|
|
2449
|
-
);
|
|
2450
2464
|
const legacyReviewPath = claudePath(root, "commands", "haus-review.md");
|
|
2451
2465
|
if (await fs14.pathExists(legacyReviewPath)) {
|
|
2452
2466
|
const content2 = await fs14.readFile(legacyReviewPath, "utf8");
|
|
@@ -2461,6 +2475,20 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2461
2475
|
}
|
|
2462
2476
|
}
|
|
2463
2477
|
}
|
|
2478
|
+
const legacyDoctorPath = claudePath(root, "commands", "haus-doctor.md");
|
|
2479
|
+
if (await fs14.pathExists(legacyDoctorPath)) {
|
|
2480
|
+
const content2 = await fs14.readFile(legacyDoctorPath, "utf8");
|
|
2481
|
+
const stub = "Run `haus doctor`.";
|
|
2482
|
+
if (content2 === stub || content2 === `${stub}
|
|
2483
|
+
` || content2 === `${stub}\r
|
|
2484
|
+
`) {
|
|
2485
|
+
if (dryRun) {
|
|
2486
|
+
say(`[dry-run] would remove stale ${displayPath(root, legacyDoctorPath)}`);
|
|
2487
|
+
} else {
|
|
2488
|
+
await fs14.remove(legacyDoctorPath);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2464
2492
|
await writeManagedText(
|
|
2465
2493
|
root,
|
|
2466
2494
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -2479,7 +2507,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2479
2507
|
"results in plain language \u2014 never make them use a terminal or read JSON.",
|
|
2480
2508
|
"- Set up / configure / fix / check \u2192 `haus setup-project`, `haus apply --write`, `haus doctor`",
|
|
2481
2509
|
"- Update package + catalog \u2192 `haus update`",
|
|
2482
|
-
"-
|
|
2510
|
+
"- `/haus-workflow <task>` does the same conversationally (e.g. `init`, `fix`, `doctor`, `reinit`).",
|
|
2483
2511
|
""
|
|
2484
2512
|
].join("\n"),
|
|
2485
2513
|
dryRun
|
|
@@ -2781,8 +2809,25 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
2781
2809
|
return { result: "failed" };
|
|
2782
2810
|
}
|
|
2783
2811
|
}
|
|
2812
|
+
function collectLlmsTxtUrls(items) {
|
|
2813
|
+
return [...new Set(items.flatMap((item) => (item.references ?? []).filter(isLlmsTxtUrl)))];
|
|
2814
|
+
}
|
|
2815
|
+
async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
2816
|
+
const meta = await readCacheMeta(cacheDir);
|
|
2817
|
+
let removed = 0;
|
|
2818
|
+
for (const [url, entry] of Object.entries(meta)) {
|
|
2819
|
+
if (keepUrls.has(url)) continue;
|
|
2820
|
+
delete meta[url];
|
|
2821
|
+
removed++;
|
|
2822
|
+
const safeFile = typeof entry.file === "string" && entry.file === path16.basename(entry.file);
|
|
2823
|
+
if (safeFile) await fs16.remove(path16.join(cacheDir, entry.file)).catch(() => {
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
2827
|
+
return removed;
|
|
2828
|
+
}
|
|
2784
2829
|
async function fetchRefsForItems(items, cacheDir) {
|
|
2785
|
-
const urls =
|
|
2830
|
+
const urls = collectLlmsTxtUrls(items);
|
|
2786
2831
|
const summary = {
|
|
2787
2832
|
fetched: 0,
|
|
2788
2833
|
unchanged: 0,
|
|
@@ -2917,14 +2962,22 @@ async function runApply(options) {
|
|
|
2917
2962
|
if (!isDryRun) {
|
|
2918
2963
|
try {
|
|
2919
2964
|
const cacheDir = hausPath(root, "llms-cache");
|
|
2965
|
+
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
2966
|
+
const installedIds = new Set(lock.map((entry) => entry.id));
|
|
2920
2967
|
const allItems = await loadCatalog(root);
|
|
2921
|
-
const
|
|
2968
|
+
const installedItems = allItems.filter((item) => installedIds.has(item.id));
|
|
2969
|
+
const summary = await fetchRefsForItems(installedItems, cacheDir);
|
|
2922
2970
|
if (summary.fetched > 0)
|
|
2923
2971
|
log(`Fetched ${summary.fetched} llms.txt reference(s) to ${cacheDir}`);
|
|
2924
2972
|
if (summary.failed > 0)
|
|
2925
2973
|
warn(
|
|
2926
2974
|
`Failed to fetch ${summary.failed} llms.txt reference(s): ${summary.failedUrls.join(", ")}`
|
|
2927
2975
|
);
|
|
2976
|
+
const pruned = await pruneOrphanedRefs(cacheDir, new Set(collectLlmsTxtUrls(installedItems)));
|
|
2977
|
+
if (pruned > 0)
|
|
2978
|
+
log(
|
|
2979
|
+
`Removed ${pruned} orphaned llms.txt cache entr${pruned === 1 ? "y" : "ies"} from ${cacheDir}`
|
|
2980
|
+
);
|
|
2928
2981
|
} catch (err) {
|
|
2929
2982
|
warn(
|
|
2930
2983
|
`Could not fetch llms.txt references: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -3011,8 +3064,59 @@ async function runClone(url, opts = {}) {
|
|
|
3011
3064
|
log(`\u2713 cloned ${url} \u2192 ${target}`);
|
|
3012
3065
|
}
|
|
3013
3066
|
|
|
3014
|
-
// src/
|
|
3015
|
-
import
|
|
3067
|
+
// src/claude/hook-io.ts
|
|
3068
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3069
|
+
|
|
3070
|
+
// src/utils/audit-checks.ts
|
|
3071
|
+
function isRecord(v) {
|
|
3072
|
+
return typeof v === "object" && v !== null;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// src/claude/hook-io.ts
|
|
3076
|
+
function stdin() {
|
|
3077
|
+
try {
|
|
3078
|
+
if (process.stdin.isTTY) return "";
|
|
3079
|
+
return readFileSync2(0, "utf8");
|
|
3080
|
+
} catch {
|
|
3081
|
+
return "";
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function readHookPayload() {
|
|
3085
|
+
const raw = stdin();
|
|
3086
|
+
if (!raw) return { payload: {}, malformed: false };
|
|
3087
|
+
try {
|
|
3088
|
+
const parsed = JSON.parse(raw);
|
|
3089
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
3090
|
+
} catch {
|
|
3091
|
+
return { payload: {}, malformed: true };
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
function emitPreToolUseDeny(reason) {
|
|
3095
|
+
log(
|
|
3096
|
+
JSON.stringify({
|
|
3097
|
+
hookSpecificOutput: {
|
|
3098
|
+
hookEventName: "PreToolUse",
|
|
3099
|
+
permissionDecision: "deny",
|
|
3100
|
+
permissionDecisionReason: reason
|
|
3101
|
+
}
|
|
3102
|
+
})
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/decisions/base-ref.ts
|
|
3107
|
+
init_exec();
|
|
3108
|
+
async function resolveBaseRef(root) {
|
|
3109
|
+
const symbolic = await runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: root });
|
|
3110
|
+
if (symbolic.exitCode === 0) {
|
|
3111
|
+
const ref = symbolic.stdout.trim().replace(/^refs\/remotes\//, "");
|
|
3112
|
+
if (ref) return ref;
|
|
3113
|
+
}
|
|
3114
|
+
for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
|
|
3115
|
+
const check = await runGit(["rev-parse", "--verify", "--quiet", candidate], { cwd: root });
|
|
3116
|
+
if (check.exitCode === 0) return candidate;
|
|
3117
|
+
}
|
|
3118
|
+
return void 0;
|
|
3119
|
+
}
|
|
3016
3120
|
|
|
3017
3121
|
// src/decisions/check.ts
|
|
3018
3122
|
import path21 from "node:path";
|
|
@@ -3261,6 +3365,46 @@ async function listDecisionNumbers(decisionsDir) {
|
|
|
3261
3365
|
return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
|
|
3262
3366
|
}
|
|
3263
3367
|
|
|
3368
|
+
// src/commands/decisions-guard.ts
|
|
3369
|
+
init_exec();
|
|
3370
|
+
var PR_CREATE_PATTERN = /\bgh\s+pr\s+create\b/;
|
|
3371
|
+
function readPayload(options) {
|
|
3372
|
+
if (options.stdinPayload != null) {
|
|
3373
|
+
try {
|
|
3374
|
+
const parsed = JSON.parse(options.stdinPayload);
|
|
3375
|
+
return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
|
|
3376
|
+
} catch {
|
|
3377
|
+
return { payload: {}, malformed: true };
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
return readHookPayload();
|
|
3381
|
+
}
|
|
3382
|
+
async function runDecisionsGuard(options = {}) {
|
|
3383
|
+
const { payload, malformed } = readPayload(options);
|
|
3384
|
+
if (malformed) {
|
|
3385
|
+
emitPreToolUseDeny("Malformed hook payload");
|
|
3386
|
+
return;
|
|
3387
|
+
}
|
|
3388
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
3389
|
+
const command = String(toolInput.command ?? "");
|
|
3390
|
+
if (!PR_CREATE_PATTERN.test(command)) return;
|
|
3391
|
+
const root = process.cwd();
|
|
3392
|
+
const baseRef = await resolveBaseRef(root);
|
|
3393
|
+
if (!baseRef) return;
|
|
3394
|
+
const range = `${baseRef}...HEAD`;
|
|
3395
|
+
const logRange = `${baseRef}..HEAD`;
|
|
3396
|
+
const commitMessages = (await runGit(["log", "--format=%B", logRange], { cwd: root })).stdout;
|
|
3397
|
+
const result = await runDecisionsCheck(root, { range, commitMessages });
|
|
3398
|
+
if (result.triggered && !result.satisfied) {
|
|
3399
|
+
emitPreToolUseDeny(
|
|
3400
|
+
`I didn't run that \u2014 this change needs an ADR before opening a PR (${result.reasons.join("; ")}). Run haus decisions suggest, add the decision under docs/decisions/, then retry.`
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
// src/commands/decisions.ts
|
|
3406
|
+
import path22 from "node:path";
|
|
3407
|
+
|
|
3264
3408
|
// src/decisions/next-number.ts
|
|
3265
3409
|
async function nextDecisionNumber(root) {
|
|
3266
3410
|
const decisionsDir = await resolveDecisionsDir(root);
|
|
@@ -3469,11 +3613,6 @@ async function auditDecisionsLayout(root) {
|
|
|
3469
3613
|
// src/scanner/scan-project.ts
|
|
3470
3614
|
import path27 from "node:path";
|
|
3471
3615
|
|
|
3472
|
-
// src/utils/audit-checks.ts
|
|
3473
|
-
function isRecord(v) {
|
|
3474
|
-
return typeof v === "object" && v !== null;
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
3616
|
// src/utils/versions.ts
|
|
3478
3617
|
import semver from "semver";
|
|
3479
3618
|
function normalizeVersion(version) {
|
|
@@ -3927,7 +4066,13 @@ async function readContextOrScan(root) {
|
|
|
3927
4066
|
|
|
3928
4067
|
// src/update/npm-version.ts
|
|
3929
4068
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
4069
|
+
function isTestMode2() {
|
|
4070
|
+
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
4071
|
+
}
|
|
3930
4072
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
4073
|
+
if (isTestMode2() && process.env["HAUS_SKIP_NPM_CHECK"]) {
|
|
4074
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
4075
|
+
}
|
|
3931
4076
|
try {
|
|
3932
4077
|
const res = await fetch(
|
|
3933
4078
|
`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`,
|
|
@@ -4363,9 +4508,6 @@ async function runFetchRefs(options) {
|
|
|
4363
4508
|
}
|
|
4364
4509
|
}
|
|
4365
4510
|
|
|
4366
|
-
// src/commands/guard.ts
|
|
4367
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4368
|
-
|
|
4369
4511
|
// src/security/guard-bash.ts
|
|
4370
4512
|
function matchesDenyToken(command, denyPhrase) {
|
|
4371
4513
|
if (denyPhrase === "sudo") {
|
|
@@ -4390,50 +4532,25 @@ function guardFileAccess(candidate) {
|
|
|
4390
4532
|
}
|
|
4391
4533
|
|
|
4392
4534
|
// src/commands/guard.ts
|
|
4393
|
-
function stdin() {
|
|
4394
|
-
try {
|
|
4395
|
-
if (process.stdin.isTTY) return "";
|
|
4396
|
-
return readFileSync2(0, "utf8");
|
|
4397
|
-
} catch {
|
|
4398
|
-
return "";
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
function deny(reason) {
|
|
4402
|
-
log(
|
|
4403
|
-
JSON.stringify({
|
|
4404
|
-
hookSpecificOutput: {
|
|
4405
|
-
hookEventName: "PreToolUse",
|
|
4406
|
-
permissionDecision: "deny",
|
|
4407
|
-
permissionDecisionReason: reason
|
|
4408
|
-
}
|
|
4409
|
-
})
|
|
4410
|
-
);
|
|
4411
|
-
}
|
|
4412
4535
|
async function runGuard(kind, _options) {
|
|
4413
|
-
const
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
const parsed = JSON.parse(raw);
|
|
4418
|
-
if (isRecord(parsed)) payload = parsed;
|
|
4419
|
-
} catch {
|
|
4420
|
-
deny("Malformed hook payload");
|
|
4421
|
-
return;
|
|
4422
|
-
}
|
|
4536
|
+
const { payload, malformed } = readHookPayload();
|
|
4537
|
+
if (malformed) {
|
|
4538
|
+
emitPreToolUseDeny("Malformed hook payload");
|
|
4539
|
+
return;
|
|
4423
4540
|
}
|
|
4424
4541
|
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
|
|
4425
4542
|
if (kind === "file-access") {
|
|
4426
4543
|
const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
|
|
4427
4544
|
const reason2 = guardFileAccess(candidate);
|
|
4428
4545
|
if (reason2) {
|
|
4429
|
-
|
|
4546
|
+
emitPreToolUseDeny(reason2);
|
|
4430
4547
|
}
|
|
4431
4548
|
return;
|
|
4432
4549
|
}
|
|
4433
4550
|
const command = String(toolInput.command ?? "");
|
|
4434
4551
|
const reason = guardBash(command);
|
|
4435
4552
|
if (reason) {
|
|
4436
|
-
|
|
4553
|
+
emitPreToolUseDeny(reason);
|
|
4437
4554
|
}
|
|
4438
4555
|
}
|
|
4439
4556
|
|
|
@@ -5081,6 +5198,22 @@ function sourceVersion() {
|
|
|
5081
5198
|
function globalSrcDir() {
|
|
5082
5199
|
return path30.join(packageRoot(), "library", "global");
|
|
5083
5200
|
}
|
|
5201
|
+
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5202
|
+
const skillDir = path30.join(skillsDir, skillName);
|
|
5203
|
+
const entries = [];
|
|
5204
|
+
for (const rel of fs24.readdirSync(skillDir, { recursive: true })) {
|
|
5205
|
+
if (rel === "SKILL.md") continue;
|
|
5206
|
+
const abs = path30.join(skillDir, rel);
|
|
5207
|
+
if (!fs24.statSync(abs).isFile()) continue;
|
|
5208
|
+
const posixRel = rel.split(path30.sep).join("/");
|
|
5209
|
+
entries.push({
|
|
5210
|
+
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5211
|
+
srcRelPath: path30.join("library", "global", "skills", skillName, rel),
|
|
5212
|
+
destPath: path30.join(claudeDir, "skills", skillName, rel)
|
|
5213
|
+
});
|
|
5214
|
+
}
|
|
5215
|
+
return entries;
|
|
5216
|
+
}
|
|
5084
5217
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
5085
5218
|
const entries = [];
|
|
5086
5219
|
const skillsDir = path30.join(srcDir, "skills");
|
|
@@ -5093,6 +5226,7 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
5093
5226
|
srcRelPath: path30.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
5094
5227
|
destPath: path30.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
5095
5228
|
});
|
|
5229
|
+
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
5096
5230
|
}
|
|
5097
5231
|
}
|
|
5098
5232
|
}
|
|
@@ -5534,7 +5668,7 @@ import path32 from "node:path";
|
|
|
5534
5668
|
import fs26 from "fs-extra";
|
|
5535
5669
|
|
|
5536
5670
|
// src/claude/managed-paths.ts
|
|
5537
|
-
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"
|
|
5671
|
+
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"];
|
|
5538
5672
|
var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"];
|
|
5539
5673
|
function coreManagedAbsolutePaths(root) {
|
|
5540
5674
|
const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
|
|
@@ -5778,6 +5912,11 @@ function summarizeLockDiff(before, after) {
|
|
|
5778
5912
|
// src/update/lockfile.ts
|
|
5779
5913
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
5780
5914
|
import path34 from "node:path";
|
|
5915
|
+
async function readLockSummary(root) {
|
|
5916
|
+
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5917
|
+
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
5918
|
+
return { count: lock.length, catalogRef };
|
|
5919
|
+
}
|
|
5781
5920
|
async function checkLock(root) {
|
|
5782
5921
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5783
5922
|
const hasValidVersions = lock.every(
|
|
@@ -5841,6 +5980,10 @@ async function hasLocalOverrides(root) {
|
|
|
5841
5980
|
var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
5842
5981
|
async function runUpdate(options) {
|
|
5843
5982
|
const root = process.cwd();
|
|
5983
|
+
if (options.fromHook) {
|
|
5984
|
+
await runFromHookCheck(root);
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5844
5987
|
if (options.check) {
|
|
5845
5988
|
const pkgJson2 = await readJson(path35.join(packageRoot(), "package.json"));
|
|
5846
5989
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
@@ -5906,6 +6049,36 @@ async function runUpdate(options) {
|
|
|
5906
6049
|
await refreshProjectFiles(root);
|
|
5907
6050
|
log("Update applied with backup in .haus-workflow/backups/. Run haus doctor.");
|
|
5908
6051
|
}
|
|
6052
|
+
async function runFromHookCheck(root) {
|
|
6053
|
+
try {
|
|
6054
|
+
const summary = await readLockSummary(root);
|
|
6055
|
+
if (summary.count === 0) return;
|
|
6056
|
+
const pkgJson = await readJson(path35.join(packageRoot(), "package.json"));
|
|
6057
|
+
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
6058
|
+
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
6059
|
+
fetchNpmVersionStatus(currentVersion),
|
|
6060
|
+
fetchLatestCatalogTag()
|
|
6061
|
+
]);
|
|
6062
|
+
const installedRef = summary.catalogRef;
|
|
6063
|
+
const catalogBehind = installedRef !== null && latestCatalogTag !== null && installedRef !== latestCatalogTag;
|
|
6064
|
+
const npmBehind = npmVersion.updateAvailable && npmVersion.latest !== null;
|
|
6065
|
+
if (!npmBehind && !catalogBehind) return;
|
|
6066
|
+
const reasons = [];
|
|
6067
|
+
if (npmBehind) reasons.push(`haus package ${currentVersion} \u2192 ${npmVersion.latest} available`);
|
|
6068
|
+
if (catalogBehind) {
|
|
6069
|
+
reasons.push(`catalog installed from ${installedRef}, latest is ${latestCatalogTag}`);
|
|
6070
|
+
}
|
|
6071
|
+
log(
|
|
6072
|
+
JSON.stringify({
|
|
6073
|
+
hookSpecificOutput: {
|
|
6074
|
+
hookEventName: "SessionStart",
|
|
6075
|
+
additionalContext: `This project's haus setup is behind: ${reasons.join("; ")}. Suggest running \`/haus-workflow project:refresh\` to bring it up to date.`
|
|
6076
|
+
}
|
|
6077
|
+
})
|
|
6078
|
+
);
|
|
6079
|
+
} catch {
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
5909
6082
|
async function refreshProjectFiles(root) {
|
|
5910
6083
|
log("Refreshing project .claude/ files...");
|
|
5911
6084
|
try {
|
|
@@ -6981,7 +7154,7 @@ program.command("clone").description("Clone a single git repository by URL").arg
|
|
|
6981
7154
|
program.command("refresh").action(runRefresh);
|
|
6982
7155
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
6983
7156
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
6984
|
-
program.command("update").option("--check").action(runUpdate);
|
|
7157
|
+
program.command("update").option("--check").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
6985
7158
|
program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
|
|
6986
7159
|
program.command("scaffold [ids...]").description(
|
|
6987
7160
|
"Copy catalog config items (ESLint, Prettier) into the project root. Pass item IDs to scaffold specific items, or omit to scaffold all available config items."
|
|
@@ -6997,6 +7170,7 @@ function registerDecisionsCommand(name) {
|
|
|
6997
7170
|
(opts) => runDecisions("suggest", void 0, opts)
|
|
6998
7171
|
);
|
|
6999
7172
|
cmd.command("next-number").action(() => runDecisions("next-number"));
|
|
7173
|
+
cmd.command("guard").option("--from-hook", "Emit JSON for Claude Code PreToolUse hook").action((opts) => runDecisionsGuard(opts));
|
|
7000
7174
|
cmd.command("validate").argument("[path]", "Decision markdown file").action((filePath) => runDecisions("validate", filePath));
|
|
7001
7175
|
}
|
|
7002
7176
|
registerDecisionsCommand("decisions");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.3.0",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "haus.agentic-workflow-standard",
|
|
@@ -1334,7 +1334,7 @@
|
|
|
1334
1334
|
"ecosystem": "general",
|
|
1335
1335
|
"installMode": "copy-selected",
|
|
1336
1336
|
"tokenEstimate": 120,
|
|
1337
|
-
"default":
|
|
1337
|
+
"default": true,
|
|
1338
1338
|
"reviewStatus": "approved",
|
|
1339
1339
|
"riskLevel": "low"
|
|
1340
1340
|
},
|
|
@@ -1353,7 +1353,7 @@
|
|
|
1353
1353
|
"ecosystem": "general",
|
|
1354
1354
|
"tokenEstimate": 650,
|
|
1355
1355
|
"tokenBudget": 900,
|
|
1356
|
-
"default":
|
|
1356
|
+
"default": true,
|
|
1357
1357
|
"reviewStatus": "approved",
|
|
1358
1358
|
"riskLevel": "low"
|
|
1359
1359
|
},
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
"matcher": "Bash",
|
|
16
16
|
"command": "haus guard bash --from-hook"
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
"id": "hook.decisions.guard",
|
|
20
|
+
"gate": "keep",
|
|
21
|
+
"event": "PreToolUse",
|
|
22
|
+
"matcher": "Bash",
|
|
23
|
+
"command": "haus decisions guard --from-hook"
|
|
24
|
+
},
|
|
18
25
|
{
|
|
19
26
|
"id": "hook.decisions.suggest",
|
|
20
27
|
"gate": "keep",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: haus-workflow
|
|
3
|
-
description: Haus all-in-one workflow skill. Handles project setup, update, catalog refresh, and CLAUDE.md regeneration. Invoke with a task name or without to get a menu.
|
|
3
|
+
description: Haus all-in-one workflow skill. Handles project setup, re-init, update, catalog refresh, health checks, and CLAUDE.md regeneration. Invoke with a task name or without to get a menu.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# haus-workflow
|
|
7
7
|
|
|
8
|
-
All-in-one entry point for the Haus AI workflow.
|
|
8
|
+
All-in-one entry point for the Haus AI workflow. Everything haus-related runs through
|
|
9
|
+
this skill — there are no separate `/haus-setup`, `/haus-clone`, `/haus-cloneandsetup`,
|
|
10
|
+
`/haus-doctor`, or `/haus-fix` commands.
|
|
9
11
|
|
|
10
12
|
## Invocation
|
|
11
13
|
|
|
@@ -17,43 +19,56 @@ All-in-one entry point for the Haus AI workflow.
|
|
|
17
19
|
|
|
18
20
|
Task names use an asymmetric scope convention. The **project:** namespace marks tasks that
|
|
19
21
|
act on **this repo** (`./.claude`, `./.haus-workflow`) — type `project:` to see them all.
|
|
20
|
-
The unprefixed verbs (`update`, `install`, `uninstall`) act on **this machine's
|
|
21
|
-
haus install** (`~/.claude`, npm)
|
|
22
|
-
The short legacy aliases still work but the names below are canonical.
|
|
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:
|
|
28
|
-
| `project:
|
|
29
|
-
| `project:
|
|
30
|
-
| `project:
|
|
31
|
-
| `project:
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
22
|
+
The unprefixed verbs (`update`, `install`, `uninstall`, `help`) act on **this machine's
|
|
23
|
+
haus install** (`~/.claude`, npm) or are purely informational — they don't act on this
|
|
24
|
+
repo's files. The short legacy aliases still work but the names below are canonical.
|
|
25
|
+
|
|
26
|
+
| Task name (legacy aliases) | Command | Scope | What it does |
|
|
27
|
+
| ----------------------------------------------------------------- | ------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
|
|
29
|
+
| `project:reinit` (`reinit`, `re-init`) | _Reinit procedure below_ | project | **Full re-setup.** Removes all haus-managed project files (backed up first, user content preserved), then re-runs `project:init` from scratch |
|
|
30
|
+
| `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 |
|
|
31
|
+
| `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) |
|
|
32
|
+
| `project:add-skills` (`add-skills`, `opt-in`) | _Add-skills procedure below_ | project | Add optional skills, agents, or config (ESLint/Prettier) to an already-set-up project without re-running full setup |
|
|
33
|
+
| `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | _Refresh procedure below_ | project | **Full non-destructive sync.** Updates the haus package + catalog first, then re-runs `project:init`'s pipeline in place — nothing is removed up front; new catalog items get added, changed ones updated, and anything removed upstream gets pruned by `apply`'s existing orphan-pruning |
|
|
34
|
+
| `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
|
|
35
|
+
| `project:fix` (`fix`) | _Fix procedure below_ | project | Diagnose and fix install drift: run doctor, apply the suggested fixes, confirm green |
|
|
36
|
+
| `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/`; if run inside an already-set-up project, fully refreshes it too (same pipeline as `project:refresh`) |
|
|
37
|
+
| `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
|
|
38
|
+
| `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
|
|
39
|
+
| `help` (`?`) | _Answer directly below_ | — | Explain what haus-workflow is, list available tasks, point to deeper docs — touches no files |
|
|
35
40
|
|
|
36
41
|
## Step 1 — Determine the task
|
|
37
42
|
|
|
38
43
|
**If a task argument was passed:** look it up in the alias table above. If no match, tell the user and list valid options. If matched, skip to Step 2.
|
|
39
44
|
|
|
40
|
-
**If no task was passed:**
|
|
45
|
+
**If no task was passed:** the menu has more options than one `AskUserQuestion` question can
|
|
46
|
+
hold (max 4 options per question). Ask two separate questions, one after another — first
|
|
47
|
+
Question 1, read the answer, then ask Question 2 (a second, sequential `AskUserQuestion`
|
|
48
|
+
call; do not try to cram both into a single question):
|
|
41
49
|
|
|
42
50
|
```
|
|
43
|
-
Question: "What would you like to do?"
|
|
51
|
+
Question 1: "What would you like to do?"
|
|
44
52
|
Options:
|
|
45
53
|
1. [project] project:init — add haus to an existing project for the first time
|
|
46
54
|
(AI skills + commands + workflow into a repo you already have, then a deep read to write the CLAUDE.md docs body + docs/)
|
|
47
|
-
2. [project] project:refresh —
|
|
48
|
-
(haus
|
|
49
|
-
3. [
|
|
50
|
-
(haus update — checks npm for new version, fetches catalog, refreshes ~/.claude/)
|
|
51
|
-
4. [project] project:clone [name] — clone repos
|
|
55
|
+
2. [project] project:refresh — full non-destructive sync with the latest haus
|
|
56
|
+
(updates the haus package + catalog, then re-runs setup in place — nothing removed first)
|
|
57
|
+
3. [project] project:clone [name] — clone repos
|
|
52
58
|
(no name: clone a workspace from repos.manifest.json; with a name: find & clone one repo by name from GitHub)
|
|
53
|
-
|
|
59
|
+
4. [project] project:cloneandsetup [name] — clone repos, then set them up for local dev
|
|
54
60
|
(project:clone, then per-repo deps + databases + cross-repo links + env from localdev.yml)
|
|
55
|
-
|
|
61
|
+
|
|
62
|
+
Question 2: "Anything else?"
|
|
63
|
+
Options:
|
|
64
|
+
1. [project] project:add-skills — add optional skills, agents, or config to this project
|
|
56
65
|
(offers opt-in helpers matching your stack that aren't installed yet — no full re-setup)
|
|
66
|
+
2. [project] project:reinit — full re-setup from scratch
|
|
67
|
+
(removes all haus-managed project files, backed up first, then re-runs project:init)
|
|
68
|
+
3. [global] update — update haus package + catalog + global files
|
|
69
|
+
(haus update — checks npm for new version, fetches catalog, refreshes ~/.claude/)
|
|
70
|
+
4. [—] help — explain what haus-workflow is and list all available tasks
|
|
71
|
+
(touches no files — points to docs/cli.md and docs/runbook.md for depth)
|
|
57
72
|
```
|
|
58
73
|
|
|
59
74
|
Map the user's selection to the command from the alias table, then continue to Step 2.
|
|
@@ -64,28 +79,69 @@ Run the mapped command via Bash. Quote the exact command you are running before
|
|
|
64
79
|
|
|
65
80
|
**Exception — `project:init` (`setup` / `init`):** this maps to a multi-step procedure, not a single command. Do not run a bare `haus init`. Skip to **Setup (`project:init`)** under Step 3 and follow it.
|
|
66
81
|
|
|
82
|
+
**Exception — `project:reinit` (`reinit` / `re-init`):** removes files before re-running setup. Skip to **Reinit (`project:reinit`)** under Step 3 and follow it.
|
|
83
|
+
|
|
67
84
|
**Exception — `project:clone` (`clone`):** this asks the user a question before running, so it is a short procedure too. Skip to **Clone (`project:clone`)** under Step 3 and follow it.
|
|
68
85
|
|
|
69
86
|
**Exception — `project:cloneandsetup` (`cloneandsetup`):** clone followed by a per-repo setup pass, with confirmations. Skip to **Clone & setup (`project:cloneandsetup`)** under Step 3 and follow it.
|
|
70
87
|
|
|
71
88
|
**Exception — `project:add-skills` (`add-skills` / `opt-in`):** a short scan → recommend → choose → apply procedure. Skip to **Add optional skills (`project:add-skills`)** under Step 3 and follow it.
|
|
72
89
|
|
|
90
|
+
**Exception — `project:refresh` (`apply` / `refresh` / `claude-md` / `regenerate`):** updates the haus package first, then re-runs the setup pipeline — not a single command. Skip to **Refresh (`project:refresh`)** under Step 3 and follow it.
|
|
91
|
+
|
|
92
|
+
**Exception — `project:fix` (`fix`):** diagnose-then-fix loop, not a single command. Skip to **Fix (`project:fix`)** under Step 3 and follow it.
|
|
93
|
+
|
|
94
|
+
**Exception — `help` (`?`):** no command runs at all. Skip Step 2 and Step 3 — answer directly from the task alias table above, plus `docs/cli.md` and `docs/runbook.md` for deeper detail on any specific task.
|
|
95
|
+
|
|
73
96
|
## Step 3 — Post-run steps
|
|
74
97
|
|
|
75
98
|
After the command completes, follow the relevant post-run steps below.
|
|
76
99
|
|
|
77
100
|
### Setup (`project:init`)
|
|
78
101
|
|
|
79
|
-
1. Open and follow
|
|
102
|
+
1. Open and follow `references/init.md` in this skill's directory. 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.
|
|
80
103
|
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.
|
|
81
104
|
|
|
105
|
+
### Reinit (`project:reinit`)
|
|
106
|
+
|
|
107
|
+
Full re-setup — removes everything haus manages in this project and starts over. Use when
|
|
108
|
+
the project's haus setup is badly out of sync and a plain `project:fix`/`project:refresh`
|
|
109
|
+
isn't enough, or the user explicitly wants a clean slate.
|
|
110
|
+
|
|
111
|
+
1. **Confirm first, always** — via `AskUserQuestion`. Explain plainly: this removes all
|
|
112
|
+
haus-managed files in this project (commands, skills, rules, workflow docs; `haus undo`
|
|
113
|
+
backs them up first and never touches user-owned `.claude/` content or `.haus-workflow`
|
|
114
|
+
docs you've edited), then re-runs setup from scratch. Offer **Cancel**.
|
|
115
|
+
2. On confirm, run `haus undo --yes` (already backs up + preserves user content — no need
|
|
116
|
+
to ask a second time).
|
|
117
|
+
3. Immediately follow **Setup (`project:init`)** above, end to end.
|
|
118
|
+
4. Report what was removed, backed up (path), and re-added, in plain language.
|
|
119
|
+
|
|
120
|
+
### Refresh (`project:refresh`)
|
|
121
|
+
|
|
122
|
+
Full non-destructive sync — nothing is removed up front (contrast with `project:reinit`,
|
|
123
|
+
which wipes first). Use for routine "bring this project up to date with the latest haus"
|
|
124
|
+
maintenance: new catalog items get added, changed ones updated, and anything removed
|
|
125
|
+
upstream gets pruned automatically by `apply`'s existing orphan-pruning.
|
|
126
|
+
|
|
127
|
+
1. Run `haus update`. This bumps the npm package (or reports one's available), syncs the
|
|
128
|
+
catalog cache, refreshes `~/.claude/`, and does a light re-apply of this project's
|
|
129
|
+
already-tracked files. Apply the same reactions as **After `haus update`** below
|
|
130
|
+
(version-bump command, `WORKFLOW.md` update nudge, catalog-changes summary).
|
|
131
|
+
2. Immediately follow **Setup (`project:init`)** above, end to end. Re-running it is safe
|
|
132
|
+
on an already-set-up project — it re-scans, re-applies, redoes the deep docs read (so
|
|
133
|
+
`CLAUDE.md`/`docs/` catch up with any code drift), and re-recommends against the
|
|
134
|
+
now-current catalog.
|
|
135
|
+
3. Report what changed in plain language — package/catalog version bumps, docs
|
|
136
|
+
refreshed, helpers added/updated/removed.
|
|
137
|
+
|
|
82
138
|
### Clone (`project:clone`)
|
|
83
139
|
|
|
84
|
-
1. Open and follow
|
|
140
|
+
1. Open and follow `references/clone.md` in this skill's directory. With a `name` argument it finds and clones one matching repo from GitHub; with no argument it clones a workspace's repos from `repos.manifest.json`. This task only clones — to also install dependencies, use `project:cloneandsetup`.
|
|
85
141
|
|
|
86
142
|
### Clone & setup (`project:cloneandsetup`)
|
|
87
143
|
|
|
88
|
-
1. Open and follow
|
|
144
|
+
1. Open and follow `references/cloneandsetup.md` in this skill's directory — it runs the full `project:clone` flow, then sets up each cloned repo locally: selects the node version (`nvm install` from `.nvmrc`), enables corepack, installs JS/PHP dependencies, and seeds `.env`, confirming before each phase. It does not start servers.
|
|
89
145
|
|
|
90
146
|
### Add optional skills (`project:add-skills`)
|
|
91
147
|
|
|
@@ -112,27 +168,40 @@ choices.
|
|
|
112
168
|
5. Confirm with the names added and their combined token estimate, e.g.
|
|
113
169
|
"Added 2 optional helpers: Code review workflow, TDD workflow (~3k tokens)."
|
|
114
170
|
|
|
115
|
-
###
|
|
116
|
-
|
|
117
|
-
Verify that the root `CLAUDE.md` imports all three haus files:
|
|
171
|
+
### Fix (`project:fix`)
|
|
118
172
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
1. Run `haus doctor` and read the verdict.
|
|
174
|
+
2. If the project is already healthy, say so in one line and stop.
|
|
175
|
+
3. Otherwise, for each item that needs attention, run the exact fix command the
|
|
176
|
+
doctor named (commonly `haus apply --write` or `haus apply --refill-config`).
|
|
177
|
+
4. Re-run `haus doctor` to confirm the verdict is now green.
|
|
178
|
+
5. Report what you changed in plain language — what was wrong, what you ran, and
|
|
179
|
+
that it's now resolved.
|
|
124
180
|
|
|
125
|
-
|
|
181
|
+
Only run haus commands and the fixes the doctor suggests. If a fix needs a
|
|
182
|
+
decision you can't make safely, stop and ask in plain language.
|
|
126
183
|
|
|
127
184
|
### After `haus update`
|
|
128
185
|
|
|
129
186
|
1. If CLI output says a new version is available, run `npm i -g @haus-tech/haus-workflow` and confirm the version bump.
|
|
130
187
|
2. If `WORKFLOW.md` was updated, remind the user: "WORKFLOW.md updated — review `workflow-config.md` for any new sections that need project-specific values."
|
|
131
188
|
3. If CLI output shows catalog changes, summarise which templates changed.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
4. **If the current directory has a `.haus-workflow/` folder** (this project was already
|
|
190
|
+
set up by haus), don't stop at the global update — continue immediately into
|
|
191
|
+
**Setup (`project:init`)** above to fully sync this project too (rescan, redo the deep
|
|
192
|
+
docs read, re-recommend, apply). This is what makes `update`'s scope note ("also
|
|
193
|
+
refreshes this project") literally true, not just a light lock-replay. No `.haus-workflow/`
|
|
194
|
+
folder → nothing more to do here, `update` only touched global files.
|
|
195
|
+
|
|
196
|
+
### After `haus doctor` (`project:doctor`)
|
|
197
|
+
|
|
198
|
+
1. Read the output. The first line is a plain-language verdict (green = healthy,
|
|
199
|
+
amber = things need attention).
|
|
200
|
+
2. Relay that verdict to the person in one or two sentences. If anything needs
|
|
201
|
+
attention, list each item as a short sentence plus the exact command that
|
|
202
|
+
fixes it — and offer to run the fix for them (or suggest `project:fix`).
|
|
203
|
+
|
|
204
|
+
Don't show raw command output unless they ask for detail.
|
|
136
205
|
|
|
137
206
|
### After `haus install` / `haus uninstall`
|
|
138
207
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
description: Clone this project's repositories onto disk (per-repo setup is separate).
|
|
3
|
-
---
|
|
1
|
+
# project:clone procedure
|
|
4
2
|
|
|
5
3
|
Clone repositories for this project. Per-repo setup (install, Docker, `.env`) is a separate step that isn't wired yet — this command only gets repos onto disk.
|
|
6
4
|
|
|
7
|
-
Cloning a single repo is always `haus clone <url> [dir]`. This
|
|
5
|
+
Cloning a single repo is always `haus clone <url> [dir]`. This procedure picks _which_ repos to clone and runs that primitive for each. There are two modes, chosen by whether a name was given.
|
|
8
6
|
|
|
9
7
|
**Always ask before cloning — never assume.** The user may already have the repos on disk. Do not start cloning until they have confirmed. A missing `repos.local.json` does **not** mean they want a fresh clone; it just means nothing is recorded yet — you must still ask.
|
|
10
8
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
description: Clone a project's repos and set each one up for local development.
|
|
3
|
-
---
|
|
1
|
+
# project:cloneandsetup procedure
|
|
4
2
|
|
|
5
3
|
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.
|
|
6
4
|
|
|
@@ -8,7 +6,7 @@ Clone a project's repos **and** set each one up for local development — node v
|
|
|
8
6
|
|
|
9
7
|
## Step 1 — Clone
|
|
10
8
|
|
|
11
|
-
Run the full `project:clone` flow by following
|
|
9
|
+
Run the full `project:clone` flow by following `references/clone.md` end to end (name → one repo; no name → workspace repos from `repos.manifest.json`). Carry the resulting repo list into Step 2.
|
|
12
10
|
|
|
13
11
|
## Step 2 — Prerequisite gate (one consolidated check)
|
|
14
12
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
description: Set up this project with haus, conversationally — no terminal needed.
|
|
3
|
-
---
|
|
1
|
+
# project:init procedure
|
|
4
2
|
|
|
5
3
|
Set up this project with haus, conversationally. The person you're helping may
|
|
6
4
|
not be a developer — never make them open a terminal or read JSON. You run the
|
|
@@ -59,11 +57,18 @@ Do this in order:
|
|
|
59
57
|
|
|
60
58
|
6. **Apply the rest.** Run `haus apply --write` again. It only writes what changed,
|
|
61
59
|
so this adds the newly-matched helpers from step 4 plus any opt-ins from step 5.
|
|
60
|
+
Then verify the root `CLAUDE.md` imports all three haus files — add any missing:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
@.haus-workflow/WORKFLOW.md
|
|
64
|
+
@.haus-workflow/workflow-config.md
|
|
65
|
+
@.haus-workflow/project.md
|
|
66
|
+
```
|
|
62
67
|
|
|
63
68
|
7. **Confirm.** End with one plain-language line, for example:
|
|
64
69
|
"✅ Your project is configured — I wrote your project docs, added N guardrails
|
|
65
70
|
and M coding helpers (K matched after reading your code in depth), plus the J
|
|
66
|
-
optional helpers you chose: <name them>. Run `/haus-doctor` any time to
|
|
71
|
+
optional helpers you chose: <name them>. Run `/haus-workflow doctor` any time to
|
|
67
72
|
re-check." Fill the numbers from the apply output, and name the opt-ins the
|
|
68
73
|
user picked in step 5 (omit that clause if they chose none).
|
|
69
74
|
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Check this project's haus setup health and explain it in plain language.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Check the health of this project's haus setup and explain it in plain language.
|
|
6
|
-
|
|
7
|
-
1. Run `haus doctor`.
|
|
8
|
-
2. Read the output. The first line is a plain-language verdict (green = healthy,
|
|
9
|
-
amber = things need attention).
|
|
10
|
-
3. Relay that verdict to the person in one or two sentences. If anything needs
|
|
11
|
-
attention, list each item as a short sentence plus the exact command that
|
|
12
|
-
fixes it — and offer to run the fix for them (or suggest `/haus-fix`).
|
|
13
|
-
|
|
14
|
-
Don't show raw command output unless they ask for detail.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Diagnose and fix this project's haus setup.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Diagnose and fix this project's haus setup.
|
|
6
|
-
|
|
7
|
-
1. Run `haus doctor` and read the verdict.
|
|
8
|
-
2. If the project is already healthy, say so in one line and stop.
|
|
9
|
-
3. Otherwise, for each item that needs attention, run the exact fix command the
|
|
10
|
-
doctor named (commonly `haus apply --write` or `haus apply --refill-config`).
|
|
11
|
-
4. Re-run `haus doctor` to confirm the verdict is now green.
|
|
12
|
-
5. Report what you changed in plain language — what was wrong, what you ran, and
|
|
13
|
-
that it's now resolved.
|
|
14
|
-
|
|
15
|
-
Only run haus commands and the fixes the doctor suggests. If a fix needs a
|
|
16
|
-
decision you can't make safely, stop and ask in plain language.
|