@haus-tech/haus-workflow 0.32.0 → 1.0.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 +285 -68
- package/library/catalog/manifest.json +14 -14
- 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 +3 -3
- 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.0.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.32.1...v1.0.0) (2026-07-02)
|
|
4
|
+
|
|
5
|
+
### ⚠ BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
- **workflow:** the standalone /haus-setup, /haus-clone,
|
|
8
|
+
/haus-cloneandsetup, /haus-doctor, and /haus-fix slash commands no longer
|
|
9
|
+
exist. Use /haus-workflow <task> instead (e.g. /haus-workflow project:init,
|
|
10
|
+
/haus-workflow project:doctor). Existing global installs self-clean the
|
|
11
|
+
old files on the next `haus update`/`haus install` via the installer's
|
|
12
|
+
existing orphan-pruning.
|
|
13
|
+
|
|
14
|
+
- docs(decisions): add ADR-0011 for the haus-workflow command consolidation
|
|
15
|
+
|
|
16
|
+
Satisfies the decisions-gate CI check for the breaking-change command
|
|
17
|
+
removal + skill-install mechanism change in the prior commit.
|
|
18
|
+
|
|
19
|
+
- fix(workflow): clarify the Step 1 menu is two sequential AskUserQuestion calls
|
|
20
|
+
|
|
21
|
+
Copilot review flagged ambiguous wording ("split into two grouped
|
|
22
|
+
questions in one call") that could read as cramming both question
|
|
23
|
+
blocks into a single AskUserQuestion invocation. AskUserQuestion is
|
|
24
|
+
not guaranteed multi-question across every environment this skill
|
|
25
|
+
runs in, so make the instruction unambiguous: ask Question 1, read
|
|
26
|
+
the answer, then ask Question 2 as a second, separate call.
|
|
27
|
+
|
|
28
|
+
- feat(workflow): upgrade project:refresh to a full non-destructive sync
|
|
29
|
+
|
|
30
|
+
Previously project:refresh just ran `haus apply --write` (reapply from
|
|
31
|
+
the already-computed recommendation, no package/catalog update, no
|
|
32
|
+
rescan, no docs refresh). Upgrade it to: run `haus update` first (npm
|
|
33
|
+
package + catalog + global install), then re-run the project:init
|
|
34
|
+
pipeline in place (rescan, redo the deep docs read, re-recommend,
|
|
35
|
+
apply) — nothing removed first, contrasting with project:reinit which
|
|
36
|
+
wipes everything before re-running the same pipeline.
|
|
37
|
+
|
|
38
|
+
Moves the root-CLAUDE.md-imports check from a now-orphaned SKILL.md
|
|
39
|
+
section into references/init.md's own apply step, since project:init,
|
|
40
|
+
project:reinit, and project:refresh all funnel through it.
|
|
41
|
+
|
|
42
|
+
- feat(workflow): full project:refresh sync + SessionStart staleness hook
|
|
43
|
+
|
|
44
|
+
project:refresh now runs `haus update` (npm package + catalog) first,
|
|
45
|
+
then re-runs the project:init pipeline in place (rescan, redo docs,
|
|
46
|
+
re-recommend, apply) instead of just `haus apply --write`. The `update`
|
|
47
|
+
task's own post-run steps cascade into the same pipeline when run
|
|
48
|
+
inside an already-set-up project, so its "also refreshes this project"
|
|
49
|
+
scope note is now literally true.
|
|
50
|
+
|
|
51
|
+
Adds a new SessionStart hook (`haus.update-check`, in both
|
|
52
|
+
PROJECT_HOOK_FRAGMENTS and CANONICAL_HOOKS) running `haus update
|
|
53
|
+
--from-hook`: silent when the project is up to date, emits a
|
|
54
|
+
SessionStart hookSpecificOutput note nudging `/haus-workflow
|
|
55
|
+
project:refresh` when the npm package, catalog, or lockfile has
|
|
56
|
+
drifted. Never blocks session start on a network failure.
|
|
57
|
+
|
|
58
|
+
Fixes found by adversarial review before landing:
|
|
59
|
+
|
|
60
|
+
- fetchNpmVersionStatus had no test-mode gate, so the new --from-hook
|
|
61
|
+
tests hit the real npm registry; added HAUS_SKIP_NPM_CHECK (mirrors
|
|
62
|
+
HAUS_CATALOG_REMOTE_BASE's test-only-when-explicit pattern) rather
|
|
63
|
+
than gating on bare HAUS_TEST_MODE, which the whole test run already
|
|
64
|
+
sets and would have broken tests/npm-version.test.js's fetch mocks.
|
|
65
|
+
- checkLock() now runs before the npm/catalog network calls in the
|
|
66
|
+
hook check, so a non-haus directory costs nothing.
|
|
67
|
+
- matcher parity between the two hardcoded hook-fragment lists.
|
|
68
|
+
- project:refresh's step 1 now explicitly inherits the "After `haus
|
|
69
|
+
update`" reactions (version bump, WORKFLOW.md nudge, catalog
|
|
70
|
+
summary), which it was silently skipping despite running the same
|
|
71
|
+
command.
|
|
72
|
+
- runbook entry: doctor --hooks going red right after an upgrade,
|
|
73
|
+
before the next apply, is expected (same as any new required rule)
|
|
74
|
+
— documented with the fix (re-apply once).
|
|
75
|
+
|
|
76
|
+
yarn verify: 627/627 tests green.
|
|
77
|
+
|
|
78
|
+
- fix(workflow): drop expensive lock-hash check from the SessionStart hook, add help to the menu
|
|
79
|
+
|
|
80
|
+
Copilot review, two fixes:
|
|
81
|
+
|
|
82
|
+
- runFromHookCheck() called checkLock(), which hashes the content of
|
|
83
|
+
every installed managed path on every single SessionStart. Replaced
|
|
84
|
+
with a new readLockSummary() (count + catalogRef only, no hashing) —
|
|
85
|
+
the hook only ever needed those two cheap facts. Drops the
|
|
86
|
+
lock-drift reason from the hook's output entirely; doctor/project:fix
|
|
87
|
+
remain the tools for catching local file tamper, which is a
|
|
88
|
+
different concern than "is this project behind the installed
|
|
89
|
+
package/catalog" (what this hook actually checks).
|
|
90
|
+
- The Step 1 no-arg menu didn't offer `help`, even though it's a real
|
|
91
|
+
task in the alias table. Added as Question 2 option 4 (still within
|
|
92
|
+
the 4-option cap).
|
|
93
|
+
|
|
94
|
+
Test changes: replaced the lock-drift `--from-hook` test (no longer
|
|
95
|
+
applicable — the hook no longer looks at hashes) with a "healthy
|
|
96
|
+
project with a real lock stays silent" integration test, plus a new
|
|
97
|
+
direct unit test for the "npm behind" JSON-emission path (mocked
|
|
98
|
+
fetch, same style as tests/npm-version.test.js) — there's no way to
|
|
99
|
+
force fetchLatestCatalogTag/fetchNpmVersionStatus to report "behind"
|
|
100
|
+
via env vars alone, so this exercises the actual behind branch and its
|
|
101
|
+
JSON shape deterministically instead of only ever testing silence.
|
|
102
|
+
|
|
103
|
+
yarn verify: 628/628 tests green.
|
|
104
|
+
|
|
105
|
+
- fix(test): use exact hostname match instead of substring for npm registry mock
|
|
106
|
+
|
|
107
|
+
CodeQL flagged String(url).includes('registry.npmjs.org') as an
|
|
108
|
+
incomplete URL substring sanitization (a host like
|
|
109
|
+
evil.com/registry.npmjs.org or registry.npmjs.org.evil.com would also
|
|
110
|
+
match). Not exploitable here (url comes from our own hardcoded fetch
|
|
111
|
+
call, not untrusted input), but the check should still be precise —
|
|
112
|
+
parse the URL and compare new URL(url).hostname exactly.
|
|
113
|
+
|
|
114
|
+
- fix(workflow): stop false-positive catalog-behind nudges, fix docs
|
|
115
|
+
|
|
116
|
+
Copilot review, four fixes:
|
|
117
|
+
|
|
118
|
+
- docs/runbook.md: a backtick code span was split across a line wrap
|
|
119
|
+
("`/haus-workflow\nproject:refresh`"), rendering broken. Rewrapped.
|
|
120
|
+
- docs/cli.md: --from-hook's description said it checks npm/catalog/
|
|
121
|
+
lock; it never inspected lock content (no hashing) and only reads
|
|
122
|
+
the lock to confirm this is a haus project and get its catalogRef.
|
|
123
|
+
Corrected the wording.
|
|
124
|
+
- readLockSummary() only ever read catalogRef from lock item 0. An
|
|
125
|
+
older/mixed lock could have it missing there while later items carry
|
|
126
|
+
it. Now searches all items for the first defined catalogRef.
|
|
127
|
+
- runFromHookCheck() fell back to 'main' when catalogRef was absent,
|
|
128
|
+
which then almost always compared "behind" against any real release
|
|
129
|
+
tag — a false-positive nudge, and a wrong claim ("installed from
|
|
130
|
+
main") when the ref was actually unknown. Now skips the catalog
|
|
131
|
+
comparison entirely when catalogRef is null.
|
|
132
|
+
|
|
133
|
+
New tests: readLockSummary's cross-item catalogRef fallback (and that
|
|
134
|
+
it never hashes file content, unlike checkLock), plus a regression
|
|
135
|
+
test proving no false "behind" nudge fires when catalogRef is missing
|
|
136
|
+
even if the catalog has newer tags (mocked GitHub tags API).
|
|
137
|
+
|
|
138
|
+
yarn verify: 633/633 tests green.
|
|
139
|
+
|
|
140
|
+
- fix(docs): avoid a prettier-reflowed broken inline code span in cli.md
|
|
141
|
+
|
|
142
|
+
The previous commit's own pre-commit prettier pass reflowed the
|
|
143
|
+
--from-hook bullet and split a `/haus-workflow project:refresh`
|
|
144
|
+
code span across the wrap (the same bug class Copilot flagged in
|
|
145
|
+
runbook.md moments earlier) — verified by re-running prettier on
|
|
146
|
+
docs/cli.md, docs/runbook.md, and SKILL.md directly; all three are
|
|
147
|
+
now stable ("unchanged") under formatting.
|
|
148
|
+
|
|
149
|
+
### Features
|
|
150
|
+
|
|
151
|
+
- **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))
|
|
152
|
+
|
|
153
|
+
### Bug Fixes
|
|
154
|
+
|
|
155
|
+
- **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))
|
|
156
|
+
|
|
157
|
+
## [0.32.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.32.0...v0.32.1) (2026-06-30)
|
|
158
|
+
|
|
159
|
+
### Bug Fixes
|
|
160
|
+
|
|
161
|
+
- **security:** audit hardening — fail-closed, recommender gates, CI hardening ([#157](https://github.com/WeAreHausTech/haus-workflow/issues/157)) ([e545194](https://github.com/WeAreHausTech/haus-workflow/commit/e545194fd3d3eb553bc2955a7ca17574bd98be71))
|
|
162
|
+
|
|
3
163
|
## [0.32.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.31.1...v0.32.0) (2026-06-24)
|
|
4
164
|
|
|
5
165
|
### Features
|
package/dist/cli.js
CHANGED
|
@@ -650,48 +650,103 @@ function parseManifest(json) {
|
|
|
650
650
|
}
|
|
651
651
|
|
|
652
652
|
// src/catalog/remote-catalog.ts
|
|
653
|
+
function isTestMode() {
|
|
654
|
+
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
655
|
+
}
|
|
653
656
|
function getCacheDir() {
|
|
654
657
|
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path3.join(os2.homedir(), CATALOG_CACHE_SUBDIR);
|
|
655
658
|
}
|
|
656
659
|
var cachedCatalogRef;
|
|
657
660
|
var cachedBlobPaths;
|
|
661
|
+
function getBundledCatalogRef() {
|
|
662
|
+
try {
|
|
663
|
+
const manifestPath2 = path3.join(packageRoot(), "library/catalog/manifest.json");
|
|
664
|
+
const raw = fs2.readFileSync(manifestPath2, "utf8");
|
|
665
|
+
const data = JSON.parse(raw);
|
|
666
|
+
if (typeof data.version === "string" && data.version) {
|
|
667
|
+
return data.version.startsWith("v") ? data.version : `v${data.version}`;
|
|
668
|
+
}
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
return void 0;
|
|
672
|
+
}
|
|
658
673
|
function getResolvedCatalogRef() {
|
|
659
|
-
|
|
674
|
+
const resolved = cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? getBundledCatalogRef();
|
|
675
|
+
if (!resolved) {
|
|
676
|
+
warn(
|
|
677
|
+
"Could not determine catalog ref from cache, env, or bundled snapshot \u2014 falling back to main (moving target)."
|
|
678
|
+
);
|
|
679
|
+
return "main";
|
|
680
|
+
}
|
|
681
|
+
return resolved;
|
|
660
682
|
}
|
|
661
683
|
function isCatalogRefResolved() {
|
|
662
684
|
return cachedCatalogRef !== void 0 || process.env["HAUS_CATALOG_REF"] !== void 0;
|
|
663
685
|
}
|
|
664
686
|
async function resolveCatalogRef(opts) {
|
|
665
687
|
const env = opts?.env ?? process.env;
|
|
666
|
-
if (env["HAUS_CATALOG_REF"])
|
|
688
|
+
if (env["HAUS_CATALOG_REF"]) {
|
|
689
|
+
if (env["HAUS_CATALOG_REF"] === "main") {
|
|
690
|
+
warn(
|
|
691
|
+
"HAUS_CATALOG_REF=main is set \u2014 fetching from the moving main branch. Pin to a release tag for reproducible installs."
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
return env["HAUS_CATALOG_REF"];
|
|
695
|
+
}
|
|
667
696
|
const fetchLatest = opts?.fetchLatestTag ?? fetchLatestCatalogTag;
|
|
668
697
|
const tag = await fetchLatest();
|
|
669
|
-
|
|
698
|
+
if (tag !== null) return tag;
|
|
699
|
+
const fallback = opts?.fallbackRef;
|
|
700
|
+
if (fallback) {
|
|
701
|
+
warn(
|
|
702
|
+
`Tag resolution failed \u2014 using cached ref ${fallback}. To use latest, retry or set HAUS_CATALOG_REF explicitly.`
|
|
703
|
+
);
|
|
704
|
+
return fallback;
|
|
705
|
+
}
|
|
706
|
+
const bundled = getBundledCatalogRef();
|
|
707
|
+
if (bundled) {
|
|
708
|
+
warn(
|
|
709
|
+
`Tag resolution failed \u2014 using bundled snapshot ref ${bundled}. To use latest, retry or set HAUS_CATALOG_REF explicitly.`
|
|
710
|
+
);
|
|
711
|
+
return bundled;
|
|
712
|
+
}
|
|
713
|
+
warn(
|
|
714
|
+
"Tag resolution failed and no fallback ref is available. Set HAUS_CATALOG_REF explicitly to avoid fetching from main."
|
|
715
|
+
);
|
|
716
|
+
return "main";
|
|
670
717
|
}
|
|
671
718
|
async function remoteBase() {
|
|
672
|
-
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
719
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
673
720
|
return process.env["HAUS_CATALOG_REMOTE_BASE"];
|
|
674
721
|
}
|
|
675
722
|
if (cachedCatalogRef === void 0) {
|
|
676
|
-
cachedCatalogRef = await resolveCatalogRef();
|
|
723
|
+
cachedCatalogRef = await resolveCatalogRef({ fallbackRef: getBundledCatalogRef() });
|
|
677
724
|
}
|
|
678
725
|
return `${CATALOG_REPO_URL}/${cachedCatalogRef}`;
|
|
679
726
|
}
|
|
680
727
|
async function fetchText(url) {
|
|
681
728
|
try {
|
|
682
729
|
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
683
|
-
if (!res.ok)
|
|
730
|
+
if (!res.ok) {
|
|
731
|
+
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
684
734
|
return await res.text();
|
|
685
|
-
} catch {
|
|
735
|
+
} catch (e) {
|
|
736
|
+
warn(`Catalog fetch failed (${e instanceof Error ? e.constructor.name : String(e)}): ${url}`);
|
|
686
737
|
return null;
|
|
687
738
|
}
|
|
688
739
|
}
|
|
689
740
|
async function fetchBytes(url) {
|
|
690
741
|
try {
|
|
691
742
|
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
692
|
-
if (!res.ok)
|
|
743
|
+
if (!res.ok) {
|
|
744
|
+
warn(`Catalog fetch HTTP ${res.status}: ${url}`);
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
693
747
|
return Buffer.from(await res.arrayBuffer());
|
|
694
|
-
} catch {
|
|
748
|
+
} catch (e) {
|
|
749
|
+
warn(`Catalog fetch failed (${e instanceof Error ? e.constructor.name : String(e)}): ${url}`);
|
|
695
750
|
return null;
|
|
696
751
|
}
|
|
697
752
|
}
|
|
@@ -828,7 +883,7 @@ async function fetchGitHubRecursiveBlobPaths(ref) {
|
|
|
828
883
|
}
|
|
829
884
|
async function fetchCatalogBlobPaths(_base) {
|
|
830
885
|
if (cachedBlobPaths) return cachedBlobPaths;
|
|
831
|
-
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
886
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
832
887
|
const ref = getResolvedCatalogRef();
|
|
833
888
|
const paths = await fetchGitHubRecursiveBlobPaths(ref);
|
|
834
889
|
if (paths) cachedBlobPaths = paths;
|
|
@@ -838,7 +893,7 @@ async function listFilesUnderCatalogPrefix(prefix, base) {
|
|
|
838
893
|
const normalized = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
839
894
|
const prefixSlash = `${normalized}/`;
|
|
840
895
|
let relFiles;
|
|
841
|
-
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
896
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
842
897
|
relFiles = await listMockPrefixFiles(base, normalized);
|
|
843
898
|
} else {
|
|
844
899
|
const blobs = await fetchCatalogBlobPaths(base);
|
|
@@ -985,7 +1040,7 @@ async function syncConfigItem(item, base) {
|
|
|
985
1040
|
if (relFiles && relFiles.length > 0) {
|
|
986
1041
|
try {
|
|
987
1042
|
return await syncDirectoryFromPrefix(item, item.path, dest, base, {
|
|
988
|
-
validateMarkdown:
|
|
1043
|
+
validateMarkdown: true,
|
|
989
1044
|
requireSkillMd: false,
|
|
990
1045
|
relFiles
|
|
991
1046
|
});
|
|
@@ -1125,7 +1180,7 @@ function compareSemver(a, b) {
|
|
|
1125
1180
|
return 0;
|
|
1126
1181
|
}
|
|
1127
1182
|
async function fetchLatestCatalogTag() {
|
|
1128
|
-
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
1183
|
+
if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) return null;
|
|
1129
1184
|
try {
|
|
1130
1185
|
const res = await fetch(CATALOG_TAGS_API_URL, {
|
|
1131
1186
|
signal: AbortSignal.timeout(5e3),
|
|
@@ -1756,6 +1811,13 @@ var PROJECT_HOOK_FRAGMENTS = [
|
|
|
1756
1811
|
event: "Stop",
|
|
1757
1812
|
matcher: "*",
|
|
1758
1813
|
command: "haus decisions suggest --from-hook"
|
|
1814
|
+
},
|
|
1815
|
+
{
|
|
1816
|
+
id: "haus.update-check",
|
|
1817
|
+
gate: "keep",
|
|
1818
|
+
event: "SessionStart",
|
|
1819
|
+
matcher: "*",
|
|
1820
|
+
command: "haus update --from-hook"
|
|
1759
1821
|
}
|
|
1760
1822
|
];
|
|
1761
1823
|
async function readProjectSettings(root) {
|
|
@@ -1855,6 +1917,12 @@ var CANONICAL_HOOKS = {
|
|
|
1855
1917
|
matcher: "*",
|
|
1856
1918
|
hooks: [{ type: "command", command: "haus decisions suggest --from-hook" }]
|
|
1857
1919
|
}
|
|
1920
|
+
],
|
|
1921
|
+
SessionStart: [
|
|
1922
|
+
{
|
|
1923
|
+
matcher: "*",
|
|
1924
|
+
hooks: [{ type: "command", command: "haus update --from-hook" }]
|
|
1925
|
+
}
|
|
1858
1926
|
]
|
|
1859
1927
|
}
|
|
1860
1928
|
};
|
|
@@ -2290,15 +2358,32 @@ ${templateContent}`;
|
|
|
2290
2358
|
return null;
|
|
2291
2359
|
}
|
|
2292
2360
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
)
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2361
|
+
const bodyMatchesTemplate = hashText(normaliseLF(existingContent)) === contentHash;
|
|
2362
|
+
if (parsed.hash) {
|
|
2363
|
+
const bodyIntact = hashText(normaliseLF(existingContent)) === parsed.hash;
|
|
2364
|
+
if (!bodyIntact && !force) {
|
|
2365
|
+
warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
|
|
2366
|
+
return null;
|
|
2367
|
+
}
|
|
2368
|
+
} else {
|
|
2369
|
+
if (!bodyMatchesTemplate && !force) {
|
|
2370
|
+
const migratedHeader = makeWorkflowHeader(
|
|
2371
|
+
pkgVersion,
|
|
2372
|
+
hashText(normaliseLF(existingContent))
|
|
2373
|
+
);
|
|
2374
|
+
const migrated = `${migratedHeader}
|
|
2375
|
+
${existingContent}`;
|
|
2376
|
+
if (hasTextChanged(existing, migrated)) {
|
|
2377
|
+
if (dryRun) {
|
|
2378
|
+
log(`${printable}: migrating legacy hash header (body preserved)`);
|
|
2379
|
+
} else {
|
|
2380
|
+
await writeText(destPath, migrated);
|
|
2381
|
+
}
|
|
2382
|
+
} else if (dryRun) {
|
|
2383
|
+
log(`${printable}: unchanged`);
|
|
2384
|
+
}
|
|
2385
|
+
return destPath;
|
|
2386
|
+
}
|
|
2302
2387
|
}
|
|
2303
2388
|
if (!hasTextChanged(existing, next)) {
|
|
2304
2389
|
if (dryRun) log(`${printable}: unchanged`);
|
|
@@ -2342,11 +2427,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2342
2427
|
};
|
|
2343
2428
|
const pkgRoot = packageRoot();
|
|
2344
2429
|
const hausVersion2 = (await readJson(path14.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
2345
|
-
const coreFiles = [
|
|
2346
|
-
claudePath(root, "settings.json"),
|
|
2347
|
-
claudePath(root, "rules", "haus.md"),
|
|
2348
|
-
claudePath(root, "commands", "haus-doctor.md")
|
|
2349
|
-
];
|
|
2430
|
+
const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
|
|
2350
2431
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
2351
2432
|
const decisionsSeedPath = await writeDecisionsSeed(root, dryRun);
|
|
2352
2433
|
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force);
|
|
@@ -2369,12 +2450,6 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2369
2450
|
await applyProjectSettingsMerge(root);
|
|
2370
2451
|
await assertPostApplySettingsHausContract(root);
|
|
2371
2452
|
}
|
|
2372
|
-
await writeManagedText(
|
|
2373
|
-
root,
|
|
2374
|
-
claudePath(root, "commands", "haus-doctor.md"),
|
|
2375
|
-
"Run `haus doctor`.",
|
|
2376
|
-
dryRun
|
|
2377
|
-
);
|
|
2378
2453
|
const legacyReviewPath = claudePath(root, "commands", "haus-review.md");
|
|
2379
2454
|
if (await fs14.pathExists(legacyReviewPath)) {
|
|
2380
2455
|
const content2 = await fs14.readFile(legacyReviewPath, "utf8");
|
|
@@ -2389,6 +2464,20 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2389
2464
|
}
|
|
2390
2465
|
}
|
|
2391
2466
|
}
|
|
2467
|
+
const legacyDoctorPath = claudePath(root, "commands", "haus-doctor.md");
|
|
2468
|
+
if (await fs14.pathExists(legacyDoctorPath)) {
|
|
2469
|
+
const content2 = await fs14.readFile(legacyDoctorPath, "utf8");
|
|
2470
|
+
const stub = "Run `haus doctor`.";
|
|
2471
|
+
if (content2 === stub || content2 === `${stub}
|
|
2472
|
+
` || content2 === `${stub}\r
|
|
2473
|
+
`) {
|
|
2474
|
+
if (dryRun) {
|
|
2475
|
+
say(`[dry-run] would remove stale ${displayPath(root, legacyDoctorPath)}`);
|
|
2476
|
+
} else {
|
|
2477
|
+
await fs14.remove(legacyDoctorPath);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2392
2481
|
await writeManagedText(
|
|
2393
2482
|
root,
|
|
2394
2483
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -2407,7 +2496,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2407
2496
|
"results in plain language \u2014 never make them use a terminal or read JSON.",
|
|
2408
2497
|
"- Set up / configure / fix / check \u2192 `haus setup-project`, `haus apply --write`, `haus doctor`",
|
|
2409
2498
|
"- Update package + catalog \u2192 `haus update`",
|
|
2410
|
-
"-
|
|
2499
|
+
"- `/haus-workflow <task>` does the same conversationally (e.g. `init`, `fix`, `doctor`, `reinit`).",
|
|
2411
2500
|
""
|
|
2412
2501
|
].join("\n"),
|
|
2413
2502
|
dryRun
|
|
@@ -2491,11 +2580,50 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
2491
2580
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
2492
2581
|
);
|
|
2493
2582
|
} else if (item.type === "skill") {
|
|
2583
|
+
const skillFiles = (await fs14.readdir(sourcePath, { recursive: true })).filter(
|
|
2584
|
+
(f) => typeof f === "string" && f.endsWith(".md")
|
|
2585
|
+
);
|
|
2586
|
+
let skillValid = true;
|
|
2587
|
+
for (const relFile of skillFiles) {
|
|
2588
|
+
const mdPath = path14.join(sourcePath, relFile);
|
|
2589
|
+
const mdContent = await fs14.readFile(mdPath, "utf8");
|
|
2590
|
+
const mdValidation = validateCatalogItem(
|
|
2591
|
+
{
|
|
2592
|
+
id: manifestItem.id,
|
|
2593
|
+
type: "skill",
|
|
2594
|
+
path: manifestItem.path,
|
|
2595
|
+
source: manifestItem.source
|
|
2596
|
+
},
|
|
2597
|
+
mdContent
|
|
2598
|
+
);
|
|
2599
|
+
if (!mdValidation.ok) {
|
|
2600
|
+
warn(
|
|
2601
|
+
`Skipping ${item.id}: pre-copy validation failed (${relFile}) \u2014 ${mdValidation.reason}`
|
|
2602
|
+
);
|
|
2603
|
+
skillValid = false;
|
|
2604
|
+
break;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
if (!skillValid) continue;
|
|
2494
2608
|
await installCatalogSkill(sourcePath, destination, {
|
|
2495
2609
|
originSourceId: manifestItem.originSourceId,
|
|
2496
2610
|
dryRun: false
|
|
2497
2611
|
});
|
|
2498
2612
|
} else {
|
|
2613
|
+
const fileContent = await fs14.readFile(sourcePath, "utf8");
|
|
2614
|
+
const validation = validateCatalogItem(
|
|
2615
|
+
{
|
|
2616
|
+
id: manifestItem.id,
|
|
2617
|
+
type: manifestItem.type,
|
|
2618
|
+
path: manifestItem.path,
|
|
2619
|
+
source: manifestItem.source
|
|
2620
|
+
},
|
|
2621
|
+
fileContent
|
|
2622
|
+
);
|
|
2623
|
+
if (!validation.ok) {
|
|
2624
|
+
warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2499
2627
|
await fs14.ensureDir(path14.dirname(destination));
|
|
2500
2628
|
await fs14.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
2501
2629
|
}
|
|
@@ -2670,8 +2798,25 @@ async function fetchSingleRef(url, cacheDir, meta) {
|
|
|
2670
2798
|
return { result: "failed" };
|
|
2671
2799
|
}
|
|
2672
2800
|
}
|
|
2801
|
+
function collectLlmsTxtUrls(items) {
|
|
2802
|
+
return [...new Set(items.flatMap((item) => (item.references ?? []).filter(isLlmsTxtUrl)))];
|
|
2803
|
+
}
|
|
2804
|
+
async function pruneOrphanedRefs(cacheDir, keepUrls) {
|
|
2805
|
+
const meta = await readCacheMeta(cacheDir);
|
|
2806
|
+
let removed = 0;
|
|
2807
|
+
for (const [url, entry] of Object.entries(meta)) {
|
|
2808
|
+
if (keepUrls.has(url)) continue;
|
|
2809
|
+
delete meta[url];
|
|
2810
|
+
removed++;
|
|
2811
|
+
const safeFile = typeof entry.file === "string" && entry.file === path16.basename(entry.file);
|
|
2812
|
+
if (safeFile) await fs16.remove(path16.join(cacheDir, entry.file)).catch(() => {
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
if (removed > 0) await writeCacheMeta(cacheDir, meta);
|
|
2816
|
+
return removed;
|
|
2817
|
+
}
|
|
2673
2818
|
async function fetchRefsForItems(items, cacheDir) {
|
|
2674
|
-
const urls =
|
|
2819
|
+
const urls = collectLlmsTxtUrls(items);
|
|
2675
2820
|
const summary = {
|
|
2676
2821
|
fetched: 0,
|
|
2677
2822
|
unchanged: 0,
|
|
@@ -2806,14 +2951,22 @@ async function runApply(options) {
|
|
|
2806
2951
|
if (!isDryRun) {
|
|
2807
2952
|
try {
|
|
2808
2953
|
const cacheDir = hausPath(root, "llms-cache");
|
|
2954
|
+
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
2955
|
+
const installedIds = new Set(lock.map((entry) => entry.id));
|
|
2809
2956
|
const allItems = await loadCatalog(root);
|
|
2810
|
-
const
|
|
2957
|
+
const installedItems = allItems.filter((item) => installedIds.has(item.id));
|
|
2958
|
+
const summary = await fetchRefsForItems(installedItems, cacheDir);
|
|
2811
2959
|
if (summary.fetched > 0)
|
|
2812
2960
|
log(`Fetched ${summary.fetched} llms.txt reference(s) to ${cacheDir}`);
|
|
2813
2961
|
if (summary.failed > 0)
|
|
2814
2962
|
warn(
|
|
2815
2963
|
`Failed to fetch ${summary.failed} llms.txt reference(s): ${summary.failedUrls.join(", ")}`
|
|
2816
2964
|
);
|
|
2965
|
+
const pruned = await pruneOrphanedRefs(cacheDir, new Set(collectLlmsTxtUrls(installedItems)));
|
|
2966
|
+
if (pruned > 0)
|
|
2967
|
+
log(
|
|
2968
|
+
`Removed ${pruned} orphaned llms.txt cache entr${pruned === 1 ? "y" : "ies"} from ${cacheDir}`
|
|
2969
|
+
);
|
|
2817
2970
|
} catch (err) {
|
|
2818
2971
|
warn(
|
|
2819
2972
|
`Could not fetch llms.txt references: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -3705,7 +3858,7 @@ function buildSourcesReport(items) {
|
|
|
3705
3858
|
}
|
|
3706
3859
|
return {
|
|
3707
3860
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3708
|
-
items: [...statusBySource.entries()].map(([
|
|
3861
|
+
items: [...statusBySource.entries()].map(([source, status]) => ({ source, status })).sort((a, b) => a.source.localeCompare(b.source))
|
|
3709
3862
|
};
|
|
3710
3863
|
}
|
|
3711
3864
|
async function writeSourcesReport(root) {
|
|
@@ -3816,7 +3969,13 @@ async function readContextOrScan(root) {
|
|
|
3816
3969
|
|
|
3817
3970
|
// src/update/npm-version.ts
|
|
3818
3971
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
3972
|
+
function isTestMode2() {
|
|
3973
|
+
return process.env["HAUS_TEST_MODE"] === "1" || process.env["NODE_ENV"] === "test";
|
|
3974
|
+
}
|
|
3819
3975
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
3976
|
+
if (isTestMode2() && process.env["HAUS_SKIP_NPM_CHECK"]) {
|
|
3977
|
+
return { current: currentVersion, latest: null, updateAvailable: false };
|
|
3978
|
+
}
|
|
3820
3979
|
try {
|
|
3821
3980
|
const res = await fetch(
|
|
3822
3981
|
`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`,
|
|
@@ -4417,9 +4576,7 @@ function mergeRecommendationWarnings(context) {
|
|
|
4417
4576
|
// src/recommender/recommend.ts
|
|
4418
4577
|
async function recommend(root, context, opts = {}) {
|
|
4419
4578
|
const items = await loadCatalog(root);
|
|
4420
|
-
const sources =
|
|
4421
|
-
hausPath(root, "sources-report.json")
|
|
4422
|
-
) ?? {};
|
|
4579
|
+
const sources = buildSourcesReport(items);
|
|
4423
4580
|
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
4424
4581
|
const scannerStacks = buildStackSet(context);
|
|
4425
4582
|
const scannerRoles = new Set(context.repoRoles.map((r) => r.toLowerCase()));
|
|
@@ -4437,7 +4594,7 @@ async function recommend(root, context, opts = {}) {
|
|
|
4437
4594
|
const depSet = scannerDeps;
|
|
4438
4595
|
const recommended = [];
|
|
4439
4596
|
const skipped = [];
|
|
4440
|
-
const sourceTrust = new Map(
|
|
4597
|
+
const sourceTrust = new Map(sources.items.map((x) => [x.source, x.status]));
|
|
4441
4598
|
const changedFiles = await readChangedFiles(root);
|
|
4442
4599
|
const skip = (id, code, message, signal) => {
|
|
4443
4600
|
skipped.push({ id, reason: message, skipReasons: [{ code, message, signal }] });
|
|
@@ -4445,8 +4602,13 @@ async function recommend(root, context, opts = {}) {
|
|
|
4445
4602
|
const roleSignal = (name) => scannerRoles.has(name.toLowerCase()) ? `role:${name}` : `deep:role:${name}`;
|
|
4446
4603
|
const stackSignal = (name) => scannerStacks.has(name.toLowerCase()) ? `tag:${name}` : `deep:tag:${name}`;
|
|
4447
4604
|
for (const item of items) {
|
|
4448
|
-
const
|
|
4449
|
-
if (
|
|
4605
|
+
const normSource = typeof item.source === "string" ? item.source.trim() : "";
|
|
4606
|
+
if (!normSource) {
|
|
4607
|
+
skip(item.id, "invalid-source", "Item source is missing or empty");
|
|
4608
|
+
continue;
|
|
4609
|
+
}
|
|
4610
|
+
const itemTags = item.tags.map((t) => t.toLowerCase());
|
|
4611
|
+
if (UNSUPPORTED.some((x) => itemTags.includes(x))) {
|
|
4450
4612
|
skip(item.id, "unsupported-policy", "Unsupported stack policy");
|
|
4451
4613
|
continue;
|
|
4452
4614
|
}
|
|
@@ -4454,7 +4616,7 @@ async function recommend(root, context, opts = {}) {
|
|
|
4454
4616
|
skip(item.id, "deprecated", "Catalog item is deprecated", "reviewStatus:deprecated");
|
|
4455
4617
|
continue;
|
|
4456
4618
|
}
|
|
4457
|
-
if (
|
|
4619
|
+
if (normSource === "curated") {
|
|
4458
4620
|
const rs = item.reviewStatus;
|
|
4459
4621
|
if (!rs || rs !== "approved") {
|
|
4460
4622
|
skip(
|
|
@@ -4475,17 +4637,19 @@ async function recommend(root, context, opts = {}) {
|
|
|
4475
4637
|
continue;
|
|
4476
4638
|
}
|
|
4477
4639
|
}
|
|
4478
|
-
|
|
4640
|
+
const sensitiveInId = SENSITIVE_ITEM_KEYWORDS.some((x) => item.id.toLowerCase().includes(x));
|
|
4641
|
+
const sensitiveInTags = SENSITIVE_ITEM_KEYWORDS.some((x) => itemTags.includes(x));
|
|
4642
|
+
if (sensitiveInId || sensitiveInTags) {
|
|
4479
4643
|
skip(item.id, "sensitive-policy", "Sensitive content policy block");
|
|
4480
4644
|
continue;
|
|
4481
4645
|
}
|
|
4482
|
-
const trust = sourceTrust.get(
|
|
4646
|
+
const trust = sourceTrust.get(normSource);
|
|
4483
4647
|
if (trust === "candidate" || trust === "rejected") {
|
|
4484
4648
|
skip(item.id, "source-trust", "Source trust policy block", `trust:${trust}`);
|
|
4485
4649
|
continue;
|
|
4486
4650
|
}
|
|
4487
|
-
if (
|
|
4488
|
-
skip(item.id, "source-approval", "Source not approved", `source:${
|
|
4651
|
+
if (normSource !== "haus" && trust !== "approved") {
|
|
4652
|
+
skip(item.id, "source-approval", "Source not approved", `source:${normSource}`);
|
|
4489
4653
|
continue;
|
|
4490
4654
|
}
|
|
4491
4655
|
if (item.id === "haus.nx21-monorepo-patterns" && !roleSet.has("nx-monorepo")) {
|
|
@@ -4510,15 +4674,6 @@ async function recommend(root, context, opts = {}) {
|
|
|
4510
4674
|
skip(item.id, "required-role-missing", "Required role missing: database", "role:database");
|
|
4511
4675
|
continue;
|
|
4512
4676
|
}
|
|
4513
|
-
if (item.id === "haus.ecc-typescript-reviewer" && (stackSet.has("react") || stackSet.has("react19") || stackSet.has("nextjs") || stackSet.has("next") || roleSet.has("react-app") || roleSet.has("next-app"))) {
|
|
4514
|
-
skip(
|
|
4515
|
-
item.id,
|
|
4516
|
-
"co-install-react-reviewer",
|
|
4517
|
-
"Skip typescript-reviewer on React/Next stacks (use react-reviewer)",
|
|
4518
|
-
"stack:react|nextjs|role:react-app|next-app"
|
|
4519
|
-
);
|
|
4520
|
-
continue;
|
|
4521
|
-
}
|
|
4522
4677
|
const isDefaultBaseline = item.default === true;
|
|
4523
4678
|
const reasons = [];
|
|
4524
4679
|
const push = (code, message, signal) => reasons.push({ code, message, signal });
|
|
@@ -4536,9 +4691,11 @@ async function recommend(root, context, opts = {}) {
|
|
|
4536
4691
|
`packageManager:${context.packageManager}`
|
|
4537
4692
|
);
|
|
4538
4693
|
}
|
|
4539
|
-
const configSignal = item.tags.find(
|
|
4540
|
-
|
|
4541
|
-
|
|
4694
|
+
const configSignal = item.tags.find((t) => {
|
|
4695
|
+
const escaped = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4696
|
+
const re = new RegExp(`(?:^|\\W)${escaped}(?:\\W|$)`, "i");
|
|
4697
|
+
return context.warnings.some((w) => re.test(w));
|
|
4698
|
+
});
|
|
4542
4699
|
if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
|
|
4543
4700
|
const idSegment = item.id.split(".").pop() ?? "";
|
|
4544
4701
|
const changedMatch = idSegment ? changedFiles.find((f) => f.includes(idSegment)) : void 0;
|
|
@@ -4671,6 +4828,13 @@ function applyCoInstallSuppression(recommended, skipped) {
|
|
|
4671
4828
|
code: "co-install-redis-official",
|
|
4672
4829
|
message: "Skip ecc-redis-patterns when official redis-connections skill is selected",
|
|
4673
4830
|
signal: "co-install:redis-connections"
|
|
4831
|
+
},
|
|
4832
|
+
{
|
|
4833
|
+
suppress: "haus.ecc-typescript-reviewer",
|
|
4834
|
+
whenAnyRecommended: ["haus.ecc-react-reviewer"],
|
|
4835
|
+
code: "co-install-react-reviewer",
|
|
4836
|
+
message: "Skip typescript-reviewer on React/Next stacks (use react-reviewer)",
|
|
4837
|
+
signal: "co-install:react-reviewer"
|
|
4674
4838
|
}
|
|
4675
4839
|
];
|
|
4676
4840
|
for (const rule of rules) {
|
|
@@ -4951,7 +5115,7 @@ ${content2}`;
|
|
|
4951
5115
|
// src/install/apply.ts
|
|
4952
5116
|
var SCHEMA_VERSION2 = "1";
|
|
4953
5117
|
function hashContent(content2) {
|
|
4954
|
-
return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
|
|
5118
|
+
return `sha256-${crypto2.createHash("sha256").update(normaliseLF(content2)).digest("hex")}`;
|
|
4955
5119
|
}
|
|
4956
5120
|
function sourceVersion() {
|
|
4957
5121
|
try {
|
|
@@ -4965,6 +5129,22 @@ function sourceVersion() {
|
|
|
4965
5129
|
function globalSrcDir() {
|
|
4966
5130
|
return path30.join(packageRoot(), "library", "global");
|
|
4967
5131
|
}
|
|
5132
|
+
function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
|
|
5133
|
+
const skillDir = path30.join(skillsDir, skillName);
|
|
5134
|
+
const entries = [];
|
|
5135
|
+
for (const rel of fs24.readdirSync(skillDir, { recursive: true })) {
|
|
5136
|
+
if (rel === "SKILL.md") continue;
|
|
5137
|
+
const abs = path30.join(skillDir, rel);
|
|
5138
|
+
if (!fs24.statSync(abs).isFile()) continue;
|
|
5139
|
+
const posixRel = rel.split(path30.sep).join("/");
|
|
5140
|
+
entries.push({
|
|
5141
|
+
stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
|
|
5142
|
+
srcRelPath: path30.join("library", "global", "skills", skillName, rel),
|
|
5143
|
+
destPath: path30.join(claudeDir, "skills", skillName, rel)
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
5146
|
+
return entries;
|
|
5147
|
+
}
|
|
4968
5148
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
4969
5149
|
const entries = [];
|
|
4970
5150
|
const skillsDir = path30.join(srcDir, "skills");
|
|
@@ -4977,6 +5157,7 @@ function collectSourceFiles(srcDir, claudeDir) {
|
|
|
4977
5157
|
srcRelPath: path30.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
4978
5158
|
destPath: path30.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
4979
5159
|
});
|
|
5160
|
+
entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
|
|
4980
5161
|
}
|
|
4981
5162
|
}
|
|
4982
5163
|
}
|
|
@@ -5418,7 +5599,7 @@ import path32 from "node:path";
|
|
|
5418
5599
|
import fs26 from "fs-extra";
|
|
5419
5600
|
|
|
5420
5601
|
// src/claude/managed-paths.ts
|
|
5421
|
-
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"
|
|
5602
|
+
var PROJECT_MANAGED_CLAUDE_REL = ["rules/haus.md"];
|
|
5422
5603
|
var PROJECT_MANAGED_HAUS_REL = ["haus.lock.json"];
|
|
5423
5604
|
function coreManagedAbsolutePaths(root) {
|
|
5424
5605
|
const claude = PROJECT_MANAGED_CLAUDE_REL.map((rel) => claudePath(root, rel));
|
|
@@ -5662,6 +5843,11 @@ function summarizeLockDiff(before, after) {
|
|
|
5662
5843
|
// src/update/lockfile.ts
|
|
5663
5844
|
import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
|
|
5664
5845
|
import path34 from "node:path";
|
|
5846
|
+
async function readLockSummary(root) {
|
|
5847
|
+
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5848
|
+
const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
|
|
5849
|
+
return { count: lock.length, catalogRef };
|
|
5850
|
+
}
|
|
5665
5851
|
async function checkLock(root) {
|
|
5666
5852
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
5667
5853
|
const hasValidVersions = lock.every(
|
|
@@ -5725,6 +5911,10 @@ async function hasLocalOverrides(root) {
|
|
|
5725
5911
|
var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
5726
5912
|
async function runUpdate(options) {
|
|
5727
5913
|
const root = process.cwd();
|
|
5914
|
+
if (options.fromHook) {
|
|
5915
|
+
await runFromHookCheck(root);
|
|
5916
|
+
return;
|
|
5917
|
+
}
|
|
5728
5918
|
if (options.check) {
|
|
5729
5919
|
const pkgJson2 = await readJson(path35.join(packageRoot(), "package.json"));
|
|
5730
5920
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
@@ -5790,6 +5980,36 @@ async function runUpdate(options) {
|
|
|
5790
5980
|
await refreshProjectFiles(root);
|
|
5791
5981
|
log("Update applied with backup in .haus-workflow/backups/. Run haus doctor.");
|
|
5792
5982
|
}
|
|
5983
|
+
async function runFromHookCheck(root) {
|
|
5984
|
+
try {
|
|
5985
|
+
const summary = await readLockSummary(root);
|
|
5986
|
+
if (summary.count === 0) return;
|
|
5987
|
+
const pkgJson = await readJson(path35.join(packageRoot(), "package.json"));
|
|
5988
|
+
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
5989
|
+
const [npmVersion, latestCatalogTag] = await Promise.all([
|
|
5990
|
+
fetchNpmVersionStatus(currentVersion),
|
|
5991
|
+
fetchLatestCatalogTag()
|
|
5992
|
+
]);
|
|
5993
|
+
const installedRef = summary.catalogRef;
|
|
5994
|
+
const catalogBehind = installedRef !== null && latestCatalogTag !== null && installedRef !== latestCatalogTag;
|
|
5995
|
+
const npmBehind = npmVersion.updateAvailable && npmVersion.latest !== null;
|
|
5996
|
+
if (!npmBehind && !catalogBehind) return;
|
|
5997
|
+
const reasons = [];
|
|
5998
|
+
if (npmBehind) reasons.push(`haus package ${currentVersion} \u2192 ${npmVersion.latest} available`);
|
|
5999
|
+
if (catalogBehind) {
|
|
6000
|
+
reasons.push(`catalog installed from ${installedRef}, latest is ${latestCatalogTag}`);
|
|
6001
|
+
}
|
|
6002
|
+
log(
|
|
6003
|
+
JSON.stringify({
|
|
6004
|
+
hookSpecificOutput: {
|
|
6005
|
+
hookEventName: "SessionStart",
|
|
6006
|
+
additionalContext: `This project's haus setup is behind: ${reasons.join("; ")}. Suggest running \`/haus-workflow project:refresh\` to bring it up to date.`
|
|
6007
|
+
}
|
|
6008
|
+
})
|
|
6009
|
+
);
|
|
6010
|
+
} catch {
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
5793
6013
|
async function refreshProjectFiles(root) {
|
|
5794
6014
|
log("Refreshing project .claude/ files...");
|
|
5795
6015
|
try {
|
|
@@ -5838,7 +6058,6 @@ import path37 from "node:path";
|
|
|
5838
6058
|
import fs29 from "node:fs";
|
|
5839
6059
|
import path36 from "node:path";
|
|
5840
6060
|
var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
5841
|
-
var MANIFEST_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
5842
6061
|
function isSafeCatalogPath2(itemPath) {
|
|
5843
6062
|
if (!itemPath || path36.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
5844
6063
|
const normalized = path36.normalize(itemPath);
|
|
@@ -5892,10 +6111,8 @@ function auditManifestStructure(manifestVersion, items) {
|
|
|
5892
6111
|
const failures = [];
|
|
5893
6112
|
const seenIds = /* @__PURE__ */ new Map();
|
|
5894
6113
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
5895
|
-
if (typeof manifestVersion !== "string" || !
|
|
5896
|
-
failures.push(
|
|
5897
|
-
'manifest.json: top-level "version" is not valid semver (expected X.Y.Z or X.Y.Z-pre)'
|
|
5898
|
-
);
|
|
6114
|
+
if (typeof manifestVersion !== "string" || !ITEM_SEMVER_RE.test(manifestVersion)) {
|
|
6115
|
+
failures.push('manifest.json: top-level "version" is not valid semver (expected X.Y.Z)');
|
|
5899
6116
|
}
|
|
5900
6117
|
for (let i = 0; i < items.length; i++) {
|
|
5901
6118
|
const item = items[i];
|
|
@@ -6868,7 +7085,7 @@ program.command("clone").description("Clone a single git repository by URL").arg
|
|
|
6868
7085
|
program.command("refresh").action(runRefresh);
|
|
6869
7086
|
program.command("catalog-audit").action(runCatalogAudit);
|
|
6870
7087
|
program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
|
|
6871
|
-
program.command("update").option("--check").action(runUpdate);
|
|
7088
|
+
program.command("update").option("--check").option("--from-hook", "Emit JSON for the Claude Code SessionStart hook (silent if up to date)").action(runUpdate);
|
|
6872
7089
|
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);
|
|
6873
7090
|
program.command("scaffold [ids...]").description(
|
|
6874
7091
|
"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."
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.2.
|
|
2
|
+
"version": "3.2.1",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "haus.agentic-workflow-standard",
|
|
@@ -1754,7 +1754,7 @@
|
|
|
1754
1754
|
},
|
|
1755
1755
|
{
|
|
1756
1756
|
"id": "haus.sentry-sentry-nestjs-sdk",
|
|
1757
|
-
"version": "1.0.
|
|
1757
|
+
"version": "1.0.1",
|
|
1758
1758
|
"source": "curated",
|
|
1759
1759
|
"type": "skill",
|
|
1760
1760
|
"path": "skills/sentry/sentry-nestjs-sdk",
|
|
@@ -1773,8 +1773,8 @@
|
|
|
1773
1773
|
"license": "MIT",
|
|
1774
1774
|
"licenseConfidence": "high",
|
|
1775
1775
|
"originSourceId": "getsentry-sentry-for-ai",
|
|
1776
|
-
"originUrl": "https://github.com/getsentry/sentry-for-ai/blob/
|
|
1777
|
-
"pinnedRef": "
|
|
1776
|
+
"originUrl": "https://github.com/getsentry/sentry-for-ai/blob/cc302403ebf647fd21c3ca736e1ec7ef6e4ed5a3/skills/sentry-nestjs-sdk/SKILL.md",
|
|
1777
|
+
"pinnedRef": "cc302403ebf647fd21c3ca736e1ec7ef6e4ed5a3",
|
|
1778
1778
|
"ecosystem": "sentry",
|
|
1779
1779
|
"requiresAny": [
|
|
1780
1780
|
{
|
|
@@ -1787,7 +1787,7 @@
|
|
|
1787
1787
|
},
|
|
1788
1788
|
{
|
|
1789
1789
|
"id": "haus.sentry-sentry-nextjs-sdk",
|
|
1790
|
-
"version": "1.0.
|
|
1790
|
+
"version": "1.0.2",
|
|
1791
1791
|
"source": "curated",
|
|
1792
1792
|
"type": "skill",
|
|
1793
1793
|
"path": "skills/sentry/sentry-nextjs-sdk",
|
|
@@ -1806,8 +1806,8 @@
|
|
|
1806
1806
|
"license": "MIT",
|
|
1807
1807
|
"licenseConfidence": "high",
|
|
1808
1808
|
"originSourceId": "getsentry-sentry-for-ai",
|
|
1809
|
-
"originUrl": "https://github.com/getsentry/sentry-for-ai/blob/
|
|
1810
|
-
"pinnedRef": "
|
|
1809
|
+
"originUrl": "https://github.com/getsentry/sentry-for-ai/blob/cc302403ebf647fd21c3ca736e1ec7ef6e4ed5a3/skills/sentry-nextjs-sdk/SKILL.md",
|
|
1810
|
+
"pinnedRef": "cc302403ebf647fd21c3ca736e1ec7ef6e4ed5a3",
|
|
1811
1811
|
"ecosystem": "sentry",
|
|
1812
1812
|
"requiresAny": [
|
|
1813
1813
|
{
|
|
@@ -1820,7 +1820,7 @@
|
|
|
1820
1820
|
},
|
|
1821
1821
|
{
|
|
1822
1822
|
"id": "haus.sentry-sentry-node-sdk",
|
|
1823
|
-
"version": "1.0.
|
|
1823
|
+
"version": "1.0.2",
|
|
1824
1824
|
"source": "curated",
|
|
1825
1825
|
"type": "skill",
|
|
1826
1826
|
"path": "skills/sentry/sentry-node-sdk",
|
|
@@ -1839,8 +1839,8 @@
|
|
|
1839
1839
|
"license": "MIT",
|
|
1840
1840
|
"licenseConfidence": "high",
|
|
1841
1841
|
"originSourceId": "getsentry-sentry-for-ai",
|
|
1842
|
-
"originUrl": "https://github.com/getsentry/sentry-for-ai/blob/
|
|
1843
|
-
"pinnedRef": "
|
|
1842
|
+
"originUrl": "https://github.com/getsentry/sentry-for-ai/blob/cc302403ebf647fd21c3ca736e1ec7ef6e4ed5a3/skills/sentry-node-sdk/SKILL.md",
|
|
1843
|
+
"pinnedRef": "cc302403ebf647fd21c3ca736e1ec7ef6e4ed5a3",
|
|
1844
1844
|
"ecosystem": "sentry",
|
|
1845
1845
|
"requiresAny": [
|
|
1846
1846
|
{
|
|
@@ -1978,7 +1978,7 @@
|
|
|
1978
1978
|
},
|
|
1979
1979
|
{
|
|
1980
1980
|
"id": "haus.stripe-stripe-best-practices",
|
|
1981
|
-
"version": "1.0.
|
|
1981
|
+
"version": "1.0.3",
|
|
1982
1982
|
"source": "curated",
|
|
1983
1983
|
"type": "skill",
|
|
1984
1984
|
"path": "skills/stripe/stripe-best-practices",
|
|
@@ -1988,7 +1988,7 @@
|
|
|
1988
1988
|
"whenNotToUse": "Do not use when a more specific skill or command applies.",
|
|
1989
1989
|
"tags": ["workflow", "stripe", "payments"],
|
|
1990
1990
|
"repoRoles": [],
|
|
1991
|
-
"tokenEstimate":
|
|
1991
|
+
"tokenEstimate": 1018,
|
|
1992
1992
|
"installMode": "copy-selected",
|
|
1993
1993
|
"default": false,
|
|
1994
1994
|
"reviewStatus": "approved",
|
|
@@ -1997,8 +1997,8 @@
|
|
|
1997
1997
|
"license": "MIT",
|
|
1998
1998
|
"licenseConfidence": "medium",
|
|
1999
1999
|
"originSourceId": "stripe-ai",
|
|
2000
|
-
"originUrl": "https://github.com/stripe/ai/blob/
|
|
2001
|
-
"pinnedRef": "
|
|
2000
|
+
"originUrl": "https://github.com/stripe/ai/blob/f54c9e6c0e8b6cfe4e648745eec68ecab3ddc994/skills/stripe-best-practices/SKILL.md",
|
|
2001
|
+
"pinnedRef": "f54c9e6c0e8b6cfe4e648745eec68ecab3ddc994",
|
|
2002
2002
|
"ecosystem": "stripe",
|
|
2003
2003
|
"requiresAny": [
|
|
2004
2004
|
{
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/haus-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Haus AI workflow CLI for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"execa": "9.6.1",
|
|
48
48
|
"fast-glob": "3.3.3",
|
|
49
49
|
"fs-extra": "11.3.5",
|
|
50
|
-
"semver": "7.8.
|
|
50
|
+
"semver": "7.8.5",
|
|
51
51
|
"yaml": "2.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@release-it/conventional-changelog": "11.0.1",
|
|
55
55
|
"@types/fs-extra": "11.0.4",
|
|
56
|
-
"@types/node": "22.
|
|
56
|
+
"@types/node": "22.20.0",
|
|
57
57
|
"@types/semver": "7.7.1",
|
|
58
58
|
"@typescript-eslint/eslint-plugin": "8.59.3",
|
|
59
59
|
"@typescript-eslint/parser": "8.59.3",
|
|
@@ -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.
|