@haus-tech/haus-workflow 0.11.1 → 0.12.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 path28 from "path";
5
+ import path30 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path11 from "path";
9
+ import path12 from "path";
10
10
  import checkbox from "@inquirer/checkbox";
11
11
 
12
12
  // src/catalog/remote-catalog.ts
@@ -158,8 +158,8 @@ async function getCacheManifestAge() {
158
158
  }
159
159
 
160
160
  // src/claude/write-claude-files.ts
161
- import path10 from "path";
162
- import fs9 from "fs-extra";
161
+ import path11 from "path";
162
+ import fs10 from "fs-extra";
163
163
 
164
164
  // src/update/hash-installed.ts
165
165
  import path3 from "path";
@@ -254,10 +254,10 @@ function summarizeDiff(diffText) {
254
254
  const lines = diffText.split("\n");
255
255
  let additions = 0;
256
256
  let deletions = 0;
257
- for (const line of lines) {
258
- if (line.startsWith("+++ ") || line.startsWith("--- ")) continue;
259
- if (line.startsWith("+")) additions += 1;
260
- if (line.startsWith("-")) deletions += 1;
257
+ for (const line2 of lines) {
258
+ if (line2.startsWith("+++ ") || line2.startsWith("--- ")) continue;
259
+ if (line2.startsWith("+")) additions += 1;
260
+ if (line2.startsWith("-")) deletions += 1;
261
261
  }
262
262
  return { additions, deletions };
263
263
  }
@@ -313,8 +313,7 @@ import path5 from "path";
313
313
  var CONFIG_PATH = ".haus-workflow/config.json";
314
314
  var DEFAULT_HOOKS_CONFIG = {
315
315
  hooks: {
316
- context: { enabled: false },
317
- memoryInject: { enabled: false }
316
+ context: { enabled: false }
318
317
  }
319
318
  };
320
319
  async function isHookEnabled(root, key) {
@@ -322,15 +321,103 @@ async function isHookEnabled(root, key) {
322
321
  return cfg?.hooks?.[key]?.enabled === true;
323
322
  }
324
323
 
324
+ // src/security/dangerous-commands.ts
325
+ var DANGEROUS_COMMANDS = [
326
+ "rm -rf",
327
+ "sudo",
328
+ "chmod -R 777",
329
+ "chown -R",
330
+ "git push --force",
331
+ "git reset --hard",
332
+ "docker system prune",
333
+ "drop database",
334
+ "truncate table",
335
+ "php artisan migrate --force",
336
+ "npm publish",
337
+ "yarn npm publish",
338
+ "pnpm publish"
339
+ ];
340
+
341
+ // src/security/sensitive-paths.ts
342
+ var SENSITIVE_PATHS = [
343
+ ".env",
344
+ ".env.*",
345
+ "*.pem",
346
+ "*.key",
347
+ "*.p12",
348
+ "*.pfx",
349
+ "id_rsa",
350
+ "id_ed25519",
351
+ "*.sql",
352
+ "*.dump",
353
+ "*.backup",
354
+ "*.bak",
355
+ "storage/logs",
356
+ "wp-content/uploads",
357
+ "uploads",
358
+ "customer-data",
359
+ "exports",
360
+ "secrets",
361
+ "certs"
362
+ ];
363
+ var SENSITIVE_PATH_REGEXES = [
364
+ /^\.env(\.|$)/,
365
+ /(^|\/)\.env(\.|$)/,
366
+ /\.pem$/,
367
+ /\.key$/,
368
+ /\.p12$/,
369
+ /\.pfx$/,
370
+ /\.sql$/,
371
+ /\.dump$/,
372
+ /customer-data/,
373
+ /exports/,
374
+ /certs/,
375
+ /secrets/,
376
+ /(^|\/)storage\/logs(\/|$)/,
377
+ /(^|\/)wp-content\/uploads(\/|$)/,
378
+ /(^|\/)uploads(\/|$)/
379
+ ];
380
+ var SENSITIVE_ITEM_KEYWORDS = [
381
+ ".env",
382
+ "secrets",
383
+ "certs",
384
+ "customer-data",
385
+ "exports",
386
+ ".pem",
387
+ ".key"
388
+ ];
389
+
390
+ // src/security/deny-rules.ts
391
+ var SENSITIVE_DIRS = /* @__PURE__ */ new Set([
392
+ "storage/logs",
393
+ "wp-content/uploads",
394
+ "uploads",
395
+ "customer-data",
396
+ "exports",
397
+ "secrets",
398
+ "certs"
399
+ ]);
400
+ var FILE_TOOLS = ["Read", "Edit", "Write"];
401
+ function buildDenyRules() {
402
+ const rules = [];
403
+ for (const command of DANGEROUS_COMMANDS) {
404
+ rules.push(`Bash(${command}:*)`);
405
+ }
406
+ for (const path31 of SENSITIVE_PATHS) {
407
+ const pattern = SENSITIVE_DIRS.has(path31) ? `${path31}/**` : path31;
408
+ for (const tool of FILE_TOOLS) {
409
+ rules.push(`${tool}(${pattern})`);
410
+ }
411
+ }
412
+ return [...new Set(rules)];
413
+ }
414
+
325
415
  // src/claude/load-hooks.ts
326
416
  var CANONICAL_HOOKS = {
327
417
  hooks: {
328
418
  UserPromptSubmit: [
329
419
  {
330
- hooks: [
331
- { type: "command", command: "haus context --from-hook || true" },
332
- { type: "command", command: "haus memory inject --from-hook || true" }
333
- ]
420
+ hooks: [{ type: "command", command: "haus context --from-hook || true" }]
334
421
  }
335
422
  ],
336
423
  PreToolUse: [
@@ -347,12 +434,11 @@ var CANONICAL_HOOKS = {
347
434
  };
348
435
  var STABLE_HOOK_IDS = {
349
436
  "haus context --from-hook || true": "haus.context-hook",
350
- "haus memory inject --from-hook || true": "haus.memory-hook",
351
437
  "haus guard file-access --from-hook || true": "haus.guard-file",
352
438
  "haus guard bash --from-hook || true": "haus.guard-bash"
353
439
  };
354
440
  async function loadClaudeHooksSettings() {
355
- return CANONICAL_HOOKS;
441
+ return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
356
442
  }
357
443
  function flattenRecommendedHooks(settings) {
358
444
  const out = [];
@@ -438,9 +524,11 @@ function renderProjectFacts(ctx, rec, pkgVersion) {
438
524
  const repoName = ctx.repoName ?? path6.basename(ctx.root ?? "unknown");
439
525
  return `${header}
440
526
 
441
- # Project facts
527
+ # What haus found in this project
442
528
 
443
- > Auto-generated by \`haus apply\`. Do not edit \u2014 changes will be overwritten on next apply.
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.
444
532
 
445
533
  **Repo:** ${repoName}
446
534
  **Package manager:** ${ctx.packageManager ?? "unknown"}
@@ -472,7 +560,9 @@ async function writeProjectFacts(root, pkgVersion, dryRun) {
472
560
  dependencies: [],
473
561
  securityRisks: [],
474
562
  crossRepoHints: [],
475
- warnings: []
563
+ warnings: [],
564
+ detectionStatus: "unknown",
565
+ unsupportedSignals: []
476
566
  };
477
567
  const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
478
568
  mode: "fast",
@@ -564,84 +654,190 @@ async function writeRootClaudeMd(root, dryRun) {
564
654
  }
565
655
 
566
656
  // 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
567
661
  import path8 from "path";
568
662
  import fs7 from "fs-extra";
569
- function buildWorkflowConfig(ctx) {
663
+ var VALIDATION_LIBS = [
664
+ "zod",
665
+ "valibot",
666
+ "yup",
667
+ "joi",
668
+ "@hapi/joi",
669
+ "class-validator",
670
+ "superstruct",
671
+ "ajv"
672
+ ];
673
+ function binCmd(pm, bin, args) {
674
+ const tail = args ? ` ${args}` : "";
675
+ if (pm === "yarn") return `yarn ${bin}${tail}`;
676
+ if (pm === "pnpm") return `pnpm exec ${bin}${tail}`;
677
+ return `npx --no-install ${bin}${tail}`;
678
+ }
679
+ async function deriveWorkflowConfig(root, ctx) {
570
680
  const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
571
- const testCmd = pm + " test";
572
- const auditCmd = pm + " audit";
573
- return "# Project workflow configuration\n\n> Project-specific values for the workflow standard in WORKFLOW.md.\n> Edit freely \u2014 this file is project-owned and will not be overwritten by haus.\n\n## Source-of-truth documents\n- Spec: <!-- fill in path, e.g. docs/SPEC.md -->\n- Design: <!-- fill in path, e.g. docs/DESIGN.md -->\n- UX flows: <!-- fill in path, e.g. docs/UX.md -->\n\n## Commands\n- Test (unit + integration): `" + testCmd + "`\n- Test (E2E): <!-- fill in command -->\n- Type check: <!-- fill in command, e.g. tsc --noEmit -->\n- Lint: <!-- fill in command, e.g. npm run lint -->\n- Lint fix: <!-- fill in command, e.g. npm run lint -- --fix -->\n- Format check: <!-- fill in command, e.g. prettier --check . -->\n- Security audit: `" + auditCmd + "`\n\n## Validation library\n<!-- fill in, e.g. zod, yup, joi -->\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<!-- fill in, e.g. lefthook, husky -->\n";
681
+ const pkg = await readJson(path8.join(root, "package.json"));
682
+ const scripts = pkg?.scripts ?? {};
683
+ const deps = new Set(ctx.dependencies);
684
+ const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
685
+ const script = (name) => scripts[name] ? `${pm} run ${name}` : null;
686
+ const firstScript = (...names) => {
687
+ for (const n of names) if (scripts[n]) return `${pm} run ${n}`;
688
+ return null;
689
+ };
690
+ const hasDep = (name) => deps.has(name);
691
+ const exists = (rel) => fs7.pathExistsSync(path8.join(root, rel));
692
+ const hasTypeScript = hasDep("typescript") || exists("tsconfig.json");
693
+ const hasEslint = hasDep("eslint");
694
+ const hasPrettier = hasDep("prettier");
695
+ const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
696
+ const hasCypress = hasDep("cypress");
697
+ 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
+ return {
699
+ test: script("test") ?? `${pm} test`,
700
+ 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
+ preCommitTool,
708
+ specPath: exists("docs/SPEC.md") ? "docs/SPEC.md" : null,
709
+ designPath: exists("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
710
+ uxPath: exists("docs/UX.md") ? "docs/UX.md" : null
711
+ };
712
+ }
713
+
714
+ // src/claude/write-workflow-config.ts
715
+ function fields(v) {
716
+ return [
717
+ { prefix: "- Spec: ", value: v.specPath, hint: "path, e.g. docs/SPEC.md" },
718
+ { prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
719
+ { prefix: "- UX flows: ", value: v.uxPath, hint: "path, e.g. docs/UX.md" },
720
+ { prefix: "- Test (unit + integration): ", value: v.test, hint: "command", code: true },
721
+ { prefix: "- Test (E2E): ", value: v.testE2E, hint: "command, e.g. playwright test", code: true },
722
+ { prefix: "- Type check: ", value: v.typecheck, hint: "command, e.g. tsc --noEmit", code: true },
723
+ { prefix: "- Lint: ", value: v.lint, hint: "command, e.g. eslint .", code: true },
724
+ { prefix: "- Lint fix: ", value: v.lintFix, hint: "command, e.g. eslint . --fix", code: true },
725
+ { prefix: "- Format check: ", value: v.formatCheck, hint: "command, e.g. prettier --check .", code: true },
726
+ { prefix: "- Security audit: ", value: v.securityAudit, hint: "command", code: true },
727
+ { prefix: "- Library: ", value: v.validationLibrary, hint: "e.g. zod, yup, joi" },
728
+ { prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
729
+ ];
574
730
  }
575
- async function writeWorkflowConfig(root, dryRun) {
731
+ function renderValue(f) {
732
+ if (f.value === null) return `<!-- fill in ${f.hint} -->`;
733
+ return f.code ? `\`${f.value}\`` : f.value;
734
+ }
735
+ function line(f) {
736
+ return `${f.prefix}${renderValue(f)}`;
737
+ }
738
+ function buildWorkflowConfig(v) {
739
+ const f = fields(v);
740
+ const byPrefix = (p) => line(f.find((x) => x.prefix === p));
741
+ 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";
742
+ }
743
+ function refillContent(existing, v) {
744
+ const f = fields(v);
745
+ return existing.split("\n").map((ln) => {
746
+ const field = f.find((x) => ln.startsWith(x.prefix));
747
+ if (!field || field.value === null) return ln;
748
+ const rest = ln.slice(field.prefix.length).trim();
749
+ return rest.startsWith("<!-- fill in") ? line(field) : ln;
750
+ }).join("\n");
751
+ }
752
+ var FALLBACK_CONTEXT = {
753
+ mode: "fast",
754
+ generatedAt: "",
755
+ root: "",
756
+ repoName: "",
757
+ packageManager: "unknown",
758
+ repoRoles: [],
759
+ confidence: 0,
760
+ detectedStacks: {},
761
+ dependencies: [],
762
+ securityRisks: [],
763
+ crossRepoHints: [],
764
+ warnings: [],
765
+ detectionStatus: "unknown",
766
+ unsupportedSignals: []
767
+ };
768
+ async function writeWorkflowConfig(root, dryRun, opts = {}) {
576
769
  const destPath = hausPath(root, "workflow-config.md");
577
770
  const printable = displayPath(root, destPath);
578
- if (await fs7.pathExists(destPath)) {
771
+ const exists = await fs8.pathExists(destPath);
772
+ if (exists && !opts.refill) {
579
773
  if (dryRun) log(printable + ": exists (project-owned, skipping)");
580
774
  return null;
581
775
  }
582
776
  const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
583
- mode: "fast",
584
- generatedAt: "",
777
+ ...FALLBACK_CONTEXT,
585
778
  root,
586
- repoName: path8.basename(root),
587
- packageManager: "unknown",
588
- repoRoles: [],
589
- confidence: 0,
590
- detectedStacks: {},
591
- dependencies: [],
592
- securityRisks: [],
593
- crossRepoHints: [],
594
- warnings: []
779
+ repoName: path9.basename(root)
595
780
  };
596
- const content = buildWorkflowConfig(ctx);
781
+ const values = await deriveWorkflowConfig(root, ctx);
782
+ if (exists) {
783
+ const current = await fs8.readFile(destPath, "utf8");
784
+ const refilled = refillContent(current, values);
785
+ if (refilled === current) {
786
+ if (dryRun) log(printable + ": no blank fields to refill");
787
+ return null;
788
+ }
789
+ if (dryRun) {
790
+ log(printable + ": would refill blank fields");
791
+ return destPath;
792
+ }
793
+ await writeText(destPath, refilled);
794
+ return destPath;
795
+ }
597
796
  if (dryRun) {
598
797
  log(printable + ": would create");
599
798
  return destPath;
600
799
  }
601
- await writeText(destPath, content);
800
+ await writeText(destPath, buildWorkflowConfig(values));
602
801
  return destPath;
603
802
  }
604
803
 
605
804
  // src/claude/write-workflow.ts
606
- import path9 from "path";
607
- import fs8 from "fs-extra";
805
+ import path10 from "path";
806
+ import fs9 from "fs-extra";
608
807
 
609
808
  // src/claude/managed-template.ts
610
- function normaliseLF(content) {
611
- return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
809
+ function normaliseLF(content2) {
810
+ return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
612
811
  }
613
- function parseHausManagedHeader(line) {
614
- const match = line.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
812
+ function parseHausManagedHeader(line2) {
813
+ const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
615
814
  if (!match) return null;
616
- const hashMatch = line.match(/hash=(sha256-[a-f0-9]+)/);
815
+ const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
617
816
  return { id: match[1], hash: hashMatch?.[1] };
618
817
  }
619
818
 
620
819
  // src/claude/write-workflow.ts
621
820
  var STABLE_ID2 = "template.workflow";
622
821
  var SCHEMA_VERSION2 = "1";
623
- var TEMPLATE_REL = "library/global/templates/agentic-workflow-standard.md";
624
- var CATALOG_CACHE_TEMPLATE = path9.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
822
+ var CATALOG_CACHE_TEMPLATE = path10.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
625
823
  function makeWorkflowHeader(pkgVersion, contentHash) {
626
824
  return `<!-- HAUS-MANAGED id=${STABLE_ID2} v=${SCHEMA_VERSION2} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
627
825
  }
628
826
  async function writeWorkflow(root, pkgVersion, dryRun) {
629
- const cachePath = CATALOG_CACHE_TEMPLATE;
630
- const packagePath = path9.join(packageRoot(), TEMPLATE_REL);
631
- const templatePath = await fs8.pathExists(cachePath) ? cachePath : packagePath;
632
- if (!await fs8.pathExists(templatePath)) {
827
+ if (!await fs9.pathExists(CATALOG_CACHE_TEMPLATE)) {
633
828
  warn(`Workflow template not found \u2014 run \`haus update\` to fetch from catalog`);
634
829
  return null;
635
830
  }
636
- const templateContent = await fs8.readFile(templatePath, "utf8");
831
+ const templatePath = CATALOG_CACHE_TEMPLATE;
832
+ const templateContent = await fs9.readFile(templatePath, "utf8");
637
833
  const contentHash = hashText(normaliseLF(templateContent));
638
834
  const header = makeWorkflowHeader(pkgVersion, contentHash);
639
835
  const next = `${header}
640
836
  ${templateContent}`;
641
837
  const destPath = hausPath(root, "WORKFLOW.md");
642
838
  const printable = displayPath(root, destPath);
643
- if (await fs8.pathExists(destPath)) {
644
- const existing = await fs8.readFile(destPath, "utf8");
839
+ if (await fs9.pathExists(destPath)) {
840
+ const existing = await fs9.readFile(destPath, "utf8");
645
841
  const firstLine = existing.split("\n")[0] ?? "";
646
842
  const parsed = parseHausManagedHeader(firstLine);
647
843
  if (!parsed) {
@@ -663,7 +859,7 @@ ${templateContent}`;
663
859
  }
664
860
  }
665
861
  if (dryRun) {
666
- const prev = await fs8.pathExists(destPath) ? await fs8.readFile(destPath, "utf8") : "";
862
+ const prev = await fs9.pathExists(destPath) ? await fs9.readFile(destPath, "utf8") : "";
667
863
  if (!prev) {
668
864
  log(createUnifiedDiff(printable, "", next));
669
865
  } else {
@@ -678,7 +874,7 @@ ${templateContent}`;
678
874
  }
679
875
 
680
876
  // src/claude/write-claude-files.ts
681
- async function writeClaudeFiles(root, dryRun, selectedIds) {
877
+ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
682
878
  const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
683
879
  mode: "fast",
684
880
  recommended: [],
@@ -690,7 +886,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
690
886
  estimatedTokenReductionPct: 0
691
887
  };
692
888
  const pkgRoot = packageRoot();
693
- const hausVersion = (await readJson(path10.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
889
+ const hausVersion = (await readJson(path11.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
694
890
  const coreFiles = [
695
891
  claudePath(root, "settings.json"),
696
892
  claudePath(root, "rules", "haus.md"),
@@ -700,7 +896,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
700
896
  ];
701
897
  const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
702
898
  const workflowPath = await writeWorkflow(root, hausVersion, dryRun);
703
- const workflowConfigPath = await writeWorkflowConfig(root, dryRun);
899
+ const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
900
+ refill: opts.refillConfig
901
+ });
704
902
  const projectFactsPath = await writeProjectFacts(root, hausVersion, dryRun);
705
903
  const p6Files = [
706
904
  rootClaudeMdPath,
@@ -718,7 +916,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
718
916
  await writeManagedJson(root, claudePath(root, "settings.json"), hookSettings, dryRun);
719
917
  if (!dryRun) await assertPostApplySettingsMatchCanonical(root, hookSettings);
720
918
  const configPath = hausPath(root, "config.json");
721
- if (!await fs9.pathExists(configPath)) {
919
+ if (!await fs10.pathExists(configPath)) {
722
920
  await writeManagedJson(root, configPath, DEFAULT_HOOKS_CONFIG, dryRun);
723
921
  }
724
922
  await writeManagedText(
@@ -736,7 +934,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
736
934
  await writeManagedText(
737
935
  root,
738
936
  claudePath(root, "rules", "haus.md"),
739
- "- Keep context minimal.\n- Follow project conventions.\n",
937
+ "- Keep context minimal.\n- Follow project conventions.\n\n## Driving haus\nWhen the user asks to set up, configure, check, or fix the project, run `haus setup-project` or `haus doctor` and narrate results in plain language \u2014 never make them use a terminal or read JSON. The `/haus-setup`, `/haus-doctor`, and `/haus-fix` commands do the same.\n",
740
938
  dryRun
741
939
  );
742
940
  await writeManagedText(
@@ -746,12 +944,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
746
944
  dryRun
747
945
  );
748
946
  const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
749
- const manifestPath = fixtureManifestPath ?? path10.join(pkgRoot, "library", "catalog", "manifest.json");
750
- const manifestDir = path10.dirname(manifestPath);
947
+ const manifestPath = fixtureManifestPath ?? path11.join(pkgRoot, "library", "catalog", "manifest.json");
948
+ const manifestDir = path11.dirname(manifestPath);
751
949
  const manifest = await readJson(manifestPath) ?? { items: [] };
752
950
  const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
753
951
  const cacheManifest = await readJson(
754
- path10.join(CACHE_DIR, "manifest.json")
952
+ path11.join(CACHE_DIR, "manifest.json")
755
953
  );
756
954
  const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
757
955
  const installedPathsByItem = /* @__PURE__ */ new Map();
@@ -773,23 +971,23 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
773
971
  }
774
972
  }
775
973
  const cachedItem = cacheManifestById.get(item.id);
776
- const cachePath = cachedItem?.path ? path10.join(CACHE_DIR, cachedItem.path) : null;
777
- const sourcePath = cachePath && await fs9.pathExists(cachePath) ? cachePath : path10.join(manifestDir, manifestItem.path);
974
+ const cachePath = cachedItem?.path ? path11.join(CACHE_DIR, cachedItem.path) : null;
975
+ const sourcePath = cachePath && await fs10.pathExists(cachePath) ? cachePath : path11.join(manifestDir, manifestItem.path);
778
976
  const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
779
- const destination = claudePath(root, target, path10.basename(sourcePath));
780
- if (await fs9.pathExists(sourcePath)) {
977
+ const destination = claudePath(root, target, path11.basename(sourcePath));
978
+ if (await fs10.pathExists(sourcePath)) {
781
979
  if (dryRun) {
782
- const exists = await fs9.pathExists(destination);
980
+ const exists = await fs10.pathExists(destination);
783
981
  log(
784
982
  `${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
785
983
  );
786
984
  } else {
787
- await fs9.ensureDir(path10.dirname(destination));
788
- await fs9.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
985
+ await fs10.ensureDir(path11.dirname(destination));
986
+ await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
789
987
  }
790
988
  files.push(destination);
791
989
  const current = installedPathsByItem.get(item.id) ?? [];
792
- installedPathsByItem.set(item.id, [...current, path10.relative(root, destination)]);
990
+ installedPathsByItem.set(item.id, [...current, path11.relative(root, destination)]);
793
991
  installedIds.add(item.id);
794
992
  } else {
795
993
  warn(
@@ -840,7 +1038,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds) {
840
1038
  return [...new Set(files)];
841
1039
  }
842
1040
  async function writeManagedText(root, filePath, nextText, dryRun) {
843
- const prev = await fs9.pathExists(filePath) ? await fs9.readFile(filePath, "utf8") : "";
1041
+ const prev = await fs10.pathExists(filePath) ? await fs10.readFile(filePath, "utf8") : "";
844
1042
  const printable = displayPath(root, filePath);
845
1043
  if (dryRun) {
846
1044
  if (!prev) {
@@ -867,7 +1065,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
867
1065
 
868
1066
  // src/commands/apply.ts
869
1067
  async function cacheHasItems() {
870
- const data = await readJson(path11.join(CACHE_DIR, "manifest.json"));
1068
+ const data = await readJson(path12.join(CACHE_DIR, "manifest.json"));
871
1069
  return Array.isArray(data?.items) && data.items.length > 0;
872
1070
  }
873
1071
  async function runApply(options) {
@@ -924,7 +1122,9 @@ async function runApply(options) {
924
1122
  }
925
1123
  }
926
1124
  }
927
- const files = await writeClaudeFiles(root, isDryRun, selectedIds);
1125
+ const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
1126
+ refillConfig: options.refillConfig
1127
+ });
928
1128
  if (isDryRun) {
929
1129
  log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
930
1130
  } else {
@@ -935,8 +1135,8 @@ async function runApply(options) {
935
1135
 
936
1136
  // src/catalog/load-catalog.ts
937
1137
  import os3 from "os";
938
- import path12 from "path";
939
- var CACHE_MANIFEST = path12.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
1138
+ import path13 from "path";
1139
+ var CACHE_MANIFEST = path13.join(os3.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
940
1140
  async function loadCatalog(root) {
941
1141
  const envPath = process.env["HAUS_FIXTURE_CATALOG"];
942
1142
  if (envPath) {
@@ -945,10 +1145,10 @@ async function loadCatalog(root) {
945
1145
  }
946
1146
  const cacheData = await readJson(CACHE_MANIFEST);
947
1147
  if (cacheData?.items?.length) return cacheData.items;
948
- const localManifest = path12.join(root, "library/catalog/manifest.json");
1148
+ const localManifest = path13.join(root, "library/catalog/manifest.json");
949
1149
  const localData = await readJson(localManifest);
950
1150
  if (localData?.items?.length) return localData.items;
951
- const packageManifest = path12.join(packageRoot(), "library/catalog/manifest.json");
1151
+ const packageManifest = path13.join(packageRoot(), "library/catalog/manifest.json");
952
1152
  const data = await readJson(packageManifest);
953
1153
  return data?.items ?? [];
954
1154
  }
@@ -988,11 +1188,10 @@ async function runCatalogAudit() {
988
1188
  }
989
1189
 
990
1190
  // src/commands/config.ts
991
- import path13 from "path";
1191
+ import path14 from "path";
992
1192
  var CONFIG_PATH2 = ".haus-workflow/config.json";
993
1193
  var HOOK_ALIASES = {
994
- "hook.context": "context",
995
- "hook.memory": "memoryInject"
1194
+ "hook.context": "context"
996
1195
  };
997
1196
  async function runConfig(key, action) {
998
1197
  const hookKey = HOOK_ALIASES[key];
@@ -1002,7 +1201,7 @@ async function runConfig(key, action) {
1002
1201
  );
1003
1202
  }
1004
1203
  const root = process.cwd();
1005
- const configPath = path13.join(root, CONFIG_PATH2);
1204
+ const configPath = path14.join(root, CONFIG_PATH2);
1006
1205
  const existing = await readJson(configPath);
1007
1206
  const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
1008
1207
  cfg.hooks ??= {};
@@ -1045,7 +1244,8 @@ function normalizeRecommendation(input2) {
1045
1244
  finalScore: item.score ?? 0
1046
1245
  },
1047
1246
  tags: item.tags,
1048
- ecosystem: item.ecosystem
1247
+ ecosystem: item.ecosystem,
1248
+ tokenEstimate: item.tokenEstimate
1049
1249
  };
1050
1250
  });
1051
1251
  const skipped = (input2.skipped ?? []).map((item) => ({
@@ -1105,46 +1305,7 @@ function buildRecommendationExplanation(recommendation) {
1105
1305
  };
1106
1306
  }
1107
1307
 
1108
- // src/recommender/task-intent.ts
1109
- function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set()) {
1110
- const recommended = recommendation?.recommended ?? [];
1111
- if (!task) return recommended;
1112
- if (taskIntents.size > 0) {
1113
- const intentMatches = recommended.filter((rule) => {
1114
- if (rule.selectionMode === "baseline") return false;
1115
- const ruleIntents = computeRuleIntents(rule);
1116
- if (ruleIntents.size === 0) return false;
1117
- for (const ti of taskIntents) {
1118
- if (ruleIntents.has(ti)) return true;
1119
- }
1120
- return false;
1121
- });
1122
- if (intentMatches.length > 0) return intentMatches;
1123
- }
1124
- const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
1125
- const tokenMatches = recommended.filter((rule) => {
1126
- if (rule.selectionMode === "baseline") return false;
1127
- const corpus = [
1128
- rule.id,
1129
- rule.ecosystem ?? "",
1130
- ...rule.tags ?? [],
1131
- rule.reason ?? "",
1132
- ...rule.reasons.map((r) => r.message)
1133
- ].join(" ").toLowerCase();
1134
- return tokens.some((token) => corpus.includes(token));
1135
- });
1136
- if (tokenMatches.length > 0) return tokenMatches;
1137
- const taskWantsTesting = taskIntents.has("testing");
1138
- const cappedMediumOrHigh = recommended.filter((rule) => {
1139
- if (rule.selectionMode === "baseline") return false;
1140
- if (rule.confidenceLevel === "low") return false;
1141
- if (taskWantsTesting) return true;
1142
- const ruleIntents = computeRuleIntents(rule);
1143
- const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
1144
- return !isTestingOnly;
1145
- });
1146
- return cappedMediumOrHigh.slice(0, 8);
1147
- }
1308
+ // src/recommender/task-classification.ts
1148
1309
  var ALL_INTENTS = [
1149
1310
  "backend",
1150
1311
  "frontend",
@@ -1358,9 +1519,76 @@ function computeRuleIntents(rule) {
1358
1519
  return intents;
1359
1520
  }
1360
1521
 
1522
+ // src/recommender/rule-selection.ts
1523
+ var DEFAULT_CONTEXT_TOKEN_BUDGET = 12e3;
1524
+ function pickTaskRelevantRules(recommendation, task, taskIntents = /* @__PURE__ */ new Set(), opts = {}) {
1525
+ const recommended = recommendation?.recommended ?? [];
1526
+ return applyTokenBudget(selectRules(recommended, task, taskIntents), opts.tokenBudget);
1527
+ }
1528
+ function applyTokenBudget(rules, budget) {
1529
+ if (!budget || budget <= 0) return rules;
1530
+ const total = rules.reduce((sum, r) => sum + (r.tokenEstimate ?? 0), 0);
1531
+ if (total <= budget) return rules;
1532
+ const keep = /* @__PURE__ */ new Set();
1533
+ let used = 0;
1534
+ for (const r of rules) {
1535
+ if (r.selectionMode === "baseline") {
1536
+ keep.add(r.id);
1537
+ used += r.tokenEstimate ?? 0;
1538
+ }
1539
+ }
1540
+ const matched = rules.filter((r) => r.selectionMode !== "baseline").sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
1541
+ for (const r of matched) {
1542
+ const est = r.tokenEstimate ?? 0;
1543
+ if (used + est <= budget) {
1544
+ keep.add(r.id);
1545
+ used += est;
1546
+ }
1547
+ }
1548
+ return rules.filter((r) => keep.has(r.id));
1549
+ }
1550
+ function selectRules(recommended, task, taskIntents) {
1551
+ if (!task) return recommended;
1552
+ if (taskIntents.size > 0) {
1553
+ const intentMatches = recommended.filter((rule) => {
1554
+ if (rule.selectionMode === "baseline") return false;
1555
+ const ruleIntents = computeRuleIntents(rule);
1556
+ if (ruleIntents.size === 0) return false;
1557
+ for (const ti of taskIntents) {
1558
+ if (ruleIntents.has(ti)) return true;
1559
+ }
1560
+ return false;
1561
+ });
1562
+ if (intentMatches.length > 0) return intentMatches;
1563
+ }
1564
+ const tokens = task.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
1565
+ const tokenMatches = recommended.filter((rule) => {
1566
+ if (rule.selectionMode === "baseline") return false;
1567
+ const corpus = [
1568
+ rule.id,
1569
+ rule.ecosystem ?? "",
1570
+ ...rule.tags ?? [],
1571
+ rule.reason ?? "",
1572
+ ...rule.reasons.map((r) => r.message)
1573
+ ].join(" ").toLowerCase();
1574
+ return tokens.some((token) => corpus.includes(token));
1575
+ });
1576
+ if (tokenMatches.length > 0) return tokenMatches;
1577
+ const taskWantsTesting = taskIntents.has("testing");
1578
+ const cappedMediumOrHigh = recommended.filter((rule) => {
1579
+ if (rule.selectionMode === "baseline") return false;
1580
+ if (rule.confidenceLevel === "low") return false;
1581
+ if (taskWantsTesting) return true;
1582
+ const ruleIntents = computeRuleIntents(rule);
1583
+ const isTestingOnly = ruleIntents.size > 0 && [...ruleIntents].every((i) => i === "testing");
1584
+ return !isTestingOnly;
1585
+ });
1586
+ return cappedMediumOrHigh.slice(0, 8);
1587
+ }
1588
+
1361
1589
  // src/scanner/scan-project.ts
1362
- import { readFile } from "fs/promises";
1363
- import path15 from "path";
1590
+ import { readFile as readFile2 } from "fs/promises";
1591
+ import path18 from "path";
1364
1592
 
1365
1593
  // src/utils/audit-checks.ts
1366
1594
  function isRecord(v) {
@@ -1387,8 +1615,8 @@ function compareVersions(a, b) {
1387
1615
  }
1388
1616
 
1389
1617
  // src/scanner/detect-package-manager.ts
1390
- import path14 from "path";
1391
- import fs10 from "fs-extra";
1618
+ import path15 from "path";
1619
+ import fs11 from "fs-extra";
1392
1620
  function detectPackageManager(root, packageManagerField) {
1393
1621
  const field = String(packageManagerField ?? "").trim();
1394
1622
  if (field.startsWith("yarn@")) {
@@ -1406,77 +1634,394 @@ function detectPackageManager(root, packageManagerField) {
1406
1634
  if (satisfiesVersion(version, ">=9")) return "npm";
1407
1635
  return "unknown";
1408
1636
  }
1409
- if (fs10.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
1410
- if (fs10.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
1411
- if (fs10.existsSync(path14.join(root, "package-lock.json"))) return "npm";
1637
+ if (fs11.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
1638
+ if (fs11.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
1639
+ if (fs11.existsSync(path15.join(root, "package-lock.json"))) return "npm";
1412
1640
  return "unknown";
1413
1641
  }
1414
1642
 
1415
- // src/scanner/scan-project.ts
1416
- var SAFE_FILES = [
1417
- "package.json",
1418
- "yarn.lock",
1419
- "pnpm-lock.yaml",
1420
- "composer.json",
1421
- "composer.lock",
1422
- "nx.json",
1423
- "turbo.json",
1424
- "tsconfig.json",
1425
- "vite.config.*",
1426
- "next.config.*",
1427
- "tailwind.config.*",
1428
- "components.json",
1429
- "playwright.config.*",
1430
- "phpunit.xml",
1431
- "artisan",
1432
- "routes/*.php",
1433
- "app/Providers/*.php",
1434
- "schema.graphql",
1435
- "**/*.graphql",
1436
- "**/vendure-config.*",
1437
- "**/*module.ts",
1438
- "web/app/**",
1439
- "wp-content/plugins/**",
1440
- "wp-content/themes/**",
1441
- "wp-content/mu-plugins/**",
1442
- "wp-content/acf-json/**",
1443
- ".storybook/**",
1444
- ".env.example",
1445
- "wp-config.php",
1446
- "**/*.csproj",
1447
- "**/*.sln",
1448
- "docker-compose.*",
1449
- "Dockerfile"
1643
+ // src/scanner/detection-registry.ts
1644
+ var dep = (value) => ({ kind: "dep", value });
1645
+ var depPrefix = (value) => ({ kind: "depPrefix", value });
1646
+ var depAbsent = (value) => ({ kind: "depAbsent", value });
1647
+ var fileEndsWith = (value) => ({ kind: "file", value, mode: "endsWith" });
1648
+ var fileIncludes = (value) => ({ kind: "file", value, mode: "includes" });
1649
+ var fileEquals = (value) => ({ kind: "file", value, mode: "equals" });
1650
+ var fileStartsWith = (value) => ({ kind: "file", value, mode: "startsWith" });
1651
+ var content = (value) => ({ kind: "content", value });
1652
+ var STACK_BUCKETS = [
1653
+ "backend",
1654
+ "frontend",
1655
+ "databases",
1656
+ "testing",
1657
+ "auth",
1658
+ "tooling",
1659
+ "packageManagers"
1450
1660
  ];
1451
- var SENSITIVE = [
1452
- /^\.env(\.|$)/,
1453
- /(^|\/)\.env(\.|$)/,
1454
- /\.pem$/,
1455
- /\.key$/,
1456
- /\.p12$/,
1457
- /\.pfx$/,
1458
- /\.sql$/,
1459
- /\.dump$/,
1460
- /customer-data/,
1461
- /exports/,
1462
- /certs/,
1463
- /secrets/,
1464
- /(^|\/)storage\/logs(\/|$)/,
1465
- /(^|\/)wp-content\/uploads(\/|$)/,
1466
- /(^|\/)uploads(\/|$)/
1661
+ function matchSignal(sig, ctx) {
1662
+ switch (sig.kind) {
1663
+ case "dep":
1664
+ return ctx.deps.has(sig.value);
1665
+ case "depPrefix":
1666
+ for (const d of ctx.deps) if (d.startsWith(sig.value)) return true;
1667
+ return false;
1668
+ case "depAbsent":
1669
+ return !ctx.deps.has(sig.value);
1670
+ case "content":
1671
+ return ctx.contentBlob.includes(sig.value);
1672
+ case "file":
1673
+ return ctx.files.some((f) => {
1674
+ switch (sig.mode) {
1675
+ case "endsWith":
1676
+ return f.endsWith(sig.value);
1677
+ case "includes":
1678
+ return f.includes(sig.value);
1679
+ case "equals":
1680
+ return f === sig.value;
1681
+ case "startsWith":
1682
+ return f.startsWith(sig.value);
1683
+ }
1684
+ });
1685
+ }
1686
+ }
1687
+ function matchRule(rule, ctx) {
1688
+ if (rule.all) return rule.all.every((s) => matchSignal(s, ctx));
1689
+ if (rule.any) return rule.any.some((s) => matchSignal(s, ctx));
1690
+ return false;
1691
+ }
1692
+ var ROLE_RULES = [
1693
+ { role: "next-app", any: [dep("next"), fileIncludes("next.config.")] },
1694
+ { role: "react-app", any: [dep("react")] },
1695
+ { role: "vite-app", any: [dep("vite"), fileIncludes("vite.config.")] },
1696
+ { role: "react-router-app", all: [dep("react-router"), dep("@react-router/node")] },
1697
+ { role: "sanity-studio", any: [dep("sanity")] },
1698
+ { role: "strapi-app", any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
1699
+ { role: "expo-app", any: [dep("expo")] },
1700
+ { role: "vendure-app", any: [dep("@vendure/core")] },
1701
+ { role: "vendure-plugin", any: [depPrefix("@haus/vendure-"), fileIncludes("vendure-config")] },
1702
+ { role: "nestjs-api", any: [dep("@nestjs/core")] },
1703
+ { role: "graphql-api", any: [dep("graphql"), dep("@nestjs/graphql")] },
1704
+ { role: "nx-monorepo", any: [fileEndsWith("nx.json")] },
1705
+ { role: "turbo-monorepo", any: [fileEndsWith("turbo.json")] },
1706
+ { role: "laravel-app", any: [fileEndsWith("artisan"), dep("laravel/framework")] },
1707
+ { role: "laravel-nova-app", any: [dep("laravel/nova")] },
1708
+ { role: "dotnet-service", any: [fileEndsWith(".csproj"), fileEndsWith(".sln")] },
1709
+ { role: "express-service", any: [dep("express")] }
1467
1710
  ];
1711
+ var STACK_RULES = [
1712
+ { stack: ["frontend", "nextjs"], any: [dep("next")] },
1713
+ { stack: ["frontend", "react19"], any: [dep("react")] },
1714
+ { stack: ["frontend", "vue"], any: [dep("vue")] },
1715
+ { stack: ["frontend", "vite8"], any: [dep("vite")] },
1716
+ { stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
1717
+ { stack: ["frontend", "tailwindcss"], any: [dep("tailwindcss"), fileIncludes("tailwind.config.")] },
1718
+ {
1719
+ stack: ["frontend", "shadcn"],
1720
+ all: [fileEndsWith("components.json"), dep("class-variance-authority")]
1721
+ },
1722
+ { stack: ["tooling", "typescript5"], any: [dep("typescript")] },
1723
+ { stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
1724
+ { stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
1725
+ { stack: ["backend", "prisma"], any: [dep("prisma"), dep("@prisma/client")] },
1726
+ { stack: ["frontend", "expo"], any: [dep("expo")] },
1727
+ { stack: ["frontend", "react-native"], any: [dep("react-native")] },
1728
+ { stack: ["tooling", "i18next"], any: [dep("i18next"), dep("react-i18next")] },
1729
+ { stack: ["tooling", "bullmq"], any: [dep("bullmq")] },
1730
+ { stack: ["tooling", "docker"], any: [fileEquals("Dockerfile"), fileStartsWith("docker-compose")] },
1731
+ { stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
1732
+ { stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
1733
+ { stack: ["tooling", "deployer-php"], any: [dep("deployer/deployer")] },
1734
+ { stack: ["tooling", "missing-prettier"], any: [depAbsent("prettier")] },
1735
+ { stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
1736
+ {
1737
+ stack: ["tooling", "stripe"],
1738
+ any: [dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
1739
+ },
1740
+ { stack: ["tooling", "qliro"], any: [dep("@haus-tech/qliro-plugin")] },
1741
+ {
1742
+ stack: ["databases", "supabase"],
1743
+ any: [dep("@supabase/supabase-js"), depPrefix("@supabase/")]
1744
+ },
1745
+ { stack: ["backend", "vendure3"], any: [dep("@vendure/core")] },
1746
+ { stack: ["backend", "nestjs"], any: [dep("@nestjs/core")] },
1747
+ { stack: ["backend", "nestjs"], any: [content("NestFactory")] },
1748
+ { stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
1749
+ { stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
1750
+ { stack: ["backend", "graphql"], any: [fileEndsWith(".graphql"), fileEndsWith("schema.graphql")] },
1751
+ { stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
1752
+ { stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
1753
+ { stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
1754
+ {
1755
+ stack: ["backend", "elementor"],
1756
+ any: [
1757
+ dep("wpackagist-plugin/elementor"),
1758
+ dep("wearehaus/elementor-pro"),
1759
+ dep("wpackagist-theme/hello-elementor")
1760
+ ]
1761
+ },
1762
+ {
1763
+ stack: ["backend", "acf-pro"],
1764
+ any: [
1765
+ dep("wearehaus/advanced-custom-fields-pro"),
1766
+ dep("wpackagist-plugin/advanced-custom-fields")
1767
+ ]
1768
+ },
1769
+ { stack: ["backend", "jetengine"], any: [dep("wearehaus/jet-engine")] },
1770
+ { stack: ["backend", "jetsmartfilters"], any: [dep("wearehaus/jet-smart-filters")] },
1771
+ { stack: ["backend", "gravityforms"], any: [dep("wearehaus/gravityforms")] },
1772
+ { stack: ["backend", "dotnet"], any: [fileEndsWith(".csproj"), fileEndsWith(".sln")] },
1773
+ { stack: ["testing", "playwright"], any: [dep("@playwright/test")] },
1774
+ { stack: ["testing", "storybook"], any: [fileIncludes(".storybook")] },
1775
+ { stack: ["testing", "testing-library"], any: [depPrefix("@testing-library/")] },
1776
+ { stack: ["testing", "phpunit"], any: [fileEndsWith("phpunit.xml")] },
1777
+ { stack: ["testing", "storybook"], any: [depPrefix("@storybook/")] },
1778
+ { stack: ["testing", "vitest"], any: [dep("vitest")] },
1779
+ { stack: ["testing", "jest"], any: [dep("jest"), dep("jest-environment-jsdom")] },
1780
+ { stack: ["databases", "postgresql"], any: [dep("pg")] },
1781
+ { stack: ["databases", "mariadb"], any: [dep("mariadb"), dep("mysql2")] },
1782
+ { stack: ["databases", "mysql"], any: [dep("mysql"), dep("mysql2")] },
1783
+ { stack: ["databases", "mssql"], any: [dep("mssql")] },
1784
+ { stack: ["databases", "elasticsearch"], any: [dep("@elastic/elasticsearch")] },
1785
+ { stack: ["databases", "redis"], any: [dep("predis/predis"), dep("ioredis"), dep("redis")] },
1786
+ { stack: ["auth", "oidc"], any: [content("openid")] },
1787
+ { stack: ["auth", "azure-ad"], any: [content("AZURE_AD")] },
1788
+ { stack: ["auth", "bankid"], any: [content("BANKID")] },
1789
+ {
1790
+ stack: ["auth", "saml2"],
1791
+ any: [dep("24slides/laravel-saml2"), dep("aacotroneo/laravel-saml2")]
1792
+ },
1793
+ { stack: ["auth", "next-auth"], any: [dep("next-auth"), dep("@auth/core")] }
1794
+ ];
1795
+ function runDetection(ctx, rules = STACK_RULES) {
1796
+ const roles = [];
1797
+ for (const rule of ROLE_RULES) {
1798
+ if (rule.role && matchRule(rule, ctx) && !roles.includes(rule.role)) roles.push(rule.role);
1799
+ }
1800
+ const stacks = {};
1801
+ for (const bucket of STACK_BUCKETS) stacks[bucket] = [];
1802
+ for (const rule of rules) {
1803
+ if (!rule.stack || !matchRule(rule, ctx)) continue;
1804
+ const [bucket, name] = rule.stack;
1805
+ stacks[bucket] ??= [];
1806
+ if (!stacks[bucket].includes(name)) stacks[bucket].push(name);
1807
+ }
1808
+ return { roles, stacks };
1809
+ }
1810
+
1811
+ // src/scanner/detection.ts
1812
+ import path16 from "path";
1813
+ var UNSUPPORTED_MARKERS = {
1814
+ "requirements.txt": "python",
1815
+ "pyproject.toml": "python",
1816
+ "go.mod": "go",
1817
+ "Cargo.toml": "rust",
1818
+ "pom.xml": "java",
1819
+ "build.gradle": "java",
1820
+ "build.gradle.kts": "java",
1821
+ Gemfile: "ruby"
1822
+ };
1823
+ var WEAK_STACK_SIGNALS = /* @__PURE__ */ new Set(["missing-prettier", "missing-eslint"]);
1824
+ function computeDetectionStatus(roles, stacks, unsupportedSignals) {
1825
+ const hasRealStack = Object.entries(stacks).some(
1826
+ ([bucket, names]) => bucket !== "packageManagers" && names.some((n) => !WEAK_STACK_SIGNALS.has(n))
1827
+ );
1828
+ const hasRealSignal = roles.length > 0 || hasRealStack;
1829
+ if (!hasRealSignal) return "unknown";
1830
+ return unsupportedSignals.length > 0 ? "partial" : "supported";
1831
+ }
1468
1832
  function blocked(rel) {
1469
- return SENSITIVE.some((x) => x.test(rel));
1833
+ return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
1834
+ }
1835
+ function dependencySet(pkg, composer) {
1836
+ const depNames = /* @__PURE__ */ new Set();
1837
+ const pushObj = (obj) => {
1838
+ if (!isRecord(obj)) return;
1839
+ for (const key of Object.keys(obj)) depNames.add(key);
1840
+ };
1841
+ pushObj(pkg?.dependencies);
1842
+ pushObj(pkg?.devDependencies);
1843
+ pushObj(composer?.require);
1844
+ pushObj(composer?.["require-dev"]);
1845
+ return [...depNames].sort();
1846
+ }
1847
+ function finalizeRoles(registryRoles, deps, files) {
1848
+ const roles = new Set(registryRoles);
1849
+ const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
1850
+ const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
1851
+ if (hasWpConfig && hasBedrockLayout) {
1852
+ roles.add("wordpress-bedrock-site");
1853
+ roles.add("wordpress-site");
1854
+ } else if (hasWpConfig) {
1855
+ roles.add("wordpress-vanilla-site");
1856
+ roles.add("wordpress-site");
1857
+ } else if (deps.includes("roots/wordpress")) {
1858
+ roles.add("wordpress-bedrock-site");
1859
+ roles.add("wordpress-site");
1860
+ }
1861
+ return [...roles].sort();
1862
+ }
1863
+ function collectUnsupportedSignals(files) {
1864
+ return [
1865
+ ...new Set(
1866
+ files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
1867
+ )
1868
+ ].sort();
1869
+ }
1870
+
1871
+ // src/scanner/render.ts
1872
+ import { readFile } from "fs/promises";
1873
+ import path17 from "path";
1874
+
1875
+ // src/scanner/role-labels.ts
1876
+ var ROLE_LABELS = {
1877
+ "next-app": "a Next.js app",
1878
+ "react-app": "a React app",
1879
+ "vite-app": "a Vite app",
1880
+ "react-router-app": "a React Router app",
1881
+ "sanity-studio": "a Sanity Studio",
1882
+ "strapi-app": "a Strapi app",
1883
+ "expo-app": "an Expo app",
1884
+ "vendure-app": "a Vendure server",
1885
+ "vendure-plugin": "a Vendure plugin",
1886
+ "nestjs-api": "a NestJS API",
1887
+ "graphql-api": "a GraphQL API",
1888
+ "nx-monorepo": "an Nx monorepo",
1889
+ "turbo-monorepo": "a Turborepo monorepo",
1890
+ "laravel-app": "a Laravel app",
1891
+ "laravel-nova-app": "a Laravel Nova app",
1892
+ "dotnet-service": "a .NET service",
1893
+ "express-service": "an Express service",
1894
+ "wordpress-bedrock-site": "a WordPress (Bedrock) site",
1895
+ "wordpress-vanilla-site": "a WordPress site",
1896
+ "wordpress-site": "a WordPress site"
1897
+ };
1898
+ function article(word) {
1899
+ return /^[aeiou]/i.test(word) ? "an" : "a";
1900
+ }
1901
+ function friendlyRole(role) {
1902
+ const known = ROLE_LABELS[role];
1903
+ if (known) return known;
1904
+ const words = role.replace(/[-_]+/g, " ").trim();
1905
+ return words ? `${article(words)} ${words}` : "a project";
1906
+ }
1907
+ function joinRoles(labels) {
1908
+ if (labels.length === 0) return "";
1909
+ if (labels.length === 1) return labels[0];
1910
+ return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
1911
+ }
1912
+ function describeRepo(context) {
1913
+ const labels = context.repoRoles.map(friendlyRole);
1914
+ const roleText = joinRoles(labels);
1915
+ if (context.detectionStatus === "unknown") {
1916
+ const markers = context.unsupportedSignals.join(", ");
1917
+ const detail = markers ? ` (I see ${markers})` : "";
1918
+ return `I couldn't fully recognise this stack${detail}, so I'll apply the general workflow and security guidance rather than framework-specific help.`;
1919
+ }
1920
+ const base = roleText ? `This looks like ${roleText}, using ${context.packageManager}.` : `I recognised this project's tooling (${context.packageManager}) but not a specific framework.`;
1921
+ if (context.detectionStatus === "partial" && context.unsupportedSignals.length > 0) {
1922
+ return `${base} I also see ${context.unsupportedSignals.join(", ")}, which haus doesn't fully support \u2014 guidance covers the recognised parts.`;
1923
+ }
1924
+ return base;
1470
1925
  }
1926
+
1927
+ // src/scanner/render.ts
1928
+ async function buildContentBlob(root, files) {
1929
+ const candidates = files.filter(
1930
+ (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
1931
+ );
1932
+ const slice = candidates.slice(0, 300);
1933
+ const CHUNK = 24;
1934
+ const parts = [];
1935
+ for (let i = 0; i < slice.length; i += CHUNK) {
1936
+ const batch = await Promise.all(
1937
+ slice.slice(i, i + CHUNK).map(async (rel) => {
1938
+ try {
1939
+ return await readFile(path17.join(root, rel), "utf8");
1940
+ } catch {
1941
+ return "";
1942
+ }
1943
+ })
1944
+ );
1945
+ parts.push(...batch);
1946
+ }
1947
+ return parts.join("\n");
1948
+ }
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
+ function renderSummary(context) {
1955
+ return `# Repo summary
1956
+
1957
+ ${describeRepo(context)}
1958
+
1959
+ - Repo: ${context.repoName}
1960
+ - Package manager: ${context.packageManager}
1961
+ - Roles: ${context.repoRoles.join(", ") || "unknown"}
1962
+ - Generated: ${context.generatedAt}
1963
+ `;
1964
+ }
1965
+
1966
+ // src/scanner/scan-project.ts
1967
+ var SAFE_FILES = [
1968
+ "package.json",
1969
+ "yarn.lock",
1970
+ "pnpm-lock.yaml",
1971
+ "composer.json",
1972
+ "composer.lock",
1973
+ "nx.json",
1974
+ "turbo.json",
1975
+ "tsconfig.json",
1976
+ "vite.config.*",
1977
+ "next.config.*",
1978
+ "tailwind.config.*",
1979
+ "components.json",
1980
+ "playwright.config.*",
1981
+ "phpunit.xml",
1982
+ "artisan",
1983
+ "routes/*.php",
1984
+ "app/Providers/*.php",
1985
+ "schema.graphql",
1986
+ "**/*.graphql",
1987
+ "**/vendure-config.*",
1988
+ "**/*module.ts",
1989
+ "web/app/**",
1990
+ "wp-content/plugins/**",
1991
+ "wp-content/themes/**",
1992
+ "wp-content/mu-plugins/**",
1993
+ "wp-content/acf-json/**",
1994
+ ".storybook/**",
1995
+ ".env.example",
1996
+ "wp-config.php",
1997
+ "**/*.csproj",
1998
+ "**/*.sln",
1999
+ "docker-compose.*",
2000
+ "Dockerfile",
2001
+ // Unsupported-ecosystem markers — matched by PRESENCE only (never content-read; none
2002
+ // match the content-blob extensions). Drive detectionStatus / unsupportedSignals.
2003
+ "requirements.txt",
2004
+ "pyproject.toml",
2005
+ "go.mod",
2006
+ "Cargo.toml",
2007
+ "pom.xml",
2008
+ "build.gradle",
2009
+ "build.gradle.kts",
2010
+ "Gemfile"
2011
+ ];
1471
2012
  async function scanProject(root, mode = "fast") {
1472
- const pkg = await readJson(path15.join(root, "package.json"));
1473
- const composer = await readJson(path15.join(root, "composer.json"));
2013
+ const pkg = await readJson(path18.join(root, "package.json"));
2014
+ const composer = await readJson(path18.join(root, "composer.json"));
1474
2015
  const files = await listFiles(root, SAFE_FILES);
1475
2016
  const safeFiles = files.filter((f) => !blocked(f));
1476
2017
  const deps = dependencySet(pkg, composer);
1477
2018
  const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
1478
- const roles = detectRoles(deps, safeFiles);
1479
- const stacks = await detectStacks(root, deps, safeFiles, packageManager);
2019
+ const contentBlob = await buildContentBlob(root, safeFiles);
2020
+ const detection = runDetection({ deps: new Set(deps), files: safeFiles, contentBlob });
2021
+ const roles = finalizeRoles(detection.roles, deps, safeFiles);
2022
+ const stacks = detection.stacks;
2023
+ if (packageManager === "yarn") stacks.packageManagers.push("yarn4");
2024
+ if (packageManager === "pnpm") stacks.packageManagers.push("pnpm89");
1480
2025
  const warnings = [];
1481
2026
  const securityRisks = [];
1482
2027
  const crossRepoHints = [];
@@ -1494,11 +2039,13 @@ async function scanProject(root, mode = "fast") {
1494
2039
  if (!safeFiles.some((f) => f.endsWith(".env.example"))) securityRisks.push("Missing env template");
1495
2040
  if (safeFiles.some((f) => f.includes("wp-content/uploads")))
1496
2041
  securityRisks.push("Uploads directory present");
2042
+ const unsupportedSignals = collectUnsupportedSignals(safeFiles);
2043
+ const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
1497
2044
  const context = {
1498
2045
  mode,
1499
2046
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1500
2047
  root,
1501
- repoName: String(pkg?.name ?? path15.basename(root)),
2048
+ repoName: String(pkg?.name ?? path18.basename(root)),
1502
2049
  packageManager,
1503
2050
  repoRoles: roles,
1504
2051
  confidence: computeConfidence(roles, stacks),
@@ -1506,7 +2053,9 @@ async function scanProject(root, mode = "fast") {
1506
2053
  dependencies: deps,
1507
2054
  securityRisks,
1508
2055
  crossRepoHints,
1509
- warnings
2056
+ warnings,
2057
+ detectionStatus,
2058
+ unsupportedSignals
1510
2059
  };
1511
2060
  const dependencyMap = {
1512
2061
  node: deps.filter((d) => !d.includes("/")),
@@ -1515,7 +2064,7 @@ async function scanProject(root, mode = "fast") {
1515
2064
  const scanHashes = Object.fromEntries(
1516
2065
  await Promise.all(
1517
2066
  safeFiles.map(
1518
- async (f) => [f, hashText(await readFile(path15.join(root, f), "utf8"))]
2067
+ async (f) => [f, hashText(await readFile2(path18.join(root, f), "utf8"))]
1519
2068
  )
1520
2069
  )
1521
2070
  );
@@ -1526,184 +2075,6 @@ async function scanProject(root, mode = "fast") {
1526
2075
  await writeText(hausPath(root, "repo-summary.md"), repoSummary);
1527
2076
  return { ...context, dependencyMap, scanHashes, repoSummary };
1528
2077
  }
1529
- function dependencySet(pkg, composer) {
1530
- const out = /* @__PURE__ */ new Set();
1531
- const pushObj = (obj) => {
1532
- if (!isRecord(obj)) return;
1533
- for (const key of Object.keys(obj)) out.add(key);
1534
- };
1535
- pushObj(pkg?.dependencies);
1536
- pushObj(pkg?.devDependencies);
1537
- pushObj(composer?.require);
1538
- pushObj(composer?.["require-dev"]);
1539
- return [...out].sort();
1540
- }
1541
- function detectRoles(deps, files) {
1542
- const roles = /* @__PURE__ */ new Set();
1543
- if (deps.includes("next") || files.some((f) => f.includes("next.config."))) roles.add("next-app");
1544
- if (deps.includes("react")) roles.add("react-app");
1545
- if (deps.includes("vite") || files.some((f) => f.includes("vite.config."))) roles.add("vite-app");
1546
- if (deps.includes("react-router") && deps.includes("@react-router/node"))
1547
- roles.add("react-router-app");
1548
- if (deps.includes("sanity")) roles.add("sanity-studio");
1549
- if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/")))
1550
- roles.add("strapi-app");
1551
- if (deps.includes("expo")) roles.add("expo-app");
1552
- if (deps.includes("@vendure/core")) roles.add("vendure-app");
1553
- if (deps.some((d) => d.startsWith("@haus/vendure-")) || files.some((f) => f.includes("vendure-config")))
1554
- roles.add("vendure-plugin");
1555
- if (deps.includes("@nestjs/core")) roles.add("nestjs-api");
1556
- if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) roles.add("graphql-api");
1557
- if (files.some((f) => f.endsWith("nx.json"))) roles.add("nx-monorepo");
1558
- if (files.some((f) => f.endsWith("turbo.json"))) roles.add("turbo-monorepo");
1559
- if (files.some((f) => f.endsWith("artisan")) || deps.includes("laravel/framework"))
1560
- roles.add("laravel-app");
1561
- if (deps.includes("laravel/nova")) roles.add("laravel-nova-app");
1562
- const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
1563
- const hasBedrockLayout = files.some((f) => f.includes("web/app")) || deps.includes("roots/wordpress");
1564
- if (hasWpConfig && hasBedrockLayout) {
1565
- roles.add("wordpress-bedrock-site");
1566
- roles.add("wordpress-site");
1567
- } else if (hasWpConfig) {
1568
- roles.add("wordpress-vanilla-site");
1569
- roles.add("wordpress-site");
1570
- } else if (deps.includes("roots/wordpress")) {
1571
- roles.add("wordpress-bedrock-site");
1572
- roles.add("wordpress-site");
1573
- }
1574
- if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) roles.add("dotnet-service");
1575
- if (deps.includes("express")) roles.add("express-service");
1576
- return [...roles].sort();
1577
- }
1578
- async function detectStacks(root, deps, files, packageManager) {
1579
- const out = {
1580
- backend: [],
1581
- frontend: [],
1582
- databases: [],
1583
- testing: [],
1584
- auth: [],
1585
- tooling: [],
1586
- packageManagers: []
1587
- };
1588
- const add = (k, v) => {
1589
- out[k] ??= [];
1590
- if (!out[k].includes(v)) out[k].push(v);
1591
- };
1592
- if (deps.includes("next")) add("frontend", "nextjs");
1593
- if (deps.includes("react")) add("frontend", "react19");
1594
- if (deps.includes("vue")) add("frontend", "vue");
1595
- if (deps.includes("vite")) add("frontend", "vite8");
1596
- if (deps.includes("react-router") && deps.includes("@react-router/node"))
1597
- add("frontend", "react-router-v7");
1598
- if (deps.includes("tailwindcss") || files.some((f) => f.includes("tailwind.config."))) {
1599
- add("frontend", "tailwindcss");
1600
- }
1601
- if (files.some((f) => f.endsWith("components.json")) && deps.includes("class-variance-authority")) {
1602
- add("frontend", "shadcn");
1603
- }
1604
- if (deps.includes("typescript")) add("tooling", "typescript5");
1605
- if (deps.includes("sanity") || deps.includes("next-sanity") || deps.includes("@sanity/client")) {
1606
- add("backend", "sanity");
1607
- }
1608
- if (deps.includes("@strapi/strapi") || deps.some((d) => d.startsWith("@strapi/"))) {
1609
- add("backend", "strapi");
1610
- }
1611
- if (deps.includes("prisma") || deps.includes("@prisma/client")) add("backend", "prisma");
1612
- if (deps.includes("expo")) add("frontend", "expo");
1613
- if (deps.includes("react-native")) add("frontend", "react-native");
1614
- if (deps.includes("i18next") || deps.includes("react-i18next")) add("tooling", "i18next");
1615
- if (deps.includes("bullmq")) add("tooling", "bullmq");
1616
- if (files.some((f) => f === "Dockerfile" || f.startsWith("docker-compose")))
1617
- add("tooling", "docker");
1618
- if (deps.includes("pm2") || files.some((f) => f.includes("ecosystem.config")))
1619
- add("tooling", "pm2");
1620
- if (deps.some((d) => d.startsWith("@sentry/"))) add("tooling", "sentry");
1621
- if (deps.includes("deployer/deployer")) add("tooling", "deployer-php");
1622
- if (!deps.includes("prettier")) add("tooling", "missing-prettier");
1623
- if (!deps.includes("eslint")) add("tooling", "missing-eslint");
1624
- if (deps.includes("@stripe/stripe-js") || deps.includes("@stripe/react-stripe-js")) {
1625
- add("tooling", "stripe");
1626
- }
1627
- if (deps.includes("@haus-tech/qliro-plugin")) add("tooling", "qliro");
1628
- if (deps.includes("@supabase/supabase-js") || deps.some((d) => d.startsWith("@supabase/"))) {
1629
- add("databases", "supabase");
1630
- }
1631
- if (deps.includes("@vendure/core")) add("backend", "vendure3");
1632
- if (deps.includes("@nestjs/core")) add("backend", "nestjs");
1633
- if (await hasNeedle(root, files, "NestFactory")) add("backend", "nestjs");
1634
- if (await hasNeedle(root, files, "@VendurePlugin")) add("backend", "vendure3");
1635
- if (deps.includes("graphql") || deps.includes("@nestjs/graphql")) add("backend", "graphql");
1636
- if (files.some((f) => f.endsWith(".graphql") || f.endsWith("schema.graphql")))
1637
- add("backend", "graphql");
1638
- if (deps.includes("laravel/framework")) add("backend", "laravel");
1639
- if (files.some((f) => f.includes("app/Providers/") || f.includes("routes/")))
1640
- add("backend", "laravel");
1641
- if (files.some((f) => f.endsWith("wp-config.php")) || deps.includes("roots/wordpress"))
1642
- add("backend", "wordpress");
1643
- if (deps.includes("wpackagist-plugin/elementor") || deps.includes("wearehaus/elementor-pro") || deps.includes("wpackagist-theme/hello-elementor")) {
1644
- add("backend", "elementor");
1645
- }
1646
- if (deps.includes("wearehaus/advanced-custom-fields-pro") || deps.includes("wpackagist-plugin/advanced-custom-fields")) {
1647
- add("backend", "acf-pro");
1648
- }
1649
- if (deps.includes("wearehaus/jet-engine")) add("backend", "jetengine");
1650
- if (deps.includes("wearehaus/jet-smart-filters")) add("backend", "jetsmartfilters");
1651
- if (deps.includes("wearehaus/gravityforms")) add("backend", "gravityforms");
1652
- if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) add("backend", "dotnet");
1653
- if (deps.includes("@playwright/test")) add("testing", "playwright");
1654
- if (files.some((f) => f.includes(".storybook"))) add("testing", "storybook");
1655
- if (deps.some((d) => d.startsWith("@testing-library/"))) add("testing", "testing-library");
1656
- if (files.some((f) => f.endsWith("phpunit.xml"))) add("testing", "phpunit");
1657
- if (deps.some((d) => d.startsWith("@storybook/"))) add("testing", "storybook");
1658
- if (deps.includes("vitest")) add("testing", "vitest");
1659
- if (deps.includes("jest") || deps.includes("jest-environment-jsdom")) add("testing", "jest");
1660
- if (deps.includes("pg")) add("databases", "postgresql");
1661
- if (deps.includes("mariadb") || deps.includes("mysql2")) add("databases", "mariadb");
1662
- if (deps.includes("mysql") || deps.includes("mysql2")) add("databases", "mysql");
1663
- if (deps.includes("mssql")) add("databases", "mssql");
1664
- if (deps.includes("@elastic/elasticsearch")) add("databases", "elasticsearch");
1665
- if (deps.includes("predis/predis") || deps.includes("ioredis") || deps.includes("redis")) {
1666
- add("databases", "redis");
1667
- }
1668
- if (await hasNeedle(root, files, "openid")) add("auth", "oidc");
1669
- if (await hasNeedle(root, files, "AZURE_AD")) add("auth", "azure-ad");
1670
- if (await hasNeedle(root, files, "BANKID")) add("auth", "bankid");
1671
- if (deps.includes("24slides/laravel-saml2") || deps.includes("aacotroneo/laravel-saml2")) {
1672
- add("auth", "saml2");
1673
- }
1674
- if (deps.includes("next-auth") || deps.includes("@auth/core")) add("auth", "next-auth");
1675
- if (packageManager === "yarn") add("packageManagers", "yarn4");
1676
- if (packageManager === "pnpm") add("packageManagers", "pnpm89");
1677
- return out;
1678
- }
1679
- async function hasNeedle(root, files, needle) {
1680
- const candidates = files.filter(
1681
- (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
1682
- );
1683
- for (const rel of candidates.slice(0, 300)) {
1684
- try {
1685
- const content = await readFile(path15.join(root, rel), "utf8");
1686
- if (content.includes(needle)) return true;
1687
- } catch {
1688
- continue;
1689
- }
1690
- }
1691
- return false;
1692
- }
1693
- function computeConfidence(roles, stacks) {
1694
- const stackCount = Object.values(stacks).reduce((sum, arr) => sum + arr.length, 0);
1695
- if (roles.length === 0) return 0.15;
1696
- return Math.min(0.99, Number((0.4 + roles.length * 0.08 + stackCount * 0.02).toFixed(2)));
1697
- }
1698
- function renderSummary(context) {
1699
- return `# Repo summary
1700
-
1701
- - Repo: ${context.repoName}
1702
- - Package manager: ${context.packageManager}
1703
- - Roles: ${context.repoRoles.join(", ") || "unknown"}
1704
- - Generated: ${context.generatedAt}
1705
- `;
1706
- }
1707
2078
 
1708
2079
  // src/scanner/read-context.ts
1709
2080
  async function readContextOrScan(root) {
@@ -1727,7 +2098,9 @@ async function runContext(options) {
1727
2098
  (recommendationRaw?.recommended ?? []).map((item) => [item.id, item.scoreBreakdown])
1728
2099
  );
1729
2100
  const taskIntents = options.task ? classifyTaskIntents(options.task) : /* @__PURE__ */ new Set();
1730
- const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents);
2101
+ const selected = pickTaskRelevantRules(recommendation, options.task, taskIntents, {
2102
+ tokenBudget: DEFAULT_CONTEXT_TOKEN_BUDGET
2103
+ });
1731
2104
  const payload = {
1732
2105
  task: options.task ?? "not provided",
1733
2106
  taskIntents: [...taskIntents].sort(),
@@ -1775,8 +2148,8 @@ async function runContext(options) {
1775
2148
  }
1776
2149
 
1777
2150
  // src/commands/doctor.ts
1778
- import path16 from "path";
1779
- import fs11 from "fs-extra";
2151
+ import path19 from "path";
2152
+ import fs12 from "fs-extra";
1780
2153
 
1781
2154
  // src/update/npm-version.ts
1782
2155
  var NPM_PACKAGE_NAME = "@haus-tech/haus-workflow";
@@ -1823,114 +2196,200 @@ async function runDoctor(options) {
1823
2196
  const recommendation = await readJson(
1824
2197
  hausPath(root, "recommendation.json")
1825
2198
  );
1826
- log("Haus Doctor");
1827
- log(`Repo: ${context.repoName}`);
1828
- log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
1829
- log(`Package manager: ${context.packageManager}`);
1830
- log(`Recommended items: ${recommendation?.recommended?.length ?? 0}`);
2199
+ const detail = [];
2200
+ const attention = [];
2201
+ const ok = (text) => detail.push({ stream: "log", text });
2202
+ const flag = (text, sentence, fix) => {
2203
+ detail.push({ stream: "warn", text });
2204
+ attention.push({ sentence, fix });
2205
+ };
2206
+ ok(`Repo: ${context.repoName}`);
2207
+ ok(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
2208
+ ok(`Package manager: ${context.packageManager}`);
2209
+ ok(`Recommended items: ${recommendation?.recommended?.length ?? 0}`);
1831
2210
  const warningLines = [.../* @__PURE__ */ new Set([...context.warnings, ...recommendation?.warnings ?? []])];
1832
2211
  for (const warning of warningLines) {
1833
- log(`- WARN: ${warning}`);
2212
+ ok(`- WARN: ${warning}`);
1834
2213
  }
1835
2214
  const hooks = await verifyProjectSettingsHooksContract(root);
1836
2215
  if (hooks.skipped) {
1837
- log(`- HOOKS: (skipped) ${hooks.message}`);
2216
+ ok(`- HOOKS: (skipped) ${hooks.message}`);
1838
2217
  } else if (!hooks.ok) {
1839
- log(`- HOOKS FAIL: ${hooks.message}`);
2218
+ flag(
2219
+ `- HOOKS FAIL: ${hooks.message}`,
2220
+ "The Claude Code hooks don't match what haus expects",
2221
+ "haus apply --write"
2222
+ );
1840
2223
  process.exitCode = 1;
1841
2224
  } else {
1842
- log(`- HOOKS OK: ${hooks.message}`);
2225
+ ok(`- HOOKS OK: ${hooks.message}`);
1843
2226
  }
1844
- const gatedHooks = ["context", "memoryInject"];
2227
+ const gatedHooks = ["context"];
1845
2228
  for (const key of gatedHooks) {
1846
2229
  const enabled = await isHookEnabled(root, key);
1847
- log(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
2230
+ ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
1848
2231
  }
1849
- const rootClaudeMdPath = path16.join(root, "CLAUDE.md");
2232
+ const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
1850
2233
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
1851
2234
  if (!rootClaudeMdContent) {
1852
- warn("- CLAUDE.md: missing (run `haus apply --write` to create)");
2235
+ flag(
2236
+ "- CLAUDE.md: missing (run `haus apply --write` to create)",
2237
+ "Your project's CLAUDE.md is missing, so haus guidance never loads",
2238
+ "haus apply --write"
2239
+ );
1853
2240
  } else if (!rootClaudeMdContent.includes(BLOCK_BEGIN)) {
1854
- warn("- CLAUDE.md: haus import block missing (run `haus apply --write` to add)");
2241
+ flag(
2242
+ "- CLAUDE.md: haus import block missing (run `haus apply --write` to add)",
2243
+ "The haus import block is missing from CLAUDE.md, so its guidance never loads",
2244
+ "haus apply --write"
2245
+ );
1855
2246
  } else {
1856
- log("- CLAUDE.md: import block present");
2247
+ const beginIdx = rootClaudeMdContent.indexOf(BLOCK_BEGIN);
2248
+ const endIdx = rootClaudeMdContent.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
2249
+ if (endIdx < 0) {
2250
+ flag(
2251
+ "- CLAUDE.md: haus import block is not closed (run `haus apply --write` to repair)",
2252
+ "The haus import block in CLAUDE.md is broken, so its guidance may not load",
2253
+ "haus apply --write"
2254
+ );
2255
+ } else {
2256
+ ok("- CLAUDE.md: import block present");
2257
+ const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
2258
+ const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
2259
+ for (const target of importTargets) {
2260
+ if (!await fs12.pathExists(hausPath(root, target))) {
2261
+ flag(
2262
+ `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
2263
+ `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
2264
+ "haus apply --write"
2265
+ );
2266
+ }
2267
+ }
2268
+ }
1857
2269
  }
1858
2270
  const workflowPath = hausPath(root, "WORKFLOW.md");
1859
- const workflowExists = await fs11.pathExists(workflowPath);
2271
+ const workflowExists = await fs12.pathExists(workflowPath);
1860
2272
  if (!workflowExists) {
1861
- warn("- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)");
2273
+ flag(
2274
+ "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
2275
+ "The workflow standard file is missing",
2276
+ "haus apply --write"
2277
+ );
1862
2278
  } else {
1863
2279
  const workflowContent = await readText(workflowPath);
1864
2280
  const firstLine = workflowContent?.split("\n")[0] ?? "";
1865
2281
  if (!firstLine.includes("HAUS-MANAGED")) {
1866
- log("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
2282
+ ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
1867
2283
  } else {
1868
2284
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
1869
- const cachePath = path16.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
1870
- const bundledPath = path16.join(
2285
+ const cachePath = path19.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
2286
+ const bundledPath = path19.join(
1871
2287
  packageRoot(),
1872
2288
  "library",
1873
2289
  "global",
1874
2290
  "templates",
1875
2291
  "agentic-workflow-standard.md"
1876
2292
  );
1877
- const templatePath = await fs11.pathExists(cachePath) ? cachePath : bundledPath;
2293
+ const templatePath = await fs12.pathExists(cachePath) ? cachePath : bundledPath;
1878
2294
  const templateContent = await readText(templatePath);
1879
2295
  if (storedHashMatch && templateContent) {
1880
2296
  const currentHash = hashText(normaliseLF(templateContent));
1881
2297
  if (storedHashMatch[1] !== currentHash) {
1882
- warn("- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)");
2298
+ flag(
2299
+ "- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
2300
+ "The workflow standard is out of date",
2301
+ "haus apply --write"
2302
+ );
1883
2303
  } else {
1884
- log("- .haus-workflow/WORKFLOW.md: OK");
2304
+ ok("- .haus-workflow/WORKFLOW.md: OK");
1885
2305
  }
1886
2306
  } else {
1887
- log("- .haus-workflow/WORKFLOW.md: OK");
2307
+ ok("- .haus-workflow/WORKFLOW.md: OK");
1888
2308
  }
1889
2309
  }
1890
2310
  }
1891
2311
  const workflowConfigPath = hausPath(root, "workflow-config.md");
1892
- const workflowConfigExists = await fs11.pathExists(workflowConfigPath);
2312
+ const workflowConfigExists = await fs12.pathExists(workflowConfigPath);
1893
2313
  if (!workflowConfigExists) {
1894
- warn("- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)");
2314
+ flag(
2315
+ "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
2316
+ "The workflow config file is missing",
2317
+ "haus apply --write"
2318
+ );
1895
2319
  } else {
1896
- log("- .haus-workflow/workflow-config.md: OK (project-owned)");
2320
+ const cfg = await fs12.readFile(workflowConfigPath, "utf8");
2321
+ const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
2322
+ if (unfilled > 0) {
2323
+ flag(
2324
+ `- .haus-workflow/workflow-config.md: ${unfilled} field(s) still unfilled (run \`haus apply --refill-config\` to auto-fill detectable ones)`,
2325
+ `${unfilled} workflow-config field(s) are still blank`,
2326
+ "haus apply --refill-config"
2327
+ );
2328
+ } else {
2329
+ ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
2330
+ }
1897
2331
  }
1898
2332
  const projectMdPath = hausPath(root, "project.md");
1899
- const projectMdExists = await fs11.pathExists(projectMdPath);
2333
+ const projectMdExists = await fs12.pathExists(projectMdPath);
1900
2334
  if (!projectMdExists) {
1901
- warn("- .haus-workflow/project.md: missing (run `haus apply --write`)");
2335
+ flag(
2336
+ "- .haus-workflow/project.md: missing (run `haus apply --write`)",
2337
+ "The project facts file is missing",
2338
+ "haus apply --write"
2339
+ );
1902
2340
  } else {
1903
2341
  const projectMdContent = await readText(projectMdPath);
1904
2342
  const hasHeader = projectMdContent?.split("\n")[0]?.includes("HAUS-MANAGED") ?? false;
1905
2343
  if (!hasHeader) {
1906
- warn("- .haus-workflow/project.md: no HAUS-MANAGED header (user-owned)");
2344
+ ok("- .haus-workflow/project.md: no HAUS-MANAGED header (user-owned)");
1907
2345
  } else {
1908
- log("- .haus-workflow/project.md: OK");
2346
+ ok("- .haus-workflow/project.md: OK");
1909
2347
  }
1910
2348
  }
1911
2349
  const cacheAgeMs = await getCacheManifestAge();
1912
2350
  if (cacheAgeMs === null) {
1913
- warn("- CATALOG CACHE: absent (run `haus update` to populate)");
2351
+ flag(
2352
+ "- CATALOG CACHE: absent (run `haus update` to populate)",
2353
+ "The catalog cache hasn't been downloaded yet",
2354
+ "haus update"
2355
+ );
1914
2356
  } else {
1915
2357
  const cacheAgeDays = Math.floor(cacheAgeMs / (1e3 * 60 * 60 * 24));
1916
2358
  if (cacheAgeDays >= 7) {
1917
- warn(`- CATALOG CACHE: stale (${cacheAgeDays}d old \u2014 run \`haus update\`)`);
2359
+ flag(
2360
+ `- CATALOG CACHE: stale (${cacheAgeDays}d old \u2014 run \`haus update\`)`,
2361
+ `The catalog cache is ${cacheAgeDays} days old`,
2362
+ "haus update"
2363
+ );
1918
2364
  } else {
1919
- log(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
2365
+ ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
1920
2366
  }
1921
2367
  }
1922
- const pkgJson = await readJson(path16.join(packageRoot(), "package.json"));
2368
+ const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
1923
2369
  const currentVersion = pkgJson?.version ?? "0.0.0";
1924
2370
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
1925
2371
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
1926
- warn(
1927
- `- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`
2372
+ flag(
2373
+ `- CLI UPDATE: ${currentVersion} \u2192 ${npmStatus.latest} available (run: npm install -g ${NPM_PACKAGE_NAME})`,
2374
+ `A newer haus (${npmStatus.latest}) is available`,
2375
+ `npm install -g ${NPM_PACKAGE_NAME}`
1928
2376
  );
1929
2377
  process.exitCode = 1;
1930
2378
  } else if (npmStatus.latest !== null) {
1931
- log(`- CLI: ${currentVersion} (up to date)`);
2379
+ ok(`- CLI: ${currentVersion} (up to date)`);
1932
2380
  } else {
1933
- log(`- CLI: ${currentVersion} (version check unavailable)`);
2381
+ ok(`- CLI: ${currentVersion} (version check unavailable)`);
2382
+ }
2383
+ if (attention.length === 0) {
2384
+ log("\u2705 Your project is set up and healthy.");
2385
+ } else {
2386
+ log(`\u26A0\uFE0F ${attention.length} thing(s) need attention:`);
2387
+ for (const a of attention) log(` \u2022 ${a.sentence} \u2014 fix: ${a.fix}`);
2388
+ }
2389
+ log("Haus Doctor");
2390
+ for (const line2 of detail) {
2391
+ if (line2.stream === "warn") warn(line2.text);
2392
+ else log(line2.text);
1934
2393
  }
1935
2394
  }
1936
2395
 
@@ -1989,57 +2448,17 @@ async function runExplainRecommendation(options) {
1989
2448
  // src/commands/guard.ts
1990
2449
  import { readFileSync as readFileSync2 } from "fs";
1991
2450
 
1992
- // src/security/dangerous-commands.ts
1993
- var DANGEROUS_COMMANDS = [
1994
- "rm -rf",
1995
- "sudo",
1996
- "chmod -R 777",
1997
- "chown -R",
1998
- "git push --force",
1999
- "git reset --hard",
2000
- "docker system prune",
2001
- "drop database",
2002
- "truncate table",
2003
- "php artisan migrate --force",
2004
- "npm publish",
2005
- "yarn npm publish",
2006
- "pnpm publish"
2007
- ];
2008
-
2009
2451
  // src/security/guard-bash.ts
2010
2452
  function guardBash(command) {
2011
2453
  const matched = DANGEROUS_COMMANDS.find((token) => command.includes(token));
2012
- if (matched) return `Blocked dangerous command: ${command}`;
2454
+ if (matched) return `I didn't run that \u2014 it can permanently change or delete things: ${command}`;
2013
2455
  return void 0;
2014
2456
  }
2015
2457
 
2016
- // src/security/sensitive-paths.ts
2017
- var SENSITIVE_PATHS = [
2018
- ".env",
2019
- ".env.*",
2020
- "*.pem",
2021
- "*.key",
2022
- "*.p12",
2023
- "*.pfx",
2024
- "id_rsa",
2025
- "id_ed25519",
2026
- "*.sql",
2027
- "*.dump",
2028
- "*.backup",
2029
- "*.bak",
2030
- "storage/logs",
2031
- "wp-content/uploads",
2032
- "uploads",
2033
- "customer-data",
2034
- "exports",
2035
- "secrets",
2036
- "certs"
2037
- ];
2038
-
2039
2458
  // src/security/guard-file-access.ts
2040
2459
  function guardFileAccess(candidate) {
2041
2460
  const matched = SENSITIVE_PATHS.find((token) => candidate.includes(token.replace("*", "")));
2042
- if (matched) return `Blocked sensitive path: ${candidate}`;
2461
+ if (matched) return `I didn't open ${candidate} \u2014 it looks like it holds secrets or sensitive data`;
2043
2462
  return void 0;
2044
2463
  }
2045
2464
 
@@ -2071,23 +2490,136 @@ async function runGuard(kind, _options) {
2071
2490
  const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
2072
2491
  if (kind === "file-access") {
2073
2492
  const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
2074
- if (guardFileAccess(candidate)) {
2075
- deny(`Blocked sensitive path: ${candidate}`);
2493
+ const reason2 = guardFileAccess(candidate);
2494
+ if (reason2) {
2495
+ deny(reason2);
2076
2496
  process.exitCode = 1;
2077
2497
  return;
2078
2498
  }
2079
2499
  return;
2080
2500
  }
2081
2501
  const command = String(toolInput.command ?? "");
2082
- if (guardBash(command) || DANGEROUS_COMMANDS.some((token) => command.includes(token))) {
2083
- deny(`Blocked dangerous command: ${command}`);
2502
+ const reason = guardBash(command);
2503
+ if (reason) {
2504
+ deny(reason);
2084
2505
  process.exitCode = 1;
2085
2506
  }
2086
2507
  }
2087
2508
 
2088
2509
  // src/commands/init.ts
2089
- import path17 from "path";
2090
- import fs12 from "fs-extra";
2510
+ import path20 from "path";
2511
+ import fs13 from "fs-extra";
2512
+
2513
+ // src/recommender/ecosystem.ts
2514
+ var ECOSYSTEM_GROUPS = {
2515
+ laravel: ["laravel-app", "laravel-nova-app"],
2516
+ wordpress: ["wordpress-site", "wordpress-bedrock-site", "wordpress-vanilla-site"],
2517
+ vendure: ["vendure-app", "vendure-plugin"],
2518
+ nestjs: ["nestjs-api"],
2519
+ nextjs: ["next-app"],
2520
+ react: ["react-app", "next-app", "design-system"],
2521
+ vue: ["vue-app"],
2522
+ dotnet: ["dotnet-service"],
2523
+ nx: ["nx-monorepo"],
2524
+ turbo: ["turbo-monorepo"]
2525
+ };
2526
+ var ECOSYSTEM_PRIMARY_BACKENDS = /* @__PURE__ */ new Set([
2527
+ "laravel",
2528
+ "wordpress",
2529
+ "vendure",
2530
+ "nestjs",
2531
+ "dotnet"
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);
2544
+ }
2545
+ return [...ecosystems];
2546
+ }
2547
+ function pickDominantBackend(ecosystems) {
2548
+ for (const eco of ecosystems) {
2549
+ if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
2550
+ }
2551
+ return void 0;
2552
+ }
2553
+ function isBackendEcosystem(eco) {
2554
+ return ECOSYSTEM_PRIMARY_BACKENDS.has(eco);
2555
+ }
2556
+
2557
+ // src/recommender/policies.ts
2558
+ var UNSUPPORTED = [
2559
+ "python",
2560
+ "django",
2561
+ "go",
2562
+ "rust",
2563
+ "java",
2564
+ "spring",
2565
+ "kotlin",
2566
+ "swift",
2567
+ "android",
2568
+ "flutter",
2569
+ "dart",
2570
+ "c++",
2571
+ "perl",
2572
+ "defi",
2573
+ "trading"
2574
+ ];
2575
+ function matchRequiresAny(clauses, ctx) {
2576
+ for (const clause of clauses) {
2577
+ if ("stack" in clause) {
2578
+ if (ctx.stackSet.has(clause.stack.toLowerCase())) {
2579
+ return { matched: true, signal: `stack:${clause.stack}` };
2580
+ }
2581
+ } else if ("dependency" in clause) {
2582
+ if (ctx.depSet.has(clause.dependency.toLowerCase())) {
2583
+ return { matched: true, signal: `dependency:${clause.dependency}` };
2584
+ }
2585
+ } else if ("packageNamePattern" in clause) {
2586
+ const pattern = clause.packageNamePattern.toLowerCase();
2587
+ const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
2588
+ for (const dep2 of ctx.depSet) {
2589
+ if (pattern.endsWith("*") ? dep2.startsWith(prefix) : dep2 === pattern) {
2590
+ return {
2591
+ matched: true,
2592
+ signal: `packageNamePattern:${clause.packageNamePattern}`
2593
+ };
2594
+ }
2595
+ }
2596
+ } else if ("role" in clause) {
2597
+ if (ctx.roleSet.has(clause.role.toLowerCase())) {
2598
+ return { matched: true, signal: `role:${clause.role}` };
2599
+ }
2600
+ }
2601
+ }
2602
+ return { matched: false };
2603
+ }
2604
+ function describeRequiresAny(clauses) {
2605
+ return clauses.map((c) => {
2606
+ if ("stack" in c) return `stack=${c.stack}`;
2607
+ if ("dependency" in c) return `dependency=${c.dependency}`;
2608
+ if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
2609
+ if ("role" in c) return `role=${c.role}`;
2610
+ return "unknown";
2611
+ }).join(" | ");
2612
+ }
2613
+ function mergeRecommendationWarnings(context) {
2614
+ const markers = context.unsupportedSignals?.join(", ");
2615
+ const statusLines = context.detectionStatus === "unknown" ? [
2616
+ markers ? `Stack not recognised \u2014 detected ${markers}, which haus does not support. Only stack-agnostic workflow and security guidance is applied.` : "Stack not recognised \u2014 no supported framework detected. Only stack-agnostic workflow and security guidance is applied."
2617
+ ] : context.detectionStatus === "partial" && markers ? [
2618
+ `Partially supported \u2014 found unsupported ${markers} alongside recognised stacks; guidance covers the supported parts only.`
2619
+ ] : [];
2620
+ const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
2621
+ return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
2622
+ }
2091
2623
 
2092
2624
  // src/utils/exec.ts
2093
2625
  import { execa } from "execa";
@@ -2110,49 +2642,43 @@ async function runCommand(command, args = [], options = {}) {
2110
2642
  throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
2111
2643
  }
2112
2644
  }
2113
- async function runGit(args, options = {}) {
2114
- return runCommand("git", args, options);
2115
- }
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
+ }
2116
2680
 
2117
2681
  // src/recommender/recommend.ts
2118
- var UNSUPPORTED = [
2119
- "python",
2120
- "django",
2121
- "go",
2122
- "rust",
2123
- "java",
2124
- "spring",
2125
- "kotlin",
2126
- "swift",
2127
- "android",
2128
- "flutter",
2129
- "dart",
2130
- "c++",
2131
- "perl",
2132
- "defi",
2133
- "trading"
2134
- ];
2135
- var SENSITIVE2 = [".env", "secrets", "certs", "customer-data", "exports", ".pem", ".key"];
2136
- var ECOSYSTEM_GROUPS = {
2137
- laravel: ["laravel-app", "laravel-nova-app"],
2138
- wordpress: ["wordpress-site", "wordpress-bedrock-site", "wordpress-vanilla-site"],
2139
- vendure: ["vendure-app", "vendure-plugin"],
2140
- nestjs: ["nestjs-api"],
2141
- nextjs: ["next-app"],
2142
- react: ["react-app", "next-app", "design-system"],
2143
- vue: ["vue-app"],
2144
- dotnet: ["dotnet-service"],
2145
- nx: ["nx-monorepo"],
2146
- turbo: ["turbo-monorepo"]
2147
- };
2148
- var ECOSYSTEM_PRIMARY_BACKENDS = /* @__PURE__ */ new Set(["laravel", "wordpress", "vendure", "nestjs", "dotnet"]);
2149
- var ECOSYSTEM_COMPATIBLE_BACKENDS = {
2150
- vendure: /* @__PURE__ */ new Set(["vendure", "nestjs"]),
2151
- nestjs: /* @__PURE__ */ new Set(["nestjs"]),
2152
- laravel: /* @__PURE__ */ new Set(["laravel"]),
2153
- wordpress: /* @__PURE__ */ new Set(["wordpress"]),
2154
- dotnet: /* @__PURE__ */ new Set(["dotnet"])
2155
- };
2156
2682
  async function recommend(root, context) {
2157
2683
  const items = await loadCatalog(root);
2158
2684
  const setupAnswers = await readJson(hausPath(root, "setup-answers.json")) ?? {};
@@ -2171,8 +2697,8 @@ async function recommend(root, context) {
2171
2697
  const changedFiles = await readChangedFiles(root);
2172
2698
  const securityRiskCount = context.securityRisks?.length ?? 0;
2173
2699
  for (const item of items) {
2174
- const blob = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
2175
- if (UNSUPPORTED.some((x) => blob.includes(x))) {
2700
+ const itemSearchText = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
2701
+ if (UNSUPPORTED.some((x) => itemSearchText.includes(x))) {
2176
2702
  skipped.push({
2177
2703
  id: item.id,
2178
2704
  reason: "Unsupported stack policy",
@@ -2334,7 +2860,7 @@ async function recommend(root, context) {
2334
2860
  );
2335
2861
  }
2336
2862
  }
2337
- if (SENSITIVE2.some((x) => blob.includes(x))) {
2863
+ if (SENSITIVE_ITEM_KEYWORDS.some((x) => itemSearchText.includes(x))) {
2338
2864
  pushSkipReason("sensitive-policy", "Sensitive content policy block", 100);
2339
2865
  }
2340
2866
  const trust = sourceTrust.get(item.source);
@@ -2389,7 +2915,8 @@ async function recommend(root, context) {
2389
2915
  finalScore: score
2390
2916
  },
2391
2917
  tags: item.tags,
2392
- ecosystem: item.ecosystem
2918
+ ecosystem: item.ecosystem,
2919
+ tokenEstimate: item.tokenEstimate
2393
2920
  });
2394
2921
  } else {
2395
2922
  if (skipReasons.length === 0) {
@@ -2430,94 +2957,6 @@ function buildStackSet(context) {
2430
2957
  )
2431
2958
  );
2432
2959
  }
2433
- function inferRepoEcosystems(roles) {
2434
- const ecosystems = /* @__PURE__ */ new Set();
2435
- for (const [eco, roleList] of Object.entries(ECOSYSTEM_GROUPS)) {
2436
- if (roleList.some((r) => roles.includes(r))) ecosystems.add(eco);
2437
- }
2438
- return [...ecosystems];
2439
- }
2440
- function pickDominantBackend(ecosystems) {
2441
- for (const eco of ecosystems) {
2442
- if (ECOSYSTEM_PRIMARY_BACKENDS.has(eco)) return eco;
2443
- }
2444
- return void 0;
2445
- }
2446
- function isBackendEcosystem(eco) {
2447
- return ECOSYSTEM_PRIMARY_BACKENDS.has(eco);
2448
- }
2449
- function matchRequiresAny(clauses, ctx) {
2450
- for (const clause of clauses) {
2451
- if ("stack" in clause) {
2452
- if (ctx.stackSet.has(clause.stack.toLowerCase())) {
2453
- return { matched: true, signal: `stack:${clause.stack}` };
2454
- }
2455
- } else if ("dependency" in clause) {
2456
- if (ctx.depSet.has(clause.dependency.toLowerCase())) {
2457
- return { matched: true, signal: `dependency:${clause.dependency}` };
2458
- }
2459
- } else if ("packageNamePattern" in clause) {
2460
- const pattern = clause.packageNamePattern.toLowerCase();
2461
- const prefix = pattern.endsWith("*") ? pattern.slice(0, -1) : pattern;
2462
- for (const dep of ctx.depSet) {
2463
- if (pattern.endsWith("*") ? dep.startsWith(prefix) : dep === pattern) {
2464
- return {
2465
- matched: true,
2466
- signal: `packageNamePattern:${clause.packageNamePattern}`
2467
- };
2468
- }
2469
- }
2470
- } else if ("role" in clause) {
2471
- if (ctx.roleSet.has(clause.role.toLowerCase())) {
2472
- return { matched: true, signal: `role:${clause.role}` };
2473
- }
2474
- }
2475
- }
2476
- return { matched: false };
2477
- }
2478
- function describeRequiresAny(clauses) {
2479
- return clauses.map((c) => {
2480
- if ("stack" in c) return `stack=${c.stack}`;
2481
- if ("dependency" in c) return `dependency=${c.dependency}`;
2482
- if ("packageNamePattern" in c) return `packageNamePattern=${c.packageNamePattern}`;
2483
- if ("role" in c) return `role=${c.role}`;
2484
- return "unknown";
2485
- }).join(" | ");
2486
- }
2487
- function computeConfidenceLevel(args) {
2488
- const { isDefaultBaseline, reasons, hasEcosystemConflict, score } = args;
2489
- const positiveCodes = new Set(reasons.map((r) => r.code));
2490
- positiveCodes.delete("default-baseline");
2491
- const distinctSignals = positiveCodes.size;
2492
- const strongCount = (positiveCodes.has("repo-role-match") ? 1 : 0) + (positiveCodes.has("stack-match") ? 1 : 0) + (positiveCodes.has("requires-any-match") ? 1 : 0);
2493
- if (hasEcosystemConflict) return "low";
2494
- if (isDefaultBaseline && distinctSignals === 0) return "medium";
2495
- if (strongCount >= 2 && score >= 70) return "high";
2496
- if (strongCount >= 1 && distinctSignals >= 2 && score >= 50) return "medium";
2497
- if (distinctSignals === 1) return "low";
2498
- return distinctSignals >= 2 ? "medium" : "low";
2499
- }
2500
- function confidenceLevelToNumber(level, score) {
2501
- const base = level === "high" ? 0.85 : level === "medium" ? 0.6 : 0.3;
2502
- const bonus = Math.min(0.1, Math.max(0, score - 40) / 1e3);
2503
- return Number(Math.min(0.99, base + bonus).toFixed(2));
2504
- }
2505
- function mergeRecommendationWarnings(context) {
2506
- const riskLines = (context.securityRisks?.length ?? 0) > 0 ? [`Scan reported security signals: ${context.securityRisks.join("; ")}`] : [];
2507
- return [.../* @__PURE__ */ new Set([...context.warnings, ...riskLines])];
2508
- }
2509
- async function readChangedFiles(root) {
2510
- if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
2511
- try {
2512
- const result = await runGit(["diff", "--name-only"], { cwd: root });
2513
- if (result.exitCode !== 0) {
2514
- return [];
2515
- }
2516
- return result.stdout.split("\n").map((x) => x.trim()).filter(Boolean).sort();
2517
- } catch {
2518
- return [];
2519
- }
2520
- }
2521
2960
 
2522
2961
  // src/utils/prompts.ts
2523
2962
  import { stdin as input, stdout as output } from "process";
@@ -2565,8 +3004,13 @@ async function runSetupProject(options) {
2565
3004
  merged[question] = existing[question] ?? "pending-user-answer";
2566
3005
  continue;
2567
3006
  }
3007
+ const prefilled = existing[question];
3008
+ if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
3009
+ merged[question] = prefilled;
3010
+ continue;
3011
+ }
2568
3012
  const answer = await ask(question);
2569
- merged[question] = answer || existing[question] || "no-answer";
3013
+ merged[question] = answer || prefilled || "no-answer";
2570
3014
  }
2571
3015
  await writeJson(hausPath(root, "setup-answers.json"), merged);
2572
3016
  }
@@ -2630,8 +3074,8 @@ async function runSetupProject(options) {
2630
3074
  // src/commands/init.ts
2631
3075
  async function runInit(options) {
2632
3076
  const root = process.cwd();
2633
- const hausDir = path17.join(root, ".haus-workflow");
2634
- const alreadyInit = await fs12.pathExists(hausDir);
3077
+ const hausDir = path20.join(root, ".haus-workflow");
3078
+ const alreadyInit = await fs13.pathExists(hausDir);
2635
3079
  if (alreadyInit) {
2636
3080
  log("Haus AI already initialized in this project.");
2637
3081
  log("Run `haus setup-project` to reconfigure.");
@@ -2643,8 +3087,21 @@ async function runInit(options) {
2643
3087
 
2644
3088
  // src/install/apply.ts
2645
3089
  import crypto2 from "crypto";
2646
- import path20 from "path";
2647
- import fs14 from "fs-extra";
3090
+ import path23 from "path";
3091
+ import fs15 from "fs-extra";
3092
+
3093
+ // src/install/allow-rules.ts
3094
+ var ALLOWED_SUBCOMMANDS = [
3095
+ "setup-project",
3096
+ "apply",
3097
+ "doctor",
3098
+ "scan",
3099
+ "context",
3100
+ "recommend"
3101
+ ];
3102
+ function buildAllowRules() {
3103
+ return [...new Set(ALLOWED_SUBCOMMANDS.map((sub) => `Bash(haus ${sub}:*)`))];
3104
+ }
2648
3105
 
2649
3106
  // src/install/header.ts
2650
3107
  var MD_PREFIX = "<!-- HAUS-MANAGED";
@@ -2656,35 +3113,35 @@ function parseAttrs(raw) {
2656
3113
  if (!idMatch || !vMatch || !srcMatch) return void 0;
2657
3114
  return { stableId: idMatch[1], schemaVersion: vMatch[1], source: srcMatch[1] };
2658
3115
  }
2659
- function parseMarkdownHeader(content) {
2660
- const firstLine = content.split("\n")[0] ?? "";
3116
+ function parseMarkdownHeader(content2) {
3117
+ const firstLine = content2.split("\n")[0] ?? "";
2661
3118
  if (!firstLine.startsWith(MD_PREFIX)) return void 0;
2662
3119
  return parseAttrs(firstLine);
2663
3120
  }
2664
3121
  function buildMarkdownHeader(h) {
2665
3122
  return `${MD_PREFIX} id=${h.stableId} v=${h.schemaVersion} source=${h.source}${MD_SUFFIX}`;
2666
3123
  }
2667
- function stampMarkdown(content, h) {
3124
+ function stampMarkdown(content2, h) {
2668
3125
  const header = buildMarkdownHeader(h);
2669
- const existing = parseMarkdownHeader(content);
3126
+ const existing = parseMarkdownHeader(content2);
2670
3127
  if (existing) {
2671
- const rest = content.slice(content.indexOf("\n") + 1);
3128
+ const rest = content2.slice(content2.indexOf("\n") + 1);
2672
3129
  return `${header}
2673
3130
  ${rest}`;
2674
3131
  }
2675
3132
  return `${header}
2676
- ${content}`;
3133
+ ${content2}`;
2677
3134
  }
2678
3135
 
2679
3136
  // src/install/manifest.ts
2680
3137
  import os4 from "os";
2681
- import path18 from "path";
3138
+ import path21 from "path";
2682
3139
  var MANIFEST_SCHEMA = "haus-install-manifest/1";
2683
3140
  function globalClaudeDir() {
2684
- return path18.join(os4.homedir(), ".claude");
3141
+ return path21.join(os4.homedir(), ".claude");
2685
3142
  }
2686
3143
  function hausManifestPath() {
2687
- return path18.join(globalClaudeDir(), "haus", "install-manifest.json");
3144
+ return path21.join(globalClaudeDir(), "haus", "install-manifest.json");
2688
3145
  }
2689
3146
  async function readManifest() {
2690
3147
  return readJson(hausManifestPath());
@@ -2703,10 +3160,10 @@ function buildManifest(source, files, hooks) {
2703
3160
  }
2704
3161
 
2705
3162
  // src/install/settings-merge.ts
2706
- import path19 from "path";
2707
- import fs13 from "fs-extra";
3163
+ import path22 from "path";
3164
+ import fs14 from "fs-extra";
2708
3165
  function settingsJsonPath() {
2709
- return path19.join(globalClaudeDir(), "settings.json");
3166
+ return path22.join(globalClaudeDir(), "settings.json");
2710
3167
  }
2711
3168
  async function readSettings() {
2712
3169
  const parsed = await readJson(settingsJsonPath());
@@ -2738,10 +3195,95 @@ function mergeHooks(settings, fragments) {
2738
3195
  }
2739
3196
  updated._haus = {
2740
3197
  hooks: [...existing, ...addedIds],
2741
- hookCommands: [...existingCommands, ...addedCommands]
3198
+ hookCommands: [...existingCommands, ...addedCommands],
3199
+ // Preserve deny/allow tracking so hook, deny, and allow merges are order-independent.
3200
+ ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
3201
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
2742
3202
  };
2743
3203
  return { settings: updated, addedIds };
2744
3204
  }
3205
+ function mergeDenyRules(settings, rules) {
3206
+ const existingDeny = settings.permissions?.deny ?? [];
3207
+ const seen = new Set(existingDeny);
3208
+ const trackedDeny = settings._haus?.denyRules ?? [];
3209
+ const addedRules = [];
3210
+ for (const rule of rules) {
3211
+ if (seen.has(rule)) continue;
3212
+ seen.add(rule);
3213
+ addedRules.push(rule);
3214
+ }
3215
+ const updated = { ...settings };
3216
+ updated.permissions = {
3217
+ ...settings.permissions ?? {},
3218
+ deny: [...existingDeny, ...addedRules]
3219
+ };
3220
+ updated._haus = {
3221
+ hooks: settings._haus?.hooks ?? [],
3222
+ ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
3223
+ denyRules: [...trackedDeny, ...addedRules],
3224
+ ...settings._haus?.allowRules ? { allowRules: settings._haus.allowRules } : {}
3225
+ };
3226
+ return { settings: updated, addedRules };
3227
+ }
3228
+ function mergeAllowRules(settings, rules) {
3229
+ const existingAllow = settings.permissions?.allow ?? [];
3230
+ const seen = new Set(existingAllow);
3231
+ const trackedAllow = settings._haus?.allowRules ?? [];
3232
+ const addedRules = [];
3233
+ for (const rule of rules) {
3234
+ if (seen.has(rule)) continue;
3235
+ seen.add(rule);
3236
+ addedRules.push(rule);
3237
+ }
3238
+ const updated = { ...settings };
3239
+ updated.permissions = {
3240
+ ...settings.permissions ?? {},
3241
+ allow: [...existingAllow, ...addedRules]
3242
+ };
3243
+ updated._haus = {
3244
+ hooks: settings._haus?.hooks ?? [],
3245
+ ...settings._haus?.hookCommands ? { hookCommands: settings._haus.hookCommands } : {},
3246
+ ...settings._haus?.denyRules ? { denyRules: settings._haus.denyRules } : {},
3247
+ allowRules: [...trackedAllow, ...addedRules]
3248
+ };
3249
+ return { settings: updated, addedRules };
3250
+ }
3251
+ function stripHausAllow(settings) {
3252
+ const prevHaus = settings._haus;
3253
+ if (!prevHaus?.allowRules || prevHaus.allowRules.length === 0) return settings;
3254
+ const ownedSet = new Set(prevHaus.allowRules);
3255
+ const updated = { ...settings };
3256
+ const remainingAllow = (settings.permissions?.allow ?? []).filter((rule) => !ownedSet.has(rule));
3257
+ const permissions = { ...settings.permissions ?? {} };
3258
+ if (remainingAllow.length > 0) permissions.allow = remainingAllow;
3259
+ else delete permissions.allow;
3260
+ if (Object.keys(permissions).length > 0) updated.permissions = permissions;
3261
+ else delete updated.permissions;
3262
+ const haus = { ...prevHaus };
3263
+ delete haus.allowRules;
3264
+ const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.denyRules?.length ?? 0) > 0;
3265
+ if (stillTracking) updated._haus = haus;
3266
+ else delete updated._haus;
3267
+ return updated;
3268
+ }
3269
+ function stripHausDeny(settings) {
3270
+ const prevHaus = settings._haus;
3271
+ if (!prevHaus?.denyRules || prevHaus.denyRules.length === 0) return settings;
3272
+ const ownedSet = new Set(prevHaus.denyRules);
3273
+ const updated = { ...settings };
3274
+ const remainingDeny = (settings.permissions?.deny ?? []).filter((rule) => !ownedSet.has(rule));
3275
+ const permissions = { ...settings.permissions ?? {} };
3276
+ if (remainingDeny.length > 0) permissions.deny = remainingDeny;
3277
+ else delete permissions.deny;
3278
+ if (Object.keys(permissions).length > 0) updated.permissions = permissions;
3279
+ else delete updated.permissions;
3280
+ const haus = { ...prevHaus };
3281
+ delete haus.denyRules;
3282
+ const stillTracking = (haus.hooks?.length ?? 0) > 0 || (haus.hookCommands?.length ?? 0) > 0 || (haus.allowRules?.length ?? 0) > 0;
3283
+ if (stillTracking) updated._haus = haus;
3284
+ else delete updated._haus;
3285
+ return updated;
3286
+ }
2745
3287
  function stripHausHooks(settings) {
2746
3288
  if (!settings._haus) return settings;
2747
3289
  const ownedCommands = new Set(settings._haus.hookCommands ?? []);
@@ -2762,7 +3304,7 @@ function stripHausHooks(settings) {
2762
3304
  async function loadHooksFragment(fragmentPath) {
2763
3305
  let raw;
2764
3306
  try {
2765
- raw = await fs13.readJson(fragmentPath);
3307
+ raw = await fs14.readJson(fragmentPath);
2766
3308
  } catch {
2767
3309
  return [];
2768
3310
  }
@@ -2772,45 +3314,45 @@ async function loadHooksFragment(fragmentPath) {
2772
3314
 
2773
3315
  // src/install/apply.ts
2774
3316
  var SCHEMA_VERSION3 = "1";
2775
- function hashContent(content) {
2776
- return `sha256-${crypto2.createHash("sha256").update(content).digest("hex")}`;
3317
+ function hashContent(content2) {
3318
+ return `sha256-${crypto2.createHash("sha256").update(content2).digest("hex")}`;
2777
3319
  }
2778
3320
  function sourceVersion() {
2779
3321
  try {
2780
- const pkgPath = path20.join(packageRoot(), "package.json");
2781
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
3322
+ const pkgPath = path23.join(packageRoot(), "package.json");
3323
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf8"));
2782
3324
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
2783
3325
  } catch {
2784
3326
  return "haus@0.0.0";
2785
3327
  }
2786
3328
  }
2787
3329
  function globalSrcDir() {
2788
- return path20.join(packageRoot(), "library", "global");
3330
+ return path23.join(packageRoot(), "library", "global");
2789
3331
  }
2790
3332
  function collectSourceFiles(srcDir, claudeDir) {
2791
3333
  const entries = [];
2792
- const skillsDir = path20.join(srcDir, "skills");
2793
- if (fs14.pathExistsSync(skillsDir)) {
2794
- for (const skillName of fs14.readdirSync(skillsDir)) {
2795
- const skillFile = path20.join(skillsDir, skillName, "SKILL.md");
2796
- if (fs14.pathExistsSync(skillFile)) {
3334
+ const skillsDir = path23.join(srcDir, "skills");
3335
+ if (fs15.pathExistsSync(skillsDir)) {
3336
+ for (const skillName of fs15.readdirSync(skillsDir)) {
3337
+ const skillFile = path23.join(skillsDir, skillName, "SKILL.md");
3338
+ if (fs15.pathExistsSync(skillFile)) {
2797
3339
  entries.push({
2798
3340
  stableId: `skill.${skillName}`,
2799
- srcRelPath: path20.join("library", "global", "skills", skillName, "SKILL.md"),
2800
- destPath: path20.join(claudeDir, "skills", skillName, "SKILL.md")
3341
+ srcRelPath: path23.join("library", "global", "skills", skillName, "SKILL.md"),
3342
+ destPath: path23.join(claudeDir, "skills", skillName, "SKILL.md")
2801
3343
  });
2802
3344
  }
2803
3345
  }
2804
3346
  }
2805
- const agentsDir = path20.join(srcDir, "agents");
2806
- if (fs14.pathExistsSync(agentsDir)) {
2807
- for (const agentFile of fs14.readdirSync(agentsDir)) {
2808
- if (!agentFile.endsWith(".md")) continue;
2809
- const agentName = agentFile.replace(/\.md$/, "");
3347
+ const commandsDir = path23.join(srcDir, "commands");
3348
+ if (fs15.pathExistsSync(commandsDir)) {
3349
+ for (const fileName of fs15.readdirSync(commandsDir)) {
3350
+ if (!fileName.endsWith(".md")) continue;
3351
+ const commandName = fileName.slice(0, -".md".length);
2810
3352
  entries.push({
2811
- stableId: `agent.${agentName}`,
2812
- srcRelPath: path20.join("library", "global", "agents", agentFile),
2813
- destPath: path20.join(claudeDir, "agents", agentFile)
3353
+ stableId: `command.${commandName}`,
3354
+ srcRelPath: path23.join("library", "global", "commands", fileName),
3355
+ destPath: path23.join(claudeDir, "commands", fileName)
2814
3356
  });
2815
3357
  }
2816
3358
  }
@@ -2834,7 +3376,7 @@ async function applyInstall(options = {}) {
2834
3376
  };
2835
3377
  const manifestFiles = [];
2836
3378
  for (const entry of sourceFiles) {
2837
- const srcPath = path20.join(packageRoot(), entry.srcRelPath);
3379
+ const srcPath = path23.join(packageRoot(), entry.srcRelPath);
2838
3380
  const rawContent = await readText(srcPath);
2839
3381
  if (rawContent === void 0) {
2840
3382
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -2854,7 +3396,7 @@ async function applyInstall(options = {}) {
2854
3396
  }
2855
3397
  continue;
2856
3398
  }
2857
- const destExists = fs14.pathExistsSync(entry.destPath);
3399
+ const destExists = fs15.pathExistsSync(entry.destPath);
2858
3400
  if (destExists) {
2859
3401
  const currentContent = await readText(entry.destPath);
2860
3402
  if (currentContent !== void 0) {
@@ -2890,22 +3432,24 @@ async function applyInstall(options = {}) {
2890
3432
  schemaVersion: SCHEMA_VERSION3
2891
3433
  });
2892
3434
  }
2893
- const fragmentPath = path20.join(srcDir, "settings-fragments", "hooks.json");
3435
+ const fragmentPath = path23.join(srcDir, "settings-fragments", "hooks.json");
2894
3436
  const fragments = await loadHooksFragment(fragmentPath);
2895
3437
  const settings = await readSettings();
2896
- const { settings: mergedSettings, addedIds } = mergeHooks(settings, fragments);
3438
+ const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
3439
+ const { settings: deniedSettings } = mergeDenyRules(hookSettings, buildDenyRules());
3440
+ const { settings: mergedSettings } = mergeAllowRules(deniedSettings, buildAllowRules());
2897
3441
  result.hookIds = addedIds;
2898
3442
  if (!check && existingManifest) {
2899
3443
  const currentDestPaths = new Set(sourceFiles.map((f) => f.destPath));
2900
3444
  for (const entry of existingManifest.files) {
2901
3445
  if (currentDestPaths.has(entry.destPath)) continue;
2902
- if (!fs14.pathExistsSync(entry.destPath)) continue;
2903
- const content = await readText(entry.destPath);
2904
- if (!content) continue;
2905
- const hasHeader = parseMarkdownHeader(content) !== void 0;
2906
- const currentHash = hashContent(content);
3446
+ if (!fs15.pathExistsSync(entry.destPath)) continue;
3447
+ const content2 = await readText(entry.destPath);
3448
+ if (!content2) continue;
3449
+ const hasHeader = parseMarkdownHeader(content2) !== void 0;
3450
+ const currentHash = hashContent(content2);
2907
3451
  if (hasHeader && currentHash === entry.hash) {
2908
- if (!dryRun) await fs14.remove(entry.destPath);
3452
+ if (!dryRun) await fs15.remove(entry.destPath);
2909
3453
  result.deleted.push(entry.destPath);
2910
3454
  } else {
2911
3455
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -2957,14 +3501,27 @@ async function runInstall(options) {
2957
3501
  force: options.force,
2958
3502
  check: options.check
2959
3503
  });
2960
- printApplyResult(result, options.dryRun ?? false);
3504
+ if (!options.postinstall) printApplyResult(result, options.dryRun ?? false);
2961
3505
  if (options.check && result.drift) {
2962
3506
  process.exitCode = 1;
2963
3507
  } else if (!options.check && !options.dryRun) {
2964
3508
  const total = result.created.length + result.updated.length;
2965
- log(
2966
- `haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
2967
- );
3509
+ if (options.postinstall) {
3510
+ log("haus configured Claude Code for you:");
3511
+ const parts = [];
3512
+ if (result.created.length) parts.push(`${result.created.length} file(s) added`);
3513
+ if (result.updated.length) parts.push(`${result.updated.length} file(s) updated`);
3514
+ log(
3515
+ parts.length ? ` \u2022 ${parts.join(", ")} in ~/.claude (skills, slash commands)` : " \u2022 already up to date \u2014 no files changed"
3516
+ );
3517
+ log(` \u2022 ensured hooks + security rules are present in ~/.claude/settings.json`);
3518
+ log("Undo any time with: haus uninstall");
3519
+ log("Disable this on install: HAUS_NO_POSTINSTALL=1");
3520
+ } else {
3521
+ log(
3522
+ `haus install complete (${total} file(s) written, ${result.hookIds.length} hook(s) added)`
3523
+ );
3524
+ }
2968
3525
  }
2969
3526
  } catch (err) {
2970
3527
  error(`haus install failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -2972,79 +3529,6 @@ async function runInstall(options) {
2972
3529
  }
2973
3530
  }
2974
3531
 
2975
- // src/memory/memory-store.ts
2976
- var FILES = [
2977
- "project-learnings.md",
2978
- "decisions.md",
2979
- "recurring-issues.md",
2980
- "client-context.md"
2981
- ];
2982
- async function ensureMemory(root) {
2983
- await Promise.all(
2984
- FILES.map(async (name) => {
2985
- const file = hausPath(root, "memory", name);
2986
- const current = await readText(file);
2987
- if (!current) await writeText(file, `# ${name}
2988
- `);
2989
- })
2990
- );
2991
- const indexFile = hausPath(root, "memory", "index.json");
2992
- const index = await readJson(indexFile);
2993
- if (!index) await writeJson(indexFile, { files: [...FILES] });
2994
- }
2995
- async function readMemory(root) {
2996
- await ensureMemory(root);
2997
- const blocks = await Promise.all(FILES.map((name) => readText(hausPath(root, "memory", name))));
2998
- return blocks.filter(Boolean).join("\n");
2999
- }
3000
- async function appendLearning(root, line) {
3001
- await ensureMemory(root);
3002
- const file = hausPath(root, "memory", "project-learnings.md");
3003
- const current = await readText(file) ?? "# project-learnings.md\n";
3004
- await writeText(file, `${current}
3005
- - ${line}
3006
- `);
3007
- }
3008
-
3009
- // src/memory/redact-memory.ts
3010
- function redactMemory(text) {
3011
- return text.replace(/(api[_-]?key|token|secret|password)\s*[:=]\s*\S+/gi, "$1=[REDACTED]").replace(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, "[REDACTED-KEY]");
3012
- }
3013
-
3014
- // src/commands/memory.ts
3015
- async function runMemory(subcommand, options) {
3016
- const root = process.cwd();
3017
- if (subcommand === "inject" && options.fromHook && !await isHookEnabled(root, "memoryInject")) {
3018
- return;
3019
- }
3020
- await ensureMemory(root);
3021
- if (subcommand === "status") {
3022
- log("Memory ready at .haus-workflow/memory");
3023
- return;
3024
- }
3025
- if (subcommand === "add") {
3026
- if (!options.text) throw new Error("memory add requires text");
3027
- await appendLearning(root, redactMemory(options.text));
3028
- log("Memory added");
3029
- return;
3030
- }
3031
- if (subcommand === "inject") {
3032
- const text = redactMemory(await readMemory(root));
3033
- if (!text.trim()) {
3034
- log("No relevant Haus memory found.");
3035
- return;
3036
- }
3037
- const compact = `Task: ${options.task ?? "n/a"}
3038
- ${text}`.slice(
3039
- 0,
3040
- options.fromHook ? 1200 : 4e3
3041
- );
3042
- log(compact);
3043
- return;
3044
- }
3045
- log("Promotion proposal: review memory and move stable rules into .claude/rules manually.");
3046
- }
3047
-
3048
3532
  // src/commands/recommend.ts
3049
3533
  async function runRecommend(options) {
3050
3534
  const root = process.cwd();
@@ -3088,20 +3572,20 @@ async function runScan(options) {
3088
3572
  }
3089
3573
 
3090
3574
  // src/commands/undo.ts
3091
- import path21 from "path";
3092
- import fs15 from "fs-extra";
3575
+ import path24 from "path";
3576
+ import fs16 from "fs-extra";
3093
3577
  var CLAUDE_DIR = ".claude";
3094
3578
  async function runUndo(options) {
3095
3579
  const root = process.cwd();
3096
- const targets = [path21.join(root, CLAUDE_DIR), path21.join(root, HAUS_DIR)];
3097
- const existing = targets.filter((p) => fs15.existsSync(p));
3580
+ const targets = [path24.join(root, CLAUDE_DIR), path24.join(root, HAUS_DIR)];
3581
+ const existing = targets.filter((p) => fs16.existsSync(p));
3098
3582
  if (existing.length === 0) {
3099
3583
  log("Nothing to remove: no .claude/ or .haus-workflow/ in this directory.");
3100
3584
  return;
3101
3585
  }
3102
3586
  if (!options.yes) {
3103
3587
  const ok = await confirm(
3104
- `Remove ${existing.map((p) => path21.relative(root, p)).join(" and ")}? This cannot be undone.`
3588
+ `Remove ${existing.map((p) => path24.relative(root, p)).join(" and ")}? This cannot be undone.`
3105
3589
  );
3106
3590
  if (!ok) {
3107
3591
  log("Cancelled.");
@@ -3109,15 +3593,15 @@ async function runUndo(options) {
3109
3593
  }
3110
3594
  }
3111
3595
  for (const p of existing) {
3112
- await fs15.remove(p);
3113
- log(`Removed ${path21.relative(root, p)}`);
3596
+ await fs16.remove(p);
3597
+ log(`Removed ${path24.relative(root, p)}`);
3114
3598
  }
3115
3599
  }
3116
3600
 
3117
3601
  // src/install/uninstall.ts
3118
3602
  import crypto3 from "crypto";
3119
- import path22 from "path";
3120
- import fs16 from "fs-extra";
3603
+ import path25 from "path";
3604
+ import fs17 from "fs-extra";
3121
3605
  async function runUninstall(options = {}) {
3122
3606
  const { force = false } = options;
3123
3607
  const manifest = await readManifest();
@@ -3127,17 +3611,17 @@ async function runUninstall(options = {}) {
3127
3611
  return result;
3128
3612
  }
3129
3613
  for (const entry of manifest.files) {
3130
- const exists = fs16.pathExistsSync(entry.destPath);
3614
+ const exists = fs17.pathExistsSync(entry.destPath);
3131
3615
  if (!exists) continue;
3132
- const content = await readText(entry.destPath);
3133
- if (content === void 0) continue;
3134
- const header = parseMarkdownHeader(content);
3616
+ const content2 = await readText(entry.destPath);
3617
+ if (content2 === void 0) continue;
3618
+ const header = parseMarkdownHeader(content2);
3135
3619
  if (!header) {
3136
3620
  warn(`Skipping user-owned file (no HAUS-MANAGED header): ${entry.destPath}`);
3137
3621
  result.skipped.push(entry.destPath);
3138
3622
  continue;
3139
3623
  }
3140
- const currentHash = `sha256-${crypto3.createHash("sha256").update(content).digest("hex")}`;
3624
+ const currentHash = `sha256-${crypto3.createHash("sha256").update(content2).digest("hex")}`;
3141
3625
  if (currentHash !== entry.hash && !force) {
3142
3626
  warn(
3143
3627
  `Skipping user-edited haus file (hash mismatch): ${entry.destPath} \u2014 use --force to delete`
@@ -3145,22 +3629,22 @@ async function runUninstall(options = {}) {
3145
3629
  result.skipped.push(entry.destPath);
3146
3630
  continue;
3147
3631
  }
3148
- await fs16.remove(entry.destPath);
3149
- await pruneEmptyDir(path22.dirname(entry.destPath));
3632
+ await fs17.remove(entry.destPath);
3633
+ await pruneEmptyDir(path25.dirname(entry.destPath));
3150
3634
  result.deleted.push(entry.destPath);
3151
3635
  }
3152
3636
  const settings = await readSettings();
3153
- const stripped = stripHausHooks(settings);
3637
+ const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
3154
3638
  await writeSettings(stripped);
3155
3639
  result.hooksStripped = true;
3156
- const hausDir = path22.join(globalClaudeDir(), "haus");
3640
+ const hausDir = path25.join(globalClaudeDir(), "haus");
3157
3641
  const manifestPath = hausManifestPath();
3158
- if (fs16.pathExistsSync(manifestPath)) {
3159
- await fs16.remove(manifestPath);
3642
+ if (fs17.pathExistsSync(manifestPath)) {
3643
+ await fs17.remove(manifestPath);
3160
3644
  }
3161
- if (fs16.pathExistsSync(hausDir)) {
3162
- const remaining = await fs16.readdir(hausDir);
3163
- if (remaining.length === 0) await fs16.remove(hausDir);
3645
+ if (fs17.pathExistsSync(hausDir)) {
3646
+ const remaining = await fs17.readdir(hausDir);
3647
+ if (remaining.length === 0) await fs17.remove(hausDir);
3164
3648
  }
3165
3649
  return result;
3166
3650
  }
@@ -3179,8 +3663,8 @@ function printUninstallResult(result) {
3179
3663
  }
3180
3664
  async function pruneEmptyDir(dir) {
3181
3665
  try {
3182
- const entries = await fs16.readdir(dir);
3183
- if (entries.length === 0) await fs16.remove(dir);
3666
+ const entries = await fs17.readdir(dir);
3667
+ if (entries.length === 0) await fs17.remove(dir);
3184
3668
  } catch {
3185
3669
  }
3186
3670
  }
@@ -3198,7 +3682,7 @@ async function runUninstallCommand(options) {
3198
3682
  }
3199
3683
 
3200
3684
  // src/commands/update.ts
3201
- import path24 from "path";
3685
+ import path27 from "path";
3202
3686
 
3203
3687
  // src/update/diff-generated-files.ts
3204
3688
  function diffGeneratedFiles() {
@@ -3224,8 +3708,8 @@ function summarizeLockDiff(before, after) {
3224
3708
  }
3225
3709
 
3226
3710
  // src/update/lockfile.ts
3227
- import { mkdir, readFile as readFile2, copyFile } from "fs/promises";
3228
- import path23 from "path";
3711
+ import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
3712
+ import path26 from "path";
3229
3713
  async function checkLock(root) {
3230
3714
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
3231
3715
  const hasValidVersions = lock.every(
@@ -3238,7 +3722,7 @@ async function applyLock(root) {
3238
3722
  const lockPath = hausPath(root, "haus.lock.json");
3239
3723
  let before = "[]";
3240
3724
  try {
3241
- before = await readFile2(lockPath, "utf8");
3725
+ before = await readFile3(lockPath, "utf8");
3242
3726
  } catch {
3243
3727
  before = "[]";
3244
3728
  }
@@ -3246,7 +3730,7 @@ async function applyLock(root) {
3246
3730
  try {
3247
3731
  const backupDir = hausPath(root, "backups");
3248
3732
  await mkdir(backupDir, { recursive: true });
3249
- await copyFile(lockPath, path23.join(backupDir, `haus.lock.${Date.now()}.json`));
3733
+ await copyFile(lockPath, path26.join(backupDir, `haus.lock.${Date.now()}.json`));
3250
3734
  } catch {
3251
3735
  }
3252
3736
  const enriched = await Promise.all(
@@ -3268,7 +3752,7 @@ function diffLock(before, after) {
3268
3752
  }
3269
3753
  async function hasLocalOverrides(root) {
3270
3754
  try {
3271
- await readFile2(path23.join(root, ".claude", "settings.json"), "utf8");
3755
+ await readFile3(path26.join(root, ".claude", "settings.json"), "utf8");
3272
3756
  return true;
3273
3757
  } catch {
3274
3758
  return false;
@@ -3280,7 +3764,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
3280
3764
  async function runUpdate(options) {
3281
3765
  const root = process.cwd();
3282
3766
  if (options.check) {
3283
- const pkgJson2 = await readJson(path24.join(packageRoot(), "package.json"));
3767
+ const pkgJson2 = await readJson(path27.join(packageRoot(), "package.json"));
3284
3768
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
3285
3769
  const [status, npmVersion, latestCatalogTag] = await Promise.all([
3286
3770
  checkLock(root),
@@ -3307,7 +3791,7 @@ async function runUpdate(options) {
3307
3791
  if (!status.ok) process.exitCode = 1;
3308
3792
  return;
3309
3793
  }
3310
- const pkgJson = await readJson(path24.join(packageRoot(), "package.json"));
3794
+ const pkgJson = await readJson(path27.join(packageRoot(), "package.json"));
3311
3795
  const currentVersion = pkgJson?.version ?? "0.0.0";
3312
3796
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
3313
3797
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -3337,49 +3821,187 @@ async function runUpdate(options) {
3337
3821
  }
3338
3822
 
3339
3823
  // src/commands/validate-catalog.ts
3340
- import fs17 from "fs";
3341
- import path26 from "path";
3824
+ import fs18 from "fs";
3825
+ import path28 from "path";
3342
3826
 
3343
- // src/catalog/allowed-stacks.ts
3344
- import path25 from "path";
3345
- async function readAllowedStacks(root) {
3346
- const data = await readJson(
3347
- path25.join(root, "library", "catalog", "allowed-stacks.json")
3348
- );
3349
- return data?.stacks ?? [];
3350
- }
3827
+ // library/catalog/validation-rules.json
3828
+ var validation_rules_default = {
3829
+ forbiddenTags: [
3830
+ "python",
3831
+ "django",
3832
+ "go",
3833
+ "rust",
3834
+ "java",
3835
+ "spring",
3836
+ "kotlin",
3837
+ "swift",
3838
+ "android",
3839
+ "flutter",
3840
+ "dart",
3841
+ "c++",
3842
+ "perl",
3843
+ "defi",
3844
+ "trading"
3845
+ ],
3846
+ bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
3847
+ requiredSkillSections: ["## Use when", "## Do not use when"],
3848
+ requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
3849
+ riskyInstallPatterns: [
3850
+ { source: "\\bnpx\\s+-y\\b", flags: "i" },
3851
+ { source: "\\bnpx\\s+--yes\\b", flags: "i" },
3852
+ { source: "\\byarn\\s+dlx\\b", flags: "i" },
3853
+ { source: "\\bpnpm\\s+dlx\\b", flags: "i" }
3854
+ ],
3855
+ allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
3856
+ anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
3857
+ httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
3858
+ placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
3859
+ allowedStacks: [
3860
+ "haus",
3861
+ "security",
3862
+ "quality",
3863
+ "frontend",
3864
+ "backend",
3865
+ "testing",
3866
+ "review",
3867
+ "workflow",
3868
+ "reference-pack",
3869
+ "core-skill",
3870
+ "workflow-skill",
3871
+ "stack-skill",
3872
+ "review-skill",
3873
+ "agent",
3874
+ "hook",
3875
+ "rule",
3876
+ "react",
3877
+ "typescript",
3878
+ "php",
3879
+ "csharp",
3880
+ "vendure",
3881
+ "vendure3",
3882
+ "nestjs",
3883
+ "graphql",
3884
+ "nx21",
3885
+ "turbo",
3886
+ "nextjs",
3887
+ "react19",
3888
+ "typescript5",
3889
+ "vite8",
3890
+ "tanstack-query",
3891
+ "tanstack-router",
3892
+ "radix",
3893
+ "radix-ui",
3894
+ "shadcn",
3895
+ "shadcn-ui",
3896
+ "tailwind",
3897
+ "tailwindcss",
3898
+ "scss",
3899
+ "scss-modules",
3900
+ "vue",
3901
+ "expressjs",
3902
+ "soup-base",
3903
+ "laravel",
3904
+ "laravel-nova",
3905
+ "wordpress",
3906
+ "bedrock",
3907
+ "elementor-pro",
3908
+ "acf-pro",
3909
+ "jetengine",
3910
+ "dotnet",
3911
+ "oidc",
3912
+ "azure-ad",
3913
+ "bankid",
3914
+ "myid",
3915
+ "cgi",
3916
+ "crypto",
3917
+ "collection2",
3918
+ "postgresql",
3919
+ "mariadb",
3920
+ "mssql",
3921
+ "elasticsearch",
3922
+ "yarn4",
3923
+ "pnpm89",
3924
+ "playwright",
3925
+ "testing-library",
3926
+ "phpunit",
3927
+ "storybook",
3928
+ "wisest",
3929
+ "vitest",
3930
+ "jest",
3931
+ "redis",
3932
+ "sanity",
3933
+ "strapi",
3934
+ "prisma",
3935
+ "cms",
3936
+ "database",
3937
+ "mysql",
3938
+ "saml2",
3939
+ "next-auth",
3940
+ "auth",
3941
+ "expo",
3942
+ "react-native",
3943
+ "mobile",
3944
+ "i18next",
3945
+ "i18n",
3946
+ "bullmq",
3947
+ "queue",
3948
+ "sentry",
3949
+ "observability",
3950
+ "tooling",
3951
+ "prettier",
3952
+ "eslint",
3953
+ "missing-prettier",
3954
+ "missing-eslint",
3955
+ "docker",
3956
+ "pm2",
3957
+ "deployer-php",
3958
+ "stripe",
3959
+ "qliro",
3960
+ "supabase",
3961
+ "payments"
3962
+ ],
3963
+ alwaysAllowedTags: [
3964
+ "haus",
3965
+ "security",
3966
+ "quality",
3967
+ "review",
3968
+ "workflow",
3969
+ "baseline",
3970
+ "project-instructions"
3971
+ ],
3972
+ patternTagSuffixes: ["-patterns"]
3973
+ };
3351
3974
 
3352
3975
  // src/catalog/validation-rules.ts
3353
- var FORBIDDEN_TAGS = [
3354
- "python",
3355
- "django",
3356
- "go",
3357
- "rust",
3358
- "java",
3359
- "spring",
3360
- "kotlin",
3361
- "swift",
3362
- "android",
3363
- "flutter",
3364
- "dart",
3365
- "c++",
3366
- "perl",
3367
- "defi",
3368
- "trading"
3369
- ];
3370
- var BANNED_AGENT_PHRASES = ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"];
3371
- var REQUIRED_SKILL_SECTIONS = ["## Use when", "## Do not use when"];
3372
- var REQUIRED_AGENT_SECTIONS = ["## Use when", "## Do not use when", "## Verification"];
3373
- var RISKY_INSTALL_PATTERNS = [
3374
- /\bnpx\s+-y\b/i,
3375
- /\bnpx\s+--yes\b/i,
3376
- /\byarn\s+dlx\b/i,
3377
- /\bpnpm\s+dlx\b/i
3378
- ];
3379
- var ALLOWED_NPX_PATTERN = /\bnpx\s+tsx\b/i;
3380
- var ANY_NPX_PATTERN = /\bnpx\s+\S+/i;
3381
- var HTTP_URL_PATTERN = /^http:\/\//i;
3382
- var PLACEHOLDER_PATTERN = /\bTODO\b|\bPLACEHOLDER\b/i;
3976
+ var toRegExp = (r) => new RegExp(r.source, r.flags);
3977
+ var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
3978
+ var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
3979
+ var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
3980
+ var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
3981
+ var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
3982
+ var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
3983
+ var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
3984
+ var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
3985
+ var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
3986
+ var ALLOWED_STACKS = validation_rules_default.allowedStacks;
3987
+ var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
3988
+ var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
3989
+ var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
3990
+ function isTagAllowed(tag) {
3991
+ const lower = tag.toLowerCase();
3992
+ if (ALLOWED_SET.has(lower)) return true;
3993
+ return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
3994
+ }
3995
+ function auditDisallowedTags(items) {
3996
+ const failures = [];
3997
+ for (const item of items) {
3998
+ if (!item.id) continue;
3999
+ for (const tag of Array.isArray(item.tags) ? item.tags : []) {
4000
+ if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
4001
+ }
4002
+ }
4003
+ return failures;
4004
+ }
3383
4005
 
3384
4006
  // src/commands/validate-catalog.ts
3385
4007
  function auditForbiddenStacks(items) {
@@ -3449,23 +4071,23 @@ function auditShippedFiles(manifestDir, items) {
3449
4071
  const failures = [];
3450
4072
  for (const item of items) {
3451
4073
  if (!item.path) continue;
3452
- const absPath = path26.join(manifestDir, item.path);
4074
+ const absPath = path28.join(manifestDir, item.path);
3453
4075
  if (item.type === "skill") {
3454
- const skillMd = path26.join(absPath, "SKILL.md");
3455
- if (!fs17.existsSync(skillMd)) {
3456
- failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4076
+ const skillMd = path28.join(absPath, "SKILL.md");
4077
+ if (!fs18.existsSync(skillMd)) {
4078
+ failures.push(`${item.id}: missing ${path28.relative(manifestDir, skillMd)}`);
3457
4079
  continue;
3458
4080
  }
3459
- const text = fs17.readFileSync(skillMd, "utf8");
4081
+ const text = fs18.readFileSync(skillMd, "utf8");
3460
4082
  for (const section of REQUIRED_SKILL_SECTIONS) {
3461
4083
  if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
3462
4084
  }
3463
4085
  } else if (item.type === "agent") {
3464
- if (!fs17.existsSync(absPath)) {
4086
+ if (!fs18.existsSync(absPath)) {
3465
4087
  failures.push(`${item.id}: missing agent file ${item.path}`);
3466
4088
  continue;
3467
4089
  }
3468
- const text = fs17.readFileSync(absPath, "utf8");
4090
+ const text = fs18.readFileSync(absPath, "utf8");
3469
4091
  if (!text.startsWith("---")) failures.push(`${item.id}: agent file missing YAML frontmatter`);
3470
4092
  for (const section of REQUIRED_AGENT_SECTIONS) {
3471
4093
  if (!text.includes(section)) failures.push(`${item.id}: agent file missing ${section}`);
@@ -3476,7 +4098,7 @@ function auditShippedFiles(manifestDir, items) {
3476
4098
  failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
3477
4099
  }
3478
4100
  } else if (item.type === "template") {
3479
- if (!fs17.existsSync(absPath)) {
4101
+ if (!fs18.existsSync(absPath)) {
3480
4102
  failures.push(`${item.id}: missing template file ${item.path}`);
3481
4103
  }
3482
4104
  }
@@ -3487,21 +4109,21 @@ function auditMarkdownContent(manifestDir) {
3487
4109
  const failures = [];
3488
4110
  const dirs = ["skills", "agents"];
3489
4111
  for (const dir of dirs) {
3490
- const abs = path26.join(manifestDir, dir);
3491
- if (!fs17.existsSync(abs)) continue;
4112
+ const abs = path28.join(manifestDir, dir);
4113
+ if (!fs18.existsSync(abs)) continue;
3492
4114
  walkMd(abs, (file) => {
3493
- const text = fs17.readFileSync(file, "utf8");
3494
- const rel = path26.relative(manifestDir, file);
4115
+ const text = fs18.readFileSync(file, "utf8");
4116
+ const rel = path28.relative(manifestDir, file);
3495
4117
  const lines = text.split(/\r?\n/);
3496
4118
  for (let i = 0; i < lines.length; i++) {
3497
- const line = lines[i] ?? "";
3498
- if (PLACEHOLDER_PATTERN.test(line)) {
4119
+ const line2 = lines[i] ?? "";
4120
+ if (PLACEHOLDER_PATTERN.test(line2)) {
3499
4121
  failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
3500
4122
  }
3501
- if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line))) {
4123
+ if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
3502
4124
  failures.push(`${rel}:${i + 1}: risky install pattern`);
3503
4125
  }
3504
- if (ANY_NPX_PATTERN.test(line) && !ALLOWED_NPX_PATTERN.test(line)) {
4126
+ if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
3505
4127
  failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
3506
4128
  }
3507
4129
  }
@@ -3510,8 +4132,8 @@ function auditMarkdownContent(manifestDir) {
3510
4132
  return failures;
3511
4133
  }
3512
4134
  function walkMd(dir, fn) {
3513
- for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
3514
- const full = path26.join(dir, entry.name);
4135
+ for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
4136
+ const full = path28.join(dir, entry.name);
3515
4137
  if (entry.isDirectory()) walkMd(full, fn);
3516
4138
  else if (entry.name.endsWith(".md")) fn(full);
3517
4139
  }
@@ -3522,8 +4144,8 @@ async function runValidateCatalog(manifestPath) {
3522
4144
  process.exitCode = 1;
3523
4145
  return;
3524
4146
  }
3525
- const abs = path26.resolve(process.cwd(), manifestPath);
3526
- const manifestDir = path26.dirname(abs);
4147
+ const abs = path28.resolve(process.cwd(), manifestPath);
4148
+ const manifestDir = path28.dirname(abs);
3527
4149
  const data = await readJson(abs);
3528
4150
  if (!data?.items) {
3529
4151
  error(`Could not read catalog manifest at ${abs}`);
@@ -3535,17 +4157,7 @@ async function runValidateCatalog(manifestPath) {
3535
4157
  const stackFailures = auditForbiddenStacks(items);
3536
4158
  const fileFailures = auditShippedFiles(manifestDir, items);
3537
4159
  const contentFailures = auditMarkdownContent(manifestDir);
3538
- const allowed = new Set((await readAllowedStacks(packageRoot())).map((x) => x.toLowerCase()));
3539
- const tagFailures = [];
3540
- if (allowed.size > 0) {
3541
- for (const item of items) {
3542
- for (const tag of Array.isArray(item.tags) ? item.tags : []) {
3543
- if (!allowed.has(tag.toLowerCase()) && !tag.includes("-patterns") && tag !== "haus" && tag !== "security" && tag !== "quality" && tag !== "review" && tag !== "workflow" && tag !== "baseline" && tag !== "project-instructions") {
3544
- tagFailures.push(`${item.id}: tag not in allowlist: "${tag}"`);
3545
- }
3546
- }
3547
- }
3548
- }
4160
+ const tagFailures = auditDisallowedTags(items);
3549
4161
  const allFailures = [
3550
4162
  ...structureFailures,
3551
4163
  ...stackFailures,
@@ -3562,7 +4174,7 @@ async function runValidateCatalog(manifestPath) {
3562
4174
  }
3563
4175
 
3564
4176
  // src/commands/workspace.ts
3565
- import path27 from "path";
4177
+ import path29 from "path";
3566
4178
  import YAML from "yaml";
3567
4179
  async function runWorkspace(action) {
3568
4180
  if (action === "init") {
@@ -3595,7 +4207,7 @@ relationships: []
3595
4207
  const summaries = [];
3596
4208
  const ownership = {};
3597
4209
  for (const repo of repos) {
3598
- const repoRoot = path27.resolve(process.cwd(), repo.path);
4210
+ const repoRoot = path29.resolve(process.cwd(), repo.path);
3599
4211
  const result = await scanProject(repoRoot, "fast");
3600
4212
  summaries.push({
3601
4213
  name: repo.name,
@@ -3604,9 +4216,9 @@ relationships: []
3604
4216
  packageManager: result.packageManager,
3605
4217
  deps: result.dependencies
3606
4218
  });
3607
- for (const dep of result.dependencies) {
3608
- ownership[dep] ??= [];
3609
- ownership[dep].push(repo.name);
4219
+ for (const dep2 of result.dependencies) {
4220
+ ownership[dep2] ??= [];
4221
+ ownership[dep2].push(repo.name);
3610
4222
  }
3611
4223
  }
3612
4224
  await writeJson(".haus-workflow/workspace-summary.json", {
@@ -3631,7 +4243,7 @@ ${summaries.map(
3631
4243
  // src/cli.ts
3632
4244
  function cliVersion() {
3633
4245
  try {
3634
- const pkgPath = path28.join(packageRoot(), "package.json");
4246
+ const pkgPath = path30.join(packageRoot(), "package.json");
3635
4247
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3636
4248
  return pkg.version ?? "0.0.0";
3637
4249
  } catch {
@@ -3641,7 +4253,7 @@ function cliVersion() {
3641
4253
  var program = new Command();
3642
4254
  function validateRuntimeNodeVersion() {
3643
4255
  try {
3644
- const pkgPath = path28.join(packageRoot(), "package.json");
4256
+ const pkgPath = path30.join(packageRoot(), "package.json");
3645
4257
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3646
4258
  const requiredRange = pkg.engines?.node;
3647
4259
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -3662,6 +4274,9 @@ program.command("doctor").option("--hooks", "Verify .claude/settings.json matche
3662
4274
  program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
3663
4275
  "--allow-empty-cache",
3664
4276
  "Apply core files only when catalog cache is empty (skip catalog items without error)"
4277
+ ).option(
4278
+ "--refill-config",
4279
+ "Fill still-blank fields in an existing workflow-config.md without touching edited ones"
3665
4280
  ).action(runApply);
3666
4281
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
3667
4282
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
@@ -3671,20 +4286,15 @@ program.command("refresh").action(runRefresh);
3671
4286
  program.command("catalog-audit").action(runCatalogAudit);
3672
4287
  program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
3673
4288
  program.command("update").option("--check").action(runUpdate);
3674
- program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").action(runInstall);
4289
+ program.command("install").option("--dry-run").option("--force").option("--check", "Exit non-zero if any HAUS-MANAGED file is out of date").option("--postinstall", "Run by the npm postinstall hook; prints a plain-language change notice").action(runInstall);
3675
4290
  program.command("uninstall").option("--force").action(runUninstallCommand);
3676
- var memory = program.command("memory");
3677
- memory.command("status").action(() => runMemory("status", {}));
3678
- memory.command("add <text>").action((text) => runMemory("add", { text }));
3679
- memory.command("inject").option("--task <task>").option("--from-hook").action((opts) => runMemory("inject", opts));
3680
- memory.command("promote").action(() => runMemory("promote", {}));
3681
4291
  var guard = program.command("guard");
3682
4292
  guard.command("file-access").option("--from-hook").action((opts) => runGuard("file-access", opts));
3683
4293
  guard.command("bash").option("--from-hook").action((opts) => runGuard("bash", opts));
3684
4294
  var config = program.command("config");
3685
- config.command("enable <key>").description("Enable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "enable"));
3686
- config.command("disable <key>").description("Disable a hook (hook.context, hook.memory)").action((key) => runConfig(key, "disable"));
3687
- config.command("status <key>").description("Show current state of a hook (hook.context, hook.memory)").action((key) => runConfig(key, "status"));
4295
+ config.command("enable <key>").description("Enable a hook (hook.context)").action((key) => runConfig(key, "enable"));
4296
+ config.command("disable <key>").description("Disable a hook (hook.context)").action((key) => runConfig(key, "disable"));
4297
+ config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
3688
4298
  var workspace = program.command("workspace");
3689
4299
  workspace.command("init").action(() => runWorkspace("init"));
3690
4300
  workspace.command("scan").action(() => runWorkspace("scan"));