@haus-tech/haus-workflow 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/cli.js +303 -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
|
|
@@ -171,8 +171,8 @@ async function getCacheManifestAge() {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
// src/claude/write-claude-files.ts
|
|
174
|
-
import
|
|
175
|
-
import
|
|
174
|
+
import path9 from "path";
|
|
175
|
+
import fs9 from "fs-extra";
|
|
176
176
|
|
|
177
177
|
// src/update/hash-installed.ts
|
|
178
178
|
import path3 from "path";
|
|
@@ -416,8 +416,8 @@ function buildDenyRules() {
|
|
|
416
416
|
for (const command of DANGEROUS_COMMANDS) {
|
|
417
417
|
rules.push(`Bash(${command}:*)`);
|
|
418
418
|
}
|
|
419
|
-
for (const
|
|
420
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
419
|
+
for (const path29 of SENSITIVE_PATHS) {
|
|
420
|
+
const pattern = SENSITIVE_DIRS.has(path29) ? `${path29}/**` : path29;
|
|
421
421
|
for (const tool of FILE_TOOLS) {
|
|
422
422
|
rules.push(`${tool}(${pattern})`);
|
|
423
423
|
}
|
|
@@ -515,109 +515,13 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
515
515
|
return { ok: true, message: "settings.json matches canonical hook contract." };
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
// src/claude/write-
|
|
518
|
+
// src/claude/write-root-claude-md.ts
|
|
519
519
|
import path6 from "path";
|
|
520
520
|
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
521
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
617
522
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
618
523
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
619
|
-
@.haus-workflow/workflow-config.md
|
|
620
|
-
@.haus-workflow/project.md`;
|
|
524
|
+
@.haus-workflow/workflow-config.md`;
|
|
621
525
|
function buildImportBlock() {
|
|
622
526
|
return `${BLOCK_BEGIN}
|
|
623
527
|
${IMPORT_CONTENT}
|
|
@@ -642,9 +546,9 @@ ${block}
|
|
|
642
546
|
`;
|
|
643
547
|
}
|
|
644
548
|
async function writeRootClaudeMd(root, dryRun) {
|
|
645
|
-
const filePath =
|
|
549
|
+
const filePath = path6.join(root, "CLAUDE.md");
|
|
646
550
|
const block = buildImportBlock();
|
|
647
|
-
const prev = await
|
|
551
|
+
const prev = await fs5.pathExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
|
|
648
552
|
const next = injectHausBlock(prev, block);
|
|
649
553
|
const printable = displayPath(root, filePath);
|
|
650
554
|
if (dryRun) {
|
|
@@ -667,22 +571,12 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
667
571
|
}
|
|
668
572
|
|
|
669
573
|
// 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
574
|
import path8 from "path";
|
|
675
575
|
import fs7 from "fs-extra";
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
"joi",
|
|
681
|
-
"@hapi/joi",
|
|
682
|
-
"class-validator",
|
|
683
|
-
"superstruct",
|
|
684
|
-
"ajv"
|
|
685
|
-
];
|
|
576
|
+
|
|
577
|
+
// src/claude/derive-workflow-config.ts
|
|
578
|
+
import path7 from "path";
|
|
579
|
+
import fs6 from "fs-extra";
|
|
686
580
|
function binCmd(pm, bin, args) {
|
|
687
581
|
const tail = args ? ` ${args}` : "";
|
|
688
582
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -691,7 +585,7 @@ function binCmd(pm, bin, args) {
|
|
|
691
585
|
}
|
|
692
586
|
async function deriveWorkflowConfig(root, ctx) {
|
|
693
587
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
694
|
-
const pkg = await readJson(
|
|
588
|
+
const pkg = await readJson(path7.join(root, "package.json"));
|
|
695
589
|
const scripts = pkg?.scripts ?? {};
|
|
696
590
|
const deps = new Set(ctx.dependencies);
|
|
697
591
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -701,22 +595,13 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
701
595
|
return null;
|
|
702
596
|
};
|
|
703
597
|
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");
|
|
598
|
+
const exists = (rel) => fs6.pathExistsSync(path7.join(root, rel));
|
|
708
599
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
709
600
|
const hasCypress = hasDep("cypress");
|
|
710
601
|
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
602
|
return {
|
|
712
603
|
test: script("test") ?? `${pm} test`,
|
|
713
604
|
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
605
|
preCommitTool,
|
|
721
606
|
specPath: exists("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
722
607
|
designPath: exists("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
@@ -731,13 +616,12 @@ function fields(v) {
|
|
|
731
616
|
{ prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
|
|
732
617
|
{ prefix: "- UX flows: ", value: v.uxPath, hint: "path, e.g. docs/UX.md" },
|
|
733
618
|
{ 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" },
|
|
619
|
+
{
|
|
620
|
+
prefix: "- Test (E2E): ",
|
|
621
|
+
value: v.testE2E,
|
|
622
|
+
hint: "command, e.g. playwright test",
|
|
623
|
+
code: true
|
|
624
|
+
},
|
|
741
625
|
{ prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
|
|
742
626
|
];
|
|
743
627
|
}
|
|
@@ -751,7 +635,7 @@ function line(f) {
|
|
|
751
635
|
function buildWorkflowConfig(v) {
|
|
752
636
|
const f = fields(v);
|
|
753
637
|
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
754
|
-
return "# How this project works (
|
|
638
|
+
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
639
|
}
|
|
756
640
|
function refillContent(existing, v) {
|
|
757
641
|
const f = fields(v);
|
|
@@ -769,7 +653,6 @@ var FALLBACK_CONTEXT = {
|
|
|
769
653
|
repoName: "",
|
|
770
654
|
packageManager: "unknown",
|
|
771
655
|
repoRoles: [],
|
|
772
|
-
confidence: 0,
|
|
773
656
|
detectedStacks: {},
|
|
774
657
|
dependencies: [],
|
|
775
658
|
securityRisks: [],
|
|
@@ -781,7 +664,7 @@ var FALLBACK_CONTEXT = {
|
|
|
781
664
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
782
665
|
const destPath = hausPath(root, "workflow-config.md");
|
|
783
666
|
const printable = displayPath(root, destPath);
|
|
784
|
-
const exists = await
|
|
667
|
+
const exists = await fs7.pathExists(destPath);
|
|
785
668
|
if (exists && !opts.refill) {
|
|
786
669
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
787
670
|
return null;
|
|
@@ -789,11 +672,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
789
672
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
790
673
|
...FALLBACK_CONTEXT,
|
|
791
674
|
root,
|
|
792
|
-
repoName:
|
|
675
|
+
repoName: path8.basename(root)
|
|
793
676
|
};
|
|
794
677
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
795
678
|
if (exists) {
|
|
796
|
-
const current = await
|
|
679
|
+
const current = await fs7.readFile(destPath, "utf8");
|
|
797
680
|
const refilled = refillContent(current, values);
|
|
798
681
|
if (refilled === current) {
|
|
799
682
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -815,7 +698,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
815
698
|
}
|
|
816
699
|
|
|
817
700
|
// src/claude/write-workflow.ts
|
|
818
|
-
import
|
|
701
|
+
import fs8 from "fs-extra";
|
|
819
702
|
|
|
820
703
|
// src/claude/managed-template.ts
|
|
821
704
|
function normaliseLF(content2) {
|
|
@@ -829,10 +712,10 @@ function parseHausManagedHeader(line2) {
|
|
|
829
712
|
}
|
|
830
713
|
|
|
831
714
|
// src/claude/write-workflow.ts
|
|
832
|
-
var
|
|
833
|
-
var
|
|
715
|
+
var STABLE_ID = "template.workflow";
|
|
716
|
+
var SCHEMA_VERSION = "1";
|
|
834
717
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
835
|
-
return `<!-- HAUS-MANAGED id=${
|
|
718
|
+
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
836
719
|
}
|
|
837
720
|
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
838
721
|
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
@@ -848,16 +731,16 @@ async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
|
848
731
|
${templateContent}`;
|
|
849
732
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
850
733
|
const printable = displayPath(root, destPath);
|
|
851
|
-
if (await
|
|
852
|
-
const existing = await
|
|
734
|
+
if (await fs8.pathExists(destPath)) {
|
|
735
|
+
const existing = await fs8.readFile(destPath, "utf8");
|
|
853
736
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
854
737
|
const parsed = parseHausManagedHeader(firstLine);
|
|
855
738
|
if (!parsed) {
|
|
856
739
|
warn(`${printable}: no HAUS-MANAGED header \u2014 file appears user-owned, skipping`);
|
|
857
740
|
return null;
|
|
858
741
|
}
|
|
859
|
-
if (parsed.id !==
|
|
860
|
-
warn(`${printable}: HAUS-MANAGED id mismatch (expected ${
|
|
742
|
+
if (parsed.id !== STABLE_ID) {
|
|
743
|
+
warn(`${printable}: HAUS-MANAGED id mismatch (expected ${STABLE_ID}) \u2014 skipping`);
|
|
861
744
|
return null;
|
|
862
745
|
}
|
|
863
746
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
@@ -871,7 +754,7 @@ ${templateContent}`;
|
|
|
871
754
|
}
|
|
872
755
|
}
|
|
873
756
|
if (dryRun) {
|
|
874
|
-
const prev = await
|
|
757
|
+
const prev = await fs8.pathExists(destPath) ? await fs8.readFile(destPath, "utf8") : "";
|
|
875
758
|
if (!prev) {
|
|
876
759
|
log(createUnifiedDiff(printable, "", next));
|
|
877
760
|
} else {
|
|
@@ -898,7 +781,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
898
781
|
estimatedTokenReductionPct: 0
|
|
899
782
|
};
|
|
900
783
|
const pkgRoot = packageRoot();
|
|
901
|
-
const hausVersion = (await readJson(
|
|
784
|
+
const hausVersion = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
902
785
|
const coreFiles = [
|
|
903
786
|
claudePath(root, "settings.json"),
|
|
904
787
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -911,10 +794,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
911
794
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
912
795
|
refill: opts.refillConfig
|
|
913
796
|
});
|
|
914
|
-
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
915
797
|
const p6Files = [
|
|
916
798
|
rootClaudeMdPath,
|
|
917
|
-
projectFactsPath,
|
|
918
799
|
...workflowPath ? [workflowPath] : [],
|
|
919
800
|
...workflowConfigPath ? [workflowConfigPath] : []
|
|
920
801
|
];
|
|
@@ -928,7 +809,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
928
809
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
929
810
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
930
811
|
const configPath = hausPath(root, "config.json");
|
|
931
|
-
if (!await
|
|
812
|
+
if (!await fs9.pathExists(configPath)) {
|
|
932
813
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
933
814
|
}
|
|
934
815
|
await writeManagedText(
|
|
@@ -956,12 +837,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
956
837
|
dryRun
|
|
957
838
|
);
|
|
958
839
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
959
|
-
const manifestPath = fixtureManifestPath ??
|
|
960
|
-
const manifestDir =
|
|
840
|
+
const manifestPath = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
841
|
+
const manifestDir = path9.dirname(manifestPath);
|
|
961
842
|
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
962
843
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
963
844
|
const cacheManifest = await readJson(
|
|
964
|
-
|
|
845
|
+
path9.join(CACHE_DIR, "manifest.json")
|
|
965
846
|
);
|
|
966
847
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
967
848
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
@@ -983,23 +864,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
983
864
|
}
|
|
984
865
|
}
|
|
985
866
|
const cachedItem = cacheManifestById.get(item.id);
|
|
986
|
-
const cachePath = cachedItem?.path ?
|
|
987
|
-
const sourcePath = cachePath && await
|
|
867
|
+
const cachePath = cachedItem?.path ? path9.join(CACHE_DIR, cachedItem.path) : null;
|
|
868
|
+
const sourcePath = cachePath && await fs9.pathExists(cachePath) ? cachePath : path9.join(manifestDir, manifestItem.path);
|
|
988
869
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
989
|
-
const destination = claudePath(root, target,
|
|
990
|
-
if (await
|
|
870
|
+
const destination = claudePath(root, target, path9.basename(sourcePath));
|
|
871
|
+
if (await fs9.pathExists(sourcePath)) {
|
|
991
872
|
if (dryRun) {
|
|
992
|
-
const exists = await
|
|
873
|
+
const exists = await fs9.pathExists(destination);
|
|
993
874
|
log(
|
|
994
875
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
995
876
|
);
|
|
996
877
|
} else {
|
|
997
|
-
await
|
|
998
|
-
await
|
|
878
|
+
await fs9.ensureDir(path9.dirname(destination));
|
|
879
|
+
await fs9.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
999
880
|
}
|
|
1000
881
|
files.push(destination);
|
|
1001
882
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
1002
|
-
installedPathsByItem.set(item.id, [...current,
|
|
883
|
+
installedPathsByItem.set(item.id, [...current, path9.relative(root, destination)]);
|
|
1003
884
|
installedIds.add(item.id);
|
|
1004
885
|
} else {
|
|
1005
886
|
warn(
|
|
@@ -1016,7 +897,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1016
897
|
id: r.id,
|
|
1017
898
|
type: r.type,
|
|
1018
899
|
reason: r.reason,
|
|
1019
|
-
|
|
900
|
+
selectionMode: r.selectionMode
|
|
1020
901
|
})),
|
|
1021
902
|
false
|
|
1022
903
|
);
|
|
@@ -1050,7 +931,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1050
931
|
return [...new Set(files)];
|
|
1051
932
|
}
|
|
1052
933
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
1053
|
-
const prev = await
|
|
934
|
+
const prev = await fs9.pathExists(filePath) ? await fs9.readFile(filePath, "utf8") : "";
|
|
1054
935
|
const printable = displayPath(root, filePath);
|
|
1055
936
|
if (dryRun) {
|
|
1056
937
|
if (!prev) {
|
|
@@ -1077,7 +958,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
1077
958
|
|
|
1078
959
|
// src/commands/apply.ts
|
|
1079
960
|
async function cacheHasItems() {
|
|
1080
|
-
const data = await readJson(
|
|
961
|
+
const data = await readJson(path10.join(CACHE_DIR, "manifest.json"));
|
|
1081
962
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
1082
963
|
}
|
|
1083
964
|
async function runApply(options) {
|
|
@@ -1104,7 +985,7 @@ async function runApply(options) {
|
|
|
1104
985
|
} else {
|
|
1105
986
|
const items = rec.recommended;
|
|
1106
987
|
const choices = items.map((item) => ({
|
|
1107
|
-
name: `${item.id} [${item.
|
|
988
|
+
name: `${item.id} [${item.selectionMode}] \u2014 ${item.reason}`,
|
|
1108
989
|
value: item.id,
|
|
1109
990
|
checked: true
|
|
1110
991
|
}));
|
|
@@ -1147,8 +1028,8 @@ async function runApply(options) {
|
|
|
1147
1028
|
|
|
1148
1029
|
// src/catalog/load-catalog.ts
|
|
1149
1030
|
import os3 from "os";
|
|
1150
|
-
import
|
|
1151
|
-
var CACHE_MANIFEST =
|
|
1031
|
+
import path11 from "path";
|
|
1032
|
+
var CACHE_MANIFEST = path11.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
|
|
1152
1033
|
async function loadCatalog(root) {
|
|
1153
1034
|
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
1154
1035
|
if (envPath) {
|
|
@@ -1157,10 +1038,10 @@ async function loadCatalog(root) {
|
|
|
1157
1038
|
}
|
|
1158
1039
|
const cacheData = await readJson(CACHE_MANIFEST);
|
|
1159
1040
|
if (cacheData?.items?.length) return cacheData.items;
|
|
1160
|
-
const localManifest =
|
|
1041
|
+
const localManifest = path11.join(root, "library/catalog/manifest.json");
|
|
1161
1042
|
const localData = await readJson(localManifest);
|
|
1162
1043
|
if (localData?.items?.length) return localData.items;
|
|
1163
|
-
const packageManifest =
|
|
1044
|
+
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
1164
1045
|
const data = await readJson(packageManifest);
|
|
1165
1046
|
return data?.items ?? [];
|
|
1166
1047
|
}
|
|
@@ -1200,7 +1081,7 @@ async function runCatalogAudit() {
|
|
|
1200
1081
|
}
|
|
1201
1082
|
|
|
1202
1083
|
// src/commands/config.ts
|
|
1203
|
-
import
|
|
1084
|
+
import path12 from "path";
|
|
1204
1085
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1205
1086
|
var HOOK_ALIASES = {
|
|
1206
1087
|
"hook.context": "context"
|
|
@@ -1213,7 +1094,7 @@ async function runConfig(key, action) {
|
|
|
1213
1094
|
);
|
|
1214
1095
|
}
|
|
1215
1096
|
const root = process.cwd();
|
|
1216
|
-
const configPath =
|
|
1097
|
+
const configPath = path12.join(root, CONFIG_PATH2);
|
|
1217
1098
|
const existing = await readJson(configPath);
|
|
1218
1099
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1219
1100
|
cfg.hooks ??= {};
|
|
@@ -1234,27 +1115,15 @@ function normalizeRecommendation(input2) {
|
|
|
1234
1115
|
const normalizedReasons = item.reasons?.map((reason) => ({
|
|
1235
1116
|
code: reason.code ?? "legacy-reason",
|
|
1236
1117
|
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
1237
|
-
weight: reason.weight ?? 0,
|
|
1238
1118
|
...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;
|
|
1119
|
+
})) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
|
|
1243
1120
|
return {
|
|
1244
1121
|
id: item.id,
|
|
1245
1122
|
type: item.type ?? "skill",
|
|
1246
1123
|
reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
|
|
1247
1124
|
reasons: normalizedReasons,
|
|
1248
|
-
confidence,
|
|
1249
|
-
confidenceLevel: item.confidenceLevel ?? (confidence >= 0.75 ? "high" : confidence >= 0.4 ? "medium" : "low"),
|
|
1250
1125
|
selectionMode: item.selectionMode ?? "matched",
|
|
1251
1126
|
install: item.install ?? true,
|
|
1252
|
-
score: item.score ?? 0,
|
|
1253
|
-
scoreBreakdown: {
|
|
1254
|
-
bonuses: normalizedReasons,
|
|
1255
|
-
penalties: [],
|
|
1256
|
-
finalScore: item.score ?? 0
|
|
1257
|
-
},
|
|
1258
1127
|
tags: item.tags,
|
|
1259
1128
|
ecosystem: item.ecosystem,
|
|
1260
1129
|
tokenEstimate: item.tokenEstimate
|
|
@@ -1266,15 +1135,8 @@ function normalizeRecommendation(input2) {
|
|
|
1266
1135
|
skipReasons: item.skipReasons?.map((reason) => ({
|
|
1267
1136
|
code: reason.code ?? "legacy-skip-reason",
|
|
1268
1137
|
message: reason.message ?? item.reason ?? "legacy skipped reason",
|
|
1269
|
-
penalty: reason.penalty ?? 0,
|
|
1270
1138
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1271
|
-
})) ?? [
|
|
1272
|
-
{
|
|
1273
|
-
code: "legacy-skip-reason",
|
|
1274
|
-
message: item.reason ?? "legacy skipped reason",
|
|
1275
|
-
penalty: 0
|
|
1276
|
-
}
|
|
1277
|
-
]
|
|
1139
|
+
})) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
|
|
1278
1140
|
}));
|
|
1279
1141
|
return {
|
|
1280
1142
|
mode: input2.mode === "guided" ? "guided" : "fast",
|
|
@@ -1294,8 +1156,6 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1294
1156
|
return {
|
|
1295
1157
|
selected: recommendation.recommended.map((item) => ({
|
|
1296
1158
|
id: item.id,
|
|
1297
|
-
confidence: item.confidence,
|
|
1298
|
-
confidenceLevel: item.confidenceLevel,
|
|
1299
1159
|
selectionMode: item.selectionMode,
|
|
1300
1160
|
reasons: item.reasons.map((reason) => reason.message)
|
|
1301
1161
|
})),
|
|
@@ -1305,7 +1165,6 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1305
1165
|
reasonDetails: item.skipReasons.map((reason) => ({
|
|
1306
1166
|
code: reason.code,
|
|
1307
1167
|
message: reason.message,
|
|
1308
|
-
penalty: reason.penalty,
|
|
1309
1168
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1310
1169
|
}))
|
|
1311
1170
|
})),
|
|
@@ -1532,6 +1391,13 @@ function computeRuleIntents(rule) {
|
|
|
1532
1391
|
}
|
|
1533
1392
|
|
|
1534
1393
|
// src/recommender/rule-selection.ts
|
|
1394
|
+
function evidenceCount(rule) {
|
|
1395
|
+
return rule.reasons.filter((r) => r.code !== "default-baseline").length;
|
|
1396
|
+
}
|
|
1397
|
+
function isRoleOnly(rule) {
|
|
1398
|
+
const codes = rule.reasons.map((r) => r.code).filter((c) => c !== "default-baseline");
|
|
1399
|
+
return codes.length > 0 && codes.every((c) => c === "repo-role-match");
|
|
1400
|
+
}
|
|
1535
1401
|
var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
|
|
1536
1402
|
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
|
|
1537
1403
|
const recommended = recommendation?.recommended ?? [];
|
|
@@ -1549,7 +1415,7 @@ function applyTokenBudget(rules, budget) {
|
|
|
1549
1415
|
used += r.tokenEstimate ?? 0;
|
|
1550
1416
|
}
|
|
1551
1417
|
}
|
|
1552
|
-
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => b
|
|
1418
|
+
const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => evidenceCount(b) - evidenceCount(a) || a.id.localeCompare(b.id));
|
|
1553
1419
|
for (const r of matched) {
|
|
1554
1420
|
const est = r.tokenEstimate ?? 0;
|
|
1555
1421
|
if (used + est <= budget) {
|
|
@@ -1587,20 +1453,20 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
1587
1453
|
});
|
|
1588
1454
|
if (tokenMatches.length > 0) return tokenMatches;
|
|
1589
1455
|
const taskWantsTesting = taskIntents.has("testing");
|
|
1590
|
-
const
|
|
1456
|
+
const capped = recommended.filter((rule) => {
|
|
1591
1457
|
if (rule.selectionMode === "baseline") return false;
|
|
1592
|
-
if (rule
|
|
1458
|
+
if (isRoleOnly(rule)) return false;
|
|
1593
1459
|
if (taskWantsTesting) return true;
|
|
1594
1460
|
const ruleIntents = computeRuleIntents(rule);
|
|
1595
1461
|
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
1596
1462
|
return !isTestingOnly;
|
|
1597
1463
|
});
|
|
1598
|
-
return
|
|
1464
|
+
return capped.slice(0, 8);
|
|
1599
1465
|
}
|
|
1600
1466
|
|
|
1601
1467
|
// src/scanner/scan-project.ts
|
|
1602
1468
|
import { readFile as readFile2 } from "fs/promises";
|
|
1603
|
-
import
|
|
1469
|
+
import path16 from "path";
|
|
1604
1470
|
|
|
1605
1471
|
// src/utils/audit-checks.ts
|
|
1606
1472
|
function isRecord(v) {
|
|
@@ -1627,8 +1493,8 @@ function compareVersions(a, b) {
|
|
|
1627
1493
|
}
|
|
1628
1494
|
|
|
1629
1495
|
// src/scanner/detect-package-manager.ts
|
|
1630
|
-
import
|
|
1631
|
-
import
|
|
1496
|
+
import path13 from "path";
|
|
1497
|
+
import fs10 from "fs-extra";
|
|
1632
1498
|
function detectPackageManager(root, packageManagerField) {
|
|
1633
1499
|
const field = String(packageManagerField ?? "").trim();
|
|
1634
1500
|
if (field.startsWith("yarn@")) {
|
|
@@ -1646,9 +1512,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1646
1512
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1647
1513
|
return "unknown";
|
|
1648
1514
|
}
|
|
1649
|
-
if (
|
|
1650
|
-
if (
|
|
1651
|
-
if (
|
|
1515
|
+
if (fs10.existsSync(path13.join(root, "yarn.lock"))) return "yarn";
|
|
1516
|
+
if (fs10.existsSync(path13.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
1517
|
+
if (fs10.existsSync(path13.join(root, "package-lock.json"))) return "npm";
|
|
1652
1518
|
return "unknown";
|
|
1653
1519
|
}
|
|
1654
1520
|
|
|
@@ -1821,7 +1687,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
1821
1687
|
}
|
|
1822
1688
|
|
|
1823
1689
|
// src/scanner/detection.ts
|
|
1824
|
-
import
|
|
1690
|
+
import path14 from "path";
|
|
1825
1691
|
var UNSUPPORTED_MARKERS = {
|
|
1826
1692
|
"requirements.txt": "python",
|
|
1827
1693
|
"pyproject.toml": "python",
|
|
@@ -1875,14 +1741,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
1875
1741
|
function collectUnsupportedSignals(files) {
|
|
1876
1742
|
return [
|
|
1877
1743
|
...new Set(
|
|
1878
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
1744
|
+
files.map((f) => UNSUPPORTED_MARKERS[path14.basename(f)]).filter((s) => Boolean(s))
|
|
1879
1745
|
)
|
|
1880
1746
|
].sort();
|
|
1881
1747
|
}
|
|
1882
1748
|
|
|
1883
1749
|
// src/scanner/render.ts
|
|
1884
1750
|
import { readFile } from "fs/promises";
|
|
1885
|
-
import
|
|
1751
|
+
import path15 from "path";
|
|
1886
1752
|
|
|
1887
1753
|
// src/scanner/role-labels.ts
|
|
1888
1754
|
var ROLE_LABELS = {
|
|
@@ -1948,7 +1814,7 @@ async function buildContentBlob(root, files) {
|
|
|
1948
1814
|
const batch = await Promise.all(
|
|
1949
1815
|
slice.slice(i, i + CHUNK).map(async (rel) => {
|
|
1950
1816
|
try {
|
|
1951
|
-
return await readFile(
|
|
1817
|
+
return await readFile(path15.join(root, rel), "utf8");
|
|
1952
1818
|
} catch {
|
|
1953
1819
|
return "";
|
|
1954
1820
|
}
|
|
@@ -1958,11 +1824,6 @@ async function buildContentBlob(root, files) {
|
|
|
1958
1824
|
}
|
|
1959
1825
|
return parts.join("\n");
|
|
1960
1826
|
}
|
|
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
1827
|
function renderSummary(context) {
|
|
1967
1828
|
return `# Repo summary
|
|
1968
1829
|
|
|
@@ -2022,8 +1883,8 @@ var SAFE_FILES = [
|
|
|
2022
1883
|
"Gemfile"
|
|
2023
1884
|
];
|
|
2024
1885
|
async function scanProject(root, mode = "fast") {
|
|
2025
|
-
const pkg = await readJson(
|
|
2026
|
-
const composer = await readJson(
|
|
1886
|
+
const pkg = await readJson(path16.join(root, "package.json"));
|
|
1887
|
+
const composer = await readJson(path16.join(root, "composer.json"));
|
|
2027
1888
|
const files = await listFiles(root, SAFE_FILES);
|
|
2028
1889
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2029
1890
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2057,10 +1918,9 @@ async function scanProject(root, mode = "fast") {
|
|
|
2057
1918
|
mode,
|
|
2058
1919
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2059
1920
|
root,
|
|
2060
|
-
repoName: String(pkg?.name ??
|
|
1921
|
+
repoName: String(pkg?.name ?? path16.basename(root)),
|
|
2061
1922
|
packageManager,
|
|
2062
1923
|
repoRoles: roles,
|
|
2063
|
-
confidence: computeConfidence(roles, stacks),
|
|
2064
1924
|
detectedStacks: stacks,
|
|
2065
1925
|
dependencies: deps,
|
|
2066
1926
|
securityRisks,
|
|
@@ -2076,7 +1936,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2076
1936
|
const scanHashes = Object.fromEntries(
|
|
2077
1937
|
await Promise.all(
|
|
2078
1938
|
safeFiles.map(
|
|
2079
|
-
async (f) => [f, hashText(await readFile2(
|
|
1939
|
+
async (f) => [f, hashText(await readFile2(path16.join(root, f), "utf8"))]
|
|
2080
1940
|
)
|
|
2081
1941
|
)
|
|
2082
1942
|
);
|
|
@@ -2106,9 +1966,6 @@ async function runContext(options) {
|
|
|
2106
1966
|
const summary = await readText(hausPath(root, "repo-summary.md")) ?? "";
|
|
2107
1967
|
const recommendationRaw = await readJson(hausPath(root, "recommendation.json"));
|
|
2108
1968
|
const recommendation = recommendationRaw ? normalizeRecommendation(recommendationRaw) : void 0;
|
|
2109
|
-
const rawBreakdownById = new Map(
|
|
2110
|
-
(recommendationRaw?.recommended ?? []).map((item) => [item.id, item.scoreBreakdown])
|
|
2111
|
-
);
|
|
2112
1969
|
const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
|
|
2113
1970
|
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
|
|
2114
1971
|
tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
|
|
@@ -2119,10 +1976,9 @@ async function runContext(options) {
|
|
|
2119
1976
|
roles: context.repoRoles,
|
|
2120
1977
|
selectedRules: selected.map((x) => ({
|
|
2121
1978
|
id: x.id,
|
|
2122
|
-
confidenceLevel: x.confidenceLevel,
|
|
2123
1979
|
selectionMode: x.selectionMode,
|
|
2124
1980
|
reasons: x.reasons.map((reason) => reason.message),
|
|
2125
|
-
...options.verbose ? {
|
|
1981
|
+
...options.verbose ? { signals: x.reasons.map((r) => r.signal).filter(Boolean) } : {}
|
|
2126
1982
|
})),
|
|
2127
1983
|
skippedCount: recommendation?.skippedRules ?? 0,
|
|
2128
1984
|
estimatedTokenReductionPct: recommendation?.estimatedTokenReductionPct ?? 0
|
|
@@ -2143,15 +1999,8 @@ async function runContext(options) {
|
|
|
2143
1999
|
...payload.selectedRules.flatMap((rule) => {
|
|
2144
2000
|
const reasonLine = `- ${rule.id}: ${rule.reasons.join(", ")}`;
|
|
2145
2001
|
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];
|
|
2002
|
+
const signals = (rule.signals ?? []).map((s) => ` \u2022 ${s}`);
|
|
2003
|
+
return [reasonLine, ...signals];
|
|
2155
2004
|
}),
|
|
2156
2005
|
summary
|
|
2157
2006
|
];
|
|
@@ -2160,8 +2009,8 @@ async function runContext(options) {
|
|
|
2160
2009
|
}
|
|
2161
2010
|
|
|
2162
2011
|
// src/commands/doctor.ts
|
|
2163
|
-
import
|
|
2164
|
-
import
|
|
2012
|
+
import path17 from "path";
|
|
2013
|
+
import fs11 from "fs-extra";
|
|
2165
2014
|
|
|
2166
2015
|
// src/update/npm-version.ts
|
|
2167
2016
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -2241,7 +2090,7 @@ async function runDoctor(options) {
|
|
|
2241
2090
|
const enabled = await isHookEnabled(root, key);
|
|
2242
2091
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
2243
2092
|
}
|
|
2244
|
-
const rootClaudeMdPath =
|
|
2093
|
+
const rootClaudeMdPath = path17.join(root, "CLAUDE.md");
|
|
2245
2094
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
2246
2095
|
if (!rootClaudeMdContent) {
|
|
2247
2096
|
flag(
|
|
@@ -2269,7 +2118,7 @@ async function runDoctor(options) {
|
|
|
2269
2118
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2270
2119
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2271
2120
|
for (const target of importTargets) {
|
|
2272
|
-
if (!await
|
|
2121
|
+
if (!await fs11.pathExists(hausPath(root, target))) {
|
|
2273
2122
|
flag(
|
|
2274
2123
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2275
2124
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -2280,7 +2129,7 @@ async function runDoctor(options) {
|
|
|
2280
2129
|
}
|
|
2281
2130
|
}
|
|
2282
2131
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
2283
|
-
const workflowExists = await
|
|
2132
|
+
const workflowExists = await fs11.pathExists(workflowPath);
|
|
2284
2133
|
if (!workflowExists) {
|
|
2285
2134
|
flag(
|
|
2286
2135
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -2294,15 +2143,15 @@ async function runDoctor(options) {
|
|
|
2294
2143
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
2295
2144
|
} else {
|
|
2296
2145
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
2297
|
-
const cachePath =
|
|
2298
|
-
const bundledPath =
|
|
2146
|
+
const cachePath = path17.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
2147
|
+
const bundledPath = path17.join(
|
|
2299
2148
|
packageRoot(),
|
|
2300
2149
|
"library",
|
|
2301
2150
|
"global",
|
|
2302
2151
|
"templates",
|
|
2303
2152
|
"agentic-workflow-standard.md"
|
|
2304
2153
|
);
|
|
2305
|
-
const templatePath = await
|
|
2154
|
+
const templatePath = await fs11.pathExists(cachePath) ? cachePath : bundledPath;
|
|
2306
2155
|
const templateContent = await readText(templatePath);
|
|
2307
2156
|
if (storedHashMatch && templateContent) {
|
|
2308
2157
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -2321,7 +2170,7 @@ async function runDoctor(options) {
|
|
|
2321
2170
|
}
|
|
2322
2171
|
}
|
|
2323
2172
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
2324
|
-
const workflowConfigExists = await
|
|
2173
|
+
const workflowConfigExists = await fs11.pathExists(workflowConfigPath);
|
|
2325
2174
|
if (!workflowConfigExists) {
|
|
2326
2175
|
flag(
|
|
2327
2176
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -2329,7 +2178,7 @@ async function runDoctor(options) {
|
|
|
2329
2178
|
"haus apply --write"
|
|
2330
2179
|
);
|
|
2331
2180
|
} else {
|
|
2332
|
-
const cfg = await
|
|
2181
|
+
const cfg = await fs11.readFile(workflowConfigPath, "utf8");
|
|
2333
2182
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2334
2183
|
if (unfilled > 0) {
|
|
2335
2184
|
flag(
|
|
@@ -2341,23 +2190,6 @@ async function runDoctor(options) {
|
|
|
2341
2190
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
2342
2191
|
}
|
|
2343
2192
|
}
|
|
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
2193
|
const cacheAgeMs = await getCacheManifestAge();
|
|
2362
2194
|
if (cacheAgeMs === null) {
|
|
2363
2195
|
flag(
|
|
@@ -2377,7 +2209,7 @@ async function runDoctor(options) {
|
|
|
2377
2209
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
2378
2210
|
}
|
|
2379
2211
|
}
|
|
2380
|
-
const pkgJson = await readJson(
|
|
2212
|
+
const pkgJson = await readJson(path17.join(packageRoot(), "package.json"));
|
|
2381
2213
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2382
2214
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
2383
2215
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -2425,7 +2257,6 @@ function formatRecommendationHuman(rec) {
|
|
|
2425
2257
|
if (rec.recommended.length === 0) lines.push(" (none)");
|
|
2426
2258
|
for (const item of rec.recommended) {
|
|
2427
2259
|
lines.push(`- ${item.id}`);
|
|
2428
|
-
lines.push(` confidence: ${item.confidenceLevel} (${item.confidence.toFixed(2)})`);
|
|
2429
2260
|
lines.push(` selection: ${item.selectionMode}`);
|
|
2430
2261
|
lines.push(" why:");
|
|
2431
2262
|
for (const reason of item.reasons) lines.push(` - ${formatReasonWithSignal(reason)}`);
|
|
@@ -2519,51 +2350,46 @@ async function runGuard(kind, _options) {
|
|
|
2519
2350
|
}
|
|
2520
2351
|
|
|
2521
2352
|
// src/commands/init.ts
|
|
2522
|
-
import
|
|
2523
|
-
import
|
|
2353
|
+
import path18 from "path";
|
|
2354
|
+
import fs12 from "fs-extra";
|
|
2524
2355
|
|
|
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);
|
|
2356
|
+
// src/utils/exec.ts
|
|
2357
|
+
import { execa } from "execa";
|
|
2358
|
+
async function runCommand(command, args = [], options = {}) {
|
|
2359
|
+
try {
|
|
2360
|
+
const result = await execa(command, args, {
|
|
2361
|
+
reject: false,
|
|
2362
|
+
// non-zero exits are returned, not thrown
|
|
2363
|
+
...options
|
|
2364
|
+
});
|
|
2365
|
+
return {
|
|
2366
|
+
command,
|
|
2367
|
+
args,
|
|
2368
|
+
stdout: String(result.stdout ?? ""),
|
|
2369
|
+
stderr: String(result.stderr ?? ""),
|
|
2370
|
+
exitCode: result.exitCode ?? 0
|
|
2371
|
+
};
|
|
2372
|
+
} catch (error2) {
|
|
2373
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2374
|
+
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2556
2375
|
}
|
|
2557
|
-
return [...ecosystems];
|
|
2558
2376
|
}
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
|
|
2562
|
-
}
|
|
2563
|
-
return void 0;
|
|
2377
|
+
async function runGit(args, options = {}) {
|
|
2378
|
+
return runCommand("git", args, options);
|
|
2564
2379
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2380
|
+
|
|
2381
|
+
// src/recommender/git-signal.ts
|
|
2382
|
+
async function readChangedFiles(root) {
|
|
2383
|
+
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2384
|
+
try {
|
|
2385
|
+
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2386
|
+
if (result.exitCode !== 0) {
|
|
2387
|
+
return [];
|
|
2388
|
+
}
|
|
2389
|
+
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2390
|
+
} catch {
|
|
2391
|
+
return [];
|
|
2392
|
+
}
|
|
2567
2393
|
}
|
|
2568
2394
|
|
|
2569
2395
|
// src/recommender/policies.ts
|
|
@@ -2633,63 +2459,6 @@ function mergeRecommendationWarnings(context) {
|
|
|
2633
2459
|
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
2634
2460
|
}
|
|
2635
2461
|
|
|
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
2462
|
// src/recommender/recommend.ts
|
|
2694
2463
|
async function recommend(root, context) {
|
|
2695
2464
|
const items = await loadCatalog(root);
|
|
@@ -2697,249 +2466,147 @@ async function recommend(root, context) {
|
|
|
2697
2466
|
const sources = await readJson(
|
|
2698
2467
|
hausPath(root, "sources-report.json")
|
|
2699
2468
|
) ?? {};
|
|
2700
|
-
const
|
|
2701
|
-
const
|
|
2702
|
-
const
|
|
2703
|
-
const
|
|
2704
|
-
const
|
|
2469
|
+
const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
|
|
2470
|
+
const scannerStacks = buildStackSet(context);
|
|
2471
|
+
const scannerRoles = new Set(context.repoRoles.map((r) => r.toLowerCase()));
|
|
2472
|
+
const scannerDeps = new Set(context.dependencies.map((d) => d.toLowerCase()));
|
|
2473
|
+
const toStrings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
2474
|
+
const deepStackValues = deep.stacks && typeof deep.stacks === "object" && !Array.isArray(deep.stacks) ? Object.values(deep.stacks).flatMap(toStrings) : [];
|
|
2475
|
+
const deepRoles = new Set(toStrings(deep.roles).map((r) => r.toLowerCase()));
|
|
2476
|
+
const deepStacks = new Set(
|
|
2477
|
+
[...toStrings(deep.roles), ...deepStackValues, ...toStrings(deep.patterns)].map(
|
|
2478
|
+
(x) => x.toLowerCase()
|
|
2479
|
+
)
|
|
2480
|
+
);
|
|
2481
|
+
const roleSet = /* @__PURE__ */ new Set([...scannerRoles, ...deepRoles]);
|
|
2482
|
+
const stackSet = /* @__PURE__ */ new Set([...scannerStacks, ...deepStacks]);
|
|
2483
|
+
const depSet = scannerDeps;
|
|
2705
2484
|
const recommended = [];
|
|
2706
2485
|
const skipped = [];
|
|
2707
2486
|
const goals = Object.values(setupAnswers).join(" ").toLowerCase();
|
|
2708
2487
|
const sourceTrust = new Map((sources.items ?? []).map((x) => [x.id, x.status ?? "candidate"]));
|
|
2709
2488
|
const changedFiles = await readChangedFiles(root);
|
|
2710
|
-
const
|
|
2489
|
+
const skip = (id, code, message, signal) => {
|
|
2490
|
+
skipped.push({ id, reason: message, skipReasons: [{ code, message, signal }] });
|
|
2491
|
+
};
|
|
2492
|
+
const roleSignal = (name) => scannerRoles.has(name.toLowerCase()) ? `role:${name}` : `deep:role:${name}`;
|
|
2493
|
+
const stackSignal = (name) => scannerStacks.has(name.toLowerCase()) ? `tag:${name}` : `deep:tag:${name}`;
|
|
2711
2494
|
for (const item of items) {
|
|
2712
2495
|
const itemSearchText = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
2713
2496
|
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
|
-
});
|
|
2497
|
+
skip(item.id, "unsupported-policy", "Unsupported stack policy");
|
|
2725
2498
|
continue;
|
|
2726
2499
|
}
|
|
2727
2500
|
if (item.source === "curated") {
|
|
2728
2501
|
const rs = item.reviewStatus;
|
|
2729
2502
|
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
|
-
});
|
|
2503
|
+
skip(
|
|
2504
|
+
item.id,
|
|
2505
|
+
"curated-not-approved",
|
|
2506
|
+
`Curated item requires reviewStatus:approved (got ${rs ?? "unset"})`,
|
|
2507
|
+
`reviewStatus:${rs ?? "unset"}`
|
|
2508
|
+
);
|
|
2742
2509
|
continue;
|
|
2743
2510
|
}
|
|
2744
2511
|
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
|
-
});
|
|
2512
|
+
skip(
|
|
2513
|
+
item.id,
|
|
2514
|
+
"curated-risk-blocked",
|
|
2515
|
+
"Curated item riskLevel is blocked",
|
|
2516
|
+
"riskLevel:blocked"
|
|
2517
|
+
);
|
|
2757
2518
|
continue;
|
|
2758
2519
|
}
|
|
2759
2520
|
}
|
|
2521
|
+
if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
|
|
2522
|
+
skip(item.id, "sensitive-policy", "Sensitive content policy block");
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
const trust = sourceTrust.get(item.source);
|
|
2526
|
+
if (trust === "candidate" || trust === "rejected") {
|
|
2527
|
+
skip(item.id, "source-trust", "Source trust policy block", `trust:${trust}`);
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
if (item.source && item.source !== "haus" && trust !== "approved") {
|
|
2531
|
+
skip(item.id, "source-approval", "Source not approved", `source:${item.source}`);
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
if (item.id === "haus.nx21-monorepo-patterns" && !roleSet.has("nx-monorepo")) {
|
|
2535
|
+
skip(
|
|
2536
|
+
item.id,
|
|
2537
|
+
"required-role-missing",
|
|
2538
|
+
"Required role missing: nx-monorepo",
|
|
2539
|
+
"role:nx-monorepo"
|
|
2540
|
+
);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (item.id === "haus.turbo-monorepo-patterns" && !roleSet.has("turbo-monorepo")) {
|
|
2544
|
+
skip(
|
|
2545
|
+
item.id,
|
|
2546
|
+
"required-role-missing",
|
|
2547
|
+
"Required role missing: turbo-monorepo",
|
|
2548
|
+
"role:turbo-monorepo"
|
|
2549
|
+
);
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2760
2552
|
const isDefaultBaseline = item.default === true;
|
|
2761
2553
|
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
|
-
}
|
|
2554
|
+
const push = (code, message, signal) => reasons.push({ code, message, signal });
|
|
2555
|
+
if (isDefaultBaseline) push("default-baseline", "catalog default baseline", "policy:default");
|
|
2775
2556
|
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
|
-
}
|
|
2557
|
+
if (roleMatch) push("repo-role-match", "repo role match", roleSignal(roleMatch));
|
|
2779
2558
|
const tagMatch = item.tags.find((t) => stackSet.has(t.toLowerCase()));
|
|
2780
|
-
if (tagMatch)
|
|
2781
|
-
pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
|
|
2782
|
-
}
|
|
2559
|
+
if (tagMatch) push("stack-match", "stack/dependency match", stackSignal(tagMatch));
|
|
2783
2560
|
const goalMatch = item.tags.find(
|
|
2784
2561
|
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
2785
2562
|
);
|
|
2786
|
-
if (goalMatch) {
|
|
2787
|
-
pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
|
|
2788
|
-
}
|
|
2563
|
+
if (goalMatch) push("goal-match", "guided goal match", `goal:${goalMatch}`);
|
|
2789
2564
|
if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
|
|
2790
|
-
|
|
2565
|
+
push(
|
|
2791
2566
|
"package-manager-match",
|
|
2792
2567
|
"package manager match",
|
|
2793
|
-
10,
|
|
2794
2568
|
`packageManager:${context.packageManager}`
|
|
2795
2569
|
);
|
|
2796
2570
|
}
|
|
2797
2571
|
const configSignal = item.tags.find(
|
|
2798
2572
|
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
2799
2573
|
);
|
|
2800
|
-
if (configSignal) {
|
|
2801
|
-
pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
|
|
2802
|
-
}
|
|
2574
|
+
if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
|
|
2803
2575
|
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
|
-
}
|
|
2576
|
+
if (changedMatch)
|
|
2577
|
+
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
2837
2578
|
const requiresAny = item.requiresAny ?? [];
|
|
2838
2579
|
if (requiresAny.length > 0) {
|
|
2839
|
-
const satisfied = matchRequiresAny(requiresAny, {
|
|
2840
|
-
stackSet,
|
|
2841
|
-
depSet,
|
|
2842
|
-
roleSet
|
|
2843
|
-
});
|
|
2580
|
+
const satisfied = matchRequiresAny(requiresAny, { stackSet, depSet, roleSet });
|
|
2844
2581
|
if (!satisfied.matched) {
|
|
2845
2582
|
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
|
-
});
|
|
2583
|
+
skip(
|
|
2584
|
+
item.id,
|
|
2585
|
+
"requires-any-unsatisfied",
|
|
2586
|
+
`requiresAny unsatisfied: needs ${description}`,
|
|
2587
|
+
description
|
|
2588
|
+
);
|
|
2858
2589
|
continue;
|
|
2859
2590
|
}
|
|
2860
2591
|
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
|
-
);
|
|
2592
|
+
push("requires-any-match", "requires-any signal match", satisfied.signal);
|
|
2873
2593
|
}
|
|
2874
2594
|
}
|
|
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);
|
|
2595
|
+
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
2596
|
+
if (isDefaultBaseline || hasEvidence) {
|
|
2914
2597
|
recommended.push({
|
|
2915
2598
|
id: item.id,
|
|
2916
2599
|
type: item.type,
|
|
2917
|
-
reason: reasons.length ? reasons.map((x) => x.message).join(", ") :
|
|
2600
|
+
reason: reasons.length ? reasons.map((x) => x.message).join(", ") : "eligible",
|
|
2918
2601
|
reasons,
|
|
2919
|
-
|
|
2920
|
-
confidenceLevel,
|
|
2921
|
-
selectionMode: isDefaultBaseline && reasons.every((r) => r.code === "default-baseline") ? "baseline" : "matched",
|
|
2602
|
+
selectionMode: isDefaultBaseline && !hasEvidence ? "baseline" : "matched",
|
|
2922
2603
|
install: true,
|
|
2923
|
-
score,
|
|
2924
|
-
scoreBreakdown: {
|
|
2925
|
-
bonuses: reasons,
|
|
2926
|
-
penalties: skipReasons,
|
|
2927
|
-
finalScore: score
|
|
2928
|
-
},
|
|
2929
2604
|
tags: item.tags,
|
|
2930
2605
|
ecosystem: item.ecosystem,
|
|
2931
2606
|
tokenEstimate: item.tokenEstimate
|
|
2932
2607
|
});
|
|
2933
2608
|
} 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 });
|
|
2609
|
+
skip(item.id, "no-role-stack-match", "No role/stack match");
|
|
2943
2610
|
}
|
|
2944
2611
|
}
|
|
2945
2612
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -3086,8 +2753,8 @@ async function runSetupProject(options) {
|
|
|
3086
2753
|
// src/commands/init.ts
|
|
3087
2754
|
async function runInit(options) {
|
|
3088
2755
|
const root = process.cwd();
|
|
3089
|
-
const hausDir =
|
|
3090
|
-
const alreadyInit = await
|
|
2756
|
+
const hausDir = path18.join(root, ".haus-workflow");
|
|
2757
|
+
const alreadyInit = await fs12.pathExists(hausDir);
|
|
3091
2758
|
if (alreadyInit) {
|
|
3092
2759
|
log("Haus AI already initialized in this project.");
|
|
3093
2760
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3099,8 +2766,8 @@ async function runInit(options) {
|
|
|
3099
2766
|
|
|
3100
2767
|
// src/install/apply.ts
|
|
3101
2768
|
import crypto2 from "crypto";
|
|
3102
|
-
import
|
|
3103
|
-
import
|
|
2769
|
+
import path21 from "path";
|
|
2770
|
+
import fs14 from "fs-extra";
|
|
3104
2771
|
|
|
3105
2772
|
// src/install/allow-rules.ts
|
|
3106
2773
|
var ALLOWED_SUBCOMMANDS = [
|
|
@@ -3147,13 +2814,13 @@ ${content2}`;
|
|
|
3147
2814
|
|
|
3148
2815
|
// src/install/manifest.ts
|
|
3149
2816
|
import os4 from "os";
|
|
3150
|
-
import
|
|
2817
|
+
import path19 from "path";
|
|
3151
2818
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
3152
2819
|
function globalClaudeDir() {
|
|
3153
|
-
return
|
|
2820
|
+
return path19.join(os4.homedir(), ".claude");
|
|
3154
2821
|
}
|
|
3155
2822
|
function hausManifestPath() {
|
|
3156
|
-
return
|
|
2823
|
+
return path19.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
3157
2824
|
}
|
|
3158
2825
|
async function readManifest() {
|
|
3159
2826
|
return readJson(hausManifestPath());
|
|
@@ -3172,10 +2839,10 @@ function buildManifest(source, files, hooks) {
|
|
|
3172
2839
|
}
|
|
3173
2840
|
|
|
3174
2841
|
// src/install/settings-merge.ts
|
|
3175
|
-
import
|
|
3176
|
-
import
|
|
2842
|
+
import path20 from "path";
|
|
2843
|
+
import fs13 from "fs-extra";
|
|
3177
2844
|
function settingsJsonPath() {
|
|
3178
|
-
return
|
|
2845
|
+
return path20.join(globalClaudeDir(), "settings.json");
|
|
3179
2846
|
}
|
|
3180
2847
|
async function readSettings() {
|
|
3181
2848
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -3316,7 +2983,7 @@ function stripHausHooks(settings) {
|
|
|
3316
2983
|
async function loadHooksFragment(fragmentPath) {
|
|
3317
2984
|
let raw;
|
|
3318
2985
|
try {
|
|
3319
|
-
raw = await
|
|
2986
|
+
raw = await fs13.readJson(fragmentPath);
|
|
3320
2987
|
} catch {
|
|
3321
2988
|
return [];
|
|
3322
2989
|
}
|
|
@@ -3325,46 +2992,46 @@ async function loadHooksFragment(fragmentPath) {
|
|
|
3325
2992
|
}
|
|
3326
2993
|
|
|
3327
2994
|
// src/install/apply.ts
|
|
3328
|
-
var
|
|
2995
|
+
var SCHEMA_VERSION2 = "1";
|
|
3329
2996
|
function hashContent(content2) {
|
|
3330
2997
|
return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
|
|
3331
2998
|
}
|
|
3332
2999
|
function sourceVersion() {
|
|
3333
3000
|
try {
|
|
3334
|
-
const pkgPath =
|
|
3335
|
-
const pkg = JSON.parse(
|
|
3001
|
+
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
3002
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
|
|
3336
3003
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3337
3004
|
} catch {
|
|
3338
3005
|
return "haus@0.0.0";
|
|
3339
3006
|
}
|
|
3340
3007
|
}
|
|
3341
3008
|
function globalSrcDir() {
|
|
3342
|
-
return
|
|
3009
|
+
return path21.join(packageRoot(), "library", "global");
|
|
3343
3010
|
}
|
|
3344
3011
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3345
3012
|
const entries = [];
|
|
3346
|
-
const skillsDir =
|
|
3347
|
-
if (
|
|
3348
|
-
for (const skillName of
|
|
3349
|
-
const skillFile =
|
|
3350
|
-
if (
|
|
3013
|
+
const skillsDir = path21.join(srcDir, "skills");
|
|
3014
|
+
if (fs14.pathExistsSync(skillsDir)) {
|
|
3015
|
+
for (const skillName of fs14.readdirSync(skillsDir)) {
|
|
3016
|
+
const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
|
|
3017
|
+
if (fs14.pathExistsSync(skillFile)) {
|
|
3351
3018
|
entries.push({
|
|
3352
3019
|
stableId: `skill.${skillName}`,
|
|
3353
|
-
srcRelPath:
|
|
3354
|
-
destPath:
|
|
3020
|
+
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3021
|
+
destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3355
3022
|
});
|
|
3356
3023
|
}
|
|
3357
3024
|
}
|
|
3358
3025
|
}
|
|
3359
|
-
const commandsDir =
|
|
3360
|
-
if (
|
|
3361
|
-
for (const fileName of
|
|
3026
|
+
const commandsDir = path21.join(srcDir, "commands");
|
|
3027
|
+
if (fs14.pathExistsSync(commandsDir)) {
|
|
3028
|
+
for (const fileName of fs14.readdirSync(commandsDir)) {
|
|
3362
3029
|
if (!fileName.endsWith(".md")) continue;
|
|
3363
3030
|
const commandName = fileName.slice(0, -".md".length);
|
|
3364
3031
|
entries.push({
|
|
3365
3032
|
stableId: `command.${commandName}`,
|
|
3366
|
-
srcRelPath:
|
|
3367
|
-
destPath:
|
|
3033
|
+
srcRelPath: path21.join("library", "global", "commands", fileName),
|
|
3034
|
+
destPath: path21.join(claudeDir, "commands", fileName)
|
|
3368
3035
|
});
|
|
3369
3036
|
}
|
|
3370
3037
|
}
|
|
@@ -3388,7 +3055,7 @@ async function applyInstall(options = {}) {
|
|
|
3388
3055
|
};
|
|
3389
3056
|
const manifestFiles = [];
|
|
3390
3057
|
for (const entry of sourceFiles) {
|
|
3391
|
-
const srcPath =
|
|
3058
|
+
const srcPath = path21.join(packageRoot(), entry.srcRelPath);
|
|
3392
3059
|
const rawContent = await readText(srcPath);
|
|
3393
3060
|
if (rawContent === void 0) {
|
|
3394
3061
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3396,7 +3063,7 @@ async function applyInstall(options = {}) {
|
|
|
3396
3063
|
}
|
|
3397
3064
|
const stamped = stampMarkdown(rawContent, {
|
|
3398
3065
|
stableId: entry.stableId,
|
|
3399
|
-
schemaVersion:
|
|
3066
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
3400
3067
|
source
|
|
3401
3068
|
});
|
|
3402
3069
|
const newHash = hashContent(stamped);
|
|
@@ -3408,7 +3075,7 @@ async function applyInstall(options = {}) {
|
|
|
3408
3075
|
}
|
|
3409
3076
|
continue;
|
|
3410
3077
|
}
|
|
3411
|
-
const destExists =
|
|
3078
|
+
const destExists = fs14.pathExistsSync(entry.destPath);
|
|
3412
3079
|
if (destExists) {
|
|
3413
3080
|
const currentContent = await readText(entry.destPath);
|
|
3414
3081
|
if (currentContent !== void 0) {
|
|
@@ -3441,10 +3108,10 @@ async function applyInstall(options = {}) {
|
|
|
3441
3108
|
destPath: entry.destPath,
|
|
3442
3109
|
srcRelPath: entry.srcRelPath,
|
|
3443
3110
|
hash: newHash,
|
|
3444
|
-
schemaVersion:
|
|
3111
|
+
schemaVersion: SCHEMA_VERSION2
|
|
3445
3112
|
});
|
|
3446
3113
|
}
|
|
3447
|
-
const fragmentPath =
|
|
3114
|
+
const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
|
|
3448
3115
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3449
3116
|
const settings = await readSettings();
|
|
3450
3117
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -3455,13 +3122,13 @@ async function applyInstall(options = {}) {
|
|
|
3455
3122
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3456
3123
|
for (const entry of existingManifest.files) {
|
|
3457
3124
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3458
|
-
if (!
|
|
3125
|
+
if (!fs14.pathExistsSync(entry.destPath)) continue;
|
|
3459
3126
|
const content2 = await readText(entry.destPath);
|
|
3460
3127
|
if (!content2) continue;
|
|
3461
3128
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3462
3129
|
const currentHash = hashContent(content2);
|
|
3463
3130
|
if (hasHeader && currentHash === entry.hash) {
|
|
3464
|
-
if (!dryRun) await
|
|
3131
|
+
if (!dryRun) await fs14.remove(entry.destPath);
|
|
3465
3132
|
result.deleted.push(entry.destPath);
|
|
3466
3133
|
} else {
|
|
3467
3134
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -3584,20 +3251,20 @@ async function runScan(options) {
|
|
|
3584
3251
|
}
|
|
3585
3252
|
|
|
3586
3253
|
// src/commands/undo.ts
|
|
3587
|
-
import
|
|
3588
|
-
import
|
|
3254
|
+
import path22 from "path";
|
|
3255
|
+
import fs15 from "fs-extra";
|
|
3589
3256
|
var CLAUDE_DIR = ".claude";
|
|
3590
3257
|
async function runUndo(options) {
|
|
3591
3258
|
const root = process.cwd();
|
|
3592
|
-
const targets = [
|
|
3593
|
-
const existing = targets.filter((p) =>
|
|
3259
|
+
const targets = [path22.join(root, CLAUDE_DIR), path22.join(root, HAUS_DIR)];
|
|
3260
|
+
const existing = targets.filter((p) => fs15.existsSync(p));
|
|
3594
3261
|
if (existing.length === 0) {
|
|
3595
3262
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
3596
3263
|
return;
|
|
3597
3264
|
}
|
|
3598
3265
|
if (!options.yes) {
|
|
3599
3266
|
const ok = await confirm(
|
|
3600
|
-
`Remove ${existing.map((p) =>
|
|
3267
|
+
`Remove ${existing.map((p) => path22.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
3601
3268
|
);
|
|
3602
3269
|
if (!ok) {
|
|
3603
3270
|
log("Cancelled.");
|
|
@@ -3605,15 +3272,15 @@ async function runUndo(options) {
|
|
|
3605
3272
|
}
|
|
3606
3273
|
}
|
|
3607
3274
|
for (const p of existing) {
|
|
3608
|
-
await
|
|
3609
|
-
log(`Removed ${
|
|
3275
|
+
await fs15.remove(p);
|
|
3276
|
+
log(`Removed ${path22.relative(root, p)}`);
|
|
3610
3277
|
}
|
|
3611
3278
|
}
|
|
3612
3279
|
|
|
3613
3280
|
// src/install/uninstall.ts
|
|
3614
3281
|
import crypto3 from "crypto";
|
|
3615
|
-
import
|
|
3616
|
-
import
|
|
3282
|
+
import path23 from "path";
|
|
3283
|
+
import fs16 from "fs-extra";
|
|
3617
3284
|
async function runUninstall(options = {}) {
|
|
3618
3285
|
const { force = false } = options;
|
|
3619
3286
|
const manifest = await readManifest();
|
|
@@ -3623,7 +3290,7 @@ async function runUninstall(options = {}) {
|
|
|
3623
3290
|
return result;
|
|
3624
3291
|
}
|
|
3625
3292
|
for (const entry of manifest.files) {
|
|
3626
|
-
const exists =
|
|
3293
|
+
const exists = fs16.pathExistsSync(entry.destPath);
|
|
3627
3294
|
if (!exists) continue;
|
|
3628
3295
|
const content2 = await readText(entry.destPath);
|
|
3629
3296
|
if (content2 === void 0) continue;
|
|
@@ -3641,22 +3308,22 @@ async function runUninstall(options = {}) {
|
|
|
3641
3308
|
result.skipped.push(entry.destPath);
|
|
3642
3309
|
continue;
|
|
3643
3310
|
}
|
|
3644
|
-
await
|
|
3645
|
-
await pruneEmptyDir(
|
|
3311
|
+
await fs16.remove(entry.destPath);
|
|
3312
|
+
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
3646
3313
|
result.deleted.push(entry.destPath);
|
|
3647
3314
|
}
|
|
3648
3315
|
const settings = await readSettings();
|
|
3649
3316
|
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3650
3317
|
await writeSettings(stripped);
|
|
3651
3318
|
result.hooksStripped = true;
|
|
3652
|
-
const hausDir =
|
|
3319
|
+
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
3653
3320
|
const manifestPath = hausManifestPath();
|
|
3654
|
-
if (
|
|
3655
|
-
await
|
|
3321
|
+
if (fs16.pathExistsSync(manifestPath)) {
|
|
3322
|
+
await fs16.remove(manifestPath);
|
|
3656
3323
|
}
|
|
3657
|
-
if (
|
|
3658
|
-
const remaining = await
|
|
3659
|
-
if (remaining.length === 0) await
|
|
3324
|
+
if (fs16.pathExistsSync(hausDir)) {
|
|
3325
|
+
const remaining = await fs16.readdir(hausDir);
|
|
3326
|
+
if (remaining.length === 0) await fs16.remove(hausDir);
|
|
3660
3327
|
}
|
|
3661
3328
|
return result;
|
|
3662
3329
|
}
|
|
@@ -3675,8 +3342,8 @@ function printUninstallResult(result) {
|
|
|
3675
3342
|
}
|
|
3676
3343
|
async function pruneEmptyDir(dir) {
|
|
3677
3344
|
try {
|
|
3678
|
-
const entries = await
|
|
3679
|
-
if (entries.length === 0) await
|
|
3345
|
+
const entries = await fs16.readdir(dir);
|
|
3346
|
+
if (entries.length === 0) await fs16.remove(dir);
|
|
3680
3347
|
} catch {
|
|
3681
3348
|
}
|
|
3682
3349
|
}
|
|
@@ -3694,7 +3361,7 @@ async function runUninstallCommand(options) {
|
|
|
3694
3361
|
}
|
|
3695
3362
|
|
|
3696
3363
|
// src/commands/update.ts
|
|
3697
|
-
import
|
|
3364
|
+
import path25 from "path";
|
|
3698
3365
|
|
|
3699
3366
|
// src/update/diff-generated-files.ts
|
|
3700
3367
|
function diffGeneratedFiles() {
|
|
@@ -3721,7 +3388,7 @@ function summarizeLockDiff(before, after) {
|
|
|
3721
3388
|
|
|
3722
3389
|
// src/update/lockfile.ts
|
|
3723
3390
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3724
|
-
import
|
|
3391
|
+
import path24 from "path";
|
|
3725
3392
|
async function checkLock(root) {
|
|
3726
3393
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3727
3394
|
const hasValidVersions = lock.every(
|
|
@@ -3742,7 +3409,7 @@ async function applyLock(root) {
|
|
|
3742
3409
|
try {
|
|
3743
3410
|
const backupDir = hausPath(root, "backups");
|
|
3744
3411
|
await mkdir(backupDir, { recursive: true });
|
|
3745
|
-
await copyFile(lockPath,
|
|
3412
|
+
await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3746
3413
|
} catch {
|
|
3747
3414
|
}
|
|
3748
3415
|
const enriched = await Promise.all(
|
|
@@ -3764,7 +3431,7 @@ function diffLock(before, after) {
|
|
|
3764
3431
|
}
|
|
3765
3432
|
async function hasLocalOverrides(root) {
|
|
3766
3433
|
try {
|
|
3767
|
-
await readFile3(
|
|
3434
|
+
await readFile3(path24.join(root, ".claude", "settings.json"), "utf8");
|
|
3768
3435
|
return true;
|
|
3769
3436
|
} catch {
|
|
3770
3437
|
return false;
|
|
@@ -3776,7 +3443,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3776
3443
|
async function runUpdate(options) {
|
|
3777
3444
|
const root = process.cwd();
|
|
3778
3445
|
if (options.check) {
|
|
3779
|
-
const pkgJson2 = await readJson(
|
|
3446
|
+
const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
|
|
3780
3447
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3781
3448
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3782
3449
|
checkLock(root),
|
|
@@ -3803,7 +3470,7 @@ async function runUpdate(options) {
|
|
|
3803
3470
|
if (!status.ok) process.exitCode = 1;
|
|
3804
3471
|
return;
|
|
3805
3472
|
}
|
|
3806
|
-
const pkgJson = await readJson(
|
|
3473
|
+
const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
|
|
3807
3474
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3808
3475
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3809
3476
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3833,8 +3500,8 @@ async function runUpdate(options) {
|
|
|
3833
3500
|
}
|
|
3834
3501
|
|
|
3835
3502
|
// src/commands/validate-catalog.ts
|
|
3836
|
-
import
|
|
3837
|
-
import
|
|
3503
|
+
import fs17 from "fs";
|
|
3504
|
+
import path26 from "path";
|
|
3838
3505
|
|
|
3839
3506
|
// library/catalog/validation-rules.json
|
|
3840
3507
|
var validation_rules_default = {
|
|
@@ -4083,23 +3750,23 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4083
3750
|
const failures = [];
|
|
4084
3751
|
for (const item of items) {
|
|
4085
3752
|
if (!item.path) continue;
|
|
4086
|
-
const absPath =
|
|
3753
|
+
const absPath = path26.join(manifestDir, item.path);
|
|
4087
3754
|
if (item.type === "skill") {
|
|
4088
|
-
const skillMd =
|
|
4089
|
-
if (!
|
|
4090
|
-
failures.push(`${item.id}: missing ${
|
|
3755
|
+
const skillMd = path26.join(absPath, "SKILL.md");
|
|
3756
|
+
if (!fs17.existsSync(skillMd)) {
|
|
3757
|
+
failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
|
|
4091
3758
|
continue;
|
|
4092
3759
|
}
|
|
4093
|
-
const text =
|
|
3760
|
+
const text = fs17.readFileSync(skillMd, "utf8");
|
|
4094
3761
|
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
4095
3762
|
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
4096
3763
|
}
|
|
4097
3764
|
} else if (item.type === "agent") {
|
|
4098
|
-
if (!
|
|
3765
|
+
if (!fs17.existsSync(absPath)) {
|
|
4099
3766
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4100
3767
|
continue;
|
|
4101
3768
|
}
|
|
4102
|
-
const text =
|
|
3769
|
+
const text = fs17.readFileSync(absPath, "utf8");
|
|
4103
3770
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
4104
3771
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
4105
3772
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -4110,7 +3777,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4110
3777
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
4111
3778
|
}
|
|
4112
3779
|
} else if (item.type === "template") {
|
|
4113
|
-
if (!
|
|
3780
|
+
if (!fs17.existsSync(absPath)) {
|
|
4114
3781
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4115
3782
|
}
|
|
4116
3783
|
}
|
|
@@ -4121,11 +3788,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4121
3788
|
const failures = [];
|
|
4122
3789
|
const dirs = ["skills", "agents"];
|
|
4123
3790
|
for (const dir of dirs) {
|
|
4124
|
-
const abs =
|
|
4125
|
-
if (!
|
|
3791
|
+
const abs = path26.join(manifestDir, dir);
|
|
3792
|
+
if (!fs17.existsSync(abs)) continue;
|
|
4126
3793
|
walkMd(abs, (file) => {
|
|
4127
|
-
const text =
|
|
4128
|
-
const rel =
|
|
3794
|
+
const text = fs17.readFileSync(file, "utf8");
|
|
3795
|
+
const rel = path26.relative(manifestDir, file);
|
|
4129
3796
|
const lines = text.split(/\r?\n/);
|
|
4130
3797
|
for (let i = 0; i < lines.length; i++) {
|
|
4131
3798
|
const line2 = lines[i] ?? "";
|
|
@@ -4144,8 +3811,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4144
3811
|
return failures;
|
|
4145
3812
|
}
|
|
4146
3813
|
function walkMd(dir, fn) {
|
|
4147
|
-
for (const entry of
|
|
4148
|
-
const full =
|
|
3814
|
+
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
3815
|
+
const full = path26.join(dir, entry.name);
|
|
4149
3816
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4150
3817
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4151
3818
|
}
|
|
@@ -4156,8 +3823,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
4156
3823
|
process.exitCode = 1;
|
|
4157
3824
|
return;
|
|
4158
3825
|
}
|
|
4159
|
-
const abs =
|
|
4160
|
-
const manifestDir =
|
|
3826
|
+
const abs = path26.resolve(process.cwd(), manifestPath);
|
|
3827
|
+
const manifestDir = path26.dirname(abs);
|
|
4161
3828
|
const data = await readJson(abs);
|
|
4162
3829
|
if (!data?.items) {
|
|
4163
3830
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4186,7 +3853,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
4186
3853
|
}
|
|
4187
3854
|
|
|
4188
3855
|
// src/commands/workspace.ts
|
|
4189
|
-
import
|
|
3856
|
+
import path27 from "path";
|
|
4190
3857
|
import YAML from "yaml";
|
|
4191
3858
|
async function runWorkspace(action) {
|
|
4192
3859
|
if (action === "init") {
|
|
@@ -4219,7 +3886,7 @@ relationships: []
|
|
|
4219
3886
|
const summaries = [];
|
|
4220
3887
|
const ownership = {};
|
|
4221
3888
|
for (const repo of repos) {
|
|
4222
|
-
const repoRoot =
|
|
3889
|
+
const repoRoot = path27.resolve(process.cwd(), repo.path);
|
|
4223
3890
|
const result = await scanProject(repoRoot, "fast");
|
|
4224
3891
|
summaries.push({
|
|
4225
3892
|
name: repo.name,
|
|
@@ -4255,7 +3922,7 @@ ${summaries.map(
|
|
|
4255
3922
|
// src/cli.ts
|
|
4256
3923
|
function cliVersion() {
|
|
4257
3924
|
try {
|
|
4258
|
-
const pkgPath =
|
|
3925
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
4259
3926
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
4260
3927
|
return pkg.version ?? "0.0.0";
|
|
4261
3928
|
} catch {
|
|
@@ -4265,7 +3932,7 @@ function cliVersion() {
|
|
|
4265
3932
|
var program = new Command();
|
|
4266
3933
|
function validateRuntimeNodeVersion() {
|
|
4267
3934
|
try {
|
|
4268
|
-
const pkgPath =
|
|
3935
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
4269
3936
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
4270
3937
|
const requiredRange = pkg.engines?.node;
|
|
4271
3938
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|