@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/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 path29 from "path";
5
+ import path28 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path11 from "path";
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 path10 from "path";
175
- import fs10 from "fs-extra";
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 path30 of SENSITIVE_PATHS) {
420
- const pattern = SENSITIVE_DIRS.has(path30) ? `${path30}/**` : path30;
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-project-facts.ts
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 = path7.join(root, "CLAUDE.md");
549
+ const filePath = path6.join(root, "CLAUDE.md");
646
550
  const block = buildImportBlock();
647
- const prev = await fs6.pathExists(filePath) ? await fs6.readFile(filePath, "utf8") : "";
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
- var VALIDATION_LIBS = [
677
- "zod",
678
- "valibot",
679
- "yup",
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(path8.join(root, "package.json"));
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) => fs7.pathExistsSync(path8.join(root, 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
- { prefix: "- Test (E2E): ", value: v.testE2E, hint: "command, e.g. playwright test", code: true },
735
- { prefix: "- Type check: ", value: v.typecheck, hint: "command, e.g. tsc --noEmit", code: true },
736
- { prefix: "- Lint: ", value: v.lint, hint: "command, e.g. eslint .", code: true },
737
- { prefix: "- Lint fix: ", value: v.lintFix, hint: "command, e.g. eslint . --fix", code: true },
738
- { prefix: "- Format check: ", value: v.formatCheck, hint: "command, e.g. prettier --check .", code: true },
739
- { prefix: "- Security audit: ", value: v.securityAudit, hint: "command", code: true },
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 (commands & conventions)\n\n> The everyday commands and conventions for this project \u2014 the build, test, and\n> lint commands, where docs live, and so on. This file is yours to edit and haus\n> will not overwrite it. haus fills in what it can detect on first setup;\n> `haus apply --refill-config` fills any still-blank fields without touching\n> anything you've edited.\n\n## Source-of-truth documents\n" + byPrefix("- Spec: ") + "\n" + byPrefix("- Design: ") + "\n" + byPrefix("- UX flows: ") + "\n\n## Commands\n" + byPrefix("- Test (unit + integration): ") + "\n" + byPrefix("- Test (E2E): ") + "\n" + byPrefix("- Type check: ") + "\n" + byPrefix("- Lint: ") + "\n" + byPrefix("- Lint fix: ") + "\n" + byPrefix("- Format check: ") + "\n" + byPrefix("- Security audit: ") + "\n\n## Validation library\n" + byPrefix("- Library: ") + "\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";
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 fs8.pathExists(destPath);
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: path9.basename(root)
675
+ repoName: path8.basename(root)
793
676
  };
794
677
  const values = await deriveWorkflowConfig(root, ctx);
795
678
  if (exists) {
796
- const current = await fs8.readFile(destPath, "utf8");
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 fs9 from "fs-extra";
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 STABLE_ID2 = "template.workflow";
833
- var SCHEMA_VERSION2 = "1";
715
+ var STABLE_ID = "template.workflow";
716
+ var SCHEMA_VERSION = "1";
834
717
  function makeWorkflowHeader(pkgVersion, contentHash) {
835
- return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
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 fs9.pathExists(destPath)) {
852
- const existing = await fs9.readFile(destPath, "utf8");
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 !== STABLE_ID2) {
860
- warn(`${printable}: HAUS-MANAGED id mismatch (expected ${STABLE_ID2}) \u2014 skipping`);
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 fs9.pathExists(destPath) ? await fs9.readFile(destPath, "utf8") : "";
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(path10.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
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 fs10.pathExists(configPath)) {
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 ?? path10.join(pkgRoot, "library", "catalog", "manifest.json");
960
- const manifestDir = path10.dirname(manifestPath);
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
- path10.join(CACHE_DIR, "manifest.json")
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 ? path10.join(CACHE_DIR, cachedItem.path) : null;
987
- const sourcePath = cachePath && await fs10.pathExists(cachePath) ? cachePath : path10.join(manifestDir, manifestItem.path);
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, path10.basename(sourcePath));
990
- if (await fs10.pathExists(sourcePath)) {
870
+ const destination = claudePath(root, target, path9.basename(sourcePath));
871
+ if (await fs9.pathExists(sourcePath)) {
991
872
  if (dryRun) {
992
- const exists = await fs10.pathExists(destination);
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 fs10.ensureDir(path10.dirname(destination));
998
- await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
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, path10.relative(root, destination)]);
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
- confidenceLevel: r.confidenceLevel
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 fs10.pathExists(filePath) ? await fs10.readFile(filePath, "utf8") : "";
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(path11.join(CACHE_DIR, "manifest.json"));
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.confidenceLevel}] \u2014 ${item.reason}`,
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 path12 from "path";
1151
- var CACHE_MANIFEST = path12.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
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 = path12.join(root, "library/catalog/manifest.json");
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 = path12.join(packageRoot(), "library/catalog/manifest.json");
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 path13 from "path";
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 = path13.join(root, CONFIG_PATH2);
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.score - a.score || a.id.localeCompare(b.id));
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 cappedMediumOrHigh = recommended.filter((rule) => {
1456
+ const capped = recommended.filter((rule) => {
1591
1457
  if (rule.selectionMode === "baseline") return false;
1592
- if (rule.confidenceLevel === "low") return false;
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 cappedMediumOrHigh.slice(0, 8);
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 path17 from "path";
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 path14 from "path";
1631
- import fs11 from "fs-extra";
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 (fs11.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
1650
- if (fs11.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
1651
- if (fs11.existsSync(path14.join(root, "package-lock.json"))) return "npm";
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 path15 from "path";
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[path15.basename(f)]).filter((s) => Boolean(s))
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 path16 from "path";
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(path16.join(root, rel), "utf8");
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(path17.join(root, "package.json"));
2026
- const composer = await readJson(path17.join(root, "composer.json"));
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 ?? path17.basename(root)),
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(path17.join(root, f), "utf8"))]
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 ? { scoreBreakdown: rawBreakdownById.get(x.id) } : {}
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 breakdown = rawBreakdownById.get(rule.id);
2147
- if (!breakdown) return [reasonLine];
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 path18 from "path";
2164
- import fs12 from "fs-extra";
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 = path18.join(root, "CLAUDE.md");
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 fs12.pathExists(hausPath(root, target))) {
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 fs12.pathExists(workflowPath);
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 = path18.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
2298
- const bundledPath = path18.join(
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 fs12.pathExists(cachePath) ? cachePath : bundledPath;
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 fs12.pathExists(workflowConfigPath);
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 fs12.readFile(workflowConfigPath, "utf8");
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(path18.join(packageRoot(), "package.json"));
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 path19 from "path";
2523
- import fs13 from "fs-extra";
2353
+ import path18 from "path";
2354
+ import fs12 from "fs-extra";
2524
2355
 
2525
- // src/recommender/ecosystem.ts
2526
- var ECOSYSTEM_GROUPS = {
2527
- laravel: ["laravel-app", "laravel-nova-app"],
2528
- wordpress: ["wordpress-site", "wordpress-bedrock-site", "wordpress-vanilla-site"],
2529
- vendure: ["vendure-app", "vendure-plugin"],
2530
- nestjs: ["nestjs-api"],
2531
- nextjs: ["next-app"],
2532
- react: ["react-app", "next-app", "design-system"],
2533
- vue: ["vue-app"],
2534
- dotnet: ["dotnet-service"],
2535
- nx: ["nx-monorepo"],
2536
- turbo: ["turbo-monorepo"]
2537
- };
2538
- var ECOSYSTEM_PRIMARY_BACKENDS = /* @__PURE__ */ new Set([
2539
- "laravel",
2540
- "wordpress",
2541
- "vendure",
2542
- "nestjs",
2543
- "dotnet"
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 pickDominantBackend(ecosystems) {
2560
- for (const eco of ecosystems) {
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
- function isBackendEcosystem(eco) {
2566
- return ECOSYSTEM_PRIMARY_BACKENDS.has(eco);
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 stackSet = buildStackSet(context);
2701
- const depSet = new Set(context.dependencies.map((d) => d.toLowerCase()));
2702
- const roleSet = new Set(context.repoRoles.map((r) => r.toLowerCase()));
2703
- const repoEcosystems = inferRepoEcosystems(context.repoRoles);
2704
- const dominantBackendEcosystem = pickDominantBackend(repoEcosystems);
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 securityRiskCount = context.securityRisks?.length ?? 0;
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
- skipped.push({
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
- skipped.push({
2731
- id: item.id,
2732
- reason: `Curated item not approved (reviewStatus=${rs ?? "unset"})`,
2733
- skipReasons: [
2734
- {
2735
- code: "curated-not-approved",
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
- skipped.push({
2746
- id: item.id,
2747
- reason: "Curated item risk level is blocked",
2748
- skipReasons: [
2749
- {
2750
- code: "curated-risk-blocked",
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 skipReasons = [];
2763
- let score = 0;
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
- pushReason(
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
- pushReason("changed-file-match", "changed file match", 10, `changedFile:${changedMatch}`);
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
- skipped.push({
2847
- id: item.id,
2848
- reason: `requiresAny unsatisfied: needs ${description}`,
2849
- skipReasons: [
2850
- {
2851
- code: "requires-any-unsatisfied",
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
- pushReason("requires-any-match", "requires-any signal match", 25, satisfied.signal);
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
- if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
2876
- pushSkipReason("sensitive-policy", "Sensitive content policy block", 100);
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(", ") : `score=${score}`,
2600
+ reason: reasons.length ? reasons.map((x) => x.message).join(", ") : "eligible",
2918
2601
  reasons,
2919
- confidence,
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
- if (skipReasons.length === 0) {
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 = path19.join(root, ".haus-workflow");
3090
- const alreadyInit = await fs13.pathExists(hausDir);
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 path22 from "path";
3103
- import fs15 from "fs-extra";
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 path20 from "path";
2817
+ import path19 from "path";
3151
2818
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
3152
2819
  function globalClaudeDir() {
3153
- return path20.join(os4.homedir(), ".claude");
2820
+ return path19.join(os4.homedir(), ".claude");
3154
2821
  }
3155
2822
  function hausManifestPath() {
3156
- return path20.join(globalClaudeDir(), "haus", "install-manifest.json");
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 path21 from "path";
3176
- import fs14 from "fs-extra";
2842
+ import path20 from "path";
2843
+ import fs13 from "fs-extra";
3177
2844
  function settingsJsonPath() {
3178
- return path21.join(globalClaudeDir(), "settings.json");
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 fs14.readJson(fragmentPath);
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 SCHEMA_VERSION3 = "1";
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 = path22.join(packageRoot(), "package.json");
3335
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
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 path22.join(packageRoot(), "library", "global");
3009
+ return path21.join(packageRoot(), "library", "global");
3343
3010
  }
3344
3011
  function collectSourceFiles(srcDir, claudeDir) {
3345
3012
  const entries = [];
3346
- const skillsDir = path22.join(srcDir, "skills");
3347
- if (fs15.pathExistsSync(skillsDir)) {
3348
- for (const skillName of fs15.readdirSync(skillsDir)) {
3349
- const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
3350
- if (fs15.pathExistsSync(skillFile)) {
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: path22.join("library", "global", "skills", skillName, "SKILL.md"),
3354
- destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
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 = path22.join(srcDir, "commands");
3360
- if (fs15.pathExistsSync(commandsDir)) {
3361
- for (const fileName of fs15.readdirSync(commandsDir)) {
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: path22.join("library", "global", "commands", fileName),
3367
- destPath: path22.join(claudeDir, "commands", fileName)
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 = path22.join(packageRoot(), entry.srcRelPath);
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: SCHEMA_VERSION3,
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 = fs15.pathExistsSync(entry.destPath);
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: SCHEMA_VERSION3
3111
+ schemaVersion: SCHEMA_VERSION2
3445
3112
  });
3446
3113
  }
3447
- const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
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 (!fs15.pathExistsSync(entry.destPath)) continue;
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 fs15.remove(entry.destPath);
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 path23 from "path";
3588
- import fs16 from "fs-extra";
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 = [path23.join(root, CLAUDE_DIR), path23.join(root, HAUS_DIR)];
3593
- const existing = targets.filter((p) => fs16.existsSync(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) => path23.relative(root, p)).join(" and ")}? This cannot be undone.`
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 fs16.remove(p);
3609
- log(`Removed ${path23.relative(root, p)}`);
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 path24 from "path";
3616
- import fs17 from "fs-extra";
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 = fs17.pathExistsSync(entry.destPath);
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 fs17.remove(entry.destPath);
3645
- await pruneEmptyDir(path24.dirname(entry.destPath));
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 = path24.join(globalClaudeDir(), "haus");
3319
+ const hausDir = path23.join(globalClaudeDir(), "haus");
3653
3320
  const manifestPath = hausManifestPath();
3654
- if (fs17.pathExistsSync(manifestPath)) {
3655
- await fs17.remove(manifestPath);
3321
+ if (fs16.pathExistsSync(manifestPath)) {
3322
+ await fs16.remove(manifestPath);
3656
3323
  }
3657
- if (fs17.pathExistsSync(hausDir)) {
3658
- const remaining = await fs17.readdir(hausDir);
3659
- if (remaining.length === 0) await fs17.remove(hausDir);
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 fs17.readdir(dir);
3679
- if (entries.length === 0) await fs17.remove(dir);
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 path26 from "path";
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 path25 from "path";
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, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
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(path25.join(root, ".claude", "settings.json"), "utf8");
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(path26.join(packageRoot(), "package.json"));
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(path26.join(packageRoot(), "package.json"));
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 fs18 from "fs";
3837
- import path27 from "path";
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 = path27.join(manifestDir, item.path);
3753
+ const absPath = path26.join(manifestDir, item.path);
4087
3754
  if (item.type === "skill") {
4088
- const skillMd = path27.join(absPath, "SKILL.md");
4089
- if (!fs18.existsSync(skillMd)) {
4090
- failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
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 = fs18.readFileSync(skillMd, "utf8");
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 (!fs18.existsSync(absPath)) {
3765
+ if (!fs17.existsSync(absPath)) {
4099
3766
  failures.push(`${item.id}: missing agent file ${item.path}`);
4100
3767
  continue;
4101
3768
  }
4102
- const text = fs18.readFileSync(absPath, "utf8");
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 (!fs18.existsSync(absPath)) {
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 = path27.join(manifestDir, dir);
4125
- if (!fs18.existsSync(abs)) continue;
3791
+ const abs = path26.join(manifestDir, dir);
3792
+ if (!fs17.existsSync(abs)) continue;
4126
3793
  walkMd(abs, (file) => {
4127
- const text = fs18.readFileSync(file, "utf8");
4128
- const rel = path27.relative(manifestDir, file);
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 fs18.readdirSync(dir, { withFileTypes: true })) {
4148
- const full = path27.join(dir, entry.name);
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 = path27.resolve(process.cwd(), manifestPath);
4160
- const manifestDir = path27.dirname(abs);
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 path28 from "path";
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 = path28.resolve(process.cwd(), repo.path);
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 = path29.join(packageRoot(), "package.json");
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 = path29.join(packageRoot(), "package.json");
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)) {