@haus-tech/haus-workflow 0.10.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 +2 -0
- package/README.md +2 -4
- package/dist/cli.js +178 -52
- package/library/catalog/manifest.json +5 -5
- package/package.json +1 -3
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Haus Workflow
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
> **Internal Haus tool.**
|
|
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
|
|
package/dist/cli.js
CHANGED
|
@@ -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));
|
|
@@ -913,7 +949,9 @@ var HOOK_ALIASES = {
|
|
|
913
949
|
async function runConfig(key, action) {
|
|
914
950
|
const hookKey = HOOK_ALIASES[key];
|
|
915
951
|
if (!hookKey) {
|
|
916
|
-
throw new Error(
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Unknown config key "${key}". Valid keys: ${Object.keys(HOOK_ALIASES).join(", ")}`
|
|
954
|
+
);
|
|
917
955
|
}
|
|
918
956
|
const root = process.cwd();
|
|
919
957
|
const configPath = path12.join(root, CONFIG_PATH2);
|
|
@@ -939,7 +977,9 @@ function normalizeRecommendation(input2) {
|
|
|
939
977
|
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
940
978
|
weight: reason.weight ?? 0,
|
|
941
979
|
...reason.signal ? { signal: reason.signal } : {}
|
|
942
|
-
})) ?? [
|
|
980
|
+
})) ?? [
|
|
981
|
+
{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }
|
|
982
|
+
];
|
|
943
983
|
const confidence = item.confidence ?? 0;
|
|
944
984
|
return {
|
|
945
985
|
id: item.id,
|
|
@@ -984,7 +1024,10 @@ function normalizeRecommendation(input2) {
|
|
|
984
1024
|
estimatedContextTokens: input2.estimatedContextTokens ?? recommended.length * 320,
|
|
985
1025
|
selectedRules: input2.selectedRules ?? recommended.length,
|
|
986
1026
|
skippedRules: input2.skippedRules ?? skipped.length,
|
|
987
|
-
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
|
+
)
|
|
988
1031
|
};
|
|
989
1032
|
}
|
|
990
1033
|
function buildRecommendationExplanation(recommendation) {
|
|
@@ -1396,11 +1439,13 @@ async function scanProject(root, mode = "fast") {
|
|
|
1396
1439
|
if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
|
|
1397
1440
|
warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
|
|
1398
1441
|
}
|
|
1399
|
-
if (safeFiles.some((f) => f.includes("docker-compose")))
|
|
1442
|
+
if (safeFiles.some((f) => f.includes("docker-compose")))
|
|
1443
|
+
crossRepoHints.push("Containerized services detected");
|
|
1400
1444
|
if (safeFiles.some((f) => f.includes("turbo.json") || f.includes("nx.json")))
|
|
1401
1445
|
crossRepoHints.push("Monorepo orchestration detected");
|
|
1402
1446
|
if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
|
|
1403
|
-
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");
|
|
1404
1449
|
const context = {
|
|
1405
1450
|
mode,
|
|
1406
1451
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1420,7 +1465,11 @@ async function scanProject(root, mode = "fast") {
|
|
|
1420
1465
|
composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
|
|
1421
1466
|
};
|
|
1422
1467
|
const scanHashes = Object.fromEntries(
|
|
1423
|
-
await Promise.all(
|
|
1468
|
+
await Promise.all(
|
|
1469
|
+
safeFiles.map(
|
|
1470
|
+
async (f) => [f, hashText(await readFile(path14.join(root, f), "utf8"))]
|
|
1471
|
+
)
|
|
1472
|
+
)
|
|
1424
1473
|
);
|
|
1425
1474
|
const repoSummary = renderSummary(context);
|
|
1426
1475
|
await writeJson(hausPath(root, "context-map.json"), context);
|
|
@@ -1446,9 +1495,11 @@ function detectRoles(deps, files) {
|
|
|
1446
1495
|
if (deps.includes("next") || files.some((f) => f.includes("next.config."))) roles.add("next-app");
|
|
1447
1496
|
if (deps.includes("react")) roles.add("react-app");
|
|
1448
1497
|
if (deps.includes("vite") || files.some((f) => f.includes("vite.config."))) roles.add("vite-app");
|
|
1449
|
-
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");
|
|
1450
1500
|
if (deps.includes("sanity")) roles.add("sanity-studio");
|
|
1451
|
-
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");
|
|
1452
1503
|
if (deps.includes("expo")) roles.add("expo-app");
|
|
1453
1504
|
if (deps.includes("@vendure/core")) roles.add("vendure-app");
|
|
1454
1505
|
if (deps.some((d) => d.startsWith("@haus/vendure-")) || files.some((f) => f.includes("vendure-config")))
|
|
@@ -1457,7 +1508,8 @@ function detectRoles(deps, files) {
|
|
|
1457
1508
|
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) roles.add("graphql-api");
|
|
1458
1509
|
if (files.some((f) => f.endsWith("nx.json"))) roles.add("nx-monorepo");
|
|
1459
1510
|
if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
|
|
1460
|
-
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");
|
|
1461
1513
|
if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
|
|
1462
1514
|
const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
|
|
1463
1515
|
const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
|
|
@@ -1493,7 +1545,8 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1493
1545
|
if (deps.includes("react")) add("frontend", "react19");
|
|
1494
1546
|
if (deps.includes("vue")) add("frontend", "vue");
|
|
1495
1547
|
if (deps.includes("vite")) add("frontend", "vite8");
|
|
1496
|
-
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");
|
|
1497
1550
|
if (deps.includes("tailwindcss") || files.some((f) => f.includes("tailwind.config."))) {
|
|
1498
1551
|
add("frontend", "tailwindcss");
|
|
1499
1552
|
}
|
|
@@ -1512,8 +1565,10 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1512
1565
|
if (deps.includes("react-native")) add("frontend", "react-native");
|
|
1513
1566
|
if (deps.includes("i18next") || deps.includes("react-i18next")) add("tooling", "i18next");
|
|
1514
1567
|
if (deps.includes("bullmq")) add("tooling", "bullmq");
|
|
1515
|
-
if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
|
|
1516
|
-
|
|
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");
|
|
1517
1572
|
if (deps.some((d) => d.startsWith("@sentry/"))) add("tooling", "sentry");
|
|
1518
1573
|
if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
|
|
1519
1574
|
if (!deps.includes("prettier")) add("tooling", "missing-prettier");
|
|
@@ -1530,10 +1585,13 @@ async function detectStacks(root, deps, files, packageManager) {
|
|
|
1530
1585
|
if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
|
|
1531
1586
|
if (await hasNeedle(root, files, "@VendurePlugin")) add("backend", "vendure3");
|
|
1532
1587
|
if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) add("backend", "graphql");
|
|
1533
|
-
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");
|
|
1534
1590
|
if (deps.includes("laravel/framework")) add("backend", "laravel");
|
|
1535
|
-
if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
|
|
1536
|
-
|
|
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");
|
|
1537
1595
|
if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
|
|
1538
1596
|
add("backend", "elementor");
|
|
1539
1597
|
}
|
|
@@ -1676,9 +1734,12 @@ import fs10 from "fs-extra";
|
|
|
1676
1734
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
1677
1735
|
async function fetchNpmVersionStatus(currentVersion) {
|
|
1678
1736
|
try {
|
|
1679
|
-
const res = await fetch(
|
|
1680
|
-
|
|
1681
|
-
|
|
1737
|
+
const res = await fetch(
|
|
1738
|
+
`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`,
|
|
1739
|
+
{
|
|
1740
|
+
signal: AbortSignal.timeout(8e3)
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1682
1743
|
if (!res.ok) return { current: currentVersion, latest: null, updateAvailable: false };
|
|
1683
1744
|
const data = await res.json();
|
|
1684
1745
|
const latest = data?.version;
|
|
@@ -1757,12 +1818,20 @@ async function runDoctor(options) {
|
|
|
1757
1818
|
warn("- .haus-workflow/haus-way-of-work.md: no HAUS-MANAGED header (user-owned)");
|
|
1758
1819
|
} else {
|
|
1759
1820
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1760
|
-
const templatePath = path15.join(
|
|
1821
|
+
const templatePath = path15.join(
|
|
1822
|
+
packageRoot(),
|
|
1823
|
+
"library",
|
|
1824
|
+
"global",
|
|
1825
|
+
"templates",
|
|
1826
|
+
"haus-way-of-work.md"
|
|
1827
|
+
);
|
|
1761
1828
|
const templateContent = await readText(templatePath);
|
|
1762
1829
|
if (storedHashMatch && templateContent) {
|
|
1763
1830
|
const currentHash = hashText(templateContent);
|
|
1764
1831
|
if (storedHashMatch[1] !== currentHash) {
|
|
1765
|
-
warn(
|
|
1832
|
+
warn(
|
|
1833
|
+
"- .haus-workflow/haus-way-of-work.md: stale (template updated \u2014 run `haus apply --write`)"
|
|
1834
|
+
);
|
|
1766
1835
|
} else {
|
|
1767
1836
|
log("- .haus-workflow/haus-way-of-work.md: OK");
|
|
1768
1837
|
}
|
|
@@ -1799,7 +1868,9 @@ async function runDoctor(options) {
|
|
|
1799
1868
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
1800
1869
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
1801
1870
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
1802
|
-
warn(
|
|
1871
|
+
warn(
|
|
1872
|
+
`- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`
|
|
1873
|
+
);
|
|
1803
1874
|
process.exitCode = 1;
|
|
1804
1875
|
} else if (npmStatus.latest !== null) {
|
|
1805
1876
|
log(`- CLI: ${currentVersion} (up to date)`);
|
|
@@ -2030,7 +2101,9 @@ var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
|
2030
2101
|
async function recommend(root, context) {
|
|
2031
2102
|
const items = await loadCatalog(root);
|
|
2032
2103
|
const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
|
|
2033
|
-
const sources = await readJson(
|
|
2104
|
+
const sources = await readJson(
|
|
2105
|
+
hausPath(root, "sources-report.json")
|
|
2106
|
+
) ?? {};
|
|
2034
2107
|
const stackSet = buildStackSet(context);
|
|
2035
2108
|
const depSet = new Set(context.dependencies.map((d) => d.toLowerCase()));
|
|
2036
2109
|
const roleSet = new Set(context.repoRoles.map((r) => r.toLowerCase()));
|
|
@@ -2114,14 +2187,23 @@ async function recommend(root, context) {
|
|
|
2114
2187
|
if (tagMatch) {
|
|
2115
2188
|
pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
|
|
2116
2189
|
}
|
|
2117
|
-
const goalMatch = item.tags.find(
|
|
2190
|
+
const goalMatch = item.tags.find(
|
|
2191
|
+
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
2192
|
+
);
|
|
2118
2193
|
if (goalMatch) {
|
|
2119
2194
|
pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
|
|
2120
2195
|
}
|
|
2121
2196
|
if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
|
|
2122
|
-
pushReason(
|
|
2197
|
+
pushReason(
|
|
2198
|
+
"package-manager-match",
|
|
2199
|
+
"package manager match",
|
|
2200
|
+
10,
|
|
2201
|
+
`packageManager:${context.packageManager}`
|
|
2202
|
+
);
|
|
2123
2203
|
}
|
|
2124
|
-
const configSignal = item.tags.find(
|
|
2204
|
+
const configSignal = item.tags.find(
|
|
2205
|
+
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
2206
|
+
);
|
|
2125
2207
|
if (configSignal) {
|
|
2126
2208
|
pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
|
|
2127
2209
|
}
|
|
@@ -2208,9 +2290,15 @@ async function recommend(root, context) {
|
|
|
2208
2290
|
pushSkipReason("source-approval", "Source not approved", 100);
|
|
2209
2291
|
}
|
|
2210
2292
|
if (securityRiskCount > 0 && !isDefaultBaseline && (item.tags.includes("security") || item.id.includes("security"))) {
|
|
2211
|
-
pushSkipReason(
|
|
2293
|
+
pushSkipReason(
|
|
2294
|
+
"security-risk-penalty",
|
|
2295
|
+
"Security-tagged item penalized by active risk signals",
|
|
2296
|
+
20
|
|
2297
|
+
);
|
|
2212
2298
|
}
|
|
2213
|
-
const positiveReasonCodes = new Set(
|
|
2299
|
+
const positiveReasonCodes = new Set(
|
|
2300
|
+
reasons.map((r) => r.code).filter((c) => c !== "default-baseline")
|
|
2301
|
+
);
|
|
2214
2302
|
const hasRoleSignal = positiveReasonCodes.has("repo-role-match");
|
|
2215
2303
|
const hasDepOrStackSignal = positiveReasonCodes.has("stack-match") || positiveReasonCodes.has("requires-any-match");
|
|
2216
2304
|
if (hasRoleSignal && !hasDepOrStackSignal && !isDefaultBaseline && requiresAny.length === 0) {
|
|
@@ -2281,7 +2369,11 @@ async function recommend(root, context) {
|
|
|
2281
2369
|
};
|
|
2282
2370
|
}
|
|
2283
2371
|
function buildStackSet(context) {
|
|
2284
|
-
return new Set(
|
|
2372
|
+
return new Set(
|
|
2373
|
+
[...context.repoRoles, ...Object.values(context.detectedStacks).flat()].map(
|
|
2374
|
+
(x) => x.toLowerCase()
|
|
2375
|
+
)
|
|
2376
|
+
);
|
|
2285
2377
|
}
|
|
2286
2378
|
function inferRepoEcosystems(roles) {
|
|
2287
2379
|
const ecosystems = /* @__PURE__ */ new Set();
|
|
@@ -2768,7 +2860,10 @@ async function applyInstall(options = {}) {
|
|
|
2768
2860
|
}
|
|
2769
2861
|
if (!dryRun && !check) {
|
|
2770
2862
|
await writeSettings(mergedSettings);
|
|
2771
|
-
const manifest = buildManifest(source, manifestFiles, [
|
|
2863
|
+
const manifest = buildManifest(source, manifestFiles, [
|
|
2864
|
+
...existingManifest?.hooks ?? [],
|
|
2865
|
+
...addedIds
|
|
2866
|
+
]);
|
|
2772
2867
|
await writeManifest(manifest);
|
|
2773
2868
|
}
|
|
2774
2869
|
return result;
|
|
@@ -2812,7 +2907,9 @@ async function runInstall(options) {
|
|
|
2812
2907
|
process.exitCode = 1;
|
|
2813
2908
|
} else if (!options.check && !options.dryRun) {
|
|
2814
2909
|
const total = result.created.length + result.updated.length;
|
|
2815
|
-
log(
|
|
2910
|
+
log(
|
|
2911
|
+
`haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
|
|
2912
|
+
);
|
|
2816
2913
|
}
|
|
2817
2914
|
} catch (err) {
|
|
2818
2915
|
error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2821,7 +2918,12 @@ async function runInstall(options) {
|
|
|
2821
2918
|
}
|
|
2822
2919
|
|
|
2823
2920
|
// src/memory/memory-store.ts
|
|
2824
|
-
var FILES = [
|
|
2921
|
+
var FILES = [
|
|
2922
|
+
"project-learnings.md",
|
|
2923
|
+
"decisions.md",
|
|
2924
|
+
"recurring-issues.md",
|
|
2925
|
+
"client-context.md"
|
|
2926
|
+
];
|
|
2825
2927
|
async function ensureMemory(root) {
|
|
2826
2928
|
await Promise.all(
|
|
2827
2929
|
FILES.map(async (name) => {
|
|
@@ -2878,7 +2980,10 @@ async function runMemory(subcommand, options) {
|
|
|
2878
2980
|
return;
|
|
2879
2981
|
}
|
|
2880
2982
|
const compact = `Task: ${options.task ?? "n/a"}
|
|
2881
|
-
${text}`.slice(
|
|
2983
|
+
${text}`.slice(
|
|
2984
|
+
0,
|
|
2985
|
+
options.fromHook ? 1200 : 4e3
|
|
2986
|
+
);
|
|
2882
2987
|
log(compact);
|
|
2883
2988
|
return;
|
|
2884
2989
|
}
|
|
@@ -2979,7 +3084,9 @@ async function runUninstall(options = {}) {
|
|
|
2979
3084
|
}
|
|
2980
3085
|
const currentHash = `sha256-${crypto3.createHash("sha256").update(content).digest("hex")}`;
|
|
2981
3086
|
if (currentHash !== entry.hash && !force) {
|
|
2982
|
-
warn(
|
|
3087
|
+
warn(
|
|
3088
|
+
`Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
|
|
3089
|
+
);
|
|
2983
3090
|
result.skipped.push(entry.destPath);
|
|
2984
3091
|
continue;
|
|
2985
3092
|
}
|
|
@@ -3066,7 +3173,9 @@ import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
|
|
|
3066
3173
|
import path22 from "path";
|
|
3067
3174
|
async function checkLock(root) {
|
|
3068
3175
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3069
|
-
const hasValidVersions = lock.every(
|
|
3176
|
+
const hasValidVersions = lock.every(
|
|
3177
|
+
(item) => !item.version || normalizeVersion(item.version) !== null
|
|
3178
|
+
);
|
|
3070
3179
|
const catalogRef = lock[0]?.catalogRef ?? null;
|
|
3071
3180
|
return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
|
|
3072
3181
|
}
|
|
@@ -3179,7 +3288,9 @@ import path25 from "path";
|
|
|
3179
3288
|
// src/catalog/allowed-stacks.ts
|
|
3180
3289
|
import path24 from "path";
|
|
3181
3290
|
async function readAllowedStacks(root) {
|
|
3182
|
-
const data = await readJson(
|
|
3291
|
+
const data = await readJson(
|
|
3292
|
+
path24.join(root, "library", "catalog", "allowed-stacks.json")
|
|
3293
|
+
);
|
|
3183
3294
|
return data?.stacks ?? [];
|
|
3184
3295
|
}
|
|
3185
3296
|
|
|
@@ -3204,7 +3315,12 @@ var FORBIDDEN_TAGS = [
|
|
|
3204
3315
|
var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
|
|
3205
3316
|
var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
|
|
3206
3317
|
var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
|
|
3207
|
-
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
|
+
];
|
|
3208
3324
|
var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
|
|
3209
3325
|
var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
|
|
3210
3326
|
var HTTP_URL_PATTERN = /^http:\/\//i;
|
|
@@ -3301,7 +3417,8 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
3301
3417
|
}
|
|
3302
3418
|
const lower = text.toLowerCase();
|
|
3303
3419
|
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
3304
|
-
if (lower.includes(phrase))
|
|
3420
|
+
if (lower.includes(phrase))
|
|
3421
|
+
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
3305
3422
|
}
|
|
3306
3423
|
} else if (item.type === "template") {
|
|
3307
3424
|
if (!fs16.existsSync(absPath)) {
|
|
@@ -3374,7 +3491,13 @@ async function runValidateCatalog(manifestPath) {
|
|
|
3374
3491
|
}
|
|
3375
3492
|
}
|
|
3376
3493
|
}
|
|
3377
|
-
const allFailures = [
|
|
3494
|
+
const allFailures = [
|
|
3495
|
+
...structureFailures,
|
|
3496
|
+
...stackFailures,
|
|
3497
|
+
...fileFailures,
|
|
3498
|
+
...contentFailures,
|
|
3499
|
+
...tagFailures
|
|
3500
|
+
];
|
|
3378
3501
|
if (allFailures.length) {
|
|
3379
3502
|
allFailures.forEach((f) => error(f));
|
|
3380
3503
|
process.exitCode = 1;
|
|
@@ -3481,7 +3604,10 @@ program.command("scan").option("--json").action(runScan);
|
|
|
3481
3604
|
program.command("recommend").option("--json").action(runRecommend);
|
|
3482
3605
|
program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
|
|
3483
3606
|
program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
|
|
3484
|
-
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);
|
|
3485
3611
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
3486
3612
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
3487
3613
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|
|
@@ -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.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Haus AI workflow CLI for Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
"format:write": "prettier --write src scripts",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"typecheck:scripts": "tsc --noEmit --project tsconfig.scripts.json",
|
|
33
|
-
"cleanup:status": "tsx scripts/cleanup-status.ts",
|
|
34
|
-
"bench:hooks": "tsx scripts/bench-hooks.ts",
|
|
35
33
|
"pack:local": "yarn pack",
|
|
36
34
|
"publish:dry": "npm pack --dry-run",
|
|
37
35
|
"publish:public": "yarn npm publish --access public",
|