@haus-tech/haus-workflow 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +13 -18
- package/dist/cli.js +271 -112
- package/library/catalog/manifest.json +5 -5
- package/package.json +1 -4
- package/docs/user-guide.md +0 -176
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.10.0...v0.10.1) (2026-05-29)
|
|
4
|
+
|
|
5
|
+
## [0.10.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.9.0...v0.10.0) (2026-05-29)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* Add config command to manage hooks ([1f2c7b4](https://github.com/WeAreHausTech/haus-workflow/commit/1f2c7b4ced3437ef4468759ec7bfc4d8adbf3efc))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* address PR review — remove stale module refs, fix plugin-era wording in docs and commands ([6520321](https://github.com/WeAreHausTech/haus-workflow/commit/6520321e1790a304036984c387fdf59d4febe3dd))
|
|
14
|
+
|
|
3
15
|
## [0.9.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.8.0...v0.9.0) (2026-05-28)
|
|
4
16
|
|
|
5
17
|
### Features
|
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Haus Workflow
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
> **Internal Haus tool.** Open-source but unsupported for external use.
|
|
3
|
+
> Internal Haus tool. Open-source but unsupported for external use. No external issues, PRs, or roadmap commitments accepted.
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
@@ -27,7 +25,7 @@ Run once inside each project:
|
|
|
27
25
|
haus init
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Scans the repo, recommends context assets, and writes `.claude/` and `.haus-workflow/`.
|
|
31
29
|
|
|
32
30
|
---
|
|
33
31
|
|
|
@@ -35,35 +33,32 @@ This scans the repo, recommends context assets, and writes `.claude/` and `.haus
|
|
|
35
33
|
|
|
36
34
|
```bash
|
|
37
35
|
haus init # first-run setup (scan → recommend → apply)
|
|
38
|
-
haus setup-project # re-run setup on
|
|
36
|
+
haus setup-project # re-run setup on existing project
|
|
37
|
+
haus scan # scan repo and write context-map
|
|
38
|
+
haus recommend # score and recommend catalog items
|
|
39
39
|
haus apply --dry-run # preview what would be written
|
|
40
40
|
haus apply --write # write .claude/ files
|
|
41
41
|
haus update # sync remote catalog + refresh lockfile
|
|
42
42
|
haus update --check # check for updates without applying
|
|
43
43
|
haus doctor # health check: hooks, CLAUDE.md, catalog cache
|
|
44
|
+
haus config # manage hook configuration
|
|
45
|
+
haus memory # view project memory store
|
|
46
|
+
haus guard # test bash/file-access guards
|
|
44
47
|
haus uninstall # remove Haus-managed files from ~/.claude/
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
---
|
|
48
51
|
|
|
49
|
-
##
|
|
52
|
+
## Development
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
55
|
yarn install
|
|
53
56
|
yarn verify # typecheck + lint + build + test
|
|
57
|
+
yarn dev <cmd> # run CLI without building (tsx)
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Docs
|
|
60
|
+
### Internal docs
|
|
61
61
|
|
|
62
|
-
- [User guide](docs/user-guide.md)
|
|
63
62
|
- [Architecture](docs/architecture.md)
|
|
64
|
-
- [
|
|
65
|
-
- [Global install layout](docs/global-install.md)
|
|
66
|
-
- [Generated files](docs/generated-files.md)
|
|
67
|
-
- [Updates and lockfile](docs/updates.md)
|
|
63
|
+
- [CLI reference](docs/cli.md)
|
|
68
64
|
- [Security](docs/security.md)
|
|
69
|
-
- [Memory](docs/memory.md)
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path27 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
@@ -70,8 +70,12 @@ async function syncRemoteCatalog() {
|
|
|
70
70
|
return { newItems: [], unchanged: 0, failed: [] };
|
|
71
71
|
}
|
|
72
72
|
await fs.ensureDir(CACHE_DIR);
|
|
73
|
-
await fs.writeFile(
|
|
74
|
-
|
|
73
|
+
await fs.writeFile(
|
|
74
|
+
path.join(CACHE_DIR, "manifest.json"),
|
|
75
|
+
`${JSON.stringify({ items }, null, 2)}
|
|
76
|
+
`,
|
|
77
|
+
"utf8"
|
|
78
|
+
);
|
|
75
79
|
const newItems = [];
|
|
76
80
|
let unchanged = 0;
|
|
77
81
|
const failed = [];
|
|
@@ -374,10 +378,14 @@ import fs4 from "fs-extra";
|
|
|
374
378
|
async function assertPostApplySettingsMatchCanonical(root, canonical) {
|
|
375
379
|
const written = await readJson(claudePath(root, "settings.json"));
|
|
376
380
|
if (written == null || typeof written !== "object") {
|
|
377
|
-
throw new Error(
|
|
381
|
+
throw new Error(
|
|
382
|
+
"haus: post-apply self-check failed: .claude/settings.json missing or unreadable"
|
|
383
|
+
);
|
|
378
384
|
}
|
|
379
385
|
if (!isDeepStrictEqual(canonical, written)) {
|
|
380
|
-
throw new Error(
|
|
386
|
+
throw new Error(
|
|
387
|
+
"haus: post-apply self-check failed: .claude/settings.json does not match canonical hook contract"
|
|
388
|
+
);
|
|
381
389
|
}
|
|
382
390
|
}
|
|
383
391
|
async function verifyProjectSettingsHooksContract(root) {
|
|
@@ -561,7 +569,11 @@ import fs7 from "fs-extra";
|
|
|
561
569
|
var STABLE_ID2 = "template.way-of-work";
|
|
562
570
|
var SCHEMA_VERSION2 = "1";
|
|
563
571
|
var TEMPLATE_REL = "library/global/templates/haus-way-of-work.md";
|
|
564
|
-
var CATALOG_CACHE_TEMPLATE = path8.join(
|
|
572
|
+
var CATALOG_CACHE_TEMPLATE = path8.join(
|
|
573
|
+
os3.homedir(),
|
|
574
|
+
CATALOG_CACHE_SUBDIR,
|
|
575
|
+
"templates/haus-way-of-work.md"
|
|
576
|
+
);
|
|
565
577
|
function makeWayOfWorkHeader(pkgVersion, contentHash) {
|
|
566
578
|
return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
567
579
|
}
|
|
@@ -648,7 +660,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
648
660
|
const wayOfWorkPath = await writeWayOfWork(root, hausVersion, dryRun);
|
|
649
661
|
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
650
662
|
const p6Files = [rootClaudeMdPath, projectFactsPath, ...wayOfWorkPath ? [wayOfWorkPath] : []];
|
|
651
|
-
const files = dryRun ? [...coreFiles, ...p6Files] : [
|
|
663
|
+
const files = dryRun ? [...coreFiles, ...p6Files] : [
|
|
664
|
+
...coreFiles,
|
|
665
|
+
...p6Files,
|
|
666
|
+
hausPath(root, "selected-context.json"),
|
|
667
|
+
hausPath(root, "haus.lock.json")
|
|
668
|
+
];
|
|
652
669
|
const hookSettings = await loadClaudeHooksSettings();
|
|
653
670
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
654
671
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
@@ -656,7 +673,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
656
673
|
if (!await fs8.pathExists(configPath)) {
|
|
657
674
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
658
675
|
}
|
|
659
|
-
await writeManagedText(
|
|
676
|
+
await writeManagedText(
|
|
677
|
+
root,
|
|
678
|
+
claudePath(root, "commands", "haus-doctor.md"),
|
|
679
|
+
"Run `haus doctor`.",
|
|
680
|
+
dryRun
|
|
681
|
+
);
|
|
660
682
|
await writeManagedText(
|
|
661
683
|
root,
|
|
662
684
|
claudePath(root, "commands", "haus-review.md"),
|
|
@@ -680,7 +702,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
680
702
|
const manifestDir = path9.dirname(manifestPath);
|
|
681
703
|
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
682
704
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
683
|
-
const cacheManifest = await readJson(
|
|
705
|
+
const cacheManifest = await readJson(
|
|
706
|
+
path9.join(CACHE_DIR, "manifest.json")
|
|
707
|
+
);
|
|
684
708
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
685
709
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
686
710
|
const installedIds = /* @__PURE__ */ new Set();
|
|
@@ -708,7 +732,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
708
732
|
if (await fs8.pathExists(sourcePath)) {
|
|
709
733
|
if (dryRun) {
|
|
710
734
|
const exists = await fs8.pathExists(destination);
|
|
711
|
-
log(
|
|
735
|
+
log(
|
|
736
|
+
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
737
|
+
);
|
|
712
738
|
} else {
|
|
713
739
|
await fs8.ensureDir(path9.dirname(destination));
|
|
714
740
|
await fs8.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
@@ -718,7 +744,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
718
744
|
installedPathsByItem.set(item.id, [...current, path9.relative(root, destination)]);
|
|
719
745
|
installedIds.add(item.id);
|
|
720
746
|
} else {
|
|
721
|
-
warn(
|
|
747
|
+
warn(
|
|
748
|
+
`Skipping ${item.id}: source not found at ${sourcePath} \u2014 run \`haus update\` to populate catalog cache`
|
|
749
|
+
);
|
|
722
750
|
}
|
|
723
751
|
}
|
|
724
752
|
if (dryRun) return [...new Set(files)];
|
|
@@ -726,7 +754,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
|
|
|
726
754
|
await writeManagedJson(
|
|
727
755
|
root,
|
|
728
756
|
hausPath(root, "selected-context.json"),
|
|
729
|
-
installedItems.map((r) => ({
|
|
757
|
+
installedItems.map((r) => ({
|
|
758
|
+
id: r.id,
|
|
759
|
+
type: r.type,
|
|
760
|
+
reason: r.reason,
|
|
761
|
+
confidenceLevel: r.confidenceLevel
|
|
762
|
+
})),
|
|
730
763
|
false
|
|
731
764
|
);
|
|
732
765
|
const lock = await Promise.all(
|
|
@@ -831,7 +864,9 @@ async function runApply(options) {
|
|
|
831
864
|
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
|
|
832
865
|
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
833
866
|
if (isDryRun) {
|
|
834
|
-
warn(
|
|
867
|
+
warn(
|
|
868
|
+
"Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first."
|
|
869
|
+
);
|
|
835
870
|
} else {
|
|
836
871
|
error(
|
|
837
872
|
"Catalog cache is empty \u2014 cannot install catalog items. Run `haus update` first, or pass --allow-empty-cache to apply core files only."
|
|
@@ -893,7 +928,8 @@ async function runCatalogAudit() {
|
|
|
893
928
|
const failures = [];
|
|
894
929
|
for (const item of items) {
|
|
895
930
|
const text = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
896
|
-
for (const word of FORBIDDEN)
|
|
931
|
+
for (const word of FORBIDDEN)
|
|
932
|
+
if (text.includes(word)) failures.push(`${item.id} has unsupported tag ${word}`);
|
|
897
933
|
}
|
|
898
934
|
if (failures.length) {
|
|
899
935
|
failures.forEach((f) => error(f));
|
|
@@ -903,6 +939,36 @@ async function runCatalogAudit() {
|
|
|
903
939
|
log("Catalog audit passed.");
|
|
904
940
|
}
|
|
905
941
|
|
|
942
|
+
// src/commands/config.ts
|
|
943
|
+
import path12 from "path";
|
|
944
|
+
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
945
|
+
var HOOK_ALIASES = {
|
|
946
|
+
"hook.context": "context",
|
|
947
|
+
"hook.memory": "memoryInject"
|
|
948
|
+
};
|
|
949
|
+
async function runConfig(key, action) {
|
|
950
|
+
const hookKey = HOOK_ALIASES[key];
|
|
951
|
+
if (!hookKey) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const root = process.cwd();
|
|
957
|
+
const configPath = path12.join(root, CONFIG_PATH2);
|
|
958
|
+
const existing = await readJson(configPath);
|
|
959
|
+
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
960
|
+
cfg.hooks ??= {};
|
|
961
|
+
cfg.hooks[hookKey] ??= {};
|
|
962
|
+
if (action === "status") {
|
|
963
|
+
const enabled = cfg.hooks[hookKey]?.enabled === true;
|
|
964
|
+
log(`${key}: ${enabled ? "enabled" : "disabled"}`);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
cfg.hooks[hookKey].enabled = action === "enable";
|
|
968
|
+
await writeJson(configPath, cfg);
|
|
969
|
+
log(`${key} ${action}d`);
|
|
970
|
+
}
|
|
971
|
+
|
|
906
972
|
// src/recommender/explain-recommendation.ts
|
|
907
973
|
function normalizeRecommendation(input2) {
|
|
908
974
|
const recommended = (input2.recommended ?? []).map((item) => {
|
|
@@ -911,7 +977,9 @@ function normalizeRecommendation(input2) {
|
|
|
911
977
|
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
912
978
|
weight: reason.weight ?? 0,
|
|
913
979
|
...reason.signal ? { signal: reason.signal } : {}
|
|
914
|
-
})) ?? [
|
|
980
|
+
})) ?? [
|
|
981
|
+
{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }
|
|
982
|
+
];
|
|
915
983
|
const confidence = item.confidence ?? 0;
|
|
916
984
|
return {
|
|
917
985
|
id: item.id,
|
|
@@ -956,7 +1024,10 @@ function normalizeRecommendation(input2) {
|
|
|
956
1024
|
estimatedContextTokens: input2.estimatedContextTokens ?? recommended.length * 320,
|
|
957
1025
|
selectedRules: input2.selectedRules ?? recommended.length,
|
|
958
1026
|
skippedRules: input2.skippedRules ?? skipped.length,
|
|
959
|
-
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? Math.max(
|
|
1027
|
+
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? Math.max(
|
|
1028
|
+
0,
|
|
1029
|
+
Math.round(skipped.length / Math.max(recommended.length + skipped.length, 1) * 100)
|
|
1030
|
+
)
|
|
960
1031
|
};
|
|
961
1032
|
}
|
|
962
1033
|
function buildRecommendationExplanation(recommendation) {
|
|
@@ -1241,7 +1312,7 @@ function computeRuleIntents(rule) {
|
|
|
1241
1312
|
|
|
1242
1313
|
// src/scanner/scan-project.ts
|
|
1243
1314
|
import { readFile } from "fs/promises";
|
|
1244
|
-
import
|
|
1315
|
+
import path14 from "path";
|
|
1245
1316
|
|
|
1246
1317
|
// src/utils/audit-checks.ts
|
|
1247
1318
|
function isRecord(v) {
|
|
@@ -1268,7 +1339,7 @@ function compareVersions(a, b) {
|
|
|
1268
1339
|
}
|
|
1269
1340
|
|
|
1270
1341
|
// src/scanner/detect-package-manager.ts
|
|
1271
|
-
import
|
|
1342
|
+
import path13 from "path";
|
|
1272
1343
|
import fs9 from "fs-extra";
|
|
1273
1344
|
function detectPackageManager(root, packageManagerField) {
|
|
1274
1345
|
const field = String(packageManagerField ?? "").trim();
|
|
@@ -1287,9 +1358,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1287
1358
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1288
1359
|
return "unknown";
|
|
1289
1360
|
}
|
|
1290
|
-
if (fs9.existsSync(
|
|
1291
|
-
if (fs9.existsSync(
|
|
1292
|
-
if (fs9.existsSync(
|
|
1361
|
+
if (fs9.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
|
|
1362
|
+
if (fs9.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1363
|
+
if (fs9.existsSync(path13.join(root, "package-lock.json"))) return "npm";
|
|
1293
1364
|
return "unknown";
|
|
1294
1365
|
}
|
|
1295
1366
|
|
|
@@ -1350,8 +1421,8 @@ function blocked(rel) {
|
|
|
1350
1421
|
return SENSITIVE.some((x) => x.test(rel));
|
|
1351
1422
|
}
|
|
1352
1423
|
async function scanProject(root, mode = "fast") {
|
|
1353
|
-
const pkg = await readJson(
|
|
1354
|
-
const composer = await readJson(
|
|
1424
|
+
const pkg = await readJson(path14.join(root, "package.json"));
|
|
1425
|
+
const composer = await readJson(path14.join(root, "composer.json"));
|
|
1355
1426
|
const files = await listFiles(root, SAFE_FILES);
|
|
1356
1427
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
1357
1428
|
const deps = dependencySet(pkg, composer);
|
|
@@ -1368,16 +1439,18 @@ async function scanProject(root, mode = "fast") {
|
|
|
1368
1439
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
1369
1440
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
1370
1441
|
}
|
|
1371
|
-
if (safeFiles.some((f) => f.includes("docker-compose")))
|
|
1442
|
+
if (safeFiles.some((f) => f.includes("docker-compose")))
|
|
1443
|
+
crossRepoHints.push("Containerized services detected");
|
|
1372
1444
|
if (safeFiles.some((f) => f.includes("turbo.json") || f.includes("nx.json")))
|
|
1373
1445
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
1374
1446
|
if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
|
|
1375
|
-
if (safeFiles.some((f) => f.includes("wp-content/uploads")))
|
|
1447
|
+
if (safeFiles.some((f) => f.includes("wp-content/uploads")))
|
|
1448
|
+
securityRisks.push("Uploads directory present");
|
|
1376
1449
|
const context = {
|
|
1377
1450
|
mode,
|
|
1378
1451
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1379
1452
|
root,
|
|
1380
|
-
repoName: String(pkg?.name ??
|
|
1453
|
+
repoName: String(pkg?.name ?? path14.basename(root)),
|
|
1381
1454
|
packageManager,
|
|
1382
1455
|
repoRoles: roles,
|
|
1383
1456
|
confidence: computeConfidence(roles, stacks),
|
|
@@ -1392,7 +1465,11 @@ async function scanProject(root, mode = "fast") {
|
|
|
1392
1465
|
composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
|
|
1393
1466
|
};
|
|
1394
1467
|
const scanHashes = Object.fromEntries(
|
|
1395
|
-
await Promise.all(
|
|
1468
|
+
await Promise.all(
|
|
1469
|
+
safeFiles.map(
|
|
1470
|
+
async (f) => [f, hashText(await readFile(path14.join(root, f), "utf8"))]
|
|
1471
|
+
)
|
|
1472
|
+
)
|
|
1396
1473
|
);
|
|
1397
1474
|
const repoSummary = renderSummary(context);
|
|
1398
1475
|
await writeJson(hausPath(root, "context-map.json"), context);
|
|
@@ -1418,9 +1495,11 @@ function detectRoles(deps, files) {
|
|
|
1418
1495
|
if (deps.includes("next") || files.some((f) => f.includes("next.config."))) roles.add("next-app");
|
|
1419
1496
|
if (deps.includes("react")) roles.add("react-app");
|
|
1420
1497
|
if (deps.includes("vite") || files.some((f) => f.includes("vite.config."))) roles.add("vite-app");
|
|
1421
|
-
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1498
|
+
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1499
|
+
roles.add("react-router-app");
|
|
1422
1500
|
if (deps.includes("sanity")) roles.add("sanity-studio");
|
|
1423
|
-
if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
|
|
1501
|
+
if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
|
|
1502
|
+
roles.add("strapi-app");
|
|
1424
1503
|
if (deps.includes("expo")) roles.add("expo-app");
|
|
1425
1504
|
if (deps.includes("@vendure/core")) roles.add("vendure-app");
|
|
1426
1505
|
if (deps.some((d) => d.startsWith("@haus/vendure-")) || files.some((f) => f.includes("vendure-config")))
|
|
@@ -1429,7 +1508,8 @@ function detectRoles(deps, files) {
|
|
|
1429
1508
|
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) roles.add("graphql-api");
|
|
1430
1509
|
if (files.some((f) => f.endsWith("nx.json"))) roles.add("nx-monorepo");
|
|
1431
1510
|
if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
|
|
1432
|
-
if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
|
|
1511
|
+
if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
|
|
1512
|
+
roles.add("laravel-app");
|
|
1433
1513
|
if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
|
|
1434
1514
|
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1435
1515
|
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
@@ -1465,7 +1545,8 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1465
1545
|
if (deps.includes("react")) add("frontend", "react19");
|
|
1466
1546
|
if (deps.includes("vue")) add("frontend", "vue");
|
|
1467
1547
|
if (deps.includes("vite")) add("frontend", "vite8");
|
|
1468
|
-
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1548
|
+
if (deps.includes("react-router") && deps.includes("@react-router/node"))
|
|
1549
|
+
add("frontend", "react-router-v7");
|
|
1469
1550
|
if (deps.includes("tailwindcss") || files.some((f) => f.includes("tailwind.config."))) {
|
|
1470
1551
|
add("frontend", "tailwindcss");
|
|
1471
1552
|
}
|
|
@@ -1484,8 +1565,10 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1484
1565
|
if (deps.includes("react-native")) add("frontend", "react-native");
|
|
1485
1566
|
if (deps.includes("i18next") || deps.includes("react-i18next")) add("tooling", "i18next");
|
|
1486
1567
|
if (deps.includes("bullmq")) add("tooling", "bullmq");
|
|
1487
|
-
if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
|
|
1488
|
-
|
|
1568
|
+
if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
|
|
1569
|
+
add("tooling", "docker");
|
|
1570
|
+
if (deps.includes("pm2") || files.some((f) => f.includes("ecosystem.config")))
|
|
1571
|
+
add("tooling", "pm2");
|
|
1489
1572
|
if (deps.some((d) => d.startsWith("@sentry/"))) add("tooling", "sentry");
|
|
1490
1573
|
if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
|
|
1491
1574
|
if (!deps.includes("prettier")) add("tooling", "missing-prettier");
|
|
@@ -1502,10 +1585,13 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1502
1585
|
if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
|
|
1503
1586
|
if (await hasNeedle(root, files, "@VendurePlugin")) add("backend", "vendure3");
|
|
1504
1587
|
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) add("backend", "graphql");
|
|
1505
|
-
if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
|
|
1588
|
+
if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
|
|
1589
|
+
add("backend", "graphql");
|
|
1506
1590
|
if (deps.includes("laravel/framework")) add("backend", "laravel");
|
|
1507
|
-
if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
|
|
1508
|
-
|
|
1591
|
+
if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
|
|
1592
|
+
add("backend", "laravel");
|
|
1593
|
+
if (files.some((f) => f.endsWith("wp-config.php")) || deps.includes("roots/wordpress"))
|
|
1594
|
+
add("backend", "wordpress");
|
|
1509
1595
|
if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
|
|
1510
1596
|
add("backend", "elementor");
|
|
1511
1597
|
}
|
|
@@ -1548,7 +1634,7 @@ async function hasNeedle(root, files, needle) {
|
|
|
1548
1634
|
);
|
|
1549
1635
|
for (const rel of candidates.slice(0, 300)) {
|
|
1550
1636
|
try {
|
|
1551
|
-
const content = await readFile(
|
|
1637
|
+
const content = await readFile(path14.join(root, rel), "utf8");
|
|
1552
1638
|
if (content.includes(needle)) return true;
|
|
1553
1639
|
} catch {
|
|
1554
1640
|
continue;
|
|
@@ -1641,16 +1727,19 @@ async function runContext(options) {
|
|
|
1641
1727
|
}
|
|
1642
1728
|
|
|
1643
1729
|
// src/commands/doctor.ts
|
|
1644
|
-
import
|
|
1730
|
+
import path15 from "path";
|
|
1645
1731
|
import fs10 from "fs-extra";
|
|
1646
1732
|
|
|
1647
1733
|
// src/update/npm-version.ts
|
|
1648
1734
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
1649
1735
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
1650
1736
|
try {
|
|
1651
|
-
const res = await fetch(
|
|
1652
|
-
|
|
1653
|
-
|
|
1737
|
+
const res = await fetch(
|
|
1738
|
+
`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`,
|
|
1739
|
+
{
|
|
1740
|
+
signal: AbortSignal.timeout(8e3)
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1654
1743
|
if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1655
1744
|
const data = await res.json();
|
|
1656
1745
|
const latest = data?.version;
|
|
@@ -1709,7 +1798,7 @@ async function runDoctor(options) {
|
|
|
1709
1798
|
const enabled = await isHookEnabled(root, key);
|
|
1710
1799
|
log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
1711
1800
|
}
|
|
1712
|
-
const rootClaudeMdPath =
|
|
1801
|
+
const rootClaudeMdPath = path15.join(root, "CLAUDE.md");
|
|
1713
1802
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
1714
1803
|
if (!rootClaudeMdContent) {
|
|
1715
1804
|
warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
|
|
@@ -1729,12 +1818,20 @@ async function runDoctor(options) {
|
|
|
1729
1818
|
warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
|
|
1730
1819
|
} else {
|
|
1731
1820
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1732
|
-
const templatePath =
|
|
1821
|
+
const templatePath = path15.join(
|
|
1822
|
+
packageRoot(),
|
|
1823
|
+
"library",
|
|
1824
|
+
"global",
|
|
1825
|
+
"templates",
|
|
1826
|
+
"haus-way-of-work.md"
|
|
1827
|
+
);
|
|
1733
1828
|
const templateContent = await readText(templatePath);
|
|
1734
1829
|
if (storedHashMatch && templateContent) {
|
|
1735
1830
|
const currentHash = hashText(templateContent);
|
|
1736
1831
|
if (storedHashMatch[1] !== currentHash) {
|
|
1737
|
-
warn(
|
|
1832
|
+
warn(
|
|
1833
|
+
"- .haus-workflow/haus-way-of-work.md: stale (template updated \u2014 run `haus apply --write`)"
|
|
1834
|
+
);
|
|
1738
1835
|
} else {
|
|
1739
1836
|
log("- .haus-workflow/haus-way-of-work.md: OK");
|
|
1740
1837
|
}
|
|
@@ -1767,11 +1864,13 @@ async function runDoctor(options) {
|
|
|
1767
1864
|
log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
1768
1865
|
}
|
|
1769
1866
|
}
|
|
1770
|
-
const pkgJson = await readJson(
|
|
1867
|
+
const pkgJson = await readJson(path15.join(packageRoot(), "package.json"));
|
|
1771
1868
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1772
1869
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1773
1870
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1774
|
-
warn(
|
|
1871
|
+
warn(
|
|
1872
|
+
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`
|
|
1873
|
+
);
|
|
1775
1874
|
process.exitCode = 1;
|
|
1776
1875
|
} else if (npmStatus.latest !== null) {
|
|
1777
1876
|
log(`- CLI: ${currentVersion} (up to date)`);
|
|
@@ -1932,7 +2031,7 @@ async function runGuard(kind, _options) {
|
|
|
1932
2031
|
}
|
|
1933
2032
|
|
|
1934
2033
|
// src/commands/init.ts
|
|
1935
|
-
import
|
|
2034
|
+
import path16 from "path";
|
|
1936
2035
|
import fs11 from "fs-extra";
|
|
1937
2036
|
|
|
1938
2037
|
// src/utils/exec.ts
|
|
@@ -1941,6 +2040,7 @@ async function runCommand(command, args = [], options = {}) {
|
|
|
1941
2040
|
try {
|
|
1942
2041
|
const result = await execa(command, args, {
|
|
1943
2042
|
reject: false,
|
|
2043
|
+
// non-zero exits are returned, not thrown
|
|
1944
2044
|
...options
|
|
1945
2045
|
});
|
|
1946
2046
|
return {
|
|
@@ -2001,7 +2101,9 @@ var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
|
2001
2101
|
async function recommend(root, context) {
|
|
2002
2102
|
const items = await loadCatalog(root);
|
|
2003
2103
|
const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
2004
|
-
const sources = await readJson(
|
|
2104
|
+
const sources = await readJson(
|
|
2105
|
+
hausPath(root, "sources-report.json")
|
|
2106
|
+
) ?? {};
|
|
2005
2107
|
const stackSet = buildStackSet(context);
|
|
2006
2108
|
const depSet = new Set(context.dependencies.map((d) => d.toLowerCase()));
|
|
2007
2109
|
const roleSet = new Set(context.repoRoles.map((r) => r.toLowerCase()));
|
|
@@ -2085,14 +2187,23 @@ async function recommend(root, context) {
|
|
|
2085
2187
|
if (tagMatch) {
|
|
2086
2188
|
pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
|
|
2087
2189
|
}
|
|
2088
|
-
const goalMatch = item.tags.find(
|
|
2190
|
+
const goalMatch = item.tags.find(
|
|
2191
|
+
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
2192
|
+
);
|
|
2089
2193
|
if (goalMatch) {
|
|
2090
2194
|
pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
|
|
2091
2195
|
}
|
|
2092
2196
|
if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
|
|
2093
|
-
pushReason(
|
|
2197
|
+
pushReason(
|
|
2198
|
+
"package-manager-match",
|
|
2199
|
+
"package manager match",
|
|
2200
|
+
10,
|
|
2201
|
+
`packageManager:${context.packageManager}`
|
|
2202
|
+
);
|
|
2094
2203
|
}
|
|
2095
|
-
const configSignal = item.tags.find(
|
|
2204
|
+
const configSignal = item.tags.find(
|
|
2205
|
+
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
2206
|
+
);
|
|
2096
2207
|
if (configSignal) {
|
|
2097
2208
|
pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
|
|
2098
2209
|
}
|
|
@@ -2179,9 +2290,15 @@ async function recommend(root, context) {
|
|
|
2179
2290
|
pushSkipReason("source-approval", "Source not approved", 100);
|
|
2180
2291
|
}
|
|
2181
2292
|
if (securityRiskCount > 0 && !isDefaultBaseline && (item.tags.includes("security") || item.id.includes("security"))) {
|
|
2182
|
-
pushSkipReason(
|
|
2293
|
+
pushSkipReason(
|
|
2294
|
+
"security-risk-penalty",
|
|
2295
|
+
"Security-tagged item penalized by active risk signals",
|
|
2296
|
+
20
|
|
2297
|
+
);
|
|
2183
2298
|
}
|
|
2184
|
-
const positiveReasonCodes = new Set(
|
|
2299
|
+
const positiveReasonCodes = new Set(
|
|
2300
|
+
reasons.map((r) => r.code).filter((c) => c !== "default-baseline")
|
|
2301
|
+
);
|
|
2185
2302
|
const hasRoleSignal = positiveReasonCodes.has("repo-role-match");
|
|
2186
2303
|
const hasDepOrStackSignal = positiveReasonCodes.has("stack-match") || positiveReasonCodes.has("requires-any-match");
|
|
2187
2304
|
if (hasRoleSignal && !hasDepOrStackSignal && !isDefaultBaseline && requiresAny.length === 0) {
|
|
@@ -2252,7 +2369,11 @@ async function recommend(root, context) {
|
|
|
2252
2369
|
};
|
|
2253
2370
|
}
|
|
2254
2371
|
function buildStackSet(context) {
|
|
2255
|
-
return new Set(
|
|
2372
|
+
return new Set(
|
|
2373
|
+
[...context.repoRoles, ...Object.values(context.detectedStacks).flat()].map(
|
|
2374
|
+
(x) => x.toLowerCase()
|
|
2375
|
+
)
|
|
2376
|
+
);
|
|
2256
2377
|
}
|
|
2257
2378
|
function inferRepoEcosystems(roles) {
|
|
2258
2379
|
const ecosystems = /* @__PURE__ */ new Set();
|
|
@@ -2454,7 +2575,7 @@ async function runSetupProject(options) {
|
|
|
2454
2575
|
// src/commands/init.ts
|
|
2455
2576
|
async function runInit(options) {
|
|
2456
2577
|
const root = process.cwd();
|
|
2457
|
-
const hausDir =
|
|
2578
|
+
const hausDir = path16.join(root, ".haus-workflow");
|
|
2458
2579
|
const alreadyInit = await fs11.pathExists(hausDir);
|
|
2459
2580
|
if (alreadyInit) {
|
|
2460
2581
|
log("Haus AI already initialized in this project.");
|
|
@@ -2467,7 +2588,7 @@ async function runInit(options) {
|
|
|
2467
2588
|
|
|
2468
2589
|
// src/install/apply.ts
|
|
2469
2590
|
import crypto2 from "crypto";
|
|
2470
|
-
import
|
|
2591
|
+
import path19 from "path";
|
|
2471
2592
|
import fs13 from "fs-extra";
|
|
2472
2593
|
|
|
2473
2594
|
// src/install/header.ts
|
|
@@ -2502,13 +2623,13 @@ ${content}`;
|
|
|
2502
2623
|
|
|
2503
2624
|
// src/install/manifest.ts
|
|
2504
2625
|
import os5 from "os";
|
|
2505
|
-
import
|
|
2626
|
+
import path17 from "path";
|
|
2506
2627
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
2507
2628
|
function globalClaudeDir() {
|
|
2508
|
-
return
|
|
2629
|
+
return path17.join(os5.homedir(), ".claude");
|
|
2509
2630
|
}
|
|
2510
2631
|
function hausManifestPath() {
|
|
2511
|
-
return
|
|
2632
|
+
return path17.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
2512
2633
|
}
|
|
2513
2634
|
async function readManifest() {
|
|
2514
2635
|
return readJson(hausManifestPath());
|
|
@@ -2527,10 +2648,10 @@ function buildManifest(source, files, hooks) {
|
|
|
2527
2648
|
}
|
|
2528
2649
|
|
|
2529
2650
|
// src/install/settings-merge.ts
|
|
2530
|
-
import
|
|
2651
|
+
import path18 from "path";
|
|
2531
2652
|
import fs12 from "fs-extra";
|
|
2532
2653
|
function settingsJsonPath() {
|
|
2533
|
-
return
|
|
2654
|
+
return path18.join(globalClaudeDir(), "settings.json");
|
|
2534
2655
|
}
|
|
2535
2656
|
async function readSettings() {
|
|
2536
2657
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -2601,7 +2722,7 @@ function hashContent(content) {
|
|
|
2601
2722
|
}
|
|
2602
2723
|
function sourceVersion() {
|
|
2603
2724
|
try {
|
|
2604
|
-
const pkgPath =
|
|
2725
|
+
const pkgPath = path19.join(packageRoot(), "package.json");
|
|
2605
2726
|
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf8"));
|
|
2606
2727
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
2607
2728
|
} catch {
|
|
@@ -2609,32 +2730,32 @@ function sourceVersion() {
|
|
|
2609
2730
|
}
|
|
2610
2731
|
}
|
|
2611
2732
|
function globalSrcDir() {
|
|
2612
|
-
return
|
|
2733
|
+
return path19.join(packageRoot(), "library", "global");
|
|
2613
2734
|
}
|
|
2614
2735
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
2615
2736
|
const entries = [];
|
|
2616
|
-
const skillsDir =
|
|
2737
|
+
const skillsDir = path19.join(srcDir, "skills");
|
|
2617
2738
|
if (fs13.pathExistsSync(skillsDir)) {
|
|
2618
2739
|
for (const skillName of fs13.readdirSync(skillsDir)) {
|
|
2619
|
-
const skillFile =
|
|
2740
|
+
const skillFile = path19.join(skillsDir, skillName, "SKILL.md");
|
|
2620
2741
|
if (fs13.pathExistsSync(skillFile)) {
|
|
2621
2742
|
entries.push({
|
|
2622
2743
|
stableId: `skill.${skillName}`,
|
|
2623
|
-
srcRelPath:
|
|
2624
|
-
destPath:
|
|
2744
|
+
srcRelPath: path19.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
2745
|
+
destPath: path19.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
2625
2746
|
});
|
|
2626
2747
|
}
|
|
2627
2748
|
}
|
|
2628
2749
|
}
|
|
2629
|
-
const agentsDir =
|
|
2750
|
+
const agentsDir = path19.join(srcDir, "agents");
|
|
2630
2751
|
if (fs13.pathExistsSync(agentsDir)) {
|
|
2631
2752
|
for (const agentFile of fs13.readdirSync(agentsDir)) {
|
|
2632
2753
|
if (!agentFile.endsWith(".md")) continue;
|
|
2633
2754
|
const agentName = agentFile.replace(/\.md$/, "");
|
|
2634
2755
|
entries.push({
|
|
2635
2756
|
stableId: `agent.${agentName}`,
|
|
2636
|
-
srcRelPath:
|
|
2637
|
-
destPath:
|
|
2757
|
+
srcRelPath: path19.join("library", "global", "agents", agentFile),
|
|
2758
|
+
destPath: path19.join(claudeDir, "agents", agentFile)
|
|
2638
2759
|
});
|
|
2639
2760
|
}
|
|
2640
2761
|
}
|
|
@@ -2658,7 +2779,7 @@ async function applyInstall(options = {}) {
|
|
|
2658
2779
|
};
|
|
2659
2780
|
const manifestFiles = [];
|
|
2660
2781
|
for (const entry of sourceFiles) {
|
|
2661
|
-
const srcPath =
|
|
2782
|
+
const srcPath = path19.join(packageRoot(), entry.srcRelPath);
|
|
2662
2783
|
const rawContent = await readText(srcPath);
|
|
2663
2784
|
if (rawContent === void 0) {
|
|
2664
2785
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -2714,7 +2835,7 @@ async function applyInstall(options = {}) {
|
|
|
2714
2835
|
schemaVersion: SCHEMA_VERSION3
|
|
2715
2836
|
});
|
|
2716
2837
|
}
|
|
2717
|
-
const fragmentPath =
|
|
2838
|
+
const fragmentPath = path19.join(srcDir, "settings-fragments", "hooks.json");
|
|
2718
2839
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
2719
2840
|
const settings = await readSettings();
|
|
2720
2841
|
const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -2739,7 +2860,10 @@ async function applyInstall(options = {}) {
|
|
|
2739
2860
|
}
|
|
2740
2861
|
if (!dryRun && !check) {
|
|
2741
2862
|
await writeSettings(mergedSettings);
|
|
2742
|
-
const manifest = buildManifest(source, manifestFiles, [
|
|
2863
|
+
const manifest = buildManifest(source, manifestFiles, [
|
|
2864
|
+
...existingManifest?.hooks ?? [],
|
|
2865
|
+
...addedIds
|
|
2866
|
+
]);
|
|
2743
2867
|
await writeManifest(manifest);
|
|
2744
2868
|
}
|
|
2745
2869
|
return result;
|
|
@@ -2783,7 +2907,9 @@ async function runInstall(options) {
|
|
|
2783
2907
|
process.exitCode = 1;
|
|
2784
2908
|
} else if (!options.check && !options.dryRun) {
|
|
2785
2909
|
const total = result.created.length + result.updated.length;
|
|
2786
|
-
log(
|
|
2910
|
+
log(
|
|
2911
|
+
`haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
|
|
2912
|
+
);
|
|
2787
2913
|
}
|
|
2788
2914
|
} catch (err) {
|
|
2789
2915
|
error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2792,7 +2918,12 @@ async function runInstall(options) {
|
|
|
2792
2918
|
}
|
|
2793
2919
|
|
|
2794
2920
|
// src/memory/memory-store.ts
|
|
2795
|
-
var FILES = [
|
|
2921
|
+
var FILES = [
|
|
2922
|
+
"project-learnings.md",
|
|
2923
|
+
"decisions.md",
|
|
2924
|
+
"recurring-issues.md",
|
|
2925
|
+
"client-context.md"
|
|
2926
|
+
];
|
|
2796
2927
|
async function ensureMemory(root) {
|
|
2797
2928
|
await Promise.all(
|
|
2798
2929
|
FILES.map(async (name) => {
|
|
@@ -2849,7 +2980,10 @@ async function runMemory(subcommand, options) {
|
|
|
2849
2980
|
return;
|
|
2850
2981
|
}
|
|
2851
2982
|
const compact = `Task: ${options.task ?? "n/a"}
|
|
2852
|
-
${text}`.slice(
|
|
2983
|
+
${text}`.slice(
|
|
2984
|
+
0,
|
|
2985
|
+
options.fromHook ? 1200 : 4e3
|
|
2986
|
+
);
|
|
2853
2987
|
log(compact);
|
|
2854
2988
|
return;
|
|
2855
2989
|
}
|
|
@@ -2899,12 +3033,12 @@ async function runScan(options) {
|
|
|
2899
3033
|
}
|
|
2900
3034
|
|
|
2901
3035
|
// src/commands/undo.ts
|
|
2902
|
-
import
|
|
3036
|
+
import path20 from "path";
|
|
2903
3037
|
import fs14 from "fs-extra";
|
|
2904
3038
|
var CLAUDE_DIR = ".claude";
|
|
2905
3039
|
async function runUndo(options) {
|
|
2906
3040
|
const root = process.cwd();
|
|
2907
|
-
const targets = [
|
|
3041
|
+
const targets = [path20.join(root, CLAUDE_DIR), path20.join(root, HAUS_DIR)];
|
|
2908
3042
|
const existing = targets.filter((p) => fs14.existsSync(p));
|
|
2909
3043
|
if (existing.length === 0) {
|
|
2910
3044
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
@@ -2912,7 +3046,7 @@ async function runUndo(options) {
|
|
|
2912
3046
|
}
|
|
2913
3047
|
if (!options.yes) {
|
|
2914
3048
|
const ok = await confirm(
|
|
2915
|
-
`Remove ${existing.map((p) =>
|
|
3049
|
+
`Remove ${existing.map((p) => path20.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
2916
3050
|
);
|
|
2917
3051
|
if (!ok) {
|
|
2918
3052
|
log("Cancelled.");
|
|
@@ -2921,13 +3055,13 @@ async function runUndo(options) {
|
|
|
2921
3055
|
}
|
|
2922
3056
|
for (const p of existing) {
|
|
2923
3057
|
await fs14.remove(p);
|
|
2924
|
-
log(`Removed ${
|
|
3058
|
+
log(`Removed ${path20.relative(root, p)}`);
|
|
2925
3059
|
}
|
|
2926
3060
|
}
|
|
2927
3061
|
|
|
2928
3062
|
// src/install/uninstall.ts
|
|
2929
3063
|
import crypto3 from "crypto";
|
|
2930
|
-
import
|
|
3064
|
+
import path21 from "path";
|
|
2931
3065
|
import fs15 from "fs-extra";
|
|
2932
3066
|
async function runUninstall(options = {}) {
|
|
2933
3067
|
const { force = false } = options;
|
|
@@ -2950,19 +3084,21 @@ async function runUninstall(options = {}) {
|
|
|
2950
3084
|
}
|
|
2951
3085
|
const currentHash = `sha256-${crypto3.createHash("sha256").update(content).digest("hex")}`;
|
|
2952
3086
|
if (currentHash !== entry.hash && !force) {
|
|
2953
|
-
warn(
|
|
3087
|
+
warn(
|
|
3088
|
+
`Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
|
|
3089
|
+
);
|
|
2954
3090
|
result.skipped.push(entry.destPath);
|
|
2955
3091
|
continue;
|
|
2956
3092
|
}
|
|
2957
3093
|
await fs15.remove(entry.destPath);
|
|
2958
|
-
await pruneEmptyDir(
|
|
3094
|
+
await pruneEmptyDir(path21.dirname(entry.destPath));
|
|
2959
3095
|
result.deleted.push(entry.destPath);
|
|
2960
3096
|
}
|
|
2961
3097
|
const settings = await readSettings();
|
|
2962
3098
|
const stripped = stripHausHooks(settings);
|
|
2963
3099
|
await writeSettings(stripped);
|
|
2964
3100
|
result.hooksStripped = true;
|
|
2965
|
-
const hausDir =
|
|
3101
|
+
const hausDir = path21.join(globalClaudeDir(), "haus");
|
|
2966
3102
|
const manifestPath = hausManifestPath();
|
|
2967
3103
|
if (fs15.pathExistsSync(manifestPath)) {
|
|
2968
3104
|
await fs15.remove(manifestPath);
|
|
@@ -3007,7 +3143,7 @@ async function runUninstallCommand(options) {
|
|
|
3007
3143
|
}
|
|
3008
3144
|
|
|
3009
3145
|
// src/commands/update.ts
|
|
3010
|
-
import
|
|
3146
|
+
import path23 from "path";
|
|
3011
3147
|
|
|
3012
3148
|
// src/update/diff-generated-files.ts
|
|
3013
3149
|
function diffGeneratedFiles() {
|
|
@@ -3034,10 +3170,12 @@ function summarizeLockDiff(before, after) {
|
|
|
3034
3170
|
|
|
3035
3171
|
// src/update/lockfile.ts
|
|
3036
3172
|
import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
3037
|
-
import
|
|
3173
|
+
import path22 from "path";
|
|
3038
3174
|
async function checkLock(root) {
|
|
3039
3175
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3040
|
-
const hasValidVersions = lock.every(
|
|
3176
|
+
const hasValidVersions = lock.every(
|
|
3177
|
+
(item) => !item.version || normalizeVersion(item.version) !== null
|
|
3178
|
+
);
|
|
3041
3179
|
const catalogRef = lock[0]?.catalogRef ?? null;
|
|
3042
3180
|
return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
|
|
3043
3181
|
}
|
|
@@ -3053,7 +3191,7 @@ async function applyLock(root) {
|
|
|
3053
3191
|
try {
|
|
3054
3192
|
const backupDir = hausPath(root, "backups");
|
|
3055
3193
|
await mkdir(backupDir, { recursive: true });
|
|
3056
|
-
await copyFile(lockPath,
|
|
3194
|
+
await copyFile(lockPath, path22.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3057
3195
|
} catch {
|
|
3058
3196
|
}
|
|
3059
3197
|
const enriched = await Promise.all(
|
|
@@ -3075,7 +3213,7 @@ function diffLock(before, after) {
|
|
|
3075
3213
|
}
|
|
3076
3214
|
async function hasLocalOverrides(root) {
|
|
3077
3215
|
try {
|
|
3078
|
-
await readFile2(
|
|
3216
|
+
await readFile2(path22.join(root, ".claude", "settings.json"), "utf8");
|
|
3079
3217
|
return true;
|
|
3080
3218
|
} catch {
|
|
3081
3219
|
return false;
|
|
@@ -3087,7 +3225,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3087
3225
|
async function runUpdate(options) {
|
|
3088
3226
|
const root = process.cwd();
|
|
3089
3227
|
if (options.check) {
|
|
3090
|
-
const pkgJson2 = await readJson(
|
|
3228
|
+
const pkgJson2 = await readJson(path23.join(packageRoot(), "package.json"));
|
|
3091
3229
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3092
3230
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3093
3231
|
checkLock(root),
|
|
@@ -3114,7 +3252,7 @@ async function runUpdate(options) {
|
|
|
3114
3252
|
if (!status.ok) process.exitCode = 1;
|
|
3115
3253
|
return;
|
|
3116
3254
|
}
|
|
3117
|
-
const pkgJson = await readJson(
|
|
3255
|
+
const pkgJson = await readJson(path23.join(packageRoot(), "package.json"));
|
|
3118
3256
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3119
3257
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3120
3258
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3145,12 +3283,14 @@ async function runUpdate(options) {
|
|
|
3145
3283
|
|
|
3146
3284
|
// src/commands/validate-catalog.ts
|
|
3147
3285
|
import fs16 from "fs";
|
|
3148
|
-
import
|
|
3286
|
+
import path25 from "path";
|
|
3149
3287
|
|
|
3150
3288
|
// src/catalog/allowed-stacks.ts
|
|
3151
|
-
import
|
|
3289
|
+
import path24 from "path";
|
|
3152
3290
|
async function readAllowedStacks(root) {
|
|
3153
|
-
const data = await readJson(
|
|
3291
|
+
const data = await readJson(
|
|
3292
|
+
path24.join(root, "library", "catalog", "allowed-stacks.json")
|
|
3293
|
+
);
|
|
3154
3294
|
return data?.stacks ?? [];
|
|
3155
3295
|
}
|
|
3156
3296
|
|
|
@@ -3175,7 +3315,12 @@ var FORBIDDEN_TAGS = [
|
|
|
3175
3315
|
var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
|
|
3176
3316
|
var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
|
|
3177
3317
|
var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
|
|
3178
|
-
var RISKY_INSTALL_PATTERNS = [
|
|
3318
|
+
var RISKY_INSTALL_PATTERNS = [
|
|
3319
|
+
/\bnpx\s+-y\b/i,
|
|
3320
|
+
/\bnpx\s+--yes\b/i,
|
|
3321
|
+
/\byarn\s+dlx\b/i,
|
|
3322
|
+
/\bpnpm\s+dlx\b/i
|
|
3323
|
+
];
|
|
3179
3324
|
var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
|
|
3180
3325
|
var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
|
|
3181
3326
|
var HTTP_URL_PATTERN = /^http:\/\//i;
|
|
@@ -3249,11 +3394,11 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3249
3394
|
const failures = [];
|
|
3250
3395
|
for (const item of items) {
|
|
3251
3396
|
if (!item.path) continue;
|
|
3252
|
-
const absPath =
|
|
3397
|
+
const absPath = path25.join(manifestDir, item.path);
|
|
3253
3398
|
if (item.type === "skill") {
|
|
3254
|
-
const skillMd =
|
|
3399
|
+
const skillMd = path25.join(absPath, "SKILL.md");
|
|
3255
3400
|
if (!fs16.existsSync(skillMd)) {
|
|
3256
|
-
failures.push(`${item.id}: missing ${
|
|
3401
|
+
failures.push(`${item.id}: missing ${path25.relative(manifestDir, skillMd)}`);
|
|
3257
3402
|
continue;
|
|
3258
3403
|
}
|
|
3259
3404
|
const text = fs16.readFileSync(skillMd, "utf8");
|
|
@@ -3272,7 +3417,8 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3272
3417
|
}
|
|
3273
3418
|
const lower = text.toLowerCase();
|
|
3274
3419
|
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
3275
|
-
if (lower.includes(phrase))
|
|
3420
|
+
if (lower.includes(phrase))
|
|
3421
|
+
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3276
3422
|
}
|
|
3277
3423
|
} else if (item.type === "template") {
|
|
3278
3424
|
if (!fs16.existsSync(absPath)) {
|
|
@@ -3286,11 +3432,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3286
3432
|
const failures = [];
|
|
3287
3433
|
const dirs = ["skills", "agents"];
|
|
3288
3434
|
for (const dir of dirs) {
|
|
3289
|
-
const abs =
|
|
3435
|
+
const abs = path25.join(manifestDir, dir);
|
|
3290
3436
|
if (!fs16.existsSync(abs)) continue;
|
|
3291
3437
|
walkMd(abs, (file) => {
|
|
3292
3438
|
const text = fs16.readFileSync(file, "utf8");
|
|
3293
|
-
const rel =
|
|
3439
|
+
const rel = path25.relative(manifestDir, file);
|
|
3294
3440
|
const lines = text.split(/\r?\n/);
|
|
3295
3441
|
for (let i = 0; i < lines.length; i++) {
|
|
3296
3442
|
const line = lines[i] ?? "";
|
|
@@ -3310,7 +3456,7 @@ function auditMarkdownContent(manifestDir) {
|
|
|
3310
3456
|
}
|
|
3311
3457
|
function walkMd(dir, fn) {
|
|
3312
3458
|
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
3313
|
-
const full =
|
|
3459
|
+
const full = path25.join(dir, entry.name);
|
|
3314
3460
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
3315
3461
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
3316
3462
|
}
|
|
@@ -3321,8 +3467,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3321
3467
|
process.exitCode = 1;
|
|
3322
3468
|
return;
|
|
3323
3469
|
}
|
|
3324
|
-
const abs =
|
|
3325
|
-
const manifestDir =
|
|
3470
|
+
const abs = path25.resolve(process.cwd(), manifestPath);
|
|
3471
|
+
const manifestDir = path25.dirname(abs);
|
|
3326
3472
|
const data = await readJson(abs);
|
|
3327
3473
|
if (!data?.items) {
|
|
3328
3474
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -3345,7 +3491,13 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3345
3491
|
}
|
|
3346
3492
|
}
|
|
3347
3493
|
}
|
|
3348
|
-
const allFailures = [
|
|
3494
|
+
const allFailures = [
|
|
3495
|
+
...structureFailures,
|
|
3496
|
+
...stackFailures,
|
|
3497
|
+
...fileFailures,
|
|
3498
|
+
...contentFailures,
|
|
3499
|
+
...tagFailures
|
|
3500
|
+
];
|
|
3349
3501
|
if (allFailures.length) {
|
|
3350
3502
|
allFailures.forEach((f) => error(f));
|
|
3351
3503
|
process.exitCode = 1;
|
|
@@ -3355,7 +3507,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3355
3507
|
}
|
|
3356
3508
|
|
|
3357
3509
|
// src/commands/workspace.ts
|
|
3358
|
-
import
|
|
3510
|
+
import path26 from "path";
|
|
3359
3511
|
import YAML from "yaml";
|
|
3360
3512
|
async function runWorkspace(action) {
|
|
3361
3513
|
if (action === "init") {
|
|
@@ -3378,8 +3530,8 @@ relationships: []
|
|
|
3378
3530
|
process.exitCode = 1;
|
|
3379
3531
|
return;
|
|
3380
3532
|
}
|
|
3381
|
-
const
|
|
3382
|
-
const repos =
|
|
3533
|
+
const config2 = YAML.parse(configText);
|
|
3534
|
+
const repos = config2.repos ?? [];
|
|
3383
3535
|
if (repos.length === 0) {
|
|
3384
3536
|
error("No repos configured in haus.workspace.yaml.");
|
|
3385
3537
|
process.exitCode = 1;
|
|
@@ -3388,7 +3540,7 @@ relationships: []
|
|
|
3388
3540
|
const summaries = [];
|
|
3389
3541
|
const ownership = {};
|
|
3390
3542
|
for (const repo of repos) {
|
|
3391
|
-
const repoRoot =
|
|
3543
|
+
const repoRoot = path26.resolve(process.cwd(), repo.path);
|
|
3392
3544
|
const result = await scanProject(repoRoot, "fast");
|
|
3393
3545
|
summaries.push({
|
|
3394
3546
|
name: repo.name,
|
|
@@ -3424,7 +3576,7 @@ ${summaries.map(
|
|
|
3424
3576
|
// src/cli.ts
|
|
3425
3577
|
function cliVersion() {
|
|
3426
3578
|
try {
|
|
3427
|
-
const pkgPath =
|
|
3579
|
+
const pkgPath = path27.join(packageRoot(), "package.json");
|
|
3428
3580
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3429
3581
|
return pkg.version ?? "0.0.0";
|
|
3430
3582
|
} catch {
|
|
@@ -3434,7 +3586,7 @@ function cliVersion() {
|
|
|
3434
3586
|
var program = new Command();
|
|
3435
3587
|
function validateRuntimeNodeVersion() {
|
|
3436
3588
|
try {
|
|
3437
|
-
const pkgPath =
|
|
3589
|
+
const pkgPath = path27.join(packageRoot(), "package.json");
|
|
3438
3590
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3439
3591
|
const requiredRange = pkg.engines?.node;
|
|
3440
3592
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|
|
@@ -3452,7 +3604,10 @@ program.command("scan").option("--json").action(runScan);
|
|
|
3452
3604
|
program.command("recommend").option("--json").action(runRecommend);
|
|
3453
3605
|
program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
|
|
3454
3606
|
program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
3455
|
-
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
3607
|
+
program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
|
|
3608
|
+
"--allow-empty-cache",
|
|
3609
|
+
"Apply core files only when catalog cache is empty (skip catalog items without error)"
|
|
3610
|
+
).action(runApply);
|
|
3456
3611
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3457
3612
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
3458
3613
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|
|
@@ -3471,6 +3626,10 @@ memory.command("promote").action(() => runMemory("promote", {}));
|
|
|
3471
3626
|
var guard = program.command("guard");
|
|
3472
3627
|
guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
|
|
3473
3628
|
guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
|
|
3629
|
+
var config = program.command("config");
|
|
3630
|
+
config.command("enable <key>").description("Enable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "enable"));
|
|
3631
|
+
config.command("disable <key>").description("Disable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "disable"));
|
|
3632
|
+
config.command("status <key>").description("Show current state of a hook (hook.context, hook.memory)").action((key) => runConfig(key, "status"));
|
|
3474
3633
|
var workspace = program.command("workspace");
|
|
3475
3634
|
workspace.command("init").action(() => runWorkspace("init"));
|
|
3476
3635
|
workspace.command("scan").action(() => runWorkspace("scan"));
|
|
@@ -407,14 +407,14 @@
|
|
|
407
407
|
},
|
|
408
408
|
{
|
|
409
409
|
"id": "haus.prettier-setup",
|
|
410
|
-
"version": "1.
|
|
410
|
+
"version": "1.1.0",
|
|
411
411
|
"source": "haus",
|
|
412
412
|
"type": "skill",
|
|
413
413
|
"path": "skills/prettier-setup",
|
|
414
414
|
"title": "Haus Prettier setup",
|
|
415
|
-
"purpose": "Install `@haus-tech/
|
|
416
|
-
"whenToUse": "Use when Prettier is missing from a haus repo
|
|
417
|
-
"whenNotToUse": "Do not use when Prettier is already configured against `@haus-tech/
|
|
415
|
+
"purpose": "Install `@haus-tech/tech-config` and wire `.prettierrc` to the shared prettier config via the `/prettier` subpath export.",
|
|
416
|
+
"whenToUse": "Use when Prettier is missing from a haus repo, when migrating an ad-hoc config, or when migrating from the deprecated `@haus-tech/prettier-config`.",
|
|
417
|
+
"whenNotToUse": "Do not use when Prettier is already configured against `@haus-tech/tech-config/prettier`.",
|
|
418
418
|
"references": [
|
|
419
419
|
"references/conventions.md",
|
|
420
420
|
"references/scope.md",
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
},
|
|
439
439
|
{
|
|
440
440
|
"id": "haus.eslint-setup",
|
|
441
|
-
"version": "1.
|
|
441
|
+
"version": "1.1.0",
|
|
442
442
|
"source": "haus",
|
|
443
443
|
"type": "skill",
|
|
444
444
|
"path": "skills/eslint-setup",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/haus-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Haus AI workflow CLI for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"library/global",
|
|
17
17
|
"library/catalog",
|
|
18
18
|
"tests/fixtures/catalog",
|
|
19
|
-
"docs/user-guide.md",
|
|
20
19
|
"README.md",
|
|
21
20
|
"CHANGELOG.md",
|
|
22
21
|
"LICENSE",
|
|
@@ -31,8 +30,6 @@
|
|
|
31
30
|
"format:write": "prettier --write src scripts",
|
|
32
31
|
"typecheck": "tsc --noEmit",
|
|
33
32
|
"typecheck:scripts": "tsc --noEmit --project tsconfig.scripts.json",
|
|
34
|
-
"cleanup:status": "tsx scripts/cleanup-status.ts",
|
|
35
|
-
"bench:hooks": "tsx scripts/bench-hooks.ts",
|
|
36
33
|
"pack:local": "yarn pack",
|
|
37
34
|
"publish:dry": "npm pack --dry-run",
|
|
38
35
|
"publish:public": "yarn npm publish --access public",
|
package/docs/user-guide.md
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# Haus AI User Guide
|
|
2
|
-
|
|
3
|
-
This guide shows how to use `haus` in a real project, even if you are not a developer.
|
|
4
|
-
|
|
5
|
-
## What Haus AI does
|
|
6
|
-
|
|
7
|
-
Haus AI scans your project, recommends context files/rules, then writes controlled files so Claude works with safer, stack-aware guidance.
|
|
8
|
-
|
|
9
|
-
Main output folders:
|
|
10
|
-
|
|
11
|
-
- `./.claude` (Claude settings/rules/commands)
|
|
12
|
-
- `./.haus-workflow` (scan/recommendation/lock/memory metadata)
|
|
13
|
-
|
|
14
|
-
## Before you start
|
|
15
|
-
|
|
16
|
-
You need:
|
|
17
|
-
|
|
18
|
-
- a project folder on your machine
|
|
19
|
-
- Node.js 22+ (`node --version`)
|
|
20
|
-
- terminal access
|
|
21
|
-
|
|
22
|
-
Check Node:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
node --version
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
If version is below 22, install/update Node first.
|
|
29
|
-
|
|
30
|
-
## Install Haus AI
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install -g @haus-tech/haus-workflow
|
|
34
|
-
haus --help
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Seed `~/.claude/` with Haus skills, agents, and hooks (once per machine):
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
haus install
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### If you switch Node versions often (nvm, Herd, Volta…)
|
|
44
|
-
|
|
45
|
-
`npm install -g` binds to the currently active Node version. Switch Node → `haus` disappears. Two options:
|
|
46
|
-
|
|
47
|
-
1. **Re-install per version.** When you change Node, carry globals forward:
|
|
48
|
-
```bash
|
|
49
|
-
nvm install <new-version> --reinstall-packages-from=current
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
2. **Use a shell alias.** No per-version install needed:
|
|
53
|
-
```bash
|
|
54
|
-
echo 'alias haus="node $(npm root -g)/@haus-tech/haus-workflow/dist/cli.js"' >> ~/.zshrc
|
|
55
|
-
source ~/.zshrc
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Use Haus in a project
|
|
59
|
-
|
|
60
|
-
Move terminal to project root (folder that contains your app code), then run:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
haus setup-project
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Setup modes:
|
|
67
|
-
|
|
68
|
-
- guided: asks simple onboarding questions
|
|
69
|
-
- fast: minimal prompts, default flow
|
|
70
|
-
|
|
71
|
-
## Typical daily workflow
|
|
72
|
-
|
|
73
|
-
### 1) Scan project
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
haus scan --json
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Writes project detection outputs to `./.haus-workflow/*`.
|
|
80
|
-
|
|
81
|
-
### 2) Generate recommendations
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
haus recommend --json
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Creates `./.haus-workflow/recommendation.json` with selected and skipped items, confidence, and reasons.
|
|
88
|
-
|
|
89
|
-
### 3) Preview generated changes
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
haus apply --dry-run
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Shows planned files without writing.
|
|
96
|
-
|
|
97
|
-
### 4) Apply generated files
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
haus apply --write
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Writes generated files and reports overwrite summaries with concise diff counts.
|
|
104
|
-
|
|
105
|
-
### 5) Verify setup health
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
haus doctor
|
|
109
|
-
haus doctor --hooks
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
`--hooks` checks that project hook settings still match the hook contract.
|
|
113
|
-
|
|
114
|
-
## Update flow
|
|
115
|
-
|
|
116
|
-
Check update state:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
haus update --check
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Apply lock refresh:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
haus update
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Update behavior:
|
|
129
|
-
|
|
130
|
-
- preserves local `.claude` overrides
|
|
131
|
-
- backs up lockfile under `./.haus-workflow/backups`
|
|
132
|
-
- prints unified lockfile diff summary
|
|
133
|
-
|
|
134
|
-
## Memory commands
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
haus memory status
|
|
138
|
-
haus memory add "Use explicit transaction boundaries in checkout service"
|
|
139
|
-
haus memory inject --task "review checkout flow"
|
|
140
|
-
haus memory promote
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Memory is local-only in `./.haus-workflow/memory`.
|
|
144
|
-
|
|
145
|
-
## Explain/context commands
|
|
146
|
-
|
|
147
|
-
Use when you need to understand why rules were selected:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
haus explain-recommendation --json
|
|
151
|
-
haus context --task "build shipping plugin" --json
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Claude slash-command usage
|
|
155
|
-
|
|
156
|
-
After `haus apply --write`, command docs are generated in:
|
|
157
|
-
|
|
158
|
-
- `./.claude/commands/haus-doctor.md`
|
|
159
|
-
- `./.claude/commands/haus-review.md`
|
|
160
|
-
|
|
161
|
-
Some environments expose these as slash commands. If not, run the CLI commands directly.
|
|
162
|
-
|
|
163
|
-
## If something fails
|
|
164
|
-
|
|
165
|
-
- `haus: command not found` -> run `npm install -g @haus-tech/haus-workflow` or check Node version
|
|
166
|
-
- Node engine error -> switch to Node 22+
|
|
167
|
-
- hook mismatch in doctor -> run `haus apply --write` again
|
|
168
|
-
- wrong project scanned -> `cd` into correct project root, rerun
|
|
169
|
-
|
|
170
|
-
## Remove generated setup
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
haus undo --yes
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Removes `./.claude` and `./.haus-workflow` in current project.
|