@haus-tech/haus-workflow 0.12.0 → 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 +19 -0
- package/README.md +1 -1
- package/dist/cli.js +322 -643
- 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
|
|
@@ -53,6 +53,18 @@ async function fetchRemoteManifest() {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
57
|
+
async function readWorkflowTemplate(opts = {}) {
|
|
58
|
+
const dest = path.join(CACHE_DIR, WORKFLOW_TEMPLATE_REL);
|
|
59
|
+
if (await fs.pathExists(dest)) return fs.readFile(dest, "utf8");
|
|
60
|
+
const text = await fetchText(`${REMOTE_BASE}/${WORKFLOW_TEMPLATE_REL}`);
|
|
61
|
+
if (text === null) return null;
|
|
62
|
+
if (!opts.dryRun) {
|
|
63
|
+
await fs.ensureDir(path.dirname(dest));
|
|
64
|
+
await fs.writeFile(dest, text, "utf8");
|
|
65
|
+
}
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
56
68
|
function isSafeCatalogPath(itemPath) {
|
|
57
69
|
if (!itemPath || path.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
58
70
|
const normalized = path.normalize(itemPath);
|
|
@@ -80,7 +92,8 @@ async function syncRemoteCatalog() {
|
|
|
80
92
|
let unchanged = 0;
|
|
81
93
|
const failed = [];
|
|
82
94
|
for (const item of items) {
|
|
83
|
-
if (item.type !== "skill" && item.type !== "agent" || !item.path)
|
|
95
|
+
if (item.type !== "skill" && item.type !== "agent" && item.type !== "template" || !item.path)
|
|
96
|
+
continue;
|
|
84
97
|
if (!isSafeCatalogPath(item.path)) {
|
|
85
98
|
warn(`Skipping ${item.id}: invalid path "${item.path}"`);
|
|
86
99
|
failed.push(item.id);
|
|
@@ -158,8 +171,8 @@ async function getCacheManifestAge() {
|
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
// src/claude/write-claude-files.ts
|
|
161
|
-
import
|
|
162
|
-
import
|
|
174
|
+
import path9 from "path";
|
|
175
|
+
import fs9 from "fs-extra";
|
|
163
176
|
|
|
164
177
|
// src/update/hash-installed.ts
|
|
165
178
|
import path3 from "path";
|
|
@@ -403,8 +416,8 @@ function buildDenyRules() {
|
|
|
403
416
|
for (const command of DANGEROUS_COMMANDS) {
|
|
404
417
|
rules.push(`Bash(${command}:*)`);
|
|
405
418
|
}
|
|
406
|
-
for (const
|
|
407
|
-
const pattern = SENSITIVE_DIRS.has(
|
|
419
|
+
for (const path29 of SENSITIVE_PATHS) {
|
|
420
|
+
const pattern = SENSITIVE_DIRS.has(path29) ? `${path29}/**` : path29;
|
|
408
421
|
for (const tool of FILE_TOOLS) {
|
|
409
422
|
rules.push(`${tool}(${pattern})`);
|
|
410
423
|
}
|
|
@@ -502,109 +515,13 @@ async function verifyProjectSettingsHooksContract(root) {
|
|
|
502
515
|
return { ok: true, message: "settings.json matches canonical hook contract." };
|
|
503
516
|
}
|
|
504
517
|
|
|
505
|
-
// src/claude/write-
|
|
518
|
+
// src/claude/write-root-claude-md.ts
|
|
506
519
|
import path6 from "path";
|
|
507
520
|
import fs5 from "fs-extra";
|
|
508
|
-
var STABLE_ID = "generated.project-facts";
|
|
509
|
-
var SCHEMA_VERSION = "1";
|
|
510
|
-
function makeHeader(pkgVersion) {
|
|
511
|
-
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} -->`;
|
|
512
|
-
}
|
|
513
|
-
function renderProjectFacts(ctx, rec, pkgVersion) {
|
|
514
|
-
const header = makeHeader(pkgVersion);
|
|
515
|
-
const stackEntries = Object.entries(ctx.detectedStacks ?? {});
|
|
516
|
-
const stackLines = stackEntries.length > 0 ? stackEntries.map(([stack, files]) => {
|
|
517
|
-
const f = files;
|
|
518
|
-
return `- **${stack}**: ${f.slice(0, 3).join(", ")}${f.length > 3 ? ", \u2026" : ""}`;
|
|
519
|
-
}).join("\n") : "- none detected";
|
|
520
|
-
const roles = ctx.repoRoles?.length > 0 ? ctx.repoRoles.join(", ") : "unknown";
|
|
521
|
-
const recLines = rec.recommended.length > 0 ? rec.recommended.map((r) => `- \`${r.id}\` (${r.type}) \u2014 ${r.reason}`).join("\n") : "- none";
|
|
522
|
-
const warningLines = [...ctx.warnings ?? [], ...rec.warnings ?? []];
|
|
523
|
-
const warnSection = warningLines.length > 0 ? warningLines.map((w) => `- ${w}`).join("\n") : "- none";
|
|
524
|
-
const repoName = ctx.repoName ?? path6.basename(ctx.root ?? "unknown");
|
|
525
|
-
return `${header}
|
|
526
|
-
|
|
527
|
-
# What haus found in this project
|
|
528
|
-
|
|
529
|
-
> This is a plain summary of your project that haus wrote automatically, so Claude
|
|
530
|
-
> always has the basics to hand. haus rewrites it on every \`haus apply\`, so don't
|
|
531
|
-
> edit it by hand \u2014 your changes would be replaced next time.
|
|
532
|
-
|
|
533
|
-
**Repo:** ${repoName}
|
|
534
|
-
**Package manager:** ${ctx.packageManager ?? "unknown"}
|
|
535
|
-
**Roles:** ${roles}
|
|
536
|
-
|
|
537
|
-
## Detected stacks
|
|
538
|
-
|
|
539
|
-
${stackLines}
|
|
540
|
-
|
|
541
|
-
## Recommended context
|
|
542
|
-
|
|
543
|
-
${recLines}
|
|
544
|
-
|
|
545
|
-
## Warnings
|
|
546
|
-
|
|
547
|
-
${warnSection}
|
|
548
|
-
`;
|
|
549
|
-
}
|
|
550
|
-
async function writeProjectFacts(root, pkgVersion, dryRun) {
|
|
551
|
-
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
552
|
-
mode: "fast",
|
|
553
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
554
|
-
root,
|
|
555
|
-
repoName: path6.basename(root),
|
|
556
|
-
packageManager: "unknown",
|
|
557
|
-
repoRoles: [],
|
|
558
|
-
confidence: 0,
|
|
559
|
-
detectedStacks: {},
|
|
560
|
-
dependencies: [],
|
|
561
|
-
securityRisks: [],
|
|
562
|
-
crossRepoHints: [],
|
|
563
|
-
warnings: [],
|
|
564
|
-
detectionStatus: "unknown",
|
|
565
|
-
unsupportedSignals: []
|
|
566
|
-
};
|
|
567
|
-
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
568
|
-
mode: "fast",
|
|
569
|
-
recommended: [],
|
|
570
|
-
skipped: [],
|
|
571
|
-
warnings: [],
|
|
572
|
-
estimatedContextTokens: 0,
|
|
573
|
-
selectedRules: 0,
|
|
574
|
-
skippedRules: 0,
|
|
575
|
-
estimatedTokenReductionPct: 0
|
|
576
|
-
};
|
|
577
|
-
const destPath = hausPath(root, "project.md");
|
|
578
|
-
const printable = displayPath(root, destPath);
|
|
579
|
-
const next = renderProjectFacts(ctx, rec, pkgVersion);
|
|
580
|
-
const prev = await fs5.pathExists(destPath) ? await fs5.readFile(destPath, "utf8") : "";
|
|
581
|
-
if (dryRun) {
|
|
582
|
-
if (!prev) {
|
|
583
|
-
log(createUnifiedDiff(printable, "", next));
|
|
584
|
-
} else if (hasTextChanged(prev, next)) {
|
|
585
|
-
log(createUnifiedDiff(printable, prev, next));
|
|
586
|
-
} else {
|
|
587
|
-
log(`${printable}: unchanged`);
|
|
588
|
-
}
|
|
589
|
-
return destPath;
|
|
590
|
-
}
|
|
591
|
-
if (hasTextChanged(prev, next) && prev.length > 0) {
|
|
592
|
-
const diffText = createUnifiedDiff(printable, prev, next);
|
|
593
|
-
const summary = summarizeDiff(diffText);
|
|
594
|
-
log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
|
|
595
|
-
}
|
|
596
|
-
await writeText(destPath, next);
|
|
597
|
-
return destPath;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// src/claude/write-root-claude-md.ts
|
|
601
|
-
import path7 from "path";
|
|
602
|
-
import fs6 from "fs-extra";
|
|
603
521
|
var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
|
|
604
522
|
var BLOCK_END = "<!-- HAUS:END haus-imports -->";
|
|
605
523
|
var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
|
|
606
|
-
@.haus-workflow/workflow-config.md
|
|
607
|
-
@.haus-workflow/project.md`;
|
|
524
|
+
@.haus-workflow/workflow-config.md`;
|
|
608
525
|
function buildImportBlock() {
|
|
609
526
|
return `${BLOCK_BEGIN}
|
|
610
527
|
${IMPORT_CONTENT}
|
|
@@ -629,9 +546,9 @@ ${block}
|
|
|
629
546
|
`;
|
|
630
547
|
}
|
|
631
548
|
async function writeRootClaudeMd(root, dryRun) {
|
|
632
|
-
const filePath =
|
|
549
|
+
const filePath = path6.join(root, "CLAUDE.md");
|
|
633
550
|
const block = buildImportBlock();
|
|
634
|
-
const prev = await
|
|
551
|
+
const prev = await fs5.pathExists(filePath) ? await fs5.readFile(filePath, "utf8") : "";
|
|
635
552
|
const next = injectHausBlock(prev, block);
|
|
636
553
|
const printable = displayPath(root, filePath);
|
|
637
554
|
if (dryRun) {
|
|
@@ -654,22 +571,12 @@ async function writeRootClaudeMd(root, dryRun) {
|
|
|
654
571
|
}
|
|
655
572
|
|
|
656
573
|
// src/claude/write-workflow-config.ts
|
|
657
|
-
import path9 from "path";
|
|
658
|
-
import fs8 from "fs-extra";
|
|
659
|
-
|
|
660
|
-
// src/claude/derive-workflow-config.ts
|
|
661
574
|
import path8 from "path";
|
|
662
575
|
import fs7 from "fs-extra";
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
"joi",
|
|
668
|
-
"@hapi/joi",
|
|
669
|
-
"class-validator",
|
|
670
|
-
"superstruct",
|
|
671
|
-
"ajv"
|
|
672
|
-
];
|
|
576
|
+
|
|
577
|
+
// src/claude/derive-workflow-config.ts
|
|
578
|
+
import path7 from "path";
|
|
579
|
+
import fs6 from "fs-extra";
|
|
673
580
|
function binCmd(pm, bin, args) {
|
|
674
581
|
const tail = args ? ` ${args}` : "";
|
|
675
582
|
if (pm === "yarn") return `yarn ${bin}${tail}`;
|
|
@@ -678,7 +585,7 @@ function binCmd(pm, bin, args) {
|
|
|
678
585
|
}
|
|
679
586
|
async function deriveWorkflowConfig(root, ctx) {
|
|
680
587
|
const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
|
|
681
|
-
const pkg = await readJson(
|
|
588
|
+
const pkg = await readJson(path7.join(root, "package.json"));
|
|
682
589
|
const scripts = pkg?.scripts ?? {};
|
|
683
590
|
const deps = new Set(ctx.dependencies);
|
|
684
591
|
const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
|
|
@@ -688,22 +595,13 @@ async function deriveWorkflowConfig(root, ctx) {
|
|
|
688
595
|
return null;
|
|
689
596
|
};
|
|
690
597
|
const hasDep = (name) => deps.has(name);
|
|
691
|
-
const exists = (rel) =>
|
|
692
|
-
const hasTypeScript = hasDep("typescript") || exists("tsconfig.json");
|
|
693
|
-
const hasEslint = hasDep("eslint");
|
|
694
|
-
const hasPrettier = hasDep("prettier");
|
|
598
|
+
const exists = (rel) => fs6.pathExistsSync(path7.join(root, rel));
|
|
695
599
|
const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
|
|
696
600
|
const hasCypress = hasDep("cypress");
|
|
697
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;
|
|
698
602
|
return {
|
|
699
603
|
test: script("test") ?? `${pm} test`,
|
|
700
604
|
testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
|
|
701
|
-
typecheck: firstScript("typecheck", "type-check", "tsc") ?? (hasTypeScript ? binCmd(pm, "tsc", "--noEmit") : null),
|
|
702
|
-
lint: script("lint") ?? (hasEslint ? binCmd(pm, "eslint", ".") : null),
|
|
703
|
-
lintFix: firstScript("lint:fix", "lint-fix") ?? (scripts.lint ? `${pm} run lint -- --fix` : hasEslint ? binCmd(pm, "eslint", ". --fix") : null),
|
|
704
|
-
formatCheck: firstScript("format:check", "format-check", "prettier:check") ?? (hasPrettier ? binCmd(pm, "prettier", "--check .") : null),
|
|
705
|
-
securityAudit: `${pm} audit`,
|
|
706
|
-
validationLibrary: VALIDATION_LIBS.find((lib) => deps.has(lib)) ?? null,
|
|
707
605
|
preCommitTool,
|
|
708
606
|
specPath: exists("docs/SPEC.md") ? "docs/SPEC.md" : null,
|
|
709
607
|
designPath: exists("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
|
|
@@ -718,13 +616,12 @@ function fields(v) {
|
|
|
718
616
|
{ prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
|
|
719
617
|
{ prefix: "- UX flows: ", value: v.uxPath, hint: "path, e.g. docs/UX.md" },
|
|
720
618
|
{ prefix: "- Test (unit + integration): ", value: v.test, hint: "command", code: true },
|
|
721
|
-
{
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
{ 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
|
+
},
|
|
728
625
|
{ prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
|
|
729
626
|
];
|
|
730
627
|
}
|
|
@@ -738,7 +635,7 @@ function line(f) {
|
|
|
738
635
|
function buildWorkflowConfig(v) {
|
|
739
636
|
const f = fields(v);
|
|
740
637
|
const byPrefix = (p) => line(f.find((x) => x.prefix === p));
|
|
741
|
-
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";
|
|
742
639
|
}
|
|
743
640
|
function refillContent(existing, v) {
|
|
744
641
|
const f = fields(v);
|
|
@@ -756,7 +653,6 @@ var FALLBACK_CONTEXT = {
|
|
|
756
653
|
repoName: "",
|
|
757
654
|
packageManager: "unknown",
|
|
758
655
|
repoRoles: [],
|
|
759
|
-
confidence: 0,
|
|
760
656
|
detectedStacks: {},
|
|
761
657
|
dependencies: [],
|
|
762
658
|
securityRisks: [],
|
|
@@ -768,7 +664,7 @@ var FALLBACK_CONTEXT = {
|
|
|
768
664
|
async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
769
665
|
const destPath = hausPath(root, "workflow-config.md");
|
|
770
666
|
const printable = displayPath(root, destPath);
|
|
771
|
-
const exists = await
|
|
667
|
+
const exists = await fs7.pathExists(destPath);
|
|
772
668
|
if (exists && !opts.refill) {
|
|
773
669
|
if (dryRun) log(printable + ": exists (project-owned, skipping)");
|
|
774
670
|
return null;
|
|
@@ -776,11 +672,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
776
672
|
const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
|
|
777
673
|
...FALLBACK_CONTEXT,
|
|
778
674
|
root,
|
|
779
|
-
repoName:
|
|
675
|
+
repoName: path8.basename(root)
|
|
780
676
|
};
|
|
781
677
|
const values = await deriveWorkflowConfig(root, ctx);
|
|
782
678
|
if (exists) {
|
|
783
|
-
const current = await
|
|
679
|
+
const current = await fs7.readFile(destPath, "utf8");
|
|
784
680
|
const refilled = refillContent(current, values);
|
|
785
681
|
if (refilled === current) {
|
|
786
682
|
if (dryRun) log(printable + ": no blank fields to refill");
|
|
@@ -802,8 +698,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
802
698
|
}
|
|
803
699
|
|
|
804
700
|
// src/claude/write-workflow.ts
|
|
805
|
-
import
|
|
806
|
-
import fs9 from "fs-extra";
|
|
701
|
+
import fs8 from "fs-extra";
|
|
807
702
|
|
|
808
703
|
// src/claude/managed-template.ts
|
|
809
704
|
function normaliseLF(content2) {
|
|
@@ -817,35 +712,35 @@ function parseHausManagedHeader(line2) {
|
|
|
817
712
|
}
|
|
818
713
|
|
|
819
714
|
// src/claude/write-workflow.ts
|
|
820
|
-
var
|
|
821
|
-
var
|
|
822
|
-
var CATALOG_CACHE_TEMPLATE = path10.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
715
|
+
var STABLE_ID = "template.workflow";
|
|
716
|
+
var SCHEMA_VERSION = "1";
|
|
823
717
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
824
|
-
return `<!-- HAUS-MANAGED id=${
|
|
718
|
+
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
825
719
|
}
|
|
826
720
|
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
827
|
-
|
|
828
|
-
|
|
721
|
+
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
722
|
+
if (templateContent === null) {
|
|
723
|
+
warn(
|
|
724
|
+
`Workflow template could not be fetched from the catalog \u2014 check your network, then re-run \`haus apply --write\` (or \`haus update\`)`
|
|
725
|
+
);
|
|
829
726
|
return null;
|
|
830
727
|
}
|
|
831
|
-
const templatePath = CATALOG_CACHE_TEMPLATE;
|
|
832
|
-
const templateContent = await fs9.readFile(templatePath, "utf8");
|
|
833
728
|
const contentHash = hashText(normaliseLF(templateContent));
|
|
834
729
|
const header = makeWorkflowHeader(pkgVersion, contentHash);
|
|
835
730
|
const next = `${header}
|
|
836
731
|
${templateContent}`;
|
|
837
732
|
const destPath = hausPath(root, "WORKFLOW.md");
|
|
838
733
|
const printable = displayPath(root, destPath);
|
|
839
|
-
if (await
|
|
840
|
-
const existing = await
|
|
734
|
+
if (await fs8.pathExists(destPath)) {
|
|
735
|
+
const existing = await fs8.readFile(destPath, "utf8");
|
|
841
736
|
const firstLine = existing.split("\n")[0] ?? "";
|
|
842
737
|
const parsed = parseHausManagedHeader(firstLine);
|
|
843
738
|
if (!parsed) {
|
|
844
739
|
warn(`${printable}: no HAUS-MANAGED header \u2014 file appears user-owned, skipping`);
|
|
845
740
|
return null;
|
|
846
741
|
}
|
|
847
|
-
if (parsed.id !==
|
|
848
|
-
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`);
|
|
849
744
|
return null;
|
|
850
745
|
}
|
|
851
746
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
@@ -859,7 +754,7 @@ ${templateContent}`;
|
|
|
859
754
|
}
|
|
860
755
|
}
|
|
861
756
|
if (dryRun) {
|
|
862
|
-
const prev = await
|
|
757
|
+
const prev = await fs8.pathExists(destPath) ? await fs8.readFile(destPath, "utf8") : "";
|
|
863
758
|
if (!prev) {
|
|
864
759
|
log(createUnifiedDiff(printable, "", next));
|
|
865
760
|
} else {
|
|
@@ -886,7 +781,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
886
781
|
estimatedTokenReductionPct: 0
|
|
887
782
|
};
|
|
888
783
|
const pkgRoot = packageRoot();
|
|
889
|
-
const hausVersion = (await readJson(
|
|
784
|
+
const hausVersion = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
|
|
890
785
|
const coreFiles = [
|
|
891
786
|
claudePath(root, "settings.json"),
|
|
892
787
|
claudePath(root, "rules", "haus.md"),
|
|
@@ -899,10 +794,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
899
794
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
900
795
|
refill: opts.refillConfig
|
|
901
796
|
});
|
|
902
|
-
const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
|
|
903
797
|
const p6Files = [
|
|
904
798
|
rootClaudeMdPath,
|
|
905
|
-
projectFactsPath,
|
|
906
799
|
...workflowPath ? [workflowPath] : [],
|
|
907
800
|
...workflowConfigPath ? [workflowConfigPath] : []
|
|
908
801
|
];
|
|
@@ -916,7 +809,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
916
809
|
await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
|
|
917
810
|
if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
|
|
918
811
|
const configPath = hausPath(root, "config.json");
|
|
919
|
-
if (!await
|
|
812
|
+
if (!await fs9.pathExists(configPath)) {
|
|
920
813
|
await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
|
|
921
814
|
}
|
|
922
815
|
await writeManagedText(
|
|
@@ -944,12 +837,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
944
837
|
dryRun
|
|
945
838
|
);
|
|
946
839
|
const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
947
|
-
const manifestPath = fixtureManifestPath ??
|
|
948
|
-
const manifestDir =
|
|
840
|
+
const manifestPath = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
|
|
841
|
+
const manifestDir = path9.dirname(manifestPath);
|
|
949
842
|
const manifest = await readJson(manifestPath) ?? { items: [] };
|
|
950
843
|
const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
|
|
951
844
|
const cacheManifest = await readJson(
|
|
952
|
-
|
|
845
|
+
path9.join(CACHE_DIR, "manifest.json")
|
|
953
846
|
);
|
|
954
847
|
const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
|
|
955
848
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
@@ -971,23 +864,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
971
864
|
}
|
|
972
865
|
}
|
|
973
866
|
const cachedItem = cacheManifestById.get(item.id);
|
|
974
|
-
const cachePath = cachedItem?.path ?
|
|
975
|
-
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);
|
|
976
869
|
const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
|
|
977
|
-
const destination = claudePath(root, target,
|
|
978
|
-
if (await
|
|
870
|
+
const destination = claudePath(root, target, path9.basename(sourcePath));
|
|
871
|
+
if (await fs9.pathExists(sourcePath)) {
|
|
979
872
|
if (dryRun) {
|
|
980
|
-
const exists = await
|
|
873
|
+
const exists = await fs9.pathExists(destination);
|
|
981
874
|
log(
|
|
982
875
|
`${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
|
|
983
876
|
);
|
|
984
877
|
} else {
|
|
985
|
-
await
|
|
986
|
-
await
|
|
878
|
+
await fs9.ensureDir(path9.dirname(destination));
|
|
879
|
+
await fs9.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
|
|
987
880
|
}
|
|
988
881
|
files.push(destination);
|
|
989
882
|
const current = installedPathsByItem.get(item.id) ?? [];
|
|
990
|
-
installedPathsByItem.set(item.id, [...current,
|
|
883
|
+
installedPathsByItem.set(item.id, [...current, path9.relative(root, destination)]);
|
|
991
884
|
installedIds.add(item.id);
|
|
992
885
|
} else {
|
|
993
886
|
warn(
|
|
@@ -1004,7 +897,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1004
897
|
id: r.id,
|
|
1005
898
|
type: r.type,
|
|
1006
899
|
reason: r.reason,
|
|
1007
|
-
|
|
900
|
+
selectionMode: r.selectionMode
|
|
1008
901
|
})),
|
|
1009
902
|
false
|
|
1010
903
|
);
|
|
@@ -1038,7 +931,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1038
931
|
return [...new Set(files)];
|
|
1039
932
|
}
|
|
1040
933
|
async function writeManagedText(root, filePath, nextText, dryRun) {
|
|
1041
|
-
const prev = await
|
|
934
|
+
const prev = await fs9.pathExists(filePath) ? await fs9.readFile(filePath, "utf8") : "";
|
|
1042
935
|
const printable = displayPath(root, filePath);
|
|
1043
936
|
if (dryRun) {
|
|
1044
937
|
if (!prev) {
|
|
@@ -1065,7 +958,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
|
|
|
1065
958
|
|
|
1066
959
|
// src/commands/apply.ts
|
|
1067
960
|
async function cacheHasItems() {
|
|
1068
|
-
const data = await readJson(
|
|
961
|
+
const data = await readJson(path10.join(CACHE_DIR, "manifest.json"));
|
|
1069
962
|
return Array.isArray(data?.items) && data.items.length > 0;
|
|
1070
963
|
}
|
|
1071
964
|
async function runApply(options) {
|
|
@@ -1092,7 +985,7 @@ async function runApply(options) {
|
|
|
1092
985
|
} else {
|
|
1093
986
|
const items = rec.recommended;
|
|
1094
987
|
const choices = items.map((item) => ({
|
|
1095
|
-
name: `${item.id} [${item.
|
|
988
|
+
name: `${item.id} [${item.selectionMode}] \u2014 ${item.reason}`,
|
|
1096
989
|
value: item.id,
|
|
1097
990
|
checked: true
|
|
1098
991
|
}));
|
|
@@ -1135,8 +1028,8 @@ async function runApply(options) {
|
|
|
1135
1028
|
|
|
1136
1029
|
// src/catalog/load-catalog.ts
|
|
1137
1030
|
import os3 from "os";
|
|
1138
|
-
import
|
|
1139
|
-
var CACHE_MANIFEST =
|
|
1031
|
+
import path11 from "path";
|
|
1032
|
+
var CACHE_MANIFEST = path11.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
|
|
1140
1033
|
async function loadCatalog(root) {
|
|
1141
1034
|
const envPath = process.env["HAUS_FIXTURE_CATALOG"];
|
|
1142
1035
|
if (envPath) {
|
|
@@ -1145,10 +1038,10 @@ async function loadCatalog(root) {
|
|
|
1145
1038
|
}
|
|
1146
1039
|
const cacheData = await readJson(CACHE_MANIFEST);
|
|
1147
1040
|
if (cacheData?.items?.length) return cacheData.items;
|
|
1148
|
-
const localManifest =
|
|
1041
|
+
const localManifest = path11.join(root, "library/catalog/manifest.json");
|
|
1149
1042
|
const localData = await readJson(localManifest);
|
|
1150
1043
|
if (localData?.items?.length) return localData.items;
|
|
1151
|
-
const packageManifest =
|
|
1044
|
+
const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
|
|
1152
1045
|
const data = await readJson(packageManifest);
|
|
1153
1046
|
return data?.items ?? [];
|
|
1154
1047
|
}
|
|
@@ -1188,7 +1081,7 @@ async function runCatalogAudit() {
|
|
|
1188
1081
|
}
|
|
1189
1082
|
|
|
1190
1083
|
// src/commands/config.ts
|
|
1191
|
-
import
|
|
1084
|
+
import path12 from "path";
|
|
1192
1085
|
var CONFIG_PATH2 = ".haus-workflow/config.json";
|
|
1193
1086
|
var HOOK_ALIASES = {
|
|
1194
1087
|
"hook.context": "context"
|
|
@@ -1201,7 +1094,7 @@ async function runConfig(key, action) {
|
|
|
1201
1094
|
);
|
|
1202
1095
|
}
|
|
1203
1096
|
const root = process.cwd();
|
|
1204
|
-
const configPath =
|
|
1097
|
+
const configPath = path12.join(root, CONFIG_PATH2);
|
|
1205
1098
|
const existing = await readJson(configPath);
|
|
1206
1099
|
const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
|
|
1207
1100
|
cfg.hooks ??= {};
|
|
@@ -1222,27 +1115,15 @@ function normalizeRecommendation(input2) {
|
|
|
1222
1115
|
const normalizedReasons = item.reasons?.map((reason) => ({
|
|
1223
1116
|
code: reason.code ?? "legacy-reason",
|
|
1224
1117
|
message: reason.message ?? item.reason ?? "legacy recommendation reason",
|
|
1225
|
-
weight: reason.weight ?? 0,
|
|
1226
1118
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1227
|
-
})) ?? [
|
|
1228
|
-
{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason", weight: 0 }
|
|
1229
|
-
];
|
|
1230
|
-
const confidence = item.confidence ?? 0;
|
|
1119
|
+
})) ?? [{ code: "legacy-reason", message: item.reason ?? "legacy recommendation reason" }];
|
|
1231
1120
|
return {
|
|
1232
1121
|
id: item.id,
|
|
1233
1122
|
type: item.type ?? "skill",
|
|
1234
1123
|
reason: item.reason ?? normalizedReasons.map((reason) => reason.message).join(", "),
|
|
1235
1124
|
reasons: normalizedReasons,
|
|
1236
|
-
confidence,
|
|
1237
|
-
confidenceLevel: item.confidenceLevel ?? (confidence >= 0.75 ? "high" : confidence >= 0.4 ? "medium" : "low"),
|
|
1238
1125
|
selectionMode: item.selectionMode ?? "matched",
|
|
1239
1126
|
install: item.install ?? true,
|
|
1240
|
-
score: item.score ?? 0,
|
|
1241
|
-
scoreBreakdown: {
|
|
1242
|
-
bonuses: normalizedReasons,
|
|
1243
|
-
penalties: [],
|
|
1244
|
-
finalScore: item.score ?? 0
|
|
1245
|
-
},
|
|
1246
1127
|
tags: item.tags,
|
|
1247
1128
|
ecosystem: item.ecosystem,
|
|
1248
1129
|
tokenEstimate: item.tokenEstimate
|
|
@@ -1254,15 +1135,8 @@ function normalizeRecommendation(input2) {
|
|
|
1254
1135
|
skipReasons: item.skipReasons?.map((reason) => ({
|
|
1255
1136
|
code: reason.code ?? "legacy-skip-reason",
|
|
1256
1137
|
message: reason.message ?? item.reason ?? "legacy skipped reason",
|
|
1257
|
-
penalty: reason.penalty ?? 0,
|
|
1258
1138
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1259
|
-
})) ?? [
|
|
1260
|
-
{
|
|
1261
|
-
code: "legacy-skip-reason",
|
|
1262
|
-
message: item.reason ?? "legacy skipped reason",
|
|
1263
|
-
penalty: 0
|
|
1264
|
-
}
|
|
1265
|
-
]
|
|
1139
|
+
})) ?? [{ code: "legacy-skip-reason", message: item.reason ?? "legacy skipped reason" }]
|
|
1266
1140
|
}));
|
|
1267
1141
|
return {
|
|
1268
1142
|
mode: input2.mode === "guided" ? "guided" : "fast",
|
|
@@ -1282,8 +1156,6 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1282
1156
|
return {
|
|
1283
1157
|
selected: recommendation.recommended.map((item) => ({
|
|
1284
1158
|
id: item.id,
|
|
1285
|
-
confidence: item.confidence,
|
|
1286
|
-
confidenceLevel: item.confidenceLevel,
|
|
1287
1159
|
selectionMode: item.selectionMode,
|
|
1288
1160
|
reasons: item.reasons.map((reason) => reason.message)
|
|
1289
1161
|
})),
|
|
@@ -1293,7 +1165,6 @@ function buildRecommendationExplanation(recommendation) {
|
|
|
1293
1165
|
reasonDetails: item.skipReasons.map((reason) => ({
|
|
1294
1166
|
code: reason.code,
|
|
1295
1167
|
message: reason.message,
|
|
1296
|
-
penalty: reason.penalty,
|
|
1297
1168
|
...reason.signal ? { signal: reason.signal } : {}
|
|
1298
1169
|
}))
|
|
1299
1170
|
})),
|
|
@@ -1520,6 +1391,13 @@ function computeRuleIntents(rule) {
|
|
|
1520
1391
|
}
|
|
1521
1392
|
|
|
1522
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
|
+
}
|
|
1523
1401
|
var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
|
|
1524
1402
|
function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
|
|
1525
1403
|
const recommended = recommendation?.recommended ?? [];
|
|
@@ -1537,7 +1415,7 @@ function applyTokenBudget(rules, budget) {
|
|
|
1537
1415
|
used += r.tokenEstimate ?? 0;
|
|
1538
1416
|
}
|
|
1539
1417
|
}
|
|
1540
|
-
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));
|
|
1541
1419
|
for (const r of matched) {
|
|
1542
1420
|
const est = r.tokenEstimate ?? 0;
|
|
1543
1421
|
if (used + est <= budget) {
|
|
@@ -1575,20 +1453,20 @@ function selectRules(recommended, task, taskIntents) {
|
|
|
1575
1453
|
});
|
|
1576
1454
|
if (tokenMatches.length > 0) return tokenMatches;
|
|
1577
1455
|
const taskWantsTesting = taskIntents.has("testing");
|
|
1578
|
-
const
|
|
1456
|
+
const capped = recommended.filter((rule) => {
|
|
1579
1457
|
if (rule.selectionMode === "baseline") return false;
|
|
1580
|
-
if (rule
|
|
1458
|
+
if (isRoleOnly(rule)) return false;
|
|
1581
1459
|
if (taskWantsTesting) return true;
|
|
1582
1460
|
const ruleIntents = computeRuleIntents(rule);
|
|
1583
1461
|
const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
|
|
1584
1462
|
return !isTestingOnly;
|
|
1585
1463
|
});
|
|
1586
|
-
return
|
|
1464
|
+
return capped.slice(0, 8);
|
|
1587
1465
|
}
|
|
1588
1466
|
|
|
1589
1467
|
// src/scanner/scan-project.ts
|
|
1590
1468
|
import { readFile as readFile2 } from "fs/promises";
|
|
1591
|
-
import
|
|
1469
|
+
import path16 from "path";
|
|
1592
1470
|
|
|
1593
1471
|
// src/utils/audit-checks.ts
|
|
1594
1472
|
function isRecord(v) {
|
|
@@ -1615,8 +1493,8 @@ function compareVersions(a, b) {
|
|
|
1615
1493
|
}
|
|
1616
1494
|
|
|
1617
1495
|
// src/scanner/detect-package-manager.ts
|
|
1618
|
-
import
|
|
1619
|
-
import
|
|
1496
|
+
import path13 from "path";
|
|
1497
|
+
import fs10 from "fs-extra";
|
|
1620
1498
|
function detectPackageManager(root, packageManagerField) {
|
|
1621
1499
|
const field = String(packageManagerField ?? "").trim();
|
|
1622
1500
|
if (field.startsWith("yarn@")) {
|
|
@@ -1634,9 +1512,9 @@ function detectPackageManager(root, packageManagerField) {
|
|
|
1634
1512
|
if (satisfiesVersion(version, ">=9")) return "npm";
|
|
1635
1513
|
return "unknown";
|
|
1636
1514
|
}
|
|
1637
|
-
if (
|
|
1638
|
-
if (
|
|
1639
|
-
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";
|
|
1640
1518
|
return "unknown";
|
|
1641
1519
|
}
|
|
1642
1520
|
|
|
@@ -1809,7 +1687,7 @@ function runDetection(ctx, rules = STACK_RULES) {
|
|
|
1809
1687
|
}
|
|
1810
1688
|
|
|
1811
1689
|
// src/scanner/detection.ts
|
|
1812
|
-
import
|
|
1690
|
+
import path14 from "path";
|
|
1813
1691
|
var UNSUPPORTED_MARKERS = {
|
|
1814
1692
|
"requirements.txt": "python",
|
|
1815
1693
|
"pyproject.toml": "python",
|
|
@@ -1863,14 +1741,14 @@ function finalizeRoles(registryRoles, deps, files) {
|
|
|
1863
1741
|
function collectUnsupportedSignals(files) {
|
|
1864
1742
|
return [
|
|
1865
1743
|
...new Set(
|
|
1866
|
-
files.map((f) => UNSUPPORTED_MARKERS[
|
|
1744
|
+
files.map((f) => UNSUPPORTED_MARKERS[path14.basename(f)]).filter((s) => Boolean(s))
|
|
1867
1745
|
)
|
|
1868
1746
|
].sort();
|
|
1869
1747
|
}
|
|
1870
1748
|
|
|
1871
1749
|
// src/scanner/render.ts
|
|
1872
1750
|
import { readFile } from "fs/promises";
|
|
1873
|
-
import
|
|
1751
|
+
import path15 from "path";
|
|
1874
1752
|
|
|
1875
1753
|
// src/scanner/role-labels.ts
|
|
1876
1754
|
var ROLE_LABELS = {
|
|
@@ -1936,7 +1814,7 @@ async function buildContentBlob(root, files) {
|
|
|
1936
1814
|
const batch = await Promise.all(
|
|
1937
1815
|
slice.slice(i, i + CHUNK).map(async (rel) => {
|
|
1938
1816
|
try {
|
|
1939
|
-
return await readFile(
|
|
1817
|
+
return await readFile(path15.join(root, rel), "utf8");
|
|
1940
1818
|
} catch {
|
|
1941
1819
|
return "";
|
|
1942
1820
|
}
|
|
@@ -1946,11 +1824,6 @@ async function buildContentBlob(root, files) {
|
|
|
1946
1824
|
}
|
|
1947
1825
|
return parts.join("\n");
|
|
1948
1826
|
}
|
|
1949
|
-
function computeConfidence(roles, stacks) {
|
|
1950
|
-
const stackCount = Object.values(stacks).reduce((sum, arr) => sum + arr.length, 0);
|
|
1951
|
-
if (roles.length === 0) return 0.15;
|
|
1952
|
-
return Math.min(0.99, Number((0.4 + roles.length * 0.08 + stackCount * 0.02).toFixed(2)));
|
|
1953
|
-
}
|
|
1954
1827
|
function renderSummary(context) {
|
|
1955
1828
|
return `# Repo summary
|
|
1956
1829
|
|
|
@@ -2010,8 +1883,8 @@ var SAFE_FILES = [
|
|
|
2010
1883
|
"Gemfile"
|
|
2011
1884
|
];
|
|
2012
1885
|
async function scanProject(root, mode = "fast") {
|
|
2013
|
-
const pkg = await readJson(
|
|
2014
|
-
const composer = await readJson(
|
|
1886
|
+
const pkg = await readJson(path16.join(root, "package.json"));
|
|
1887
|
+
const composer = await readJson(path16.join(root, "composer.json"));
|
|
2015
1888
|
const files = await listFiles(root, SAFE_FILES);
|
|
2016
1889
|
const safeFiles = files.filter((f) => !blocked(f));
|
|
2017
1890
|
const deps = dependencySet(pkg, composer);
|
|
@@ -2045,10 +1918,9 @@ async function scanProject(root, mode = "fast") {
|
|
|
2045
1918
|
mode,
|
|
2046
1919
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2047
1920
|
root,
|
|
2048
|
-
repoName: String(pkg?.name ??
|
|
1921
|
+
repoName: String(pkg?.name ?? path16.basename(root)),
|
|
2049
1922
|
packageManager,
|
|
2050
1923
|
repoRoles: roles,
|
|
2051
|
-
confidence: computeConfidence(roles, stacks),
|
|
2052
1924
|
detectedStacks: stacks,
|
|
2053
1925
|
dependencies: deps,
|
|
2054
1926
|
securityRisks,
|
|
@@ -2064,7 +1936,7 @@ async function scanProject(root, mode = "fast") {
|
|
|
2064
1936
|
const scanHashes = Object.fromEntries(
|
|
2065
1937
|
await Promise.all(
|
|
2066
1938
|
safeFiles.map(
|
|
2067
|
-
async (f) => [f, hashText(await readFile2(
|
|
1939
|
+
async (f) => [f, hashText(await readFile2(path16.join(root, f), "utf8"))]
|
|
2068
1940
|
)
|
|
2069
1941
|
)
|
|
2070
1942
|
);
|
|
@@ -2094,9 +1966,6 @@ async function runContext(options) {
|
|
|
2094
1966
|
const summary = await readText(hausPath(root, "repo-summary.md")) ?? "";
|
|
2095
1967
|
const recommendationRaw = await readJson(hausPath(root, "recommendation.json"));
|
|
2096
1968
|
const recommendation = recommendationRaw ? normalizeRecommendation(recommendationRaw) : void 0;
|
|
2097
|
-
const rawBreakdownById = new Map(
|
|
2098
|
-
(recommendationRaw?.recommended ?? []).map((item) => [item.id, item.scoreBreakdown])
|
|
2099
|
-
);
|
|
2100
1969
|
const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
|
|
2101
1970
|
const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
|
|
2102
1971
|
tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
|
|
@@ -2107,10 +1976,9 @@ async function runContext(options) {
|
|
|
2107
1976
|
roles: context.repoRoles,
|
|
2108
1977
|
selectedRules: selected.map((x) => ({
|
|
2109
1978
|
id: x.id,
|
|
2110
|
-
confidenceLevel: x.confidenceLevel,
|
|
2111
1979
|
selectionMode: x.selectionMode,
|
|
2112
1980
|
reasons: x.reasons.map((reason) => reason.message),
|
|
2113
|
-
...options.verbose ? {
|
|
1981
|
+
...options.verbose ? { signals: x.reasons.map((r) => r.signal).filter(Boolean) } : {}
|
|
2114
1982
|
})),
|
|
2115
1983
|
skippedCount: recommendation?.skippedRules ?? 0,
|
|
2116
1984
|
estimatedTokenReductionPct: recommendation?.estimatedTokenReductionPct ?? 0
|
|
@@ -2131,15 +1999,8 @@ async function runContext(options) {
|
|
|
2131
1999
|
...payload.selectedRules.flatMap((rule) => {
|
|
2132
2000
|
const reasonLine = `- ${rule.id}: ${rule.reasons.join(", ")}`;
|
|
2133
2001
|
if (!options.verbose) return [reasonLine];
|
|
2134
|
-
const
|
|
2135
|
-
|
|
2136
|
-
const bonuses = (breakdown.bonuses ?? []).map(
|
|
2137
|
-
(b) => ` + ${b.code}(+${b.weight})${b.signal ? ` [${b.signal}]` : ""}`
|
|
2138
|
-
);
|
|
2139
|
-
const penalties = (breakdown.penalties ?? []).map(
|
|
2140
|
-
(p) => ` - ${p.code}(${p.penalty})${p.signal ? ` [${p.signal}]` : ""}`
|
|
2141
|
-
);
|
|
2142
|
-
return [reasonLine, ...bonuses, ...penalties];
|
|
2002
|
+
const signals = (rule.signals ?? []).map((s) => ` \u2022 ${s}`);
|
|
2003
|
+
return [reasonLine, ...signals];
|
|
2143
2004
|
}),
|
|
2144
2005
|
summary
|
|
2145
2006
|
];
|
|
@@ -2148,8 +2009,8 @@ async function runContext(options) {
|
|
|
2148
2009
|
}
|
|
2149
2010
|
|
|
2150
2011
|
// src/commands/doctor.ts
|
|
2151
|
-
import
|
|
2152
|
-
import
|
|
2012
|
+
import path17 from "path";
|
|
2013
|
+
import fs11 from "fs-extra";
|
|
2153
2014
|
|
|
2154
2015
|
// src/update/npm-version.ts
|
|
2155
2016
|
var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
|
|
@@ -2229,7 +2090,7 @@ async function runDoctor(options) {
|
|
|
2229
2090
|
const enabled = await isHookEnabled(root, key);
|
|
2230
2091
|
ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
|
|
2231
2092
|
}
|
|
2232
|
-
const rootClaudeMdPath =
|
|
2093
|
+
const rootClaudeMdPath = path17.join(root, "CLAUDE.md");
|
|
2233
2094
|
const rootClaudeMdContent = await readText(rootClaudeMdPath);
|
|
2234
2095
|
if (!rootClaudeMdContent) {
|
|
2235
2096
|
flag(
|
|
@@ -2257,7 +2118,7 @@ async function runDoctor(options) {
|
|
|
2257
2118
|
const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
|
|
2258
2119
|
const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
|
|
2259
2120
|
for (const target of importTargets) {
|
|
2260
|
-
if (!await
|
|
2121
|
+
if (!await fs11.pathExists(hausPath(root, target))) {
|
|
2261
2122
|
flag(
|
|
2262
2123
|
`- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
|
|
2263
2124
|
`A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
|
|
@@ -2268,7 +2129,7 @@ async function runDoctor(options) {
|
|
|
2268
2129
|
}
|
|
2269
2130
|
}
|
|
2270
2131
|
const workflowPath = hausPath(root, "WORKFLOW.md");
|
|
2271
|
-
const workflowExists = await
|
|
2132
|
+
const workflowExists = await fs11.pathExists(workflowPath);
|
|
2272
2133
|
if (!workflowExists) {
|
|
2273
2134
|
flag(
|
|
2274
2135
|
"- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
|
|
@@ -2282,15 +2143,15 @@ async function runDoctor(options) {
|
|
|
2282
2143
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
2283
2144
|
} else {
|
|
2284
2145
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
2285
|
-
const cachePath =
|
|
2286
|
-
const bundledPath =
|
|
2146
|
+
const cachePath = path17.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
|
|
2147
|
+
const bundledPath = path17.join(
|
|
2287
2148
|
packageRoot(),
|
|
2288
2149
|
"library",
|
|
2289
2150
|
"global",
|
|
2290
2151
|
"templates",
|
|
2291
2152
|
"agentic-workflow-standard.md"
|
|
2292
2153
|
);
|
|
2293
|
-
const templatePath = await
|
|
2154
|
+
const templatePath = await fs11.pathExists(cachePath) ? cachePath : bundledPath;
|
|
2294
2155
|
const templateContent = await readText(templatePath);
|
|
2295
2156
|
if (storedHashMatch && templateContent) {
|
|
2296
2157
|
const currentHash = hashText(normaliseLF(templateContent));
|
|
@@ -2309,7 +2170,7 @@ async function runDoctor(options) {
|
|
|
2309
2170
|
}
|
|
2310
2171
|
}
|
|
2311
2172
|
const workflowConfigPath = hausPath(root, "workflow-config.md");
|
|
2312
|
-
const workflowConfigExists = await
|
|
2173
|
+
const workflowConfigExists = await fs11.pathExists(workflowConfigPath);
|
|
2313
2174
|
if (!workflowConfigExists) {
|
|
2314
2175
|
flag(
|
|
2315
2176
|
"- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
|
|
@@ -2317,7 +2178,7 @@ async function runDoctor(options) {
|
|
|
2317
2178
|
"haus apply --write"
|
|
2318
2179
|
);
|
|
2319
2180
|
} else {
|
|
2320
|
-
const cfg = await
|
|
2181
|
+
const cfg = await fs11.readFile(workflowConfigPath, "utf8");
|
|
2321
2182
|
const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
|
|
2322
2183
|
if (unfilled > 0) {
|
|
2323
2184
|
flag(
|
|
@@ -2329,23 +2190,6 @@ async function runDoctor(options) {
|
|
|
2329
2190
|
ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
|
|
2330
2191
|
}
|
|
2331
2192
|
}
|
|
2332
|
-
const projectMdPath = hausPath(root, "project.md");
|
|
2333
|
-
const projectMdExists = await fs12.pathExists(projectMdPath);
|
|
2334
|
-
if (!projectMdExists) {
|
|
2335
|
-
flag(
|
|
2336
|
-
"- .haus-workflow/project.md: missing (run `haus apply --write`)",
|
|
2337
|
-
"The project facts file is missing",
|
|
2338
|
-
"haus apply --write"
|
|
2339
|
-
);
|
|
2340
|
-
} else {
|
|
2341
|
-
const projectMdContent = await readText(projectMdPath);
|
|
2342
|
-
const hasHeader = projectMdContent?.split("\n")[0]?.includes("HAUS-MANAGED") ?? false;
|
|
2343
|
-
if (!hasHeader) {
|
|
2344
|
-
ok("- .haus-workflow/project.md: no HAUS-MANAGED header (user-owned)");
|
|
2345
|
-
} else {
|
|
2346
|
-
ok("- .haus-workflow/project.md: OK");
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
2193
|
const cacheAgeMs = await getCacheManifestAge();
|
|
2350
2194
|
if (cacheAgeMs === null) {
|
|
2351
2195
|
flag(
|
|
@@ -2365,7 +2209,7 @@ async function runDoctor(options) {
|
|
|
2365
2209
|
ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
|
|
2366
2210
|
}
|
|
2367
2211
|
}
|
|
2368
|
-
const pkgJson = await readJson(
|
|
2212
|
+
const pkgJson = await readJson(path17.join(packageRoot(), "package.json"));
|
|
2369
2213
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
2370
2214
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
2371
2215
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -2413,7 +2257,6 @@ function formatRecommendationHuman(rec) {
|
|
|
2413
2257
|
if (rec.recommended.length === 0) lines.push(" (none)");
|
|
2414
2258
|
for (const item of rec.recommended) {
|
|
2415
2259
|
lines.push(`- ${item.id}`);
|
|
2416
|
-
lines.push(` confidence: ${item.confidenceLevel} (${item.confidence.toFixed(2)})`);
|
|
2417
2260
|
lines.push(` selection: ${item.selectionMode}`);
|
|
2418
2261
|
lines.push(" why:");
|
|
2419
2262
|
for (const reason of item.reasons) lines.push(` - ${formatReasonWithSignal(reason)}`);
|
|
@@ -2507,51 +2350,46 @@ async function runGuard(kind, _options) {
|
|
|
2507
2350
|
}
|
|
2508
2351
|
|
|
2509
2352
|
// src/commands/init.ts
|
|
2510
|
-
import
|
|
2511
|
-
import
|
|
2353
|
+
import path18 from "path";
|
|
2354
|
+
import fs12 from "fs-extra";
|
|
2512
2355
|
|
|
2513
|
-
// src/
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
]);
|
|
2533
|
-
var ECOSYSTEM_COMPATIBLE_BACKENDS = {
|
|
2534
|
-
vendure: /* @__PURE__ */ new Set(["vendure", "nestjs"]),
|
|
2535
|
-
nestjs: /* @__PURE__ */ new Set(["nestjs"]),
|
|
2536
|
-
laravel: /* @__PURE__ */ new Set(["laravel"]),
|
|
2537
|
-
wordpress: /* @__PURE__ */ new Set(["wordpress"]),
|
|
2538
|
-
dotnet: /* @__PURE__ */ new Set(["dotnet"])
|
|
2539
|
-
};
|
|
2540
|
-
function inferRepoEcosystems(roles) {
|
|
2541
|
-
const ecosystems = /* @__PURE__ */ new Set();
|
|
2542
|
-
for (const [eco, roleList] of Object.entries(ECOSYSTEM_GROUPS)) {
|
|
2543
|
-
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})`);
|
|
2544
2375
|
}
|
|
2545
|
-
return [...ecosystems];
|
|
2546
2376
|
}
|
|
2547
|
-
function
|
|
2548
|
-
|
|
2549
|
-
if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
|
|
2550
|
-
}
|
|
2551
|
-
return void 0;
|
|
2377
|
+
async function runGit(args, options = {}) {
|
|
2378
|
+
return runCommand("git", args, options);
|
|
2552
2379
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
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
|
+
}
|
|
2555
2393
|
}
|
|
2556
2394
|
|
|
2557
2395
|
// src/recommender/policies.ts
|
|
@@ -2621,63 +2459,6 @@ function mergeRecommendationWarnings(context) {
|
|
|
2621
2459
|
return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
|
|
2622
2460
|
}
|
|
2623
2461
|
|
|
2624
|
-
// src/utils/exec.ts
|
|
2625
|
-
import { execa } from "execa";
|
|
2626
|
-
async function runCommand(command, args = [], options = {}) {
|
|
2627
|
-
try {
|
|
2628
|
-
const result = await execa(command, args, {
|
|
2629
|
-
reject: false,
|
|
2630
|
-
// non-zero exits are returned, not thrown
|
|
2631
|
-
...options
|
|
2632
|
-
});
|
|
2633
|
-
return {
|
|
2634
|
-
command,
|
|
2635
|
-
args,
|
|
2636
|
-
stdout: String(result.stdout ?? ""),
|
|
2637
|
-
stderr: String(result.stderr ?? ""),
|
|
2638
|
-
exitCode: result.exitCode ?? 0
|
|
2639
|
-
};
|
|
2640
|
-
} catch (error2) {
|
|
2641
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2642
|
-
throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
async function runGit(args, options = {}) {
|
|
2646
|
-
return runCommand("git", args, options);
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
// src/recommender/scoring.ts
|
|
2650
|
-
function computeConfidenceLevel(args) {
|
|
2651
|
-
const { isDefaultBaseline, reasons, hasEcosystemConflict, score } = args;
|
|
2652
|
-
const positiveCodes = new Set(reasons.map((r) => r.code));
|
|
2653
|
-
positiveCodes.delete("default-baseline");
|
|
2654
|
-
const distinctSignals = positiveCodes.size;
|
|
2655
|
-
const strongCount = (positiveCodes.has("repo-role-match") ? 1 : 0) + (positiveCodes.has("stack-match") ? 1 : 0) + (positiveCodes.has("requires-any-match") ? 1 : 0);
|
|
2656
|
-
if (hasEcosystemConflict) return "low";
|
|
2657
|
-
if (isDefaultBaseline && distinctSignals === 0) return "medium";
|
|
2658
|
-
if (strongCount >= 2 && score >= 70) return "high";
|
|
2659
|
-
if (strongCount >= 1 && distinctSignals >= 2 && score >= 50) return "medium";
|
|
2660
|
-
if (distinctSignals === 1) return "low";
|
|
2661
|
-
return distinctSignals >= 2 ? "medium" : "low";
|
|
2662
|
-
}
|
|
2663
|
-
function confidenceLevelToNumber(level, score) {
|
|
2664
|
-
const base = level === "high" ? 0.85 : level === "medium" ? 0.6 : 0.3;
|
|
2665
|
-
const bonus = Math.min(0.1, Math.max(0, score - 40) / 1e3);
|
|
2666
|
-
return Number(Math.min(0.99, base + bonus).toFixed(2));
|
|
2667
|
-
}
|
|
2668
|
-
async function readChangedFiles(root) {
|
|
2669
|
-
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
2670
|
-
try {
|
|
2671
|
-
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
2672
|
-
if (result.exitCode !== 0) {
|
|
2673
|
-
return [];
|
|
2674
|
-
}
|
|
2675
|
-
return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
|
|
2676
|
-
} catch {
|
|
2677
|
-
return [];
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
2462
|
// src/recommender/recommend.ts
|
|
2682
2463
|
async function recommend(root, context) {
|
|
2683
2464
|
const items = await loadCatalog(root);
|
|
@@ -2685,249 +2466,147 @@ async function recommend(root, context) {
|
|
|
2685
2466
|
const sources = await readJson(
|
|
2686
2467
|
hausPath(root, "sources-report.json")
|
|
2687
2468
|
) ?? {};
|
|
2688
|
-
const
|
|
2689
|
-
const
|
|
2690
|
-
const
|
|
2691
|
-
const
|
|
2692
|
-
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;
|
|
2693
2484
|
const recommended = [];
|
|
2694
2485
|
const skipped = [];
|
|
2695
2486
|
const goals = Object.values(setupAnswers).join(" ").toLowerCase();
|
|
2696
2487
|
const sourceTrust = new Map((sources.items ?? []).map((x) => [x.id, x.status ?? "candidate"]));
|
|
2697
2488
|
const changedFiles = await readChangedFiles(root);
|
|
2698
|
-
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}`;
|
|
2699
2494
|
for (const item of items) {
|
|
2700
2495
|
const itemSearchText = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
|
|
2701
2496
|
if (UNSUPPORTED.some((x) => itemSearchText.includes(x))) {
|
|
2702
|
-
|
|
2703
|
-
id: item.id,
|
|
2704
|
-
reason: "Unsupported stack policy",
|
|
2705
|
-
skipReasons: [
|
|
2706
|
-
{
|
|
2707
|
-
code: "unsupported-policy",
|
|
2708
|
-
message: "Unsupported stack policy",
|
|
2709
|
-
penalty: 100
|
|
2710
|
-
}
|
|
2711
|
-
]
|
|
2712
|
-
});
|
|
2497
|
+
skip(item.id, "unsupported-policy", "Unsupported stack policy");
|
|
2713
2498
|
continue;
|
|
2714
2499
|
}
|
|
2715
2500
|
if (item.source === "curated") {
|
|
2716
2501
|
const rs = item.reviewStatus;
|
|
2717
2502
|
if (!rs || rs !== "approved") {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
message: `Curated item requires reviewStatus:approved (got ${rs ?? "unset"})`,
|
|
2725
|
-
penalty: 100,
|
|
2726
|
-
signal: `reviewStatus:${rs ?? "unset"}`
|
|
2727
|
-
}
|
|
2728
|
-
]
|
|
2729
|
-
});
|
|
2503
|
+
skip(
|
|
2504
|
+
item.id,
|
|
2505
|
+
"curated-not-approved",
|
|
2506
|
+
`Curated item requires reviewStatus:approved (got ${rs ?? "unset"})`,
|
|
2507
|
+
`reviewStatus:${rs ?? "unset"}`
|
|
2508
|
+
);
|
|
2730
2509
|
continue;
|
|
2731
2510
|
}
|
|
2732
2511
|
if (item.riskLevel === "blocked") {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
message: "Curated item riskLevel is blocked",
|
|
2740
|
-
penalty: 100,
|
|
2741
|
-
signal: "riskLevel:blocked"
|
|
2742
|
-
}
|
|
2743
|
-
]
|
|
2744
|
-
});
|
|
2512
|
+
skip(
|
|
2513
|
+
item.id,
|
|
2514
|
+
"curated-risk-blocked",
|
|
2515
|
+
"Curated item riskLevel is blocked",
|
|
2516
|
+
"riskLevel:blocked"
|
|
2517
|
+
);
|
|
2745
2518
|
continue;
|
|
2746
2519
|
}
|
|
2747
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
|
+
}
|
|
2748
2552
|
const isDefaultBaseline = item.default === true;
|
|
2749
2553
|
const reasons = [];
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
const pushReason = (code, message, weight, signal) => {
|
|
2753
|
-
score += weight;
|
|
2754
|
-
reasons.push({ code, message, weight, signal });
|
|
2755
|
-
};
|
|
2756
|
-
const pushSkipReason = (code, message, penalty, signal) => {
|
|
2757
|
-
score -= penalty;
|
|
2758
|
-
skipReasons.push({ code, message, penalty, signal });
|
|
2759
|
-
};
|
|
2760
|
-
if (isDefaultBaseline) {
|
|
2761
|
-
pushReason("default-baseline", "catalog default baseline", 25, "policy:default");
|
|
2762
|
-
}
|
|
2554
|
+
const push = (code, message, signal) => reasons.push({ code, message, signal });
|
|
2555
|
+
if (isDefaultBaseline) push("default-baseline", "catalog default baseline", "policy:default");
|
|
2763
2556
|
const roleMatch = item.repoRoles.find((r) => roleSet.has(r.toLowerCase()));
|
|
2764
|
-
if (roleMatch)
|
|
2765
|
-
pushReason("repo-role-match", "repo role match", 40, `role:${roleMatch}`);
|
|
2766
|
-
}
|
|
2557
|
+
if (roleMatch) push("repo-role-match", "repo role match", roleSignal(roleMatch));
|
|
2767
2558
|
const tagMatch = item.tags.find((t) => stackSet.has(t.toLowerCase()));
|
|
2768
|
-
if (tagMatch)
|
|
2769
|
-
pushReason("stack-match", "stack/dependency match", 30, `tag:${tagMatch}`);
|
|
2770
|
-
}
|
|
2559
|
+
if (tagMatch) push("stack-match", "stack/dependency match", stackSignal(tagMatch));
|
|
2771
2560
|
const goalMatch = item.tags.find(
|
|
2772
2561
|
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
2773
2562
|
);
|
|
2774
|
-
if (goalMatch) {
|
|
2775
|
-
pushReason("goal-match", "guided goal match", 15, `goal:${goalMatch}`);
|
|
2776
|
-
}
|
|
2563
|
+
if (goalMatch) push("goal-match", "guided goal match", `goal:${goalMatch}`);
|
|
2777
2564
|
if (item.tags.includes(context.packageManager) || item.tags.includes(`${context.packageManager}4`) || item.tags.includes(`${context.packageManager}89`)) {
|
|
2778
|
-
|
|
2565
|
+
push(
|
|
2779
2566
|
"package-manager-match",
|
|
2780
2567
|
"package manager match",
|
|
2781
|
-
10,
|
|
2782
2568
|
`packageManager:${context.packageManager}`
|
|
2783
2569
|
);
|
|
2784
2570
|
}
|
|
2785
2571
|
const configSignal = item.tags.find(
|
|
2786
2572
|
(t) => context.warnings.join(" ").toLowerCase().includes(t.toLowerCase())
|
|
2787
2573
|
);
|
|
2788
|
-
if (configSignal) {
|
|
2789
|
-
pushReason("config-signal-match", "config signal match", 20, `warning:${configSignal}`);
|
|
2790
|
-
}
|
|
2574
|
+
if (configSignal) push("config-signal-match", "config signal match", `warning:${configSignal}`);
|
|
2791
2575
|
const changedMatch = changedFiles.find((f) => f.includes(item.id.split(".").pop() ?? ""));
|
|
2792
|
-
if (changedMatch)
|
|
2793
|
-
|
|
2794
|
-
}
|
|
2795
|
-
if (item.id === "haus.nx21-monorepo-patterns" && !roleSet.has("nx-monorepo")) {
|
|
2796
|
-
skipped.push({
|
|
2797
|
-
id: item.id,
|
|
2798
|
-
reason: "Required role missing: nx-monorepo",
|
|
2799
|
-
skipReasons: [
|
|
2800
|
-
{
|
|
2801
|
-
code: "required-role-missing",
|
|
2802
|
-
message: "Required role missing: nx-monorepo",
|
|
2803
|
-
penalty: 100,
|
|
2804
|
-
signal: "role:nx-monorepo"
|
|
2805
|
-
}
|
|
2806
|
-
]
|
|
2807
|
-
});
|
|
2808
|
-
continue;
|
|
2809
|
-
}
|
|
2810
|
-
if (item.id === "haus.turbo-monorepo-patterns" && !roleSet.has("turbo-monorepo")) {
|
|
2811
|
-
skipped.push({
|
|
2812
|
-
id: item.id,
|
|
2813
|
-
reason: "Required role missing: turbo-monorepo",
|
|
2814
|
-
skipReasons: [
|
|
2815
|
-
{
|
|
2816
|
-
code: "required-role-missing",
|
|
2817
|
-
message: "Required role missing: turbo-monorepo",
|
|
2818
|
-
penalty: 100,
|
|
2819
|
-
signal: "role:turbo-monorepo"
|
|
2820
|
-
}
|
|
2821
|
-
]
|
|
2822
|
-
});
|
|
2823
|
-
continue;
|
|
2824
|
-
}
|
|
2576
|
+
if (changedMatch)
|
|
2577
|
+
push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
|
|
2825
2578
|
const requiresAny = item.requiresAny ?? [];
|
|
2826
2579
|
if (requiresAny.length > 0) {
|
|
2827
|
-
const satisfied = matchRequiresAny(requiresAny, {
|
|
2828
|
-
stackSet,
|
|
2829
|
-
depSet,
|
|
2830
|
-
roleSet
|
|
2831
|
-
});
|
|
2580
|
+
const satisfied = matchRequiresAny(requiresAny, { stackSet, depSet, roleSet });
|
|
2832
2581
|
if (!satisfied.matched) {
|
|
2833
2582
|
const description = describeRequiresAny(requiresAny);
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
message: `requiresAny unsatisfied: needs ${description}`,
|
|
2841
|
-
penalty: 100,
|
|
2842
|
-
signal: description
|
|
2843
|
-
}
|
|
2844
|
-
]
|
|
2845
|
-
});
|
|
2583
|
+
skip(
|
|
2584
|
+
item.id,
|
|
2585
|
+
"requires-any-unsatisfied",
|
|
2586
|
+
`requiresAny unsatisfied: needs ${description}`,
|
|
2587
|
+
description
|
|
2588
|
+
);
|
|
2846
2589
|
continue;
|
|
2847
2590
|
}
|
|
2848
2591
|
if (!reasons.some((r) => r.code === "stack-match")) {
|
|
2849
|
-
|
|
2592
|
+
push("requires-any-match", "requires-any signal match", satisfied.signal);
|
|
2850
2593
|
}
|
|
2851
2594
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
if (!compat.has(item.ecosystem)) {
|
|
2855
|
-
pushSkipReason(
|
|
2856
|
-
"ecosystem-conflict",
|
|
2857
|
-
`ecosystem conflict: rule ecosystem=${item.ecosystem} but repo dominant backend=${dominantBackendEcosystem}`,
|
|
2858
|
-
40,
|
|
2859
|
-
`ecosystem:${item.ecosystem}->${dominantBackendEcosystem}`
|
|
2860
|
-
);
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
|
|
2864
|
-
pushSkipReason("sensitive-policy", "Sensitive content policy block", 100);
|
|
2865
|
-
}
|
|
2866
|
-
const trust = sourceTrust.get(item.source);
|
|
2867
|
-
if (trust === "candidate" || trust === "rejected") {
|
|
2868
|
-
pushSkipReason("source-trust", "Source trust policy block", 100);
|
|
2869
|
-
}
|
|
2870
|
-
if (item.source && item.source !== "haus" && trust !== "approved") {
|
|
2871
|
-
pushSkipReason("source-approval", "Source not approved", 100);
|
|
2872
|
-
}
|
|
2873
|
-
if (securityRiskCount > 0 && !isDefaultBaseline && (item.tags.includes("security") || item.id.includes("security"))) {
|
|
2874
|
-
pushSkipReason(
|
|
2875
|
-
"security-risk-penalty",
|
|
2876
|
-
"Security-tagged item penalized by active risk signals",
|
|
2877
|
-
20
|
|
2878
|
-
);
|
|
2879
|
-
}
|
|
2880
|
-
const positiveReasonCodes = new Set(
|
|
2881
|
-
reasons.map((r) => r.code).filter((c) => c !== "default-baseline")
|
|
2882
|
-
);
|
|
2883
|
-
const hasRoleSignal = positiveReasonCodes.has("repo-role-match");
|
|
2884
|
-
const hasDepOrStackSignal = positiveReasonCodes.has("stack-match") || positiveReasonCodes.has("requires-any-match");
|
|
2885
|
-
if (hasRoleSignal && !hasDepOrStackSignal && !isDefaultBaseline && requiresAny.length === 0) {
|
|
2886
|
-
pushSkipReason(
|
|
2887
|
-
"role-only-bleed-guard",
|
|
2888
|
-
"role match without dep/stack signal (role-only bleed)",
|
|
2889
|
-
25,
|
|
2890
|
-
roleMatch ? `role:${roleMatch}` : void 0
|
|
2891
|
-
);
|
|
2892
|
-
}
|
|
2893
|
-
const minScore = isDefaultBaseline ? 1 : 40;
|
|
2894
|
-
if (score >= minScore) {
|
|
2895
|
-
const confidenceLevel = computeConfidenceLevel({
|
|
2896
|
-
isDefaultBaseline,
|
|
2897
|
-
reasons,
|
|
2898
|
-
hasEcosystemConflict: skipReasons.some((s) => s.code === "ecosystem-conflict"),
|
|
2899
|
-
score
|
|
2900
|
-
});
|
|
2901
|
-
const confidence = confidenceLevelToNumber(confidenceLevel, score);
|
|
2595
|
+
const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
|
|
2596
|
+
if (isDefaultBaseline || hasEvidence) {
|
|
2902
2597
|
recommended.push({
|
|
2903
2598
|
id: item.id,
|
|
2904
2599
|
type: item.type,
|
|
2905
|
-
reason: reasons.length ? reasons.map((x) => x.message).join(", ") :
|
|
2600
|
+
reason: reasons.length ? reasons.map((x) => x.message).join(", ") : "eligible",
|
|
2906
2601
|
reasons,
|
|
2907
|
-
|
|
2908
|
-
confidenceLevel,
|
|
2909
|
-
selectionMode: isDefaultBaseline && reasons.every((r) => r.code === "default-baseline") ? "baseline" : "matched",
|
|
2602
|
+
selectionMode: isDefaultBaseline && !hasEvidence ? "baseline" : "matched",
|
|
2910
2603
|
install: true,
|
|
2911
|
-
score,
|
|
2912
|
-
scoreBreakdown: {
|
|
2913
|
-
bonuses: reasons,
|
|
2914
|
-
penalties: skipReasons,
|
|
2915
|
-
finalScore: score
|
|
2916
|
-
},
|
|
2917
2604
|
tags: item.tags,
|
|
2918
2605
|
ecosystem: item.ecosystem,
|
|
2919
2606
|
tokenEstimate: item.tokenEstimate
|
|
2920
2607
|
});
|
|
2921
2608
|
} else {
|
|
2922
|
-
|
|
2923
|
-
skipReasons.push({
|
|
2924
|
-
code: "no-role-stack-match",
|
|
2925
|
-
message: "No role/stack match",
|
|
2926
|
-
penalty: 0
|
|
2927
|
-
});
|
|
2928
|
-
}
|
|
2929
|
-
const primary = skipReasons[0];
|
|
2930
|
-
skipped.push({ id: item.id, reason: primary.message, skipReasons });
|
|
2609
|
+
skip(item.id, "no-role-stack-match", "No role/stack match");
|
|
2931
2610
|
}
|
|
2932
2611
|
}
|
|
2933
2612
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -3074,8 +2753,8 @@ async function runSetupProject(options) {
|
|
|
3074
2753
|
// src/commands/init.ts
|
|
3075
2754
|
async function runInit(options) {
|
|
3076
2755
|
const root = process.cwd();
|
|
3077
|
-
const hausDir =
|
|
3078
|
-
const alreadyInit = await
|
|
2756
|
+
const hausDir = path18.join(root, ".haus-workflow");
|
|
2757
|
+
const alreadyInit = await fs12.pathExists(hausDir);
|
|
3079
2758
|
if (alreadyInit) {
|
|
3080
2759
|
log("Haus AI already initialized in this project.");
|
|
3081
2760
|
log("Run `haus setup-project` to reconfigure.");
|
|
@@ -3087,8 +2766,8 @@ async function runInit(options) {
|
|
|
3087
2766
|
|
|
3088
2767
|
// src/install/apply.ts
|
|
3089
2768
|
import crypto2 from "crypto";
|
|
3090
|
-
import
|
|
3091
|
-
import
|
|
2769
|
+
import path21 from "path";
|
|
2770
|
+
import fs14 from "fs-extra";
|
|
3092
2771
|
|
|
3093
2772
|
// src/install/allow-rules.ts
|
|
3094
2773
|
var ALLOWED_SUBCOMMANDS = [
|
|
@@ -3135,13 +2814,13 @@ ${content2}`;
|
|
|
3135
2814
|
|
|
3136
2815
|
// src/install/manifest.ts
|
|
3137
2816
|
import os4 from "os";
|
|
3138
|
-
import
|
|
2817
|
+
import path19 from "path";
|
|
3139
2818
|
var MANIFEST_SCHEMA = "haus-install-manifest/1";
|
|
3140
2819
|
function globalClaudeDir() {
|
|
3141
|
-
return
|
|
2820
|
+
return path19.join(os4.homedir(), ".claude");
|
|
3142
2821
|
}
|
|
3143
2822
|
function hausManifestPath() {
|
|
3144
|
-
return
|
|
2823
|
+
return path19.join(globalClaudeDir(), "haus", "install-manifest.json");
|
|
3145
2824
|
}
|
|
3146
2825
|
async function readManifest() {
|
|
3147
2826
|
return readJson(hausManifestPath());
|
|
@@ -3160,10 +2839,10 @@ function buildManifest(source, files, hooks) {
|
|
|
3160
2839
|
}
|
|
3161
2840
|
|
|
3162
2841
|
// src/install/settings-merge.ts
|
|
3163
|
-
import
|
|
3164
|
-
import
|
|
2842
|
+
import path20 from "path";
|
|
2843
|
+
import fs13 from "fs-extra";
|
|
3165
2844
|
function settingsJsonPath() {
|
|
3166
|
-
return
|
|
2845
|
+
return path20.join(globalClaudeDir(), "settings.json");
|
|
3167
2846
|
}
|
|
3168
2847
|
async function readSettings() {
|
|
3169
2848
|
const parsed = await readJson(settingsJsonPath());
|
|
@@ -3304,7 +2983,7 @@ function stripHausHooks(settings) {
|
|
|
3304
2983
|
async function loadHooksFragment(fragmentPath) {
|
|
3305
2984
|
let raw;
|
|
3306
2985
|
try {
|
|
3307
|
-
raw = await
|
|
2986
|
+
raw = await fs13.readJson(fragmentPath);
|
|
3308
2987
|
} catch {
|
|
3309
2988
|
return [];
|
|
3310
2989
|
}
|
|
@@ -3313,46 +2992,46 @@ async function loadHooksFragment(fragmentPath) {
|
|
|
3313
2992
|
}
|
|
3314
2993
|
|
|
3315
2994
|
// src/install/apply.ts
|
|
3316
|
-
var
|
|
2995
|
+
var SCHEMA_VERSION2 = "1";
|
|
3317
2996
|
function hashContent(content2) {
|
|
3318
2997
|
return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
|
|
3319
2998
|
}
|
|
3320
2999
|
function sourceVersion() {
|
|
3321
3000
|
try {
|
|
3322
|
-
const pkgPath =
|
|
3323
|
-
const pkg = JSON.parse(
|
|
3001
|
+
const pkgPath = path21.join(packageRoot(), "package.json");
|
|
3002
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
|
|
3324
3003
|
return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
|
|
3325
3004
|
} catch {
|
|
3326
3005
|
return "haus@0.0.0";
|
|
3327
3006
|
}
|
|
3328
3007
|
}
|
|
3329
3008
|
function globalSrcDir() {
|
|
3330
|
-
return
|
|
3009
|
+
return path21.join(packageRoot(), "library", "global");
|
|
3331
3010
|
}
|
|
3332
3011
|
function collectSourceFiles(srcDir, claudeDir) {
|
|
3333
3012
|
const entries = [];
|
|
3334
|
-
const skillsDir =
|
|
3335
|
-
if (
|
|
3336
|
-
for (const skillName of
|
|
3337
|
-
const skillFile =
|
|
3338
|
-
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)) {
|
|
3339
3018
|
entries.push({
|
|
3340
3019
|
stableId: `skill.${skillName}`,
|
|
3341
|
-
srcRelPath:
|
|
3342
|
-
destPath:
|
|
3020
|
+
srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
|
|
3021
|
+
destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
|
|
3343
3022
|
});
|
|
3344
3023
|
}
|
|
3345
3024
|
}
|
|
3346
3025
|
}
|
|
3347
|
-
const commandsDir =
|
|
3348
|
-
if (
|
|
3349
|
-
for (const fileName of
|
|
3026
|
+
const commandsDir = path21.join(srcDir, "commands");
|
|
3027
|
+
if (fs14.pathExistsSync(commandsDir)) {
|
|
3028
|
+
for (const fileName of fs14.readdirSync(commandsDir)) {
|
|
3350
3029
|
if (!fileName.endsWith(".md")) continue;
|
|
3351
3030
|
const commandName = fileName.slice(0, -".md".length);
|
|
3352
3031
|
entries.push({
|
|
3353
3032
|
stableId: `command.${commandName}`,
|
|
3354
|
-
srcRelPath:
|
|
3355
|
-
destPath:
|
|
3033
|
+
srcRelPath: path21.join("library", "global", "commands", fileName),
|
|
3034
|
+
destPath: path21.join(claudeDir, "commands", fileName)
|
|
3356
3035
|
});
|
|
3357
3036
|
}
|
|
3358
3037
|
}
|
|
@@ -3376,7 +3055,7 @@ async function applyInstall(options = {}) {
|
|
|
3376
3055
|
};
|
|
3377
3056
|
const manifestFiles = [];
|
|
3378
3057
|
for (const entry of sourceFiles) {
|
|
3379
|
-
const srcPath =
|
|
3058
|
+
const srcPath = path21.join(packageRoot(), entry.srcRelPath);
|
|
3380
3059
|
const rawContent = await readText(srcPath);
|
|
3381
3060
|
if (rawContent === void 0) {
|
|
3382
3061
|
warn(`Source file not found: ${entry.srcRelPath}`);
|
|
@@ -3384,7 +3063,7 @@ async function applyInstall(options = {}) {
|
|
|
3384
3063
|
}
|
|
3385
3064
|
const stamped = stampMarkdown(rawContent, {
|
|
3386
3065
|
stableId: entry.stableId,
|
|
3387
|
-
schemaVersion:
|
|
3066
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
3388
3067
|
source
|
|
3389
3068
|
});
|
|
3390
3069
|
const newHash = hashContent(stamped);
|
|
@@ -3396,7 +3075,7 @@ async function applyInstall(options = {}) {
|
|
|
3396
3075
|
}
|
|
3397
3076
|
continue;
|
|
3398
3077
|
}
|
|
3399
|
-
const destExists =
|
|
3078
|
+
const destExists = fs14.pathExistsSync(entry.destPath);
|
|
3400
3079
|
if (destExists) {
|
|
3401
3080
|
const currentContent = await readText(entry.destPath);
|
|
3402
3081
|
if (currentContent !== void 0) {
|
|
@@ -3429,10 +3108,10 @@ async function applyInstall(options = {}) {
|
|
|
3429
3108
|
destPath: entry.destPath,
|
|
3430
3109
|
srcRelPath: entry.srcRelPath,
|
|
3431
3110
|
hash: newHash,
|
|
3432
|
-
schemaVersion:
|
|
3111
|
+
schemaVersion: SCHEMA_VERSION2
|
|
3433
3112
|
});
|
|
3434
3113
|
}
|
|
3435
|
-
const fragmentPath =
|
|
3114
|
+
const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
|
|
3436
3115
|
const fragments = await loadHooksFragment(fragmentPath);
|
|
3437
3116
|
const settings = await readSettings();
|
|
3438
3117
|
const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
|
|
@@ -3443,13 +3122,13 @@ async function applyInstall(options = {}) {
|
|
|
3443
3122
|
const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
|
|
3444
3123
|
for (const entry of existingManifest.files) {
|
|
3445
3124
|
if (currentDestPaths.has(entry.destPath)) continue;
|
|
3446
|
-
if (!
|
|
3125
|
+
if (!fs14.pathExistsSync(entry.destPath)) continue;
|
|
3447
3126
|
const content2 = await readText(entry.destPath);
|
|
3448
3127
|
if (!content2) continue;
|
|
3449
3128
|
const hasHeader = parseMarkdownHeader(content2) !== void 0;
|
|
3450
3129
|
const currentHash = hashContent(content2);
|
|
3451
3130
|
if (hasHeader && currentHash === entry.hash) {
|
|
3452
|
-
if (!dryRun) await
|
|
3131
|
+
if (!dryRun) await fs14.remove(entry.destPath);
|
|
3453
3132
|
result.deleted.push(entry.destPath);
|
|
3454
3133
|
} else {
|
|
3455
3134
|
warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
|
|
@@ -3572,20 +3251,20 @@ async function runScan(options) {
|
|
|
3572
3251
|
}
|
|
3573
3252
|
|
|
3574
3253
|
// src/commands/undo.ts
|
|
3575
|
-
import
|
|
3576
|
-
import
|
|
3254
|
+
import path22 from "path";
|
|
3255
|
+
import fs15 from "fs-extra";
|
|
3577
3256
|
var CLAUDE_DIR = ".claude";
|
|
3578
3257
|
async function runUndo(options) {
|
|
3579
3258
|
const root = process.cwd();
|
|
3580
|
-
const targets = [
|
|
3581
|
-
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));
|
|
3582
3261
|
if (existing.length === 0) {
|
|
3583
3262
|
log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
|
|
3584
3263
|
return;
|
|
3585
3264
|
}
|
|
3586
3265
|
if (!options.yes) {
|
|
3587
3266
|
const ok = await confirm(
|
|
3588
|
-
`Remove ${existing.map((p) =>
|
|
3267
|
+
`Remove ${existing.map((p) => path22.relative(root, p)).join(" and ")}? This cannot be undone.`
|
|
3589
3268
|
);
|
|
3590
3269
|
if (!ok) {
|
|
3591
3270
|
log("Cancelled.");
|
|
@@ -3593,15 +3272,15 @@ async function runUndo(options) {
|
|
|
3593
3272
|
}
|
|
3594
3273
|
}
|
|
3595
3274
|
for (const p of existing) {
|
|
3596
|
-
await
|
|
3597
|
-
log(`Removed ${
|
|
3275
|
+
await fs15.remove(p);
|
|
3276
|
+
log(`Removed ${path22.relative(root, p)}`);
|
|
3598
3277
|
}
|
|
3599
3278
|
}
|
|
3600
3279
|
|
|
3601
3280
|
// src/install/uninstall.ts
|
|
3602
3281
|
import crypto3 from "crypto";
|
|
3603
|
-
import
|
|
3604
|
-
import
|
|
3282
|
+
import path23 from "path";
|
|
3283
|
+
import fs16 from "fs-extra";
|
|
3605
3284
|
async function runUninstall(options = {}) {
|
|
3606
3285
|
const { force = false } = options;
|
|
3607
3286
|
const manifest = await readManifest();
|
|
@@ -3611,7 +3290,7 @@ async function runUninstall(options = {}) {
|
|
|
3611
3290
|
return result;
|
|
3612
3291
|
}
|
|
3613
3292
|
for (const entry of manifest.files) {
|
|
3614
|
-
const exists =
|
|
3293
|
+
const exists = fs16.pathExistsSync(entry.destPath);
|
|
3615
3294
|
if (!exists) continue;
|
|
3616
3295
|
const content2 = await readText(entry.destPath);
|
|
3617
3296
|
if (content2 === void 0) continue;
|
|
@@ -3629,22 +3308,22 @@ async function runUninstall(options = {}) {
|
|
|
3629
3308
|
result.skipped.push(entry.destPath);
|
|
3630
3309
|
continue;
|
|
3631
3310
|
}
|
|
3632
|
-
await
|
|
3633
|
-
await pruneEmptyDir(
|
|
3311
|
+
await fs16.remove(entry.destPath);
|
|
3312
|
+
await pruneEmptyDir(path23.dirname(entry.destPath));
|
|
3634
3313
|
result.deleted.push(entry.destPath);
|
|
3635
3314
|
}
|
|
3636
3315
|
const settings = await readSettings();
|
|
3637
3316
|
const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
|
|
3638
3317
|
await writeSettings(stripped);
|
|
3639
3318
|
result.hooksStripped = true;
|
|
3640
|
-
const hausDir =
|
|
3319
|
+
const hausDir = path23.join(globalClaudeDir(), "haus");
|
|
3641
3320
|
const manifestPath = hausManifestPath();
|
|
3642
|
-
if (
|
|
3643
|
-
await
|
|
3321
|
+
if (fs16.pathExistsSync(manifestPath)) {
|
|
3322
|
+
await fs16.remove(manifestPath);
|
|
3644
3323
|
}
|
|
3645
|
-
if (
|
|
3646
|
-
const remaining = await
|
|
3647
|
-
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);
|
|
3648
3327
|
}
|
|
3649
3328
|
return result;
|
|
3650
3329
|
}
|
|
@@ -3663,8 +3342,8 @@ function printUninstallResult(result) {
|
|
|
3663
3342
|
}
|
|
3664
3343
|
async function pruneEmptyDir(dir) {
|
|
3665
3344
|
try {
|
|
3666
|
-
const entries = await
|
|
3667
|
-
if (entries.length === 0) await
|
|
3345
|
+
const entries = await fs16.readdir(dir);
|
|
3346
|
+
if (entries.length === 0) await fs16.remove(dir);
|
|
3668
3347
|
} catch {
|
|
3669
3348
|
}
|
|
3670
3349
|
}
|
|
@@ -3682,7 +3361,7 @@ async function runUninstallCommand(options) {
|
|
|
3682
3361
|
}
|
|
3683
3362
|
|
|
3684
3363
|
// src/commands/update.ts
|
|
3685
|
-
import
|
|
3364
|
+
import path25 from "path";
|
|
3686
3365
|
|
|
3687
3366
|
// src/update/diff-generated-files.ts
|
|
3688
3367
|
function diffGeneratedFiles() {
|
|
@@ -3709,7 +3388,7 @@ function summarizeLockDiff(before, after) {
|
|
|
3709
3388
|
|
|
3710
3389
|
// src/update/lockfile.ts
|
|
3711
3390
|
import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
|
|
3712
|
-
import
|
|
3391
|
+
import path24 from "path";
|
|
3713
3392
|
async function checkLock(root) {
|
|
3714
3393
|
const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
|
|
3715
3394
|
const hasValidVersions = lock.every(
|
|
@@ -3730,7 +3409,7 @@ async function applyLock(root) {
|
|
|
3730
3409
|
try {
|
|
3731
3410
|
const backupDir = hausPath(root, "backups");
|
|
3732
3411
|
await mkdir(backupDir, { recursive: true });
|
|
3733
|
-
await copyFile(lockPath,
|
|
3412
|
+
await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
|
|
3734
3413
|
} catch {
|
|
3735
3414
|
}
|
|
3736
3415
|
const enriched = await Promise.all(
|
|
@@ -3752,7 +3431,7 @@ function diffLock(before, after) {
|
|
|
3752
3431
|
}
|
|
3753
3432
|
async function hasLocalOverrides(root) {
|
|
3754
3433
|
try {
|
|
3755
|
-
await readFile3(
|
|
3434
|
+
await readFile3(path24.join(root, ".claude", "settings.json"), "utf8");
|
|
3756
3435
|
return true;
|
|
3757
3436
|
} catch {
|
|
3758
3437
|
return false;
|
|
@@ -3764,7 +3443,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
|
|
|
3764
3443
|
async function runUpdate(options) {
|
|
3765
3444
|
const root = process.cwd();
|
|
3766
3445
|
if (options.check) {
|
|
3767
|
-
const pkgJson2 = await readJson(
|
|
3446
|
+
const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
|
|
3768
3447
|
const currentVersion2 = pkgJson2?.version ?? "0.0.0";
|
|
3769
3448
|
const [status, npmVersion, latestCatalogTag] = await Promise.all([
|
|
3770
3449
|
checkLock(root),
|
|
@@ -3791,7 +3470,7 @@ async function runUpdate(options) {
|
|
|
3791
3470
|
if (!status.ok) process.exitCode = 1;
|
|
3792
3471
|
return;
|
|
3793
3472
|
}
|
|
3794
|
-
const pkgJson = await readJson(
|
|
3473
|
+
const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
|
|
3795
3474
|
const currentVersion = pkgJson?.version ?? "0.0.0";
|
|
3796
3475
|
const npmStatus = await fetchNpmVersionStatus(currentVersion);
|
|
3797
3476
|
if (npmStatus.updateAvailable && npmStatus.latest !== null) {
|
|
@@ -3821,8 +3500,8 @@ async function runUpdate(options) {
|
|
|
3821
3500
|
}
|
|
3822
3501
|
|
|
3823
3502
|
// src/commands/validate-catalog.ts
|
|
3824
|
-
import
|
|
3825
|
-
import
|
|
3503
|
+
import fs17 from "fs";
|
|
3504
|
+
import path26 from "path";
|
|
3826
3505
|
|
|
3827
3506
|
// library/catalog/validation-rules.json
|
|
3828
3507
|
var validation_rules_default = {
|
|
@@ -4071,23 +3750,23 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4071
3750
|
const failures = [];
|
|
4072
3751
|
for (const item of items) {
|
|
4073
3752
|
if (!item.path) continue;
|
|
4074
|
-
const absPath =
|
|
3753
|
+
const absPath = path26.join(manifestDir, item.path);
|
|
4075
3754
|
if (item.type === "skill") {
|
|
4076
|
-
const skillMd =
|
|
4077
|
-
if (!
|
|
4078
|
-
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)}`);
|
|
4079
3758
|
continue;
|
|
4080
3759
|
}
|
|
4081
|
-
const text =
|
|
3760
|
+
const text = fs17.readFileSync(skillMd, "utf8");
|
|
4082
3761
|
for (const section of REQUIRED_SKILL_SECTIONS) {
|
|
4083
3762
|
if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
|
|
4084
3763
|
}
|
|
4085
3764
|
} else if (item.type === "agent") {
|
|
4086
|
-
if (!
|
|
3765
|
+
if (!fs17.existsSync(absPath)) {
|
|
4087
3766
|
failures.push(`${item.id}: missing agent file ${item.path}`);
|
|
4088
3767
|
continue;
|
|
4089
3768
|
}
|
|
4090
|
-
const text =
|
|
3769
|
+
const text = fs17.readFileSync(absPath, "utf8");
|
|
4091
3770
|
if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
|
|
4092
3771
|
for (const section of REQUIRED_AGENT_SECTIONS) {
|
|
4093
3772
|
if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
|
|
@@ -4098,7 +3777,7 @@ function auditShippedFiles(manifestDir, items) {
|
|
|
4098
3777
|
failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
|
|
4099
3778
|
}
|
|
4100
3779
|
} else if (item.type === "template") {
|
|
4101
|
-
if (!
|
|
3780
|
+
if (!fs17.existsSync(absPath)) {
|
|
4102
3781
|
failures.push(`${item.id}: missing template file ${item.path}`);
|
|
4103
3782
|
}
|
|
4104
3783
|
}
|
|
@@ -4109,11 +3788,11 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4109
3788
|
const failures = [];
|
|
4110
3789
|
const dirs = ["skills", "agents"];
|
|
4111
3790
|
for (const dir of dirs) {
|
|
4112
|
-
const abs =
|
|
4113
|
-
if (!
|
|
3791
|
+
const abs = path26.join(manifestDir, dir);
|
|
3792
|
+
if (!fs17.existsSync(abs)) continue;
|
|
4114
3793
|
walkMd(abs, (file) => {
|
|
4115
|
-
const text =
|
|
4116
|
-
const rel =
|
|
3794
|
+
const text = fs17.readFileSync(file, "utf8");
|
|
3795
|
+
const rel = path26.relative(manifestDir, file);
|
|
4117
3796
|
const lines = text.split(/\r?\n/);
|
|
4118
3797
|
for (let i = 0; i < lines.length; i++) {
|
|
4119
3798
|
const line2 = lines[i] ?? "";
|
|
@@ -4132,8 +3811,8 @@ function auditMarkdownContent(manifestDir) {
|
|
|
4132
3811
|
return failures;
|
|
4133
3812
|
}
|
|
4134
3813
|
function walkMd(dir, fn) {
|
|
4135
|
-
for (const entry of
|
|
4136
|
-
const full =
|
|
3814
|
+
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
3815
|
+
const full = path26.join(dir, entry.name);
|
|
4137
3816
|
if (entry.isDirectory()) walkMd(full, fn);
|
|
4138
3817
|
else if (entry.name.endsWith(".md")) fn(full);
|
|
4139
3818
|
}
|
|
@@ -4144,8 +3823,8 @@ async function runValidateCatalog(manifestPath) {
|
|
|
4144
3823
|
process.exitCode = 1;
|
|
4145
3824
|
return;
|
|
4146
3825
|
}
|
|
4147
|
-
const abs =
|
|
4148
|
-
const manifestDir =
|
|
3826
|
+
const abs = path26.resolve(process.cwd(), manifestPath);
|
|
3827
|
+
const manifestDir = path26.dirname(abs);
|
|
4149
3828
|
const data = await readJson(abs);
|
|
4150
3829
|
if (!data?.items) {
|
|
4151
3830
|
error(`Could not read catalog manifest at ${abs}`);
|
|
@@ -4174,7 +3853,7 @@ async function runValidateCatalog(manifestPath) {
|
|
|
4174
3853
|
}
|
|
4175
3854
|
|
|
4176
3855
|
// src/commands/workspace.ts
|
|
4177
|
-
import
|
|
3856
|
+
import path27 from "path";
|
|
4178
3857
|
import YAML from "yaml";
|
|
4179
3858
|
async function runWorkspace(action) {
|
|
4180
3859
|
if (action === "init") {
|
|
@@ -4207,7 +3886,7 @@ relationships: []
|
|
|
4207
3886
|
const summaries = [];
|
|
4208
3887
|
const ownership = {};
|
|
4209
3888
|
for (const repo of repos) {
|
|
4210
|
-
const repoRoot =
|
|
3889
|
+
const repoRoot = path27.resolve(process.cwd(), repo.path);
|
|
4211
3890
|
const result = await scanProject(repoRoot, "fast");
|
|
4212
3891
|
summaries.push({
|
|
4213
3892
|
name: repo.name,
|
|
@@ -4243,7 +3922,7 @@ ${summaries.map(
|
|
|
4243
3922
|
// src/cli.ts
|
|
4244
3923
|
function cliVersion() {
|
|
4245
3924
|
try {
|
|
4246
|
-
const pkgPath =
|
|
3925
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
4247
3926
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
4248
3927
|
return pkg.version ?? "0.0.0";
|
|
4249
3928
|
} catch {
|
|
@@ -4253,7 +3932,7 @@ function cliVersion() {
|
|
|
4253
3932
|
var program = new Command();
|
|
4254
3933
|
function validateRuntimeNodeVersion() {
|
|
4255
3934
|
try {
|
|
4256
|
-
const pkgPath =
|
|
3935
|
+
const pkgPath = path28.join(packageRoot(), "package.json");
|
|
4257
3936
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
4258
3937
|
const requiredRange = pkg.engines?.node;
|
|
4259
3938
|
if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
|