@haus-tech/haus-workflow 0.12.1 → 0.13.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 +18 -0
- package/README.md +1 -1
- package/dist/cli.js +326 -636
- package/library/catalog/manifest.json +31 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path28 from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/apply.ts
|
|
9
|
-
import
|
|
9
|
+
import path10 from "path";
|
|
10
10
|
import checkbox from "@inquirer/checkbox";
|
|
11
11
|
|
|
12
12
|
// src/catalog/remote-catalog.ts
|
|
@@ -75,6 +75,27 @@ function safeJoin(base, itemPath) {
|
|
|
75
75
|
const resolved = path.resolve(base, itemPath);
|
|
76
76
|
return resolved.startsWith(base + path.sep) || resolved === base ? resolved : null;
|
|
77
77
|
}
|
|
78
|
+
function isExternalReference(ref) {
|
|
79
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
|
|
80
|
+
}
|
|
81
|
+
async function downloadSkillReferences(item, destDir) {
|
|
82
|
+
for (const ref of item.references ?? []) {
|
|
83
|
+
if (isExternalReference(ref)) continue;
|
|
84
|
+
const refDest = safeJoin(destDir, ref);
|
|
85
|
+
if (!refDest) {
|
|
86
|
+
warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (await fs.pathExists(refDest)) continue;
|
|
90
|
+
const text = await fetchText(`${REMOTE_BASE}/${item.path}/${ref}`);
|
|
91
|
+
if (text === null) {
|
|
92
|
+
warn(`Failed to fetch reference "${ref}" for ${item.id}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
await fs.ensureDir(path.dirname(refDest));
|
|
96
|
+
await fs.writeFile(refDest, text, "utf8");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
78
99
|
async function syncRemoteCatalog() {
|
|
79
100
|
const items = await fetchRemoteManifest();
|
|
80
101
|
if (!items) {
|
|
@@ -108,6 +129,7 @@ async function syncRemoteCatalog() {
|
|
|
108
129
|
}
|
|
109
130
|
const dest = path.join(destDir, "SKILL.md");
|
|
110
131
|
if (await fs.pathExists(dest)) {
|
|
132
|
+
await downloadSkillReferences(item, destDir);
|
|
111
133
|
unchanged++;
|
|
112
134
|
continue;
|
|
113
135
|
}
|
|
@@ -120,6 +142,7 @@ async function syncRemoteCatalog() {
|
|
|
120
142
|
}
|
|
121
143
|
await fs.ensureDir(path.dirname(dest));
|
|
122
144
|
await fs.writeFile(dest, text, "utf8");
|
|
145
|
+
await downloadSkillReferences(item, destDir);
|
|
123
146
|
newItems.push(item.id);
|
|
124
147
|
} else {
|
|
125
148
|
const dest = safeJoin(CACHE_DIR, item.path);
|
|
@@ -171,8 +194,8 @@ async function getCacheManifestAge() {
|
|
|
171
194
|
}
|
|
172
195
|
|
|
173
196
|
// src/claude/write-claude-files.ts
|
|
174
|
-
import
|
|
175
|
-
import
|
|
197
|
+
import path9 from "path";
|
|
198
|
+
import fs9 from "fs-extra";
|
|
176
199
|
|
|
177
200
|
// src/update/hash-installed.ts
|
|
178
201
|
import path3 from "path";
|
|
@@ -416,8 +439,8 @@ function buildDenyRules() {
|
|
|
416
439
|
for (const command of DANGEROUS_COMMANDS) {
|
|
417
440
|
rules.push(`Bash(${command}:*)`);
|
|
418
441
|
}
|
|
419
|
-
for (const
|
|
420
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
442
|
+
for (const path29 of SENSITIVE_PATHS) {
|
|
443
|
+
const pattern = SENSITIVE_DIRS.has(path29) ? `${path29}/**` : path29;
|
|
421
444
|
for (const tool of FILE_TOOLS) {
|
|
422
445
|
rules.push(`${tool}(${pattern})`);
|
|
423
446
|
}
|
|
@@ -515,109 +538,13 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
515
538
|
return { ok: true, message: "settings.json matches canonical hook contract." };
|
|
516
539
|
}
|
|
517
540
|
|
|
518
|
-
// src/claude/write-
|
|
541
|
+
// src/claude/write-root-claude-md.ts
|
|
519
542
|
import path6 from "path";
|
|
520
543
|
import fs5 from "fs-extra";
|
|
521
|
-
var STABLE_ID = "generated.project-facts";
|
|
522
|
-
var SCHEMA_VERSION = "1";
|
|
523
|
-
function makeHeader(pkgVersion) {
|
|
524
|
-
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} -->`;
|
|
525
|
-
}
|
|
526
|
-
function renderProjectFacts(ctx, rec, pkgVersion) {
|
|
527
|
-
const header = makeHeader(pkgVersion);
|
|
528
|
-
const stackEntries = Object.entries(ctx.detectedStacks ?? {});
|
|
529
|
-
const stackLines = stackEntries.length > 0 ? stackEntries.map(([stack, files]) => {
|
|
530
|
-
const f = files;
|
|
531
|
-
return `- **${stack}**: ${f.slice(0, 3).join(", ")}${f.length > 3 ? ", \u2026" : ""}`;
|
|
532
|
-
}).join("\n") : "- none detected";
|
|
533
|
-
const roles = ctx.repoRoles?.length > 0 ? ctx.repoRoles.join(", ") : "unknown";
|
|
534
|
-
const recLines = rec.recommended.length > 0 ? rec.recommended.map((r) => `- \`${r.id}\` (${r.type}) \u2014 ${r.reason}`).join("\n") : "- none";
|
|
535
|
-
const warningLines = [...ctx.warnings ?? [], ...rec.warnings ?? []];
|
|
536
|
-
const warnSection = warningLines.length > 0 ? warningLines.map((w) => `- ${w}`).join("\n") : "- none";
|
|
537
|
-
const repoName = ctx.repoName ?? path6.basename(ctx.root ?? "unknown");
|
|
538
|
-
return `${header}
|
|
539
|
-
|
|
540
|
-
# What haus found in this project
|
|
541
|
-
|
|
542
|
-
> This is a plain summary of your project that haus wrote automatically, so Claude
|
|
543
|
-
> always has the basics to hand. haus rewrites it on every \`haus apply\`, so don't
|
|
544
|
-
> edit it by hand \u2014 your changes would be replaced next time.
|
|
545
|
-
|
|
546
|
-
**Repo:** ${repoName}
|
|
547
|
-
**Package manager:** ${ctx.packageManager ?? "unknown"}
|
|
548
|
-
**Roles:** ${roles}
|
|
549
|
-
|
|
550
|
-
## Detected stacks
|
|
551
|
-
|
|
552
|
-
${stackLines}
|
|
553
|
-
|
|
554
|
-
## Recommended context
|
|
555
|
-
|
|
556
|
-
${recLines}
|
|
557
|
-
|
|
558
|
-
## Warnings
|
|
559
|
-
|
|
560
|
-
${warnSection}
|
|
561
|
-
`;
|
|
562
|
-
}
|
|
563
|
-
async function writeProjectFacts(root, pkgVersion, dryRun) {
|
|
564
|
-
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
565
|
-
mode: "fast",
|
|
566
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
567
|
-
root,
|
|
568
|
-
repoName: path6.basename(root),
|
|
569
|
-
packageManager: "unknown",
|
|
570
|
-
repoRoles: [],
|
|
571
|
-
confidence: 0,
|
|
572
|
-
detectedStacks: {},
|
|
573
|
-
dependencies: [],
|
|
574
|
-
securityRisks: [],
|
|
575
|
-
crossRepoHints: [],
|
|
576
|
-
warnings: [],
|
|
577
|
-
detectionStatus: "unknown",
|
|
578
|
-
unsupportedSignals: []
|
|
579
|
-
};
|
|
580
|
-
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
581
|
-
mode: "fast",
|
|
582
|
-
recommended: [],
|
|
583
|
-
skipped: [],
|
|
584
|
-
warnings: [],
|
|
585
|
-
estimatedContextTokens: 0,
|
|
586
|
-
selectedRules: 0,
|
|
587
|
-
skippedRules: 0,
|
|
588
|
-
estimatedTokenReductionPct: 0
|
|
589
|
-
};
|
|
590
|
-
const destPath = hausPath(root, "project.md");
|
|
591
|
-
const printable = displayPath(root, destPath);
|
|
592
|
-
const next = renderProjectFacts(ctx, rec, pkgVersion);
|
|
593
|
-
const prev = await fs5.pathExists(destPath) ? await fs5.readFile(destPath, "utf8") : "";
|
|
594
|
-
if (dryRun) {
|
|
595
|
-
if (!prev) {
|
|
596
|
-
log(createUnifiedDiff(printable, "", next));
|
|
597
|
-
} else if (hasTextChanged(prev, next)) {
|
|
598
|
-
log(createUnifiedDiff(printable, prev, next));
|
|
599
|
-
} else {
|
|
600
|
-
log(`${printable}: unchanged`);
|
|
601
|
-
}
|
|
602
|
-
return destPath;
|
|
603
|
-
}
|
|
604
|
-
if (hasTextChanged(prev, next) && prev.length > 0) {
|
|
605
|
-
const diffText = createUnifiedDiff(printable, prev, next);
|
|
606
|
-
const summary = summarizeDiff(diffText);
|
|
607
|
-
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
608
|
-
}
|
|
609
|
-
await writeText(destPath, next);
|
|
610
|
-
return destPath;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// src/claude/write-root-claude-md.ts
|
|
614
|
-
import path7 from "path";
|
|
615
|
-
import fs6 from "fs-extra";
|
|
616
544
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
617
545
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
618
546
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
619
|
-
@.haus-workflow/workflow-config.md
|
|
620
|
-
@.haus-workflow/project.md`;
|
|
547
|
+
@.haus-workflow/workflow-config.md`;
|
|
621
548
|
function buildImportBlock() {
|
|
622
549
|
return `${BLOCK_BEGIN}
|
|
623
550
|
${IMPORT_CONTENT}
|
|
@@ -642,9 +569,9 @@ ${block}
|
|
|
642
569
|
`;
|
|
643
570
|
}
|
|
644
571
|
async function writeRootClaudeMd(root, dryRun) {
|
|
645
|
-
const filePath =
|
|
572
|
+
const filePath = path6.join(root, "CLAUDE.md");
|
|
646
573
|
const block = buildImportBlock();
|
|
647
|
-
const prev = await
|
|
574
|
+
const prev = await fs5.pathExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
|
|
648
575
|
const next = injectHausBlock(prev, block);
|
|
649
576
|
const printable = displayPath(root, filePath);
|
|
650
577
|
if (dryRun) {
|
|
@@ -667,22 +594,12 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
667
594
|
}
|
|
668
595
|
|
|
669
596
|
// src/claude/write-workflow-config.ts
|
|
670
|
-
import path9 from "path";
|
|
671
|
-
import fs8 from "fs-extra";
|
|
672
|
-
|
|
673
|
-
// src/claude/derive-workflow-config.ts
|
|
674
597
|
import path8 from "path";
|
|
675
598
|
import fs7 from "fs-extra";
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
"joi",
|
|
681
|
-
"@hapi/joi",
|
|
682
|
-
"class-validator",
|
|
683
|
-
"superstruct",
|
|
684
|
-
"ajv"
|
|
685
|
-
];
|
|
599
|
+
|
|
600
|
+
// src/claude/derive-workflow-config.ts
|
|
601
|
+
import path7 from "path";
|
|
602
|
+
import fs6 from "fs-extra";
|
|
686
603
|
function binCmd(pm, bin, args) {
|
|
687
604
|
const tail = args ? ` ${args}` : "";
|
|
688
605
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -691,7 +608,7 @@ function binCmd(pm, bin, args) {
|
|
|
691
608
|
}
|
|
692
609
|
async function deriveWorkflowConfig(root, ctx) {
|
|
693
610
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
694
|
-
const pkg = await readJson(
|
|
611
|
+
const pkg = await readJson(path7.join(root, "package.json"));
|
|
695
612
|
const scripts = pkg?.scripts ?? {};
|
|
696
613
|
const deps = new Set(ctx.dependencies);
|
|
697
614
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -701,22 +618,13 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
701
618
|
return null;
|
|
702
619
|
};
|
|
703
620
|
const hasDep = (name) => deps.has(name);
|
|
704
|
-
const exists = (rel) =>
|
|
705
|
-
const hasTypeScript = hasDep("typescript") || exists("tsconfig.json");
|
|
706
|
-
const hasEslint = hasDep("eslint");
|
|
707
|
-
const hasPrettier = hasDep("prettier");
|
|
621
|
+
const exists = (rel) => fs6.pathExistsSync(path7.join(root, rel));
|
|
708
622
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
709
623
|
const hasCypress = hasDep("cypress");
|
|
710
624
|
const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
|
|
711
625
|
return {
|
|
712
626
|
test: script("test") ?? `${pm} test`,
|
|
713
627
|
testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
|
|
714
|
-
typecheck: firstScript("typecheck", "type-check", "tsc") ?? (hasTypeScript ? binCmd(pm, "tsc", "--noEmit") : null),
|
|
715
|
-
lint: script("lint") ?? (hasEslint ? binCmd(pm, "eslint", ".") : null),
|
|
716
|
-
lintFix: firstScript("lint:fix", "lint-fix") ?? (scripts.lint ? `${pm} run lint -- --fix` : hasEslint ? binCmd(pm, "eslint", ". --fix") : null),
|
|
717
|
-
formatCheck: firstScript("format:check", "format-check", "prettier:check") ?? (hasPrettier ? binCmd(pm, "prettier", "--check .") : null),
|
|
718
|
-
securityAudit: `${pm} audit`,
|
|
719
|
-
validationLibrary: VALIDATION_LIBS.find((lib) => deps.has(lib)) ?? null,
|
|
720
628
|
preCommitTool,
|
|
721
629
|
specPath: exists("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
722
630
|
designPath: exists("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
@@ -731,13 +639,12 @@ function fields(v) {
|
|
|
731
639
|
{ prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
|
|
732
640
|
{ prefix: "- UX flows: ", value: v.uxPath, hint: "path, e.g. docs/UX.md" },
|
|
733
641
|
{ prefix: "- Test (unit + integration): ", value: v.test, hint: "command", code: true },
|
|
734
|
-
{
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
{ prefix: "- Library: ", value: v.validationLibrary, hint: "e.g. zod, yup, joi" },
|
|
642
|
+
{
|
|
643
|
+
prefix: "- Test (E2E): ",
|
|
644
|
+
value: v.testE2E,
|
|
645
|
+
hint: "command, e.g. playwright test",
|
|
646
|
+
code: true
|
|
647
|
+
},
|
|
741
648
|
{ prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
|
|
742
649
|
];
|
|
743
650
|
}
|
|
@@ -751,7 +658,7 @@ function line(f) {
|
|
|
751
658
|
function buildWorkflowConfig(v) {
|
|
752
659
|
const f = fields(v);
|
|
753
660
|
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
754
|
-
return "# How this project works (
|
|
661
|
+
return "# How this project works (workflow methodology bindings)\n\n> The few project-specific values the workflow standard (WORKFLOW.md) binds to:\n> where the source-of-truth docs live, the test commands the TDD/verification gate\n> runs, the highest-stakes logic, and the pre-commit tool. This file is yours to\n> edit and haus will not overwrite it.\n>\n> Everyday commands (dev, build, lint, typecheck, format) and project documentation\n> live in `CLAUDE.md` + `docs/` \u2014 run **`/docs`** to generate/refresh them.\n\n## Source-of-truth documents\n" + byPrefix("- Spec: ") + "\n" + byPrefix("- Design: ") + "\n" + byPrefix("- UX flows: ") + "\n\n## Test commands (TDD / verification gate)\n" + byPrefix("- Test (unit + integration): ") + "\n" + byPrefix("- Test (E2E): ") + "\n\n## Highest-stakes logic\n<!-- fill in domain areas requiring TDD-only treatment, e.g. payment flows, auth, medical data -->\n\n## Pre-commit tool\n" + byPrefix("- Tool: ") + "\n";
|
|
755
662
|
}
|
|
756
663
|
function refillContent(existing, v) {
|
|
757
664
|
const f = fields(v);
|
|
@@ -769,7 +676,6 @@ var FALLBACK_CONTEXT = {
|
|
|
769
676
|
repoName: "",
|
|
770
677
|
packageManager: "unknown",
|
|
771
678
|
repoRoles: [],
|
|
772
|
-
confidence: 0,
|
|
773
679
|
detectedStacks: {},
|
|
774
680
|
dependencies: [],
|
|
775
681
|
securityRisks: [],
|
|
@@ -781,7 +687,7 @@ var FALLBACK_CONTEXT = {
|
|
|
781
687
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
782
688
|
const destPath = hausPath(root, "workflow-config.md");
|
|
783
689
|
const printable = displayPath(root, destPath);
|
|
784
|
-
const exists = await
|
|
690
|
+
const exists = await fs7.pathExists(destPath);
|
|
785
691
|
if (exists && !opts.refill) {
|
|
786
692
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
787
693
|
return null;
|
|
@@ -789,11 +695,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
789
695
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
790
696
|
...FALLBACK_CONTEXT,
|
|
791
697
|
root,
|
|
792
|
-
repoName:
|
|
698
|
+
repoName: path8.basename(root)
|
|
793
699
|
};
|
|
794
700
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
795
701
|
if (exists) {
|
|
796
|
-
const current = await
|
|
702
|
+
const current = await fs7.readFile(destPath, "utf8");
|
|
797
703
|
const refilled = refillContent(current, values);
|
|
798
704
|
if (refilled === current) {
|
|
799
705
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -815,7 +721,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
815
721
|
}
|
|
816
722
|
|
|
817
723
|
// src/claude/write-workflow.ts
|
|
818
|
-
import
|
|
724
|
+
import fs8 from "fs-extra";
|
|
819
725
|
|
|
820
726
|
// src/claude/managed-template.ts
|
|
821
727
|
function normaliseLF(content2) {
|
|
@@ -829,10 +735,10 @@ function parseHausManagedHeader(line2) {
|
|
|
829
735
|
}
|
|
830
736
|
|
|
831
737
|
// src/claude/write-workflow.ts
|
|
832
|
-
var
|
|
833
|
-
var
|
|
738
|
+
var STABLE_ID = "template.workflow";
|
|
739
|
+
var SCHEMA_VERSION = "1";
|
|
834
740
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
835
|
-
return `<!-- HAUS-MANAGED id=${
|
|
741
|
+
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
836
742
|
}
|
|
837
743
|
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
838
744
|
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
@@ -848,16 +754,16 @@ async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
|
848
754
|
${templateContent}`;
|
|
849
755
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
850
756
|
const printable = displayPath(root, destPath);
|
|
851
|
-
if (await
|
|
852
|
-
const existing = await
|
|
757
|
+
if (await fs8.pathExists(destPath)) {
|
|
758
|
+
const existing = await fs8.readFile(destPath, "utf8");
|
|
853
759
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
854
760
|
const parsed = parseHausManagedHeader(firstLine);
|
|
855
761
|
if (!parsed) {
|
|
856
762
|
warn(`${printable}: no HAUS-MANAGED header \u2014 file appears user-owned, skipping`);
|
|
857
763
|
return null;
|
|
858
764
|
}
|
|
859
|
-
if (parsed.id !==
|
|
860
|
-
warn(`${printable}: HAUS-MANAGED id mismatch (expected ${
|
|
765
|
+
if (parsed.id !== STABLE_ID) {
|
|
766
|
+
warn(`${printable}: HAUS-MANAGED id mismatch (expected ${STABLE_ID}) \u2014 skipping`);
|
|
861
767
|
return null;
|
|
862
768
|
}
|
|
863
769
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
@@ -871,7 +777,7 @@ ${templateContent}`;
|
|
|
871
777
|
}
|
|
872
778
|
}
|
|
873
779
|
if (dryRun) {
|
|
874
|
-
const prev = await
|
|
780
|
+
const prev = await fs8.pathExists(destPath) ? await fs8.readFile(destPath, "utf8") : "";
|
|
875
781
|
if (!prev) {
|
|
876
782
|
log(createUnifiedDiff(printable, "", next));
|
|
877
783
|
} else {
|
|
@@ -898,7 +804,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
898
804
|
estimatedTokenReductionPct: 0
|
|
899
805
|
};
|
|
900
806
|
const pkgRoot = packageRoot();
|
|
901
|
-
const hausVersion = (await readJson(
|
|
807
|
+
const hausVersion = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
902
808
|
const coreFiles = [
|
|
903
809
|
claudePath(root, "settings.json"),
|
|
904
810
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -911,10 +817,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
911
817
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
912
818
|
refill: opts.refillConfig
|
|
913
819
|
});
|
|
914
|
-
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
915
820
|
const p6Files = [
|
|
916
821
|
rootClaudeMdPath,
|
|
917
|
-
projectFactsPath,
|
|
918
822
|
...workflowPath ? [workflowPath] : [],
|
|
919
823
|
...workflowConfigPath ? [workflowConfigPath] : []
|
|
920
824
|
];
|
|
@@ -928,7 +832,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
928
832
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
929
833
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
930
834
|
const configPath = hausPath(root, "config.json");
|
|
931
|
-
if (!await
|
|
835
|
+
if (!await fs9.pathExists(configPath)) {
|
|
932
836
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
933
837
|
}
|
|
934
838
|
await writeManagedText(
|
|
@@ -956,12 +860,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
956
860
|
dryRun
|
|
957
861
|
);
|
|
958
862
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
959
|
-
const manifestPath = fixtureManifestPath ??
|
|
960
|
-
const manifestDir =
|
|
863
|
+
const manifestPath = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
864
|
+
const manifestDir = path9.dirname(manifestPath);
|
|
961
865
|
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
962
866
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
963
867
|
const cacheManifest = await readJson(
|
|
964
|
-
|
|
868
|
+
path9.join(CACHE_DIR, "manifest.json")
|
|
965
869
|
);
|
|
966
870
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
967
871
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
@@ -983,23 +887,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
983
887
|
}
|
|
984
888
|
}
|
|
985
889
|
const cachedItem = cacheManifestById.get(item.id);
|
|
986
|
-
const cachePath = cachedItem?.path ?
|
|
987
|
-
const sourcePath = cachePath && await
|
|
890
|
+
const cachePath = cachedItem?.path ? path9.join(CACHE_DIR, cachedItem.path) : null;
|
|
891
|
+
const sourcePath = cachePath && await fs9.pathExists(cachePath) ? cachePath : path9.join(manifestDir, manifestItem.path);
|
|
988
892
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
989
|
-
const destination = claudePath(root, target,
|
|
990
|
-
if (await
|
|
893
|
+
const destination = claudePath(root, target, path9.basename(sourcePath));
|
|
894
|
+
if (await fs9.pathExists(sourcePath)) {
|
|
991
895
|
if (dryRun) {
|
|
992
|
-
const exists = await
|
|
896
|
+
const exists = await fs9.pathExists(destination);
|
|
993
897
|
log(
|
|
994
898
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
995
899
|
);
|
|
996
900
|
} else {
|
|
997
|
-
await
|
|
998
|
-
await
|
|
901
|
+
await fs9.ensureDir(path9.dirname(destination));
|
|
902
|
+
await fs9.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
999
903
|
}
|
|
1000
904
|
files.push(destination);
|
|
1001
905
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
1002
|
-
installedPathsByItem.set(item.id, [...current,
|
|
906
|
+
installedPathsByItem.set(item.id, [...current, path9.relative(root, destination)]);
|
|
1003
907
|
installedIds.add(item.id);
|
|
1004
908
|
} else {
|
|
1005
909
|
warn(
|
|
@@ -1016,7 +920,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1016
920
|
id: r.id,
|
|
1017
921
|
type: r.type,
|
|
1018
922
|
reason: r.reason,
|
|
1019
|
-
|
|
923
|
+
selectionMode: r.selectionMode
|
|
1020
924
|
})),
|
|
1021
925
|
false
|
|
1022
926
|
);
|
|
@@ -1050,7 +954,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1050
954
|
return [...new Set(files)];
|
|
1051
955
|
}
|
|
1052
956
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
1053
|
-
const prev = await
|
|
957
|
+
const prev = await fs9.pathExists(filePath) ? await fs9.readFile(filePath, "utf8") : "";
|
|
1054
958
|
const printable = displayPath(root, filePath);
|
|
1055
959
|
if (dryRun) {
|
|
1056
960
|
if (!prev) {
|
|
@@ -1077,7 +981,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
1077
981
|
|
|
1078
982
|
// src/commands/apply.ts
|
|
1079
983
|
async function cacheHasItems() {
|
|
1080
|
-
const data = await readJson(
|
|
984
|
+
const data = await readJson(path10.join(CACHE_DIR, "manifest.json"));
|
|
1081
985
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
1082
986
|
}
|
|
1083
987
|
async function runApply(options) {
|
|
@@ -1104,7 +1008,7 @@ async function runApply(options) {
|
|
|
1104
1008
|
} else {
|
|
1105
1009
|
const items = rec.recommended;
|
|
1106
1010
|
const choices = items.map((item) => ({
|
|
1107
|
-
name: `${item.id} [${item.
|
|
1011
|
+
name: `${item.id} [${item.selectionMode}] \u2014 ${item.reason}`,
|
|
1108
1012
|
value: item.id,
|
|
1109
1013
|
checked: true
|
|
1110
1014
|
}));
|
|
@@ -1147,8 +1051,8 @@ async function runApply(options) {
|
|
|
1147
1051
|
|
|
1148
1052
|
// src/catalog/load-catalog.ts
|
|
1149
1053
|
import os3 from "os";
|
|
1150
|
-
import
|
|
1151
|
-
var CACHE_MANIFEST =
|
|
1054
|
+
import path11 from "path";
|
|
1055
|
+
var CACHE_MANIFEST = path11.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
|
|
1152
1056
|
async function loadCatalog(root) {
|
|
1153
1057
|
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
1154
1058
|
if (envPath) {
|
|
@@ -1157,10 +1061,10 @@ async function loadCatalog(root) {
|
|
|
1157
1061
|
}
|
|
1158
1062
|
const cacheData = await readJson(CACHE_MANIFEST);
|
|
1159
1063
|
if (cacheData?.items?.length) return cacheData.items;
|
|
1160
|
-
const localManifest =
|
|
1064
|
+
const localManifest = path11.join(root, "library/catalog/manifest.json");
|
|
1161
1065
|
const localData = await readJson(localManifest);
|
|
1162
1066
|
if (localData?.items?.length) return localData.items;
|
|
1163
|
-
const packageManifest =
|
|
1067
|
+
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
1164
1068
|
const data = await readJson(packageManifest);
|
|
1165
1069
|
return data?.items ?? [];
|
|
1166
1070
|
}
|
|
@@ -1200,7 +1104,7 @@ async function runCatalogAudit() {
|
|
|
1200
1104
|
}
|
|
1201
1105
|
|
|
1202
1106
|
// src/commands/config.ts
|
|
1203
|
-
import
|
|
1107
|
+
import path12 from "path";
|
|
1204
1108
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1205
1109
|
var HOOK_ALIASES = {
|
|
1206
1110
|
"hook.context": "context"
|
|
@@ -1213,7 +1117,7 @@ async function runConfig(key, action) {
|
|
|
1213
1117
|
);
|
|
1214
1118
|
}
|
|
1215
1119
|
const root = process.cwd();
|
|
1216
|
-
const configPath =
|
|
1120
|
+
const configPath = path12.join(root, CONFIG_PATH2);
|
|
1217
1121
|
const existing = await readJson(configPath);
|
|
1218
1122
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1219
1123
|
cfg.hooks ??= {};
|
|
@@ -1234,27 +1138,15 @@ function normalizeRecommendation(input2) {
|
|
|
1234
1138
|
const normalizedReasons = item.reasons?.map((reason) => ({
|
|
1235
1139
|
code: reason.code ?? "legacy-reason",
|
|
1236
1140
|
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
1237
|
-
weight: reason.weight ?? 0,
|
|
1238
1141
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1239
|
-
})) ?? [
|
|
1240
|
-
{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }
|
|
1241
|
-
];
|
|
1242
|
-
const confidence = item.confidence ?? 0;
|
|
1142
|
+
})) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
|
|
1243
1143
|
return {
|
|
1244
1144
|
id: item.id,
|
|
1245
1145
|
type: item.type ?? "skill",
|
|
1246
1146
|
reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
|
|
1247
1147
|
reasons: normalizedReasons,
|
|
1248
|
-
confidence,
|
|
1249
|
-
confidenceLevel: item.confidenceLevel ?? (confidence >= 0.75 ? "high" : confidence >= 0.4 ? "medium" : "low"),
|
|
1250
1148
|
selectionMode: item.selectionMode ?? "matched",
|
|
1251
1149
|
install: item.install ?? true,
|
|
1252
|
-
score: item.score ?? 0,
|
|
1253
|
-
scoreBreakdown: {
|
|
1254
|
-
bonuses: normalizedReasons,
|
|
1255
|
-
penalties: [],
|
|
1256
|
-
finalScore: item.score ?? 0
|
|
1257
|
-
},
|
|
1258
1150
|
tags: item.tags,
|
|
1259
1151
|
ecosystem: item.ecosystem,
|
|
1260
1152
|
tokenEstimate: item.tokenEstimate
|
|
@@ -1266,15 +1158,8 @@ function normalizeRecommendation(input2) {
|
|
|
1266
1158
|
skipReasons: item.skipReasons?.map((reason) => ({
|
|
1267
1159
|
code: reason.code ?? "legacy-skip-reason",
|
|
1268
1160
|
message: reason.message ?? item.reason ?? "legacy skipped reason",
|
|
1269
|
-
penalty: reason.penalty ?? 0,
|
|
1270
1161
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1271
|
-
})) ?? [
|
|
1272
|
-
{
|
|
1273
|
-
code: "legacy-skip-reason",
|
|
1274
|
-
message: item.reason ?? "legacy skipped reason",
|
|
1275
|
-
penalty: 0
|
|
1276
|
-
}
|
|
1277
|
-
]
|
|
1162
|
+
})) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
|
|
1278
1163
|
}));
|
|
1279
1164
|
return {
|
|
1280
1165
|
mode: input2.mode === "guided" ? "guided" : "fast",
|
|
@@ -1294,8 +1179,6 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1294
1179
|
return {
|
|
1295
1180
|
selected: recommendation.recommended.map((item) => ({
|
|
1296
1181
|
id: item.id,
|
|
1297
|
-
confidence: item.confidence,
|
|
1298
|
-
confidenceLevel: item.confidenceLevel,
|
|
1299
1182
|
selectionMode: item.selectionMode,
|
|
1300
1183
|
reasons: item.reasons.map((reason) => reason.message)
|
|
1301
1184
|
})),
|
|
@@ -1305,7 +1188,6 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1305
1188
|
reasonDetails: item.skipReasons.map((reason) => ({
|
|
1306
1189
|
code: reason.code,
|
|
1307
1190
|
message: reason.message,
|
|
1308
|
-
penalty: reason.penalty,
|
|
1309
1191
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1310
1192
|
}))
|
|
1311
1193
|
})),
|
|
@@ -1532,6 +1414,13 @@ function computeRuleIntents(rule) {
|
|
|
1532
1414
|
}
|
|
1533
1415
|
|
|
1534
1416
|
// src/recommender/rule-selection.ts
|
|
1417
|
+
function evidenceCount(rule) {
|
|
1418
|
+
return rule.reasons.filter((r) => r.code !== "default-baseline").length;
|
|
1419
|
+
}
|
|
1420
|
+
function isRoleOnly(rule) {
|
|
1421
|
+
const codes = rule.reasons.map((r) => r.code).filter((c) => c !== "default-baseline");
|
|
1422
|
+
return codes.length > 0 && codes.every((c) => c === "repo-role-match");
|
|
1423
|
+
}
|
|
1535
1424
|
var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
|
|
1536
1425
|
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
|
|
1537
1426
|
const recommended = recommendation?.recommended ?? [];
|
|
@@ -1549,7 +1438,7 @@ function applyTokenBudget(rules, budget) {
|
|
|
1549
1438
|
used += r.tokenEstimate ?? 0;
|
|
1550
1439
|
}
|
|
1551
1440
|
}
|
|
1552
|
-
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => b
|
|
1441
|
+
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => evidenceCount(b) - evidenceCount(a) || a.id.localeCompare(b.id));
|
|
1553
1442
|
for (const r of matched) {
|
|
1554
1443
|
const est = r.tokenEstimate ?? 0;
|
|
1555
1444
|
if (used + est <= budget) {
|
|
@@ -1587,20 +1476,20 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
1587
1476
|
});
|
|
1588
1477
|
if (tokenMatches.length > 0) return tokenMatches;
|
|
1589
1478
|
const taskWantsTesting = taskIntents.has("testing");
|
|
1590
|
-
const
|
|
1479
|
+
const capped = recommended.filter((rule) => {
|
|
1591
1480
|
if (rule.selectionMode === "baseline") return false;
|
|
1592
|
-
if (rule
|
|
1481
|
+
if (isRoleOnly(rule)) return false;
|
|
1593
1482
|
if (taskWantsTesting) return true;
|
|
1594
1483
|
const ruleIntents = computeRuleIntents(rule);
|
|
1595
1484
|
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
1596
1485
|
return !isTestingOnly;
|
|
1597
1486
|
});
|
|
1598
|
-
return
|
|
1487
|
+
return capped.slice(0, 8);
|
|
1599
1488
|
}
|
|
1600
1489
|
|
|
1601
1490
|
// src/scanner/scan-project.ts
|
|
1602
1491
|
import { readFile as readFile2 } from "fs/promises";
|
|
1603
|
-
import
|
|
1492
|
+
import path16 from "path";
|
|
1604
1493
|
|
|
1605
1494
|
// src/utils/audit-checks.ts
|
|
1606
1495
|
function isRecord(v) {
|
|
@@ -1627,8 +1516,8 @@ function compareVersions(a, b) {
|
|
|
1627
1516
|
}
|
|
1628
1517
|
|
|
1629
1518
|
// src/scanner/detect-package-manager.ts
|
|
1630
|
-
import
|
|
1631
|
-
import
|
|
1519
|
+
import path13 from "path";
|
|
1520
|
+
import fs10 from "fs-extra";
|
|
1632
1521
|
function detectPackageManager(root, packageManagerField) {
|
|
1633
1522
|
const field = String(packageManagerField ?? "").trim();
|
|
1634
1523
|
if (field.startsWith("yarn@")) {
|
|
@@ -1646,9 +1535,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1646
1535
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1647
1536
|
return "unknown";
|
|
1648
1537
|
}
|
|
1649
|
-
if (
|
|
1650
|
-
if (
|
|
1651
|
-
if (
|
|
1538
|
+
if (fs10.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
|
|
1539
|
+
if (fs10.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1540
|
+
if (fs10.existsSync(path13.join(root, "package-lock.json"))) return "npm";
|
|
1652
1541
|
return "unknown";
|
|
1653
1542
|
}
|
|
1654
1543
|
|
|
@@ -1821,7 +1710,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
1821
1710
|
}
|
|
1822
1711
|
|
|
1823
1712
|
// src/scanner/detection.ts
|
|
1824
|
-
import
|
|
1713
|
+
import path14 from "path";
|
|
1825
1714
|
var UNSUPPORTED_MARKERS = {
|
|
1826
1715
|
"requirements.txt": "python",
|
|
1827
1716
|
"pyproject.toml": "python",
|
|
@@ -1875,14 +1764,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
1875
1764
|
function collectUnsupportedSignals(files) {
|
|
1876
1765
|
return [
|
|
1877
1766
|
...new Set(
|
|
1878
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
1767
|
+
files.map((f) => UNSUPPORTED_MARKERS[path14.basename(f)]).filter((s) => Boolean(s))
|
|
1879
1768
|
)
|
|
1880
1769
|
].sort();
|
|
1881
1770
|
}
|
|
1882
1771
|
|
|
1883
1772
|
// src/scanner/render.ts
|
|
1884
1773
|
import { readFile } from "fs/promises";
|
|
1885
|
-
import
|
|
1774
|
+
import path15 from "path";
|
|
1886
1775
|
|
|
1887
1776
|
// src/scanner/role-labels.ts
|
|
1888
1777
|
var ROLE_LABELS = {
|
|
@@ -1948,7 +1837,7 @@ async function buildContentBlob(root, files) {
|
|
|
1948
1837
|
const batch = await Promise.all(
|
|
1949
1838
|
slice.slice(i, i + CHUNK).map(async (rel) => {
|
|
1950
1839
|
try {
|
|
1951
|
-
return await readFile(
|
|
1840
|
+
return await readFile(path15.join(root, rel), "utf8");
|
|
1952
1841
|
} catch {
|
|
1953
1842
|
return "";
|
|
1954
1843
|
}
|
|
@@ -1958,11 +1847,6 @@ async function buildContentBlob(root, files) {
|
|
|
1958
1847
|
}
|
|
1959
1848
|
return parts.join("\n");
|
|
1960
1849
|
}
|
|
1961
|
-
function computeConfidence(roles, stacks) {
|
|
1962
|
-
const stackCount = Object.values(stacks).reduce((sum, arr) => sum + arr.length, 0);
|
|
1963
|
-
if (roles.length === 0) return 0.15;
|
|
1964
|
-
return Math.min(0.99, Number((0.4 + roles.length * 0.08 + stackCount * 0.02).toFixed(2)));
|
|
1965
|
-
}
|
|
1966
1850
|
function renderSummary(context) {
|
|
1967
1851
|
return `# Repo summary
|
|
1968
1852
|
|
|
@@ -2022,8 +1906,8 @@ var SAFE_FILES = [
|
|
|
2022
1906
|
"Gemfile"
|
|
2023
1907
|
];
|
|
2024
1908
|
async function scanProject(root, mode = "fast") {
|
|
2025
|
-
const pkg = await readJson(
|
|
2026
|
-
const composer = await readJson(
|
|
1909
|
+
const pkg = await readJson(path16.join(root, "package.json"));
|
|
1910
|
+
const composer = await readJson(path16.join(root, "composer.json"));
|
|
2027
1911
|
const files = await listFiles(root, SAFE_FILES);
|
|
2028
1912
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2029
1913
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2057,10 +1941,9 @@ async function scanProject(root, mode = "fast") {
|
|
|
2057
1941
|
mode,
|
|
2058
1942
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2059
1943
|
root,
|
|
2060
|
-
repoName: String(pkg?.name ??
|
|
1944
|
+
repoName: String(pkg?.name ?? path16.basename(root)),
|
|
2061
1945
|
packageManager,
|
|
2062
1946
|
repoRoles: roles,
|
|
2063
|
-
confidence: computeConfidence(roles, stacks),
|
|
2064
1947
|
detectedStacks: stacks,
|
|
2065
1948
|
dependencies: deps,
|
|
2066
1949
|
securityRisks,
|
|
@@ -2076,7 +1959,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2076
1959
|
const scanHashes = Object.fromEntries(
|
|
2077
1960
|
await Promise.all(
|
|
2078
1961
|
safeFiles.map(
|
|
2079
|
-
async (f) => [f, hashText(await readFile2(
|
|
1962
|
+
async (f) => [f, hashText(await readFile2(path16.join(root, f), "utf8"))]
|
|
2080
1963
|
)
|
|
2081
1964
|
)
|
|
2082
1965
|
);
|
|
@@ -2106,9 +1989,6 @@ async function runContext(options) {
|
|
|
2106
1989
|
const summary = await readText(hausPath(root, "repo-summary.md")) ?? "";
|
|
2107
1990
|
const recommendationRaw = await readJson(hausPath(root, "recommendation.json"));
|
|
2108
1991
|
const recommendation = recommendationRaw ? normalizeRecommendation(recommendationRaw) : void 0;
|
|
2109
|
-
const rawBreakdownById = new Map(
|
|
2110
|
-
(recommendationRaw?.recommended ?? []).map((item) => [item.id, item.scoreBreakdown])
|
|
2111
|
-
);
|
|
2112
1992
|
const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
|
|
2113
1993
|
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
|
|
2114
1994
|
tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
|
|
@@ -2119,10 +1999,9 @@ async function runContext(options) {
|
|
|
2119
1999
|
roles: context.repoRoles,
|
|
2120
2000
|
selectedRules: selected.map((x) => ({
|
|
2121
2001
|
id: x.id,
|
|
2122
|
-
confidenceLevel: x.confidenceLevel,
|
|
2123
2002
|
selectionMode: x.selectionMode,
|
|
2124
2003
|
reasons: x.reasons.map((reason) => reason.message),
|
|
2125
|
-
...options.verbose ? {
|
|
2004
|
+
...options.verbose ? { signals: x.reasons.map((r) => r.signal).filter(Boolean) } : {}
|
|
2126
2005
|
})),
|
|
2127
2006
|
skippedCount: recommendation?.skippedRules ?? 0,
|
|
2128
2007
|
estimatedTokenReductionPct: recommendation?.estimatedTokenReductionPct ?? 0
|
|
@@ -2143,15 +2022,8 @@ async function runContext(options) {
|
|
|
2143
2022
|
...payload.selectedRules.flatMap((rule) => {
|
|
2144
2023
|
const reasonLine = `- ${rule.id}: ${rule.reasons.join(", ")}`;
|
|
2145
2024
|
if (!options.verbose) return [reasonLine];
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
const bonuses = (breakdown.bonuses ?? []).map(
|
|
2149
|
-
(b) => ` + ${b.code}(+${b.weight})${b.signal ? ` [${b.signal}]` : ""}`
|
|
2150
|
-
);
|
|
2151
|
-
const penalties = (breakdown.penalties ?? []).map(
|
|
2152
|
-
(p) => ` - ${p.code}(${p.penalty})${p.signal ? ` [${p.signal}]` : ""}`
|
|
2153
|
-
);
|
|
2154
|
-
return [reasonLine, ...bonuses, ...penalties];
|
|
2025
|
+
const signals = (rule.signals ?? []).map((s) => ` \u2022 ${s}`);
|
|
2026
|
+
return [reasonLine, ...signals];
|
|
2155
2027
|
}),
|
|
2156
2028
|
summary
|
|
2157
2029
|
];
|
|
@@ -2160,8 +2032,8 @@ async function runContext(options) {
|
|
|
2160
2032
|
}
|
|
2161
2033
|
|
|
2162
2034
|
// src/commands/doctor.ts
|
|
2163
|
-
import
|
|
2164
|
-
import
|
|
2035
|
+
import path17 from "path";
|
|
2036
|
+
import fs11 from "fs-extra";
|
|
2165
2037
|
|
|
2166
2038
|
// src/update/npm-version.ts
|
|
2167
2039
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -2241,7 +2113,7 @@ async function runDoctor(options) {
|
|
|
2241
2113
|
const enabled = await isHookEnabled(root, key);
|
|
2242
2114
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
2243
2115
|
}
|
|
2244
|
-
const rootClaudeMdPath =
|
|
2116
|
+
const rootClaudeMdPath = path17.join(root, "CLAUDE.md");
|
|
2245
2117
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
2246
2118
|
if (!rootClaudeMdContent) {
|
|
2247
2119
|
flag(
|
|
@@ -2269,7 +2141,7 @@ async function runDoctor(options) {
|
|
|
2269
2141
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2270
2142
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2271
2143
|
for (const target of importTargets) {
|
|
2272
|
-
if (!await
|
|
2144
|
+
if (!await fs11.pathExists(hausPath(root, target))) {
|
|
2273
2145
|
flag(
|
|
2274
2146
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2275
2147
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -2280,7 +2152,7 @@ async function runDoctor(options) {
|
|
|
2280
2152
|
}
|
|
2281
2153
|
}
|
|
2282
2154
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
2283
|
-
const workflowExists = await
|
|
2155
|
+
const workflowExists = await fs11.pathExists(workflowPath);
|
|
2284
2156
|
if (!workflowExists) {
|
|
2285
2157
|
flag(
|
|
2286
2158
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -2294,15 +2166,15 @@ async function runDoctor(options) {
|
|
|
2294
2166
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
2295
2167
|
} else {
|
|
2296
2168
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
2297
|
-
const cachePath =
|
|
2298
|
-
const bundledPath =
|
|
2169
|
+
const cachePath = path17.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
2170
|
+
const bundledPath = path17.join(
|
|
2299
2171
|
packageRoot(),
|
|
2300
2172
|
"library",
|
|
2301
2173
|
"global",
|
|
2302
2174
|
"templates",
|
|
2303
2175
|
"agentic-workflow-standard.md"
|
|
2304
2176
|
);
|
|
2305
|
-
const templatePath = await
|
|
2177
|
+
const templatePath = await fs11.pathExists(cachePath) ? cachePath : bundledPath;
|
|
2306
2178
|
const templateContent = await readText(templatePath);
|
|
2307
2179
|
if (storedHashMatch && templateContent) {
|
|
2308
2180
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -2321,7 +2193,7 @@ async function runDoctor(options) {
|
|
|
2321
2193
|
}
|
|
2322
2194
|
}
|
|
2323
2195
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
2324
|
-
const workflowConfigExists = await
|
|
2196
|
+
const workflowConfigExists = await fs11.pathExists(workflowConfigPath);
|
|
2325
2197
|
if (!workflowConfigExists) {
|
|
2326
2198
|
flag(
|
|
2327
2199
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -2329,7 +2201,7 @@ async function runDoctor(options) {
|
|
|
2329
2201
|
"haus apply --write"
|
|
2330
2202
|
);
|
|
2331
2203
|
} else {
|
|
2332
|
-
const cfg = await
|
|
2204
|
+
const cfg = await fs11.readFile(workflowConfigPath, "utf8");
|
|
2333
2205
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2334
2206
|
if (unfilled > 0) {
|
|
2335
2207
|
flag(
|
|
@@ -2341,23 +2213,6 @@ async function runDoctor(options) {
|
|
|
2341
2213
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
2342
2214
|
}
|
|
2343
2215
|
}
|
|
2344
|
-
const projectMdPath = hausPath(root, "project.md");
|
|
2345
|
-
const projectMdExists = await fs12.pathExists(projectMdPath);
|
|
2346
|
-
if (!projectMdExists) {
|
|
2347
|
-
flag(
|
|
2348
|
-
"- .haus-workflow/project.md: missing (run `haus apply --write`)",
|
|
2349
|
-
"The project facts file is missing",
|
|
2350
|
-
"haus apply --write"
|
|
2351
|
-
);
|
|
2352
|
-
} else {
|
|
2353
|
-
const projectMdContent = await readText(projectMdPath);
|
|
2354
|
-
const hasHeader = projectMdContent?.split("\n")[0]?.includes("HAUS-MANAGED") ?? false;
|
|
2355
|
-
if (!hasHeader) {
|
|
2356
|
-
ok("- .haus-workflow/project.md: no HAUS-MANAGED header (user-owned)");
|
|
2357
|
-
} else {
|
|
2358
|
-
ok("- .haus-workflow/project.md: OK");
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
2216
|
const cacheAgeMs = await getCacheManifestAge();
|
|
2362
2217
|
if (cacheAgeMs === null) {
|
|
2363
2218
|
flag(
|
|
@@ -2377,7 +2232,7 @@ async function runDoctor(options) {
|
|
|
2377
2232
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
2378
2233
|
}
|
|
2379
2234
|
}
|
|
2380
|
-
const pkgJson = await readJson(
|
|
2235
|
+
const pkgJson = await readJson(path17.join(packageRoot(), "package.json"));
|
|
2381
2236
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2382
2237
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
2383
2238
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -2425,7 +2280,6 @@ function formatRecommendationHuman(rec) {
|
|
|
2425
2280
|
if (rec.recommended.length === 0) lines.push(" (none)");
|
|
2426
2281
|
for (const item of rec.recommended) {
|
|
2427
2282
|
lines.push(`- ${item.id}`);
|
|
2428
|
-
lines.push(` confidence: ${item.confidenceLevel} (${item.confidence.toFixed(2)})`);
|
|
2429
2283
|
lines.push(` selection: ${item.selectionMode}`);
|
|
2430
2284
|
lines.push(" why:");
|
|
2431
2285
|
for (const reason of item.reasons) lines.push(` - ${formatReasonWithSignal(reason)}`);
|
|
@@ -2519,51 +2373,46 @@ async function runGuard(kind, _options) {
|
|
|
2519
2373
|
}
|
|
2520
2374
|
|
|
2521
2375
|
// src/commands/init.ts
|
|
2522
|
-
import
|
|
2523
|
-
import
|
|
2376
|
+
import path18 from "path";
|
|
2377
|
+
import fs12 from "fs-extra";
|
|
2524
2378
|
|
|
2525
|
-
// src/
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
]);
|
|
2545
|
-
var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
2546
|
-
vendure: /* @__PURE__ */ new Set(["vendure", "nestjs"]),
|
|
2547
|
-
nestjs: /* @__PURE__ */ new Set(["nestjs"]),
|
|
2548
|
-
laravel: /* @__PURE__ */ new Set(["laravel"]),
|
|
2549
|
-
wordpress: /* @__PURE__ */ new Set(["wordpress"]),
|
|
2550
|
-
dotnet: /* @__PURE__ */ new Set(["dotnet"])
|
|
2551
|
-
};
|
|
2552
|
-
function inferRepoEcosystems(roles) {
|
|
2553
|
-
const ecosystems = /* @__PURE__ */ new Set();
|
|
2554
|
-
for (const [eco, roleList] of Object.entries(ECOSYSTEM_GROUPS)) {
|
|
2555
|
-
if (roleList.some((r) => roles.includes(r))) ecosystems.add(eco);
|
|
2379
|
+
// src/utils/exec.ts
|
|
2380
|
+
import { execa } from "execa";
|
|
2381
|
+
async function runCommand(command, args = [], options = {}) {
|
|
2382
|
+
try {
|
|
2383
|
+
const result = await execa(command, args, {
|
|
2384
|
+
reject: false,
|
|
2385
|
+
// non-zero exits are returned, not thrown
|
|
2386
|
+
...options
|
|
2387
|
+
});
|
|
2388
|
+
return {
|
|
2389
|
+
command,
|
|
2390
|
+
args,
|
|
2391
|
+
stdout: String(result.stdout ?? ""),
|
|
2392
|
+
stderr: String(result.stderr ?? ""),
|
|
2393
|
+
exitCode: result.exitCode ?? 0
|
|
2394
|
+
};
|
|
2395
|
+
} catch (error2) {
|
|
2396
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2397
|
+
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2556
2398
|
}
|
|
2557
|
-
return [...ecosystems];
|
|
2558
2399
|
}
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
|
|
2562
|
-
}
|
|
2563
|
-
return void 0;
|
|
2400
|
+
async function runGit(args, options = {}) {
|
|
2401
|
+
return runCommand("git", args, options);
|
|
2564
2402
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2403
|
+
|
|
2404
|
+
// src/recommender/git-signal.ts
|
|
2405
|
+
async function readChangedFiles(root) {
|
|
2406
|
+
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2407
|
+
try {
|
|
2408
|
+
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2409
|
+
if (result.exitCode !== 0) {
|
|
2410
|
+
return [];
|
|
2411
|
+
}
|
|
2412
|
+
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2413
|
+
} catch {
|
|
2414
|
+
return [];
|
|
2415
|
+
}
|
|
2567
2416
|
}
|
|
2568
2417
|
|
|
2569
2418
|
// src/recommender/policies.ts
|
|
@@ -2633,63 +2482,6 @@ function mergeRecommendationWarnings(context) {
|
|
|
2633
2482
|
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
2634
2483
|
}
|
|
2635
2484
|
|
|
2636
|
-
// src/utils/exec.ts
|
|
2637
|
-
import { execa } from "execa";
|
|
2638
|
-
async function runCommand(command, args = [], options = {}) {
|
|
2639
|
-
try {
|
|
2640
|
-
const result = await execa(command, args, {
|
|
2641
|
-
reject: false,
|
|
2642
|
-
// non-zero exits are returned, not thrown
|
|
2643
|
-
...options
|
|
2644
|
-
});
|
|
2645
|
-
return {
|
|
2646
|
-
command,
|
|
2647
|
-
args,
|
|
2648
|
-
stdout: String(result.stdout ?? ""),
|
|
2649
|
-
stderr: String(result.stderr ?? ""),
|
|
2650
|
-
exitCode: result.exitCode ?? 0
|
|
2651
|
-
};
|
|
2652
|
-
} catch (error2) {
|
|
2653
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2654
|
-
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
async function runGit(args, options = {}) {
|
|
2658
|
-
return runCommand("git", args, options);
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
// src/recommender/scoring.ts
|
|
2662
|
-
function computeConfidenceLevel(args) {
|
|
2663
|
-
const { isDefaultBaseline, reasons, hasEcosystemConflict, score } = args;
|
|
2664
|
-
const positiveCodes = new Set(reasons.map((r) => r.code));
|
|
2665
|
-
positiveCodes.delete("default-baseline");
|
|
2666
|
-
const distinctSignals = positiveCodes.size;
|
|
2667
|
-
const strongCount = (positiveCodes.has("repo-role-match") ? 1 : 0) + (positiveCodes.has("stack-match") ? 1 : 0) + (positiveCodes.has("requires-any-match") ? 1 : 0);
|
|
2668
|
-
if (hasEcosystemConflict) return "low";
|
|
2669
|
-
if (isDefaultBaseline && distinctSignals === 0) return "medium";
|
|
2670
|
-
if (strongCount >= 2 && score >= 70) return "high";
|
|
2671
|
-
if (strongCount >= 1 && distinctSignals >= 2 && score >= 50) return "medium";
|
|
2672
|
-
if (distinctSignals === 1) return "low";
|
|
2673
|
-
return distinctSignals >= 2 ? "medium" : "low";
|
|
2674
|
-
}
|
|
2675
|
-
function confidenceLevelToNumber(level, score) {
|
|
2676
|
-
const base = level === "high" ? 0.85 : level === "medium" ? 0.6 : 0.3;
|
|
2677
|
-
const bonus = Math.min(0.1, Math.max(0, score - 40) / 1e3);
|
|
2678
|
-
return Number(Math.min(0.99, base + bonus).toFixed(2));
|
|
2679
|
-
}
|
|
2680
|
-
async function readChangedFiles(root) {
|
|
2681
|
-
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2682
|
-
try {
|
|
2683
|
-
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2684
|
-
if (result.exitCode !== 0) {
|
|
2685
|
-
return [];
|
|
2686
|
-
}
|
|
2687
|
-
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2688
|
-
} catch {
|
|
2689
|
-
return [];
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
2485
|
// src/recommender/recommend.ts
|
|
2694
2486
|
async function recommend(root, context) {
|
|
2695
2487
|
const items = await loadCatalog(root);
|
|
@@ -2697,249 +2489,147 @@ async function recommend(root, context) {
|
|
|
2697
2489
|
const sources = await readJson(
|
|
2698
2490
|
hausPath(root, "sources-report.json")
|
|
2699
2491
|
) ?? {};
|
|
2700
|
-
const
|
|
2701
|
-
const
|
|
2702
|
-
const
|
|
2703
|
-
const
|
|
2704
|
-
const
|
|
2492
|
+
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
2493
|
+
const scannerStacks = buildStackSet(context);
|
|
2494
|
+
const scannerRoles = new Set(context.repoRoles.map((r) => r.toLowerCase()));
|
|
2495
|
+
const scannerDeps = new Set(context.dependencies.map((d) => d.toLowerCase()));
|
|
2496
|
+
const toStrings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
2497
|
+
const deepStackValues = deep.stacks && typeof deep.stacks === "object" && !Array.isArray(deep.stacks) ? Object.values(deep.stacks).flatMap(toStrings) : [];
|
|
2498
|
+
const deepRoles = new Set(toStrings(deep.roles).map((r) => r.toLowerCase()));
|
|
2499
|
+
const deepStacks = new Set(
|
|
2500
|
+
[...toStrings(deep.roles), ...deepStackValues, ...toStrings(deep.patterns)].map(
|
|
2501
|
+
(x) => x.toLowerCase()
|
|
2502
|
+
)
|
|
2503
|
+
);
|
|
2504
|
+
const roleSet = /* @__PURE__ */ new Set([...scannerRoles, ...deepRoles]);
|
|
2505
|
+
const stackSet = /* @__PURE__ */ new Set([...scannerStacks, ...deepStacks]);
|
|
2506
|
+
const depSet = scannerDeps;
|
|
2705
2507
|
const recommended = [];
|
|
2706
2508
|
const skipped = [];
|
|
2707
2509
|
const goals = Object.values(setupAnswers).join(" ").toLowerCase();
|
|
2708
2510
|
const sourceTrust = new Map((sources.items ?? []).map((x) => [x.id, x.status ?? "candidate"]));
|
|
2709
2511
|
const changedFiles = await readChangedFiles(root);
|
|
2710
|
-
const
|
|
2512
|
+
const skip = (id, code, message, signal) => {
|
|
2513
|
+
skipped.push({ id, reason: message, skipReasons: [{ code, message, signal }] });
|
|
2514
|
+
};
|
|
2515
|
+
const roleSignal = (name) => scannerRoles.has(name.toLowerCase()) ? `role:${name}` : `deep:role:${name}`;
|
|
2516
|
+
const stackSignal = (name) => scannerStacks.has(name.toLowerCase()) ? `tag:${name}` : `deep:tag:${name}`;
|
|
2711
2517
|
for (const item of items) {
|
|
2712
2518
|
const itemSearchText = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
2713
2519
|
if (UNSUPPORTED.some((x) => itemSearchText.includes(x))) {
|
|
2714
|
-
|
|
2715
|
-
id: item.id,
|
|
2716
|
-
reason: "Unsupported stack policy",
|
|
2717
|
-
skipReasons: [
|
|
2718
|
-
{
|
|
2719
|
-
code: "unsupported-policy",
|
|
2720
|
-
message: "Unsupported stack policy",
|
|
2721
|
-
penalty: 100
|
|
2722
|
-
}
|
|
2723
|
-
]
|
|
2724
|
-
});
|
|
2520
|
+
skip(item.id, "unsupported-policy", "Unsupported stack policy");
|
|
2725
2521
|
continue;
|
|
2726
2522
|
}
|
|
2727
2523
|
if (item.source === "curated") {
|
|
2728
2524
|
const rs = item.reviewStatus;
|
|
2729
2525
|
if (!rs || rs !== "approved") {
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
message: `Curated item requires reviewStatus:approved (got ${rs ?? "unset"})`,
|
|
2737
|
-
penalty: 100,
|
|
2738
|
-
signal: `reviewStatus:${rs ?? "unset"}`
|
|
2739
|
-
}
|
|
2740
|
-
]
|
|
2741
|
-
});
|
|
2526
|
+
skip(
|
|
2527
|
+
item.id,
|
|
2528
|
+
"curated-not-approved",
|
|
2529
|
+
`Curated item requires reviewStatus:approved (got ${rs ?? "unset"})`,
|
|
2530
|
+
`reviewStatus:${rs ?? "unset"}`
|
|
2531
|
+
);
|
|
2742
2532
|
continue;
|
|
2743
2533
|
}
|
|
2744
2534
|
if (item.riskLevel === "blocked") {
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
message: "Curated item riskLevel is blocked",
|
|
2752
|
-
penalty: 100,
|
|
2753
|
-
signal: "riskLevel:blocked"
|
|
2754
|
-
}
|
|
2755
|
-
]
|
|
2756
|
-
});
|
|
2535
|
+
skip(
|
|
2536
|
+
item.id,
|
|
2537
|
+
"curated-risk-blocked",
|
|
2538
|
+
"Curated item riskLevel is blocked",
|
|
2539
|
+
"riskLevel:blocked"
|
|
2540
|
+
);
|
|
2757
2541
|
continue;
|
|
2758
2542
|
}
|
|
2759
2543
|
}
|
|
2544
|
+
if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
|
|
2545
|
+
skip(item.id, "sensitive-policy", "Sensitive content policy block");
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
const trust = sourceTrust.get(item.source);
|
|
2549
|
+
if (trust === "candidate" || trust === "rejected") {
|
|
2550
|
+
skip(item.id, "source-trust", "Source trust policy block", `trust:${trust}`);
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
if (item.source && item.source !== "haus" && trust !== "approved") {
|
|
2554
|
+
skip(item.id, "source-approval", "Source not approved", `source:${item.source}`);
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2557
|
+
if (item.id === "haus.nx21-monorepo-patterns" && !roleSet.has("nx-monorepo")) {
|
|
2558
|
+
skip(
|
|
2559
|
+
item.id,
|
|
2560
|
+
"required-role-missing",
|
|
2561
|
+
"Required role missing: nx-monorepo",
|
|
2562
|
+
"role:nx-monorepo"
|
|
2563
|
+
);
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
if (item.id === "haus.turbo-monorepo-patterns" && !roleSet.has("turbo-monorepo")) {
|
|
2567
|
+
skip(
|
|
2568
|
+
item.id,
|
|
2569
|
+
"required-role-missing",
|
|
2570
|
+
"Required role missing: turbo-monorepo",
|
|
2571
|
+
"role:turbo-monorepo"
|
|
2572
|
+
);
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2760
2575
|
const isDefaultBaseline = item.default === true;
|
|
2761
2576
|
const reasons = [];
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
const pushReason = (code, message, weight, signal) => {
|
|
2765
|
-
score += weight;
|
|
2766
|
-
reasons.push({ code, message, weight, signal });
|
|
2767
|
-
};
|
|
2768
|
-
const pushSkipReason = (code, message, penalty, signal) => {
|
|
2769
|
-
score -= penalty;
|
|
2770
|
-
skipReasons.push({ code, message, penalty, signal });
|
|
2771
|
-
};
|
|
2772
|
-
if (isDefaultBaseline) {
|
|
2773
|
-
pushReason("default-baseline", "catalog default baseline", 25, "policy:default");
|
|
2774
|
-
}
|
|
2577
|
+
const push = (code, message, signal) => reasons.push({ code, message, signal });
|
|
2578
|
+
if (isDefaultBaseline) push("default-baseline", "catalog default baseline", "policy:default");
|
|
2775
2579
|
const roleMatch = item.repoRoles.find((r) => roleSet.has(r.toLowerCase()));
|
|
2776
|
-
if (roleMatch)
|
|
2777
|
-
pushReason("repo-role-match", "repo role match", 40, `role:${roleMatch}`);
|
|
2778
|
-
}
|
|
2580
|
+
if (roleMatch) push("repo-role-match", "repo role match", roleSignal(roleMatch));
|
|
2779
2581
|
const tagMatch = item.tags.find((t) => stackSet.has(t.toLowerCase()));
|
|
2780
|
-
if (tagMatch)
|
|
2781
|
-
pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
|
|
2782
|
-
}
|
|
2582
|
+
if (tagMatch) push("stack-match", "stack/dependency match", stackSignal(tagMatch));
|
|
2783
2583
|
const goalMatch = item.tags.find(
|
|
2784
2584
|
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
2785
2585
|
);
|
|
2786
|
-
if (goalMatch) {
|
|
2787
|
-
pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
|
|
2788
|
-
}
|
|
2586
|
+
if (goalMatch) push("goal-match", "guided goal match", `goal:${goalMatch}`);
|
|
2789
2587
|
if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
|
|
2790
|
-
|
|
2588
|
+
push(
|
|
2791
2589
|
"package-manager-match",
|
|
2792
2590
|
"package manager match",
|
|
2793
|
-
10,
|
|
2794
2591
|
`packageManager:${context.packageManager}`
|
|
2795
2592
|
);
|
|
2796
2593
|
}
|
|
2797
2594
|
const configSignal = item.tags.find(
|
|
2798
2595
|
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
2799
2596
|
);
|
|
2800
|
-
if (configSignal) {
|
|
2801
|
-
pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
|
|
2802
|
-
}
|
|
2597
|
+
if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
|
|
2803
2598
|
const changedMatch = changedFiles.find((f) => f.includes(item.id.split(".").pop() ?? ""));
|
|
2804
|
-
if (changedMatch)
|
|
2805
|
-
|
|
2806
|
-
}
|
|
2807
|
-
if (item.id === "haus.nx21-monorepo-patterns" && !roleSet.has("nx-monorepo")) {
|
|
2808
|
-
skipped.push({
|
|
2809
|
-
id: item.id,
|
|
2810
|
-
reason: "Required role missing: nx-monorepo",
|
|
2811
|
-
skipReasons: [
|
|
2812
|
-
{
|
|
2813
|
-
code: "required-role-missing",
|
|
2814
|
-
message: "Required role missing: nx-monorepo",
|
|
2815
|
-
penalty: 100,
|
|
2816
|
-
signal: "role:nx-monorepo"
|
|
2817
|
-
}
|
|
2818
|
-
]
|
|
2819
|
-
});
|
|
2820
|
-
continue;
|
|
2821
|
-
}
|
|
2822
|
-
if (item.id === "haus.turbo-monorepo-patterns" && !roleSet.has("turbo-monorepo")) {
|
|
2823
|
-
skipped.push({
|
|
2824
|
-
id: item.id,
|
|
2825
|
-
reason: "Required role missing: turbo-monorepo",
|
|
2826
|
-
skipReasons: [
|
|
2827
|
-
{
|
|
2828
|
-
code: "required-role-missing",
|
|
2829
|
-
message: "Required role missing: turbo-monorepo",
|
|
2830
|
-
penalty: 100,
|
|
2831
|
-
signal: "role:turbo-monorepo"
|
|
2832
|
-
}
|
|
2833
|
-
]
|
|
2834
|
-
});
|
|
2835
|
-
continue;
|
|
2836
|
-
}
|
|
2599
|
+
if (changedMatch)
|
|
2600
|
+
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
2837
2601
|
const requiresAny = item.requiresAny ?? [];
|
|
2838
2602
|
if (requiresAny.length > 0) {
|
|
2839
|
-
const satisfied = matchRequiresAny(requiresAny, {
|
|
2840
|
-
stackSet,
|
|
2841
|
-
depSet,
|
|
2842
|
-
roleSet
|
|
2843
|
-
});
|
|
2603
|
+
const satisfied = matchRequiresAny(requiresAny, { stackSet, depSet, roleSet });
|
|
2844
2604
|
if (!satisfied.matched) {
|
|
2845
2605
|
const description = describeRequiresAny(requiresAny);
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
message: `requiresAny unsatisfied: needs ${description}`,
|
|
2853
|
-
penalty: 100,
|
|
2854
|
-
signal: description
|
|
2855
|
-
}
|
|
2856
|
-
]
|
|
2857
|
-
});
|
|
2606
|
+
skip(
|
|
2607
|
+
item.id,
|
|
2608
|
+
"requires-any-unsatisfied",
|
|
2609
|
+
`requiresAny unsatisfied: needs ${description}`,
|
|
2610
|
+
description
|
|
2611
|
+
);
|
|
2858
2612
|
continue;
|
|
2859
2613
|
}
|
|
2860
2614
|
if (!reasons.some((r) => r.code === "stack-match")) {
|
|
2861
|
-
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
if (item.ecosystem && dominantBackendEcosystem && isBackendEcosystem(item.ecosystem)) {
|
|
2865
|
-
const compat = ECOSYSTEM_COMPATIBLE_BACKENDS[dominantBackendEcosystem] ?? /* @__PURE__ */ new Set([dominantBackendEcosystem]);
|
|
2866
|
-
if (!compat.has(item.ecosystem)) {
|
|
2867
|
-
pushSkipReason(
|
|
2868
|
-
"ecosystem-conflict",
|
|
2869
|
-
`ecosystem conflict: rule ecosystem=${item.ecosystem} but repo dominant backend=${dominantBackendEcosystem}`,
|
|
2870
|
-
40,
|
|
2871
|
-
`ecosystem:${item.ecosystem}->${dominantBackendEcosystem}`
|
|
2872
|
-
);
|
|
2615
|
+
push("requires-any-match", "requires-any signal match", satisfied.signal);
|
|
2873
2616
|
}
|
|
2874
2617
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
}
|
|
2878
|
-
const trust = sourceTrust.get(item.source);
|
|
2879
|
-
if (trust === "candidate" || trust === "rejected") {
|
|
2880
|
-
pushSkipReason("source-trust", "Source trust policy block", 100);
|
|
2881
|
-
}
|
|
2882
|
-
if (item.source && item.source !== "haus" && trust !== "approved") {
|
|
2883
|
-
pushSkipReason("source-approval", "Source not approved", 100);
|
|
2884
|
-
}
|
|
2885
|
-
if (securityRiskCount > 0 && !isDefaultBaseline && (item.tags.includes("security") || item.id.includes("security"))) {
|
|
2886
|
-
pushSkipReason(
|
|
2887
|
-
"security-risk-penalty",
|
|
2888
|
-
"Security-tagged item penalized by active risk signals",
|
|
2889
|
-
20
|
|
2890
|
-
);
|
|
2891
|
-
}
|
|
2892
|
-
const positiveReasonCodes = new Set(
|
|
2893
|
-
reasons.map((r) => r.code).filter((c) => c !== "default-baseline")
|
|
2894
|
-
);
|
|
2895
|
-
const hasRoleSignal = positiveReasonCodes.has("repo-role-match");
|
|
2896
|
-
const hasDepOrStackSignal = positiveReasonCodes.has("stack-match") || positiveReasonCodes.has("requires-any-match");
|
|
2897
|
-
if (hasRoleSignal && !hasDepOrStackSignal && !isDefaultBaseline && requiresAny.length === 0) {
|
|
2898
|
-
pushSkipReason(
|
|
2899
|
-
"role-only-bleed-guard",
|
|
2900
|
-
"role match without dep/stack signal (role-only bleed)",
|
|
2901
|
-
25,
|
|
2902
|
-
roleMatch ? `role:${roleMatch}` : void 0
|
|
2903
|
-
);
|
|
2904
|
-
}
|
|
2905
|
-
const minScore = isDefaultBaseline ? 1 : 40;
|
|
2906
|
-
if (score >= minScore) {
|
|
2907
|
-
const confidenceLevel = computeConfidenceLevel({
|
|
2908
|
-
isDefaultBaseline,
|
|
2909
|
-
reasons,
|
|
2910
|
-
hasEcosystemConflict: skipReasons.some((s) => s.code === "ecosystem-conflict"),
|
|
2911
|
-
score
|
|
2912
|
-
});
|
|
2913
|
-
const confidence = confidenceLevelToNumber(confidenceLevel, score);
|
|
2618
|
+
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
2619
|
+
if (isDefaultBaseline || hasEvidence) {
|
|
2914
2620
|
recommended.push({
|
|
2915
2621
|
id: item.id,
|
|
2916
2622
|
type: item.type,
|
|
2917
|
-
reason: reasons.length ? reasons.map((x) => x.message).join(", ") :
|
|
2623
|
+
reason: reasons.length ? reasons.map((x) => x.message).join(", ") : "eligible",
|
|
2918
2624
|
reasons,
|
|
2919
|
-
|
|
2920
|
-
confidenceLevel,
|
|
2921
|
-
selectionMode: isDefaultBaseline && reasons.every((r) => r.code === "default-baseline") ? "baseline" : "matched",
|
|
2625
|
+
selectionMode: isDefaultBaseline && !hasEvidence ? "baseline" : "matched",
|
|
2922
2626
|
install: true,
|
|
2923
|
-
score,
|
|
2924
|
-
scoreBreakdown: {
|
|
2925
|
-
bonuses: reasons,
|
|
2926
|
-
penalties: skipReasons,
|
|
2927
|
-
finalScore: score
|
|
2928
|
-
},
|
|
2929
2627
|
tags: item.tags,
|
|
2930
2628
|
ecosystem: item.ecosystem,
|
|
2931
2629
|
tokenEstimate: item.tokenEstimate
|
|
2932
2630
|
});
|
|
2933
2631
|
} else {
|
|
2934
|
-
|
|
2935
|
-
skipReasons.push({
|
|
2936
|
-
code: "no-role-stack-match",
|
|
2937
|
-
message: "No role/stack match",
|
|
2938
|
-
penalty: 0
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
const primary = skipReasons[0];
|
|
2942
|
-
skipped.push({ id: item.id, reason: primary.message, skipReasons });
|
|
2632
|
+
skip(item.id, "no-role-stack-match", "No role/stack match");
|
|
2943
2633
|
}
|
|
2944
2634
|
}
|
|
2945
2635
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -3086,8 +2776,8 @@ async function runSetupProject(options) {
|
|
|
3086
2776
|
// src/commands/init.ts
|
|
3087
2777
|
async function runInit(options) {
|
|
3088
2778
|
const root = process.cwd();
|
|
3089
|
-
const hausDir =
|
|
3090
|
-
const alreadyInit = await
|
|
2779
|
+
const hausDir = path18.join(root, ".haus-workflow");
|
|
2780
|
+
const alreadyInit = await fs12.pathExists(hausDir);
|
|
3091
2781
|
if (alreadyInit) {
|
|
3092
2782
|
log("Haus AI already initialized in this project.");
|
|
3093
2783
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3099,8 +2789,8 @@ async function runInit(options) {
|
|
|
3099
2789
|
|
|
3100
2790
|
// src/install/apply.ts
|
|
3101
2791
|
import crypto2 from "crypto";
|
|
3102
|
-
import
|
|
3103
|
-
import
|
|
2792
|
+
import path21 from "path";
|
|
2793
|
+
import fs14 from "fs-extra";
|
|
3104
2794
|
|
|
3105
2795
|
// src/install/allow-rules.ts
|
|
3106
2796
|
var ALLOWED_SUBCOMMANDS = [
|
|
@@ -3147,13 +2837,13 @@ ${content2}`;
|
|
|
3147
2837
|
|
|
3148
2838
|
// src/install/manifest.ts
|
|
3149
2839
|
import os4 from "os";
|
|
3150
|
-
import
|
|
2840
|
+
import path19 from "path";
|
|
3151
2841
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
3152
2842
|
function globalClaudeDir() {
|
|
3153
|
-
return
|
|
2843
|
+
return path19.join(os4.homedir(), ".claude");
|
|
3154
2844
|
}
|
|
3155
2845
|
function hausManifestPath() {
|
|
3156
|
-
return
|
|
2846
|
+
return path19.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
3157
2847
|
}
|
|
3158
2848
|
async function readManifest() {
|
|
3159
2849
|
return readJson(hausManifestPath());
|
|
@@ -3172,10 +2862,10 @@ function buildManifest(source, files, hooks) {
|
|
|
3172
2862
|
}
|
|
3173
2863
|
|
|
3174
2864
|
// src/install/settings-merge.ts
|
|
3175
|
-
import
|
|
3176
|
-
import
|
|
2865
|
+
import path20 from "path";
|
|
2866
|
+
import fs13 from "fs-extra";
|
|
3177
2867
|
function settingsJsonPath() {
|
|
3178
|
-
return
|
|
2868
|
+
return path20.join(globalClaudeDir(), "settings.json");
|
|
3179
2869
|
}
|
|
3180
2870
|
async function readSettings() {
|
|
3181
2871
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -3316,7 +3006,7 @@ function stripHausHooks(settings) {
|
|
|
3316
3006
|
async function loadHooksFragment(fragmentPath) {
|
|
3317
3007
|
let raw;
|
|
3318
3008
|
try {
|
|
3319
|
-
raw = await
|
|
3009
|
+
raw = await fs13.readJson(fragmentPath);
|
|
3320
3010
|
} catch {
|
|
3321
3011
|
return [];
|
|
3322
3012
|
}
|
|
@@ -3325,46 +3015,46 @@ async function loadHooksFragment(fragmentPath) {
|
|
|
3325
3015
|
}
|
|
3326
3016
|
|
|
3327
3017
|
// src/install/apply.ts
|
|
3328
|
-
var
|
|
3018
|
+
var SCHEMA_VERSION2 = "1";
|
|
3329
3019
|
function hashContent(content2) {
|
|
3330
3020
|
return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
|
|
3331
3021
|
}
|
|
3332
3022
|
function sourceVersion() {
|
|
3333
3023
|
try {
|
|
3334
|
-
const pkgPath =
|
|
3335
|
-
const pkg = JSON.parse(
|
|
3024
|
+
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
3025
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
|
|
3336
3026
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3337
3027
|
} catch {
|
|
3338
3028
|
return "haus@0.0.0";
|
|
3339
3029
|
}
|
|
3340
3030
|
}
|
|
3341
3031
|
function globalSrcDir() {
|
|
3342
|
-
return
|
|
3032
|
+
return path21.join(packageRoot(), "library", "global");
|
|
3343
3033
|
}
|
|
3344
3034
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3345
3035
|
const entries = [];
|
|
3346
|
-
const skillsDir =
|
|
3347
|
-
if (
|
|
3348
|
-
for (const skillName of
|
|
3349
|
-
const skillFile =
|
|
3350
|
-
if (
|
|
3036
|
+
const skillsDir = path21.join(srcDir, "skills");
|
|
3037
|
+
if (fs14.pathExistsSync(skillsDir)) {
|
|
3038
|
+
for (const skillName of fs14.readdirSync(skillsDir)) {
|
|
3039
|
+
const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
|
|
3040
|
+
if (fs14.pathExistsSync(skillFile)) {
|
|
3351
3041
|
entries.push({
|
|
3352
3042
|
stableId: `skill.${skillName}`,
|
|
3353
|
-
srcRelPath:
|
|
3354
|
-
destPath:
|
|
3043
|
+
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3044
|
+
destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3355
3045
|
});
|
|
3356
3046
|
}
|
|
3357
3047
|
}
|
|
3358
3048
|
}
|
|
3359
|
-
const commandsDir =
|
|
3360
|
-
if (
|
|
3361
|
-
for (const fileName of
|
|
3049
|
+
const commandsDir = path21.join(srcDir, "commands");
|
|
3050
|
+
if (fs14.pathExistsSync(commandsDir)) {
|
|
3051
|
+
for (const fileName of fs14.readdirSync(commandsDir)) {
|
|
3362
3052
|
if (!fileName.endsWith(".md")) continue;
|
|
3363
3053
|
const commandName = fileName.slice(0, -".md".length);
|
|
3364
3054
|
entries.push({
|
|
3365
3055
|
stableId: `command.${commandName}`,
|
|
3366
|
-
srcRelPath:
|
|
3367
|
-
destPath:
|
|
3056
|
+
srcRelPath: path21.join("library", "global", "commands", fileName),
|
|
3057
|
+
destPath: path21.join(claudeDir, "commands", fileName)
|
|
3368
3058
|
});
|
|
3369
3059
|
}
|
|
3370
3060
|
}
|
|
@@ -3388,7 +3078,7 @@ async function applyInstall(options = {}) {
|
|
|
3388
3078
|
};
|
|
3389
3079
|
const manifestFiles = [];
|
|
3390
3080
|
for (const entry of sourceFiles) {
|
|
3391
|
-
const srcPath =
|
|
3081
|
+
const srcPath = path21.join(packageRoot(), entry.srcRelPath);
|
|
3392
3082
|
const rawContent = await readText(srcPath);
|
|
3393
3083
|
if (rawContent === void 0) {
|
|
3394
3084
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3396,7 +3086,7 @@ async function applyInstall(options = {}) {
|
|
|
3396
3086
|
}
|
|
3397
3087
|
const stamped = stampMarkdown(rawContent, {
|
|
3398
3088
|
stableId: entry.stableId,
|
|
3399
|
-
schemaVersion:
|
|
3089
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
3400
3090
|
source
|
|
3401
3091
|
});
|
|
3402
3092
|
const newHash = hashContent(stamped);
|
|
@@ -3408,7 +3098,7 @@ async function applyInstall(options = {}) {
|
|
|
3408
3098
|
}
|
|
3409
3099
|
continue;
|
|
3410
3100
|
}
|
|
3411
|
-
const destExists =
|
|
3101
|
+
const destExists = fs14.pathExistsSync(entry.destPath);
|
|
3412
3102
|
if (destExists) {
|
|
3413
3103
|
const currentContent = await readText(entry.destPath);
|
|
3414
3104
|
if (currentContent !== void 0) {
|
|
@@ -3441,10 +3131,10 @@ async function applyInstall(options = {}) {
|
|
|
3441
3131
|
destPath: entry.destPath,
|
|
3442
3132
|
srcRelPath: entry.srcRelPath,
|
|
3443
3133
|
hash: newHash,
|
|
3444
|
-
schemaVersion:
|
|
3134
|
+
schemaVersion: SCHEMA_VERSION2
|
|
3445
3135
|
});
|
|
3446
3136
|
}
|
|
3447
|
-
const fragmentPath =
|
|
3137
|
+
const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
|
|
3448
3138
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3449
3139
|
const settings = await readSettings();
|
|
3450
3140
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -3455,13 +3145,13 @@ async function applyInstall(options = {}) {
|
|
|
3455
3145
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3456
3146
|
for (const entry of existingManifest.files) {
|
|
3457
3147
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3458
|
-
if (!
|
|
3148
|
+
if (!fs14.pathExistsSync(entry.destPath)) continue;
|
|
3459
3149
|
const content2 = await readText(entry.destPath);
|
|
3460
3150
|
if (!content2) continue;
|
|
3461
3151
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3462
3152
|
const currentHash = hashContent(content2);
|
|
3463
3153
|
if (hasHeader && currentHash === entry.hash) {
|
|
3464
|
-
if (!dryRun) await
|
|
3154
|
+
if (!dryRun) await fs14.remove(entry.destPath);
|
|
3465
3155
|
result.deleted.push(entry.destPath);
|
|
3466
3156
|
} else {
|
|
3467
3157
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -3584,20 +3274,20 @@ async function runScan(options) {
|
|
|
3584
3274
|
}
|
|
3585
3275
|
|
|
3586
3276
|
// src/commands/undo.ts
|
|
3587
|
-
import
|
|
3588
|
-
import
|
|
3277
|
+
import path22 from "path";
|
|
3278
|
+
import fs15 from "fs-extra";
|
|
3589
3279
|
var CLAUDE_DIR = ".claude";
|
|
3590
3280
|
async function runUndo(options) {
|
|
3591
3281
|
const root = process.cwd();
|
|
3592
|
-
const targets = [
|
|
3593
|
-
const existing = targets.filter((p) =>
|
|
3282
|
+
const targets = [path22.join(root, CLAUDE_DIR), path22.join(root, HAUS_DIR)];
|
|
3283
|
+
const existing = targets.filter((p) => fs15.existsSync(p));
|
|
3594
3284
|
if (existing.length === 0) {
|
|
3595
3285
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
3596
3286
|
return;
|
|
3597
3287
|
}
|
|
3598
3288
|
if (!options.yes) {
|
|
3599
3289
|
const ok = await confirm(
|
|
3600
|
-
`Remove ${existing.map((p) =>
|
|
3290
|
+
`Remove ${existing.map((p) => path22.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
3601
3291
|
);
|
|
3602
3292
|
if (!ok) {
|
|
3603
3293
|
log("Cancelled.");
|
|
@@ -3605,15 +3295,15 @@ async function runUndo(options) {
|
|
|
3605
3295
|
}
|
|
3606
3296
|
}
|
|
3607
3297
|
for (const p of existing) {
|
|
3608
|
-
await
|
|
3609
|
-
log(`Removed ${
|
|
3298
|
+
await fs15.remove(p);
|
|
3299
|
+
log(`Removed ${path22.relative(root, p)}`);
|
|
3610
3300
|
}
|
|
3611
3301
|
}
|
|
3612
3302
|
|
|
3613
3303
|
// src/install/uninstall.ts
|
|
3614
3304
|
import crypto3 from "crypto";
|
|
3615
|
-
import
|
|
3616
|
-
import
|
|
3305
|
+
import path23 from "path";
|
|
3306
|
+
import fs16 from "fs-extra";
|
|
3617
3307
|
async function runUninstall(options = {}) {
|
|
3618
3308
|
const { force = false } = options;
|
|
3619
3309
|
const manifest = await readManifest();
|
|
@@ -3623,7 +3313,7 @@ async function runUninstall(options = {}) {
|
|
|
3623
3313
|
return result;
|
|
3624
3314
|
}
|
|
3625
3315
|
for (const entry of manifest.files) {
|
|
3626
|
-
const exists =
|
|
3316
|
+
const exists = fs16.pathExistsSync(entry.destPath);
|
|
3627
3317
|
if (!exists) continue;
|
|
3628
3318
|
const content2 = await readText(entry.destPath);
|
|
3629
3319
|
if (content2 === void 0) continue;
|
|
@@ -3641,22 +3331,22 @@ async function runUninstall(options = {}) {
|
|
|
3641
3331
|
result.skipped.push(entry.destPath);
|
|
3642
3332
|
continue;
|
|
3643
3333
|
}
|
|
3644
|
-
await
|
|
3645
|
-
await pruneEmptyDir(
|
|
3334
|
+
await fs16.remove(entry.destPath);
|
|
3335
|
+
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
3646
3336
|
result.deleted.push(entry.destPath);
|
|
3647
3337
|
}
|
|
3648
3338
|
const settings = await readSettings();
|
|
3649
3339
|
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3650
3340
|
await writeSettings(stripped);
|
|
3651
3341
|
result.hooksStripped = true;
|
|
3652
|
-
const hausDir =
|
|
3342
|
+
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
3653
3343
|
const manifestPath = hausManifestPath();
|
|
3654
|
-
if (
|
|
3655
|
-
await
|
|
3344
|
+
if (fs16.pathExistsSync(manifestPath)) {
|
|
3345
|
+
await fs16.remove(manifestPath);
|
|
3656
3346
|
}
|
|
3657
|
-
if (
|
|
3658
|
-
const remaining = await
|
|
3659
|
-
if (remaining.length === 0) await
|
|
3347
|
+
if (fs16.pathExistsSync(hausDir)) {
|
|
3348
|
+
const remaining = await fs16.readdir(hausDir);
|
|
3349
|
+
if (remaining.length === 0) await fs16.remove(hausDir);
|
|
3660
3350
|
}
|
|
3661
3351
|
return result;
|
|
3662
3352
|
}
|
|
@@ -3675,8 +3365,8 @@ function printUninstallResult(result) {
|
|
|
3675
3365
|
}
|
|
3676
3366
|
async function pruneEmptyDir(dir) {
|
|
3677
3367
|
try {
|
|
3678
|
-
const entries = await
|
|
3679
|
-
if (entries.length === 0) await
|
|
3368
|
+
const entries = await fs16.readdir(dir);
|
|
3369
|
+
if (entries.length === 0) await fs16.remove(dir);
|
|
3680
3370
|
} catch {
|
|
3681
3371
|
}
|
|
3682
3372
|
}
|
|
@@ -3694,7 +3384,7 @@ async function runUninstallCommand(options) {
|
|
|
3694
3384
|
}
|
|
3695
3385
|
|
|
3696
3386
|
// src/commands/update.ts
|
|
3697
|
-
import
|
|
3387
|
+
import path25 from "path";
|
|
3698
3388
|
|
|
3699
3389
|
// src/update/diff-generated-files.ts
|
|
3700
3390
|
function diffGeneratedFiles() {
|
|
@@ -3721,7 +3411,7 @@ function summarizeLockDiff(before, after) {
|
|
|
3721
3411
|
|
|
3722
3412
|
// src/update/lockfile.ts
|
|
3723
3413
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3724
|
-
import
|
|
3414
|
+
import path24 from "path";
|
|
3725
3415
|
async function checkLock(root) {
|
|
3726
3416
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3727
3417
|
const hasValidVersions = lock.every(
|
|
@@ -3742,7 +3432,7 @@ async function applyLock(root) {
|
|
|
3742
3432
|
try {
|
|
3743
3433
|
const backupDir = hausPath(root, "backups");
|
|
3744
3434
|
await mkdir(backupDir, { recursive: true });
|
|
3745
|
-
await copyFile(lockPath,
|
|
3435
|
+
await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3746
3436
|
} catch {
|
|
3747
3437
|
}
|
|
3748
3438
|
const enriched = await Promise.all(
|
|
@@ -3764,7 +3454,7 @@ function diffLock(before, after) {
|
|
|
3764
3454
|
}
|
|
3765
3455
|
async function hasLocalOverrides(root) {
|
|
3766
3456
|
try {
|
|
3767
|
-
await readFile3(
|
|
3457
|
+
await readFile3(path24.join(root, ".claude", "settings.json"), "utf8");
|
|
3768
3458
|
return true;
|
|
3769
3459
|
} catch {
|
|
3770
3460
|
return false;
|
|
@@ -3776,7 +3466,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3776
3466
|
async function runUpdate(options) {
|
|
3777
3467
|
const root = process.cwd();
|
|
3778
3468
|
if (options.check) {
|
|
3779
|
-
const pkgJson2 = await readJson(
|
|
3469
|
+
const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
|
|
3780
3470
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3781
3471
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3782
3472
|
checkLock(root),
|
|
@@ -3803,7 +3493,7 @@ async function runUpdate(options) {
|
|
|
3803
3493
|
if (!status.ok) process.exitCode = 1;
|
|
3804
3494
|
return;
|
|
3805
3495
|
}
|
|
3806
|
-
const pkgJson = await readJson(
|
|
3496
|
+
const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
|
|
3807
3497
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3808
3498
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3809
3499
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3833,8 +3523,8 @@ async function runUpdate(options) {
|
|
|
3833
3523
|
}
|
|
3834
3524
|
|
|
3835
3525
|
// src/commands/validate-catalog.ts
|
|
3836
|
-
import
|
|
3837
|
-
import
|
|
3526
|
+
import fs17 from "fs";
|
|
3527
|
+
import path26 from "path";
|
|
3838
3528
|
|
|
3839
3529
|
// library/catalog/validation-rules.json
|
|
3840
3530
|
var validation_rules_default = {
|
|
@@ -4083,23 +3773,23 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4083
3773
|
const failures = [];
|
|
4084
3774
|
for (const item of items) {
|
|
4085
3775
|
if (!item.path) continue;
|
|
4086
|
-
const absPath =
|
|
3776
|
+
const absPath = path26.join(manifestDir, item.path);
|
|
4087
3777
|
if (item.type === "skill") {
|
|
4088
|
-
const skillMd =
|
|
4089
|
-
if (!
|
|
4090
|
-
failures.push(`${item.id}: missing ${
|
|
3778
|
+
const skillMd = path26.join(absPath, "SKILL.md");
|
|
3779
|
+
if (!fs17.existsSync(skillMd)) {
|
|
3780
|
+
failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
|
|
4091
3781
|
continue;
|
|
4092
3782
|
}
|
|
4093
|
-
const text =
|
|
3783
|
+
const text = fs17.readFileSync(skillMd, "utf8");
|
|
4094
3784
|
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
4095
3785
|
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
4096
3786
|
}
|
|
4097
3787
|
} else if (item.type === "agent") {
|
|
4098
|
-
if (!
|
|
3788
|
+
if (!fs17.existsSync(absPath)) {
|
|
4099
3789
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4100
3790
|
continue;
|
|
4101
3791
|
}
|
|
4102
|
-
const text =
|
|
3792
|
+
const text = fs17.readFileSync(absPath, "utf8");
|
|
4103
3793
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
4104
3794
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
4105
3795
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -4110,7 +3800,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4110
3800
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
4111
3801
|
}
|
|
4112
3802
|
} else if (item.type === "template") {
|
|
4113
|
-
if (!
|
|
3803
|
+
if (!fs17.existsSync(absPath)) {
|
|
4114
3804
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4115
3805
|
}
|
|
4116
3806
|
}
|
|
@@ -4121,11 +3811,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4121
3811
|
const failures = [];
|
|
4122
3812
|
const dirs = ["skills", "agents"];
|
|
4123
3813
|
for (const dir of dirs) {
|
|
4124
|
-
const abs =
|
|
4125
|
-
if (!
|
|
3814
|
+
const abs = path26.join(manifestDir, dir);
|
|
3815
|
+
if (!fs17.existsSync(abs)) continue;
|
|
4126
3816
|
walkMd(abs, (file) => {
|
|
4127
|
-
const text =
|
|
4128
|
-
const rel =
|
|
3817
|
+
const text = fs17.readFileSync(file, "utf8");
|
|
3818
|
+
const rel = path26.relative(manifestDir, file);
|
|
4129
3819
|
const lines = text.split(/\r?\n/);
|
|
4130
3820
|
for (let i = 0; i < lines.length; i++) {
|
|
4131
3821
|
const line2 = lines[i] ?? "";
|
|
@@ -4144,8 +3834,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4144
3834
|
return failures;
|
|
4145
3835
|
}
|
|
4146
3836
|
function walkMd(dir, fn) {
|
|
4147
|
-
for (const entry of
|
|
4148
|
-
const full =
|
|
3837
|
+
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
3838
|
+
const full = path26.join(dir, entry.name);
|
|
4149
3839
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4150
3840
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4151
3841
|
}
|
|
@@ -4156,8 +3846,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
4156
3846
|
process.exitCode = 1;
|
|
4157
3847
|
return;
|
|
4158
3848
|
}
|
|
4159
|
-
const abs =
|
|
4160
|
-
const manifestDir =
|
|
3849
|
+
const abs = path26.resolve(process.cwd(), manifestPath);
|
|
3850
|
+
const manifestDir = path26.dirname(abs);
|
|
4161
3851
|
const data = await readJson(abs);
|
|
4162
3852
|
if (!data?.items) {
|
|
4163
3853
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4186,7 +3876,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
4186
3876
|
}
|
|
4187
3877
|
|
|
4188
3878
|
// src/commands/workspace.ts
|
|
4189
|
-
import
|
|
3879
|
+
import path27 from "path";
|
|
4190
3880
|
import YAML from "yaml";
|
|
4191
3881
|
async function runWorkspace(action) {
|
|
4192
3882
|
if (action === "init") {
|
|
@@ -4219,7 +3909,7 @@ relationships: []
|
|
|
4219
3909
|
const summaries = [];
|
|
4220
3910
|
const ownership = {};
|
|
4221
3911
|
for (const repo of repos) {
|
|
4222
|
-
const repoRoot =
|
|
3912
|
+
const repoRoot = path27.resolve(process.cwd(), repo.path);
|
|
4223
3913
|
const result = await scanProject(repoRoot, "fast");
|
|
4224
3914
|
summaries.push({
|
|
4225
3915
|
name: repo.name,
|
|
@@ -4255,7 +3945,7 @@ ${summaries.map(
|
|
|
4255
3945
|
// src/cli.ts
|
|
4256
3946
|
function cliVersion() {
|
|
4257
3947
|
try {
|
|
4258
|
-
const pkgPath =
|
|
3948
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
4259
3949
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
4260
3950
|
return pkg.version ?? "0.0.0";
|
|
4261
3951
|
} catch {
|
|
@@ -4265,7 +3955,7 @@ function cliVersion() {
|
|
|
4265
3955
|
var program = new Command();
|
|
4266
3956
|
function validateRuntimeNodeVersion() {
|
|
4267
3957
|
try {
|
|
4268
|
-
const pkgPath =
|
|
3958
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
4269
3959
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
4270
3960
|
const requiredRange = pkg.engines?.node;
|
|
4271
3961
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|