@haus-tech/haus-workflow 4.5.0 → 4.6.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
@@ -404,6 +404,9 @@ var init_exec = __esm({
404
404
  });
405
405
 
406
406
  // src/catalog/remote-catalog/github-auth.ts
407
+ function ghProbeDisabled() {
408
+ return process.env["HAUS_NO_GH_AUTH_PROBE"] === "1";
409
+ }
407
410
  async function defaultGhTokenResolver() {
408
411
  try {
409
412
  const result = await runCommand("gh", ["auth", "token"], {
@@ -425,8 +428,9 @@ async function resolveGithubAuthToken(env = process.env) {
425
428
  if (haus) return haus;
426
429
  const github = env["GITHUB_TOKEN"]?.trim();
427
430
  if (github) return github;
428
- const fromGh = await (ghResolverOverride ?? defaultGhTokenResolver)();
429
- return fromGh;
431
+ if (ghResolverOverride) return ghResolverOverride();
432
+ if (ghProbeDisabled()) return null;
433
+ return defaultGhTokenResolver();
430
434
  })().then((token) => {
431
435
  cachedToken = token;
432
436
  inFlight = void 0;
@@ -1932,7 +1936,7 @@ var init_sync = __esm({
1932
1936
  // src/cli.ts
1933
1937
  init_offline();
1934
1938
  import { readFileSync as readFileSync5 } from "node:fs";
1935
- import path80 from "node:path";
1939
+ import path81 from "node:path";
1936
1940
  import { Command } from "commander";
1937
1941
 
1938
1942
  // src/commands/apply.ts
@@ -2338,6 +2342,11 @@ var ASK_PATHS = [
2338
2342
  { pattern: ".env.*", tools: ["Edit", "Write"] },
2339
2343
  { pattern: "storage/logs/**", tools: ["Edit", "Write"] },
2340
2344
  { pattern: "exports/**", tools: ["Edit", "Write"] },
2345
+ // Dump-shaped formats: reading one is the leak, so Read is in the tool set. `*.sql`
2346
+ // belongs with them — a dump can carry an entire customer table — and was the one
2347
+ // missing from every permission tier (it appeared only in SENSITIVE_PATH_REGEXES,
2348
+ // which gates the bulk scanner, not on-demand file access). See ADR-0071.
2349
+ { pattern: "*.sql", tools: ["Read", "Edit", "Write"] },
2341
2350
  { pattern: "*.dump", tools: ["Read", "Edit", "Write"] },
2342
2351
  { pattern: "*.backup", tools: ["Read", "Edit", "Write"] },
2343
2352
  { pattern: "*.bak", tools: ["Read", "Edit", "Write"] },
@@ -2376,6 +2385,34 @@ function isRecord(v) {
2376
2385
  return typeof v === "object" && v !== null;
2377
2386
  }
2378
2387
 
2388
+ // src/utils/versions.ts
2389
+ import semver from "semver";
2390
+ function normalizeVersion(version) {
2391
+ return semver.valid(semver.coerce(version));
2392
+ }
2393
+ function satisfiesVersion(version, range) {
2394
+ const normalized = normalizeVersion(version);
2395
+ if (!normalized) return false;
2396
+ return semver.satisfies(normalized, range, { includePrerelease: true });
2397
+ }
2398
+ function compareVersions(a, b) {
2399
+ const normalizedA = normalizeVersion(a);
2400
+ const normalizedB = normalizeVersion(b);
2401
+ if (!normalizedA || !normalizedB) {
2402
+ throw new Error(`Cannot compare invalid versions: ${a} vs ${b}`);
2403
+ }
2404
+ return semver.compare(normalizedA, normalizedB);
2405
+ }
2406
+ function resolveMajorVersion(range) {
2407
+ if (!range) return null;
2408
+ try {
2409
+ const min = semver.minVersion(range);
2410
+ return min ? min.major : null;
2411
+ } catch {
2412
+ return null;
2413
+ }
2414
+ }
2415
+
2379
2416
  // src/scanner/detection.ts
2380
2417
  var UNSUPPORTED_MARKERS = {
2381
2418
  "Cargo.toml": "rust",
@@ -2402,7 +2439,7 @@ function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
2402
2439
  function blocked(rel) {
2403
2440
  return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
2404
2441
  }
2405
- function dependencyVersionMap(pkg, composer) {
2442
+ function dependencyVersionMap(pkg, composer, memberPkgs = []) {
2406
2443
  const versions = /* @__PURE__ */ new Map();
2407
2444
  const pushObj = (obj) => {
2408
2445
  if (!isRecord(obj)) return;
@@ -2416,8 +2453,34 @@ function dependencyVersionMap(pkg, composer) {
2416
2453
  pushObj(pkg?.devDependencies);
2417
2454
  pushObj(composer?.require);
2418
2455
  pushObj(composer?.["require-dev"]);
2456
+ const rootDeclared = new Set(versions.keys());
2457
+ for (const member of memberPkgs) {
2458
+ pushObj(member?.dependencies);
2459
+ pushObj(member?.devDependencies);
2460
+ }
2461
+ for (const [name, majors] of memberMajorsByName(memberPkgs)) {
2462
+ if (rootDeclared.has(name)) continue;
2463
+ if (majors.size > 1) versions.set(name, "");
2464
+ }
2419
2465
  return versions;
2420
2466
  }
2467
+ function memberMajorsByName(memberPkgs) {
2468
+ const majors = /* @__PURE__ */ new Map();
2469
+ for (const member of memberPkgs) {
2470
+ for (const section of [member?.dependencies, member?.devDependencies]) {
2471
+ if (!isRecord(section)) continue;
2472
+ for (const [name, raw] of Object.entries(section)) {
2473
+ if (typeof raw !== "string") continue;
2474
+ const major = resolveMajorVersion(raw);
2475
+ if (major == null) continue;
2476
+ const seen = majors.get(name) ?? /* @__PURE__ */ new Set();
2477
+ seen.add(major);
2478
+ majors.set(name, seen);
2479
+ }
2480
+ }
2481
+ }
2482
+ return majors;
2483
+ }
2421
2484
  function finalizeRoles(registryRoles, deps, files) {
2422
2485
  const roles = new Set(registryRoles);
2423
2486
  const hasWpConfig = files.some((f) => f.endsWith("wp-config.php"));
@@ -2514,36 +2577,6 @@ function isSupersededByPlugin(lockId, activePinIds) {
2514
2577
  // src/scanner/detect-package-manager.ts
2515
2578
  import path13 from "node:path";
2516
2579
  import fs9 from "fs-extra";
2517
-
2518
- // src/utils/versions.ts
2519
- import semver from "semver";
2520
- function normalizeVersion(version) {
2521
- return semver.valid(semver.coerce(version));
2522
- }
2523
- function satisfiesVersion(version, range) {
2524
- const normalized = normalizeVersion(version);
2525
- if (!normalized) return false;
2526
- return semver.satisfies(normalized, range, { includePrerelease: true });
2527
- }
2528
- function compareVersions(a, b) {
2529
- const normalizedA = normalizeVersion(a);
2530
- const normalizedB = normalizeVersion(b);
2531
- if (!normalizedA || !normalizedB) {
2532
- throw new Error(`Cannot compare invalid versions: ${a} vs ${b}`);
2533
- }
2534
- return semver.compare(normalizedA, normalizedB);
2535
- }
2536
- function resolveMajorVersion(range) {
2537
- if (!range) return null;
2538
- try {
2539
- const min = semver.minVersion(range);
2540
- return min ? min.major : null;
2541
- } catch {
2542
- return null;
2543
- }
2544
- }
2545
-
2546
- // src/scanner/detect-package-manager.ts
2547
2580
  function detectPackageManager(root, packageManagerField) {
2548
2581
  const field = String(packageManagerField ?? "").trim();
2549
2582
  if (field.startsWith("yarn@")) {
@@ -3160,8 +3193,8 @@ function buildDenyRules() {
3160
3193
  for (const command of DENY_COMMANDS) {
3161
3194
  rules.push(`Bash(${command}:*)`);
3162
3195
  }
3163
- for (const path81 of DENY_PATHS) {
3164
- const pattern = DENY_DIRS.has(path81) ? `${path81}/**` : path81;
3196
+ for (const path82 of DENY_PATHS) {
3197
+ const pattern = DENY_DIRS.has(path82) ? `${path82}/**` : path82;
3165
3198
  for (const tool of FILE_TOOLS) {
3166
3199
  rules.push(`${tool}(${pattern})`);
3167
3200
  }
@@ -3680,6 +3713,7 @@ var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
3680
3713
  @docs/decisions/README.md`;
3681
3714
  function buildImportBlock() {
3682
3715
  return `${BLOCK_BEGIN}
3716
+
3683
3717
  ${IMPORT_CONTENT}
3684
3718
  ${BLOCK_END}`;
3685
3719
  }
@@ -4748,7 +4782,8 @@ var GITIGNORED_ARTIFACT_PATHS = [
4748
4782
  ".haus-workflow/recommendation.json",
4749
4783
  ".haus-workflow/sources-report.json",
4750
4784
  ".haus-workflow/deep-context.json",
4751
- ".haus-workflow/backups/"
4785
+ ".haus-workflow/backups/",
4786
+ ".haus-workflow/llms-cache/"
4752
4787
  ];
4753
4788
  function findLineMarker3(content2, marker, from = 0) {
4754
4789
  let idx = content2.indexOf(marker, from);
@@ -4793,7 +4828,7 @@ function buildBlockFor(existing, beginMarker, endMarker, comment, patterns) {
4793
4828
  if (needed.length === 0) return null;
4794
4829
  return [beginMarker, comment, ...needed, endMarker].join("\n");
4795
4830
  }
4796
- var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058)";
4831
+ var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058, ADR-0009)";
4797
4832
  function injectBlockFor(existing, beginMarker, endMarker, comment, patterns) {
4798
4833
  const block = buildBlockFor(existing, beginMarker, endMarker, comment, patterns);
4799
4834
  const range = findBlockRangeFor(existing, beginMarker, endMarker);
@@ -6454,8 +6489,8 @@ async function runBundleImport(file, options = {}) {
6454
6489
  }
6455
6490
 
6456
6491
  // src/commands/doctor.ts
6457
- import path45 from "node:path";
6458
- import fs37 from "fs-extra";
6492
+ import path46 from "node:path";
6493
+ import fs38 from "fs-extra";
6459
6494
  init_manifest_format();
6460
6495
 
6461
6496
  // src/claude/former-install-orphans.ts
@@ -6490,11 +6525,132 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
6490
6525
  return orphans;
6491
6526
  }
6492
6527
 
6528
+ // src/claude/project-exclusions.ts
6529
+ init_project_pin();
6530
+ init_fs();
6531
+ init_logger();
6532
+ init_paths();
6533
+
6534
+ // src/claude/project-intent.ts
6535
+ init_project_pin();
6536
+ init_fs();
6537
+ init_logger();
6538
+ init_paths();
6539
+ import fs30 from "fs-extra";
6540
+ function normalizeIdList(value) {
6541
+ if (!Array.isArray(value)) return [];
6542
+ return [
6543
+ ...new Set(
6544
+ value.filter((entry) => typeof entry === "string" && entry.length > 0)
6545
+ )
6546
+ ];
6547
+ }
6548
+ function normalizeSignalList(value) {
6549
+ return [...new Set(normalizeIdList(value).map((entry) => entry.toLowerCase()))];
6550
+ }
6551
+ async function readProjectConfigObject(root) {
6552
+ try {
6553
+ const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
6554
+ if (data == null || typeof data !== "object" || Array.isArray(data)) return null;
6555
+ return data;
6556
+ } catch (err) {
6557
+ if (err.code === "ENOENT") return null;
6558
+ warn(
6559
+ `${PROJECT_CONFIG_REL} could not be read (${err instanceof Error ? err.message : String(err)}) \u2014 treating project intent as unset`
6560
+ );
6561
+ return null;
6562
+ }
6563
+ }
6564
+ async function updateProjectConfig(root, mutate) {
6565
+ const file = hausPath(root, PROJECT_CONFIG_FILE);
6566
+ let existing = {};
6567
+ if (await fs30.pathExists(file)) {
6568
+ try {
6569
+ const parsed = JSON.parse(await fs30.readFile(file, "utf8"));
6570
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
6571
+ existing = { ...parsed };
6572
+ } else {
6573
+ warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with haus-managed keys only`);
6574
+ }
6575
+ } catch (err) {
6576
+ warn(
6577
+ `${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with haus-managed keys only`
6578
+ );
6579
+ }
6580
+ }
6581
+ await writeJson(file, mutate(existing));
6582
+ }
6583
+ async function readProjectIncludedIds(root) {
6584
+ const data = await readProjectConfigObject(root);
6585
+ return data == null ? [] : normalizeIdList(data.includedIds);
6586
+ }
6587
+ async function addProjectIncludedIds(root, ids) {
6588
+ const toAdd = normalizeIdList(ids);
6589
+ if (toAdd.length === 0) return;
6590
+ await updateProjectConfig(root, (existing) => ({
6591
+ ...existing,
6592
+ includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
6593
+ }));
6594
+ }
6595
+ function withdrawIncludedIds(existing, ids) {
6596
+ const drop = new Set(ids);
6597
+ return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
6598
+ }
6599
+ async function readDeclaredContext(root) {
6600
+ const data = await readProjectConfigObject(root);
6601
+ if (data == null) return { roles: [], stacks: [] };
6602
+ return {
6603
+ roles: normalizeSignalList(data.declaredRoles),
6604
+ stacks: normalizeSignalList(data.declaredStacks)
6605
+ };
6606
+ }
6607
+
6608
+ // src/claude/project-exclusions.ts
6609
+ function normalizeExcludedIds(value) {
6610
+ if (!Array.isArray(value)) return [];
6611
+ return [
6612
+ ...new Set(
6613
+ value.filter((entry) => typeof entry === "string" && entry.length > 0)
6614
+ )
6615
+ ];
6616
+ }
6617
+ async function readProjectExcludedIds(root) {
6618
+ try {
6619
+ const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
6620
+ if (!data) return [];
6621
+ return normalizeExcludedIds(data.excludedIds);
6622
+ } catch (err) {
6623
+ if (err.code === "ENOENT") return [];
6624
+ warn(
6625
+ `${PROJECT_CONFIG_REL} could not be read for excludedIds (${err instanceof Error ? err.message : String(err)}) \u2014 treating as none`
6626
+ );
6627
+ return [];
6628
+ }
6629
+ }
6630
+ function isItemExcluded(item, excluded) {
6631
+ if (excluded.has(item.id)) return true;
6632
+ return normalizeFormerIds(item.formerIds).some((formerId) => excluded.has(formerId));
6633
+ }
6634
+ async function addProjectExcludedIds(root, ids) {
6635
+ const toAdd = [...new Set(ids.filter((id) => id.length > 0))];
6636
+ if (toAdd.length === 0) return;
6637
+ await updateProjectConfig(root, (existing) => {
6638
+ const next = { ...existing };
6639
+ next.excludedIds = [
6640
+ .../* @__PURE__ */ new Set([...normalizeExcludedIds(existing.excludedIds), ...toAdd])
6641
+ ].sort();
6642
+ const remainingIncludes = withdrawIncludedIds(existing, toAdd);
6643
+ if (remainingIncludes.length > 0) next.includedIds = remainingIncludes;
6644
+ else delete next.includedIds;
6645
+ return next;
6646
+ });
6647
+ }
6648
+
6493
6649
  // src/claude/skill-reference-check.ts
6494
6650
  init_paths();
6495
6651
  import path32 from "node:path";
6496
6652
  import fg4 from "fast-glob";
6497
- import fs30 from "fs-extra";
6653
+ import fs31 from "fs-extra";
6498
6654
  var REFERENCE_PATTERN = /\b([a-z][a-z0-9]*(?:-[a-z0-9]+)+):([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\b(?!:\/\/)/g;
6499
6655
  function stripFencedCodeBlocks(text) {
6500
6656
  return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " "));
@@ -6509,17 +6665,9 @@ function inlineCodeSpanRanges(line2) {
6509
6665
  }
6510
6666
  async function findUnresolvedSkillReferences(root, options = {}) {
6511
6667
  const resolvableNamespaces = new Set(options.pluginNamespaces ?? []);
6512
- const skillsRoot = claudePath(root, "skills");
6513
- if (!await fs30.pathExists(skillsRoot)) return [];
6514
- const skillDirs = (await fs30.readdir(skillsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== "shared").map((entry) => entry.name);
6515
- const installedSkillNames = new Set(skillDirs);
6516
- const files = await fg4("*/**/*.md", { cwd: skillsRoot, onlyFiles: true, dot: true });
6668
+ const installedSkillNames = await readInstalledSkillNames(root);
6517
6669
  const issues = [];
6518
- for (const rel of files) {
6519
- if (rel.startsWith("shared/")) continue;
6520
- const abs = path32.join(skillsRoot, rel);
6521
- const raw = await fs30.readFile(abs, "utf8");
6522
- const lines = stripFencedCodeBlocks(raw).split("\n");
6670
+ for (const { file, lines } of await readScannedMarkdown(root)) {
6523
6671
  lines.forEach((line2, idx) => {
6524
6672
  const codeSpans = inlineCodeSpanRanges(line2);
6525
6673
  for (const match of line2.matchAll(REFERENCE_PATTERN)) {
@@ -6531,7 +6679,7 @@ async function findUnresolvedSkillReferences(root, options = {}) {
6531
6679
  );
6532
6680
  if (inCodeSpan && !installedSkillNames.has(bareName)) continue;
6533
6681
  issues.push({
6534
- file: path32.join(".claude/skills", rel),
6682
+ file,
6535
6683
  line: idx + 1,
6536
6684
  reference,
6537
6685
  bareName,
@@ -6542,17 +6690,83 @@ async function findUnresolvedSkillReferences(root, options = {}) {
6542
6690
  }
6543
6691
  return issues;
6544
6692
  }
6693
+ var BOLD_AGENT_PATTERN = /\*\*([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\*\*/g;
6694
+ var AGENT_WORD = /\bagents?\b/i;
6695
+ function isAgentContext(line2, nearestHeading) {
6696
+ const outsideBold = line2.replace(BOLD_AGENT_PATTERN, " ");
6697
+ return AGENT_WORD.test(outsideBold) || AGENT_WORD.test(nearestHeading);
6698
+ }
6699
+ async function findUnresolvedAgentReferences(root, options = {}) {
6700
+ const installedAgentNames = await readInstalledAgentNames(root);
6701
+ const catalogAgentNames = new Set(options.catalogAgentNames ?? []);
6702
+ const issues = [];
6703
+ for (const { file, lines } of await readScannedMarkdown(root)) {
6704
+ let nearestHeading = "";
6705
+ lines.forEach((line2, idx) => {
6706
+ if (/^#{1,6}\s/.test(line2)) nearestHeading = line2.replace(/^#{1,6}\s*/, "");
6707
+ if (!isAgentContext(line2, nearestHeading)) return;
6708
+ const codeSpans = inlineCodeSpanRanges(line2);
6709
+ for (const match of line2.matchAll(BOLD_AGENT_PATTERN)) {
6710
+ const agentName = match[1];
6711
+ if (!agentName || installedAgentNames.has(agentName)) continue;
6712
+ const inCodeSpan = codeSpans.some(
6713
+ ([start, end]) => match.index >= start && match.index < end
6714
+ );
6715
+ if (inCodeSpan) continue;
6716
+ issues.push({
6717
+ file,
6718
+ line: idx + 1,
6719
+ agentName,
6720
+ inCatalog: catalogAgentNames.has(agentName)
6721
+ });
6722
+ }
6723
+ });
6724
+ }
6725
+ return issues;
6726
+ }
6727
+ async function readInstalledSkillNames(root) {
6728
+ const skillsRoot = claudePath(root, "skills");
6729
+ if (!await fs31.pathExists(skillsRoot)) return /* @__PURE__ */ new Set();
6730
+ const entries = await fs31.readdir(skillsRoot, { withFileTypes: true });
6731
+ return new Set(entries.filter((e) => e.isDirectory() && e.name !== "shared").map((e) => e.name));
6732
+ }
6733
+ async function readInstalledAgentNames(root) {
6734
+ const agentsRoot = claudePath(root, "agents");
6735
+ if (!await fs31.pathExists(agentsRoot)) return /* @__PURE__ */ new Set();
6736
+ const entries = await fs31.readdir(agentsRoot, { withFileTypes: true });
6737
+ return new Set(
6738
+ entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""))
6739
+ );
6740
+ }
6741
+ async function readScannedMarkdown(root) {
6742
+ const scanned = [];
6743
+ for (const dir of ["skills", "agents", "rules"]) {
6744
+ const dirRoot = claudePath(root, dir);
6745
+ if (!await fs31.pathExists(dirRoot)) continue;
6746
+ const pattern = dir === "skills" ? "*/**/*.md" : "**/*.md";
6747
+ const files = await fg4(pattern, { cwd: dirRoot, onlyFiles: true, dot: true });
6748
+ for (const rel of files) {
6749
+ if (dir === "skills" && rel.startsWith("shared/")) continue;
6750
+ const raw = await fs31.readFile(path32.join(dirRoot, rel), "utf8");
6751
+ scanned.push({
6752
+ file: path32.posix.join(".claude", dir, rel.split(path32.sep).join("/")),
6753
+ lines: stripFencedCodeBlocks(raw).split("\n")
6754
+ });
6755
+ }
6756
+ }
6757
+ return scanned;
6758
+ }
6545
6759
 
6546
6760
  // src/decisions/doctor.ts
6547
6761
  init_fs();
6548
6762
  import path37 from "node:path";
6549
- import fs34 from "fs-extra";
6763
+ import fs35 from "fs-extra";
6550
6764
 
6551
6765
  // src/decisions/check.ts
6552
6766
  init_fs();
6553
6767
  init_paths();
6554
6768
  import path36 from "node:path";
6555
- import fs33 from "fs-extra";
6769
+ import fs34 from "fs-extra";
6556
6770
 
6557
6771
  // src/decisions/dep-bump.ts
6558
6772
  var DEP_SECTIONS = [
@@ -6613,6 +6827,32 @@ function isDependencyBumpOnly(beforeText, afterText) {
6613
6827
  init_exec();
6614
6828
  init_fs();
6615
6829
  import path33 from "node:path";
6830
+ var NUL = String.fromCharCode(0);
6831
+ function parseNumstat(output) {
6832
+ const fields2 = output.split(NUL);
6833
+ const linesByFile = {};
6834
+ let linesAdded = 0;
6835
+ let linesRemoved = 0;
6836
+ for (let i = 0; i < fields2.length; i++) {
6837
+ const field = fields2[i];
6838
+ if (!field) continue;
6839
+ const parts = field.split(" ");
6840
+ if (parts.length < 3) continue;
6841
+ const added = parts[0] === "-" ? 0 : Number(parts[0]);
6842
+ const removed = parts[1] === "-" ? 0 : Number(parts[1]);
6843
+ if (Number.isNaN(added) || Number.isNaN(removed)) continue;
6844
+ let file = parts[2];
6845
+ if (file === "") {
6846
+ file = fields2[i + 2];
6847
+ i += 2;
6848
+ }
6849
+ if (!file) continue;
6850
+ linesAdded += added;
6851
+ linesRemoved += removed;
6852
+ linesByFile[file] = (linesByFile[file] ?? 0) + added + removed;
6853
+ }
6854
+ return { linesByFile, linesAdded, linesRemoved };
6855
+ }
6616
6856
  function isPackageJsonPath(file) {
6617
6857
  return file === "package.json" || file.endsWith("/package.json");
6618
6858
  }
@@ -6680,18 +6920,9 @@ async function collectDiffStats(root, opts) {
6680
6920
  const gitArgs = opts.staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only", opts.range ?? "HEAD"];
6681
6921
  const names = await runGit(gitArgs, { cwd: root });
6682
6922
  const files = names.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
6683
- const numstatArgs = opts.staged ? ["diff", "--cached", "--numstat"] : ["diff", "--numstat", opts.range ?? "HEAD"];
6923
+ const numstatArgs = opts.staged ? ["diff", "--cached", "--numstat", "-z"] : ["diff", "--numstat", "-z", opts.range ?? "HEAD"];
6684
6924
  const numstat = await runGit(numstatArgs, { cwd: root });
6685
- let linesAdded = 0;
6686
- let linesRemoved = 0;
6687
- for (const line2 of numstat.stdout.split("\n")) {
6688
- const parts = line2.trim().split(/\s+/);
6689
- if (parts.length < 3) continue;
6690
- const added = parts[0] === "-" ? 0 : Number(parts[0]);
6691
- const removed = parts[1] === "-" ? 0 : Number(parts[1]);
6692
- if (!Number.isNaN(added)) linesAdded += added;
6693
- if (!Number.isNaN(removed)) linesRemoved += removed;
6694
- }
6925
+ const { linesByFile, linesAdded, linesRemoved } = parseNumstat(numstat.stdout);
6695
6926
  const packageJsonPairs = {};
6696
6927
  for (const file of files) {
6697
6928
  if (!isPackageJsonPath(file)) continue;
@@ -6701,6 +6932,7 @@ async function collectDiffStats(root, opts) {
6701
6932
  files,
6702
6933
  linesAdded,
6703
6934
  linesRemoved,
6935
+ linesByFile,
6704
6936
  ...Object.keys(packageJsonPairs).length > 0 ? { packageJsonPairs } : {}
6705
6937
  };
6706
6938
  }
@@ -6745,12 +6977,12 @@ function firstMatchingRegex(filePath, patterns) {
6745
6977
 
6746
6978
  // src/decisions/paths.ts
6747
6979
  import path34 from "node:path";
6748
- import fs31 from "fs-extra";
6980
+ import fs32 from "fs-extra";
6749
6981
  async function resolveDecisionsDir(root) {
6750
6982
  const primary = path34.join(root, DECISIONS_TRIGGERS.decisionsDir);
6751
- if (await fs31.pathExists(primary)) return primary;
6983
+ if (await fs32.pathExists(primary)) return primary;
6752
6984
  const legacy = path34.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
6753
- if (await fs31.pathExists(legacy)) return legacy;
6985
+ if (await fs32.pathExists(legacy)) return legacy;
6754
6986
  return primary;
6755
6987
  }
6756
6988
  function relativeDecisionPath(decisionsDir, root) {
@@ -6760,7 +6992,7 @@ function relativeDecisionPath(decisionsDir, root) {
6760
6992
  // src/decisions/validate.ts
6761
6993
  init_fs();
6762
6994
  import path35 from "node:path";
6763
- import fs32 from "fs-extra";
6995
+ import fs33 from "fs-extra";
6764
6996
  function validateDecisionContent(content2) {
6765
6997
  const errors = [];
6766
6998
  for (const section of DECISIONS_TRIGGERS.requiredSections) {
@@ -6790,7 +7022,7 @@ function decisionNumberFromFile(name) {
6790
7022
  }
6791
7023
  async function validateIndexSync(decisionsDir, decisionFiles) {
6792
7024
  const readmePath = path35.join(decisionsDir, "README.md");
6793
- if (!await fs32.pathExists(readmePath)) {
7025
+ if (!await fs33.pathExists(readmePath)) {
6794
7026
  return ["docs/decisions/README.md is missing"];
6795
7027
  }
6796
7028
  const readme = await readText(readmePath) ?? "";
@@ -6846,6 +7078,11 @@ function countTopLevelDirs(files) {
6846
7078
  function firstMatchingGlob(file, globs) {
6847
7079
  return globs.find((g) => matchesPathGlob(file, g));
6848
7080
  }
7081
+ function relevantLineCount(stats, relevant) {
7082
+ const byFile = stats.linesByFile;
7083
+ if (byFile == null) return stats.linesAdded + stats.linesRemoved;
7084
+ return relevant.reduce((sum, file) => sum + (byFile[file] ?? 0), 0);
7085
+ }
6849
7086
  function explainDecisionTrigger(stats) {
6850
7087
  let relevant = stats.files.filter((f) => !isExempt(f));
6851
7088
  if (relevant.length === 0) return [];
@@ -6863,7 +7100,7 @@ function explainDecisionTrigger(stats) {
6863
7100
  return [`matched pathRegex: ${regex}`];
6864
7101
  }
6865
7102
  }
6866
- const totalLines = stats.linesAdded + stats.linesRemoved;
7103
+ const totalLines = relevantLineCount(stats, relevant);
6867
7104
  const dirs = countTopLevelDirs(relevant);
6868
7105
  if (relevant.length >= DECISIONS_TRIGGERS.minFilesChanged && dirs >= 2) {
6869
7106
  return [`size threshold: ${relevant.length} files across ${dirs} top-level dirs`];
@@ -6962,8 +7199,8 @@ ${opts.commitMessages ?? ""}`;
6962
7199
  changedFiles: stats.files
6963
7200
  });
6964
7201
  if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
6965
- const legacyExists = await fs33.pathExists(path36.join(root, legacyRel));
6966
- if (legacyExists && !await fs33.pathExists(decisionsDir)) {
7202
+ const legacyExists = await fs34.pathExists(path36.join(root, legacyRel));
7203
+ if (legacyExists && !await fs34.pathExists(decisionsDir)) {
6967
7204
  satisfactionErrors = await checkSatisfaction({
6968
7205
  root,
6969
7206
  decisionsRel: legacyRel,
@@ -6985,8 +7222,8 @@ ${opts.commitMessages ?? ""}`;
6985
7222
  return { triggered: true, satisfied: false, reasons, securityTriggered };
6986
7223
  }
6987
7224
  async function listDecisionNumbers(decisionsDir) {
6988
- if (!await fs33.pathExists(decisionsDir)) return [];
6989
- const entries = await fs33.readdir(decisionsDir);
7225
+ if (!await fs34.pathExists(decisionsDir)) return [];
7226
+ const entries = await fs34.readdir(decisionsDir);
6990
7227
  return entries.map((name) => decisionNumberFromFile(name)).filter((n) => n != null).map((n) => Number(n));
6991
7228
  }
6992
7229
 
@@ -6995,8 +7232,8 @@ async function auditDecisionsLayout(root) {
6995
7232
  const findings = [];
6996
7233
  const primary = path37.join(root, DECISIONS_TRIGGERS.decisionsDir);
6997
7234
  const legacy = path37.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
6998
- const hasPrimary = await fs34.pathExists(primary);
6999
- const hasLegacy = await fs34.pathExists(legacy);
7235
+ const hasPrimary = await fs35.pathExists(primary);
7236
+ const hasLegacy = await fs35.pathExists(legacy);
7000
7237
  if (!hasPrimary && hasLegacy) {
7001
7238
  findings.push({
7002
7239
  level: "advisory",
@@ -7010,7 +7247,7 @@ async function auditDecisionsLayout(root) {
7010
7247
  const decisionsDir = await resolveDecisionsDir(root);
7011
7248
  const rel = relativeDecisionPath(decisionsDir, root);
7012
7249
  const readmePath = path37.join(decisionsDir, "README.md");
7013
- if (!await fs34.pathExists(readmePath)) {
7250
+ if (!await fs35.pathExists(readmePath)) {
7014
7251
  findings.push({
7015
7252
  level: "advisory",
7016
7253
  line: `- DECISIONS: missing ${rel}/README.md index`,
@@ -7020,7 +7257,7 @@ async function auditDecisionsLayout(root) {
7020
7257
  return findings;
7021
7258
  }
7022
7259
  const readme = await readText(readmePath);
7023
- const entries = (await fs34.readdir(decisionsDir)).filter(
7260
+ const entries = (await fs35.readdir(decisionsDir)).filter(
7024
7261
  (f) => f.endsWith(".md") && f !== "README.md"
7025
7262
  );
7026
7263
  for (const file of entries) {
@@ -7069,7 +7306,7 @@ init_fs();
7069
7306
  init_logger();
7070
7307
  init_paths();
7071
7308
  import path38 from "node:path";
7072
- import fs35 from "fs-extra";
7309
+ import fs36 from "fs-extra";
7073
7310
  function manifestPathIn(configDir) {
7074
7311
  return path38.join(configDir, "haus", "install-manifest.json");
7075
7312
  }
@@ -7099,9 +7336,9 @@ async function pruneVacatedDirs(root, vacatedFiles) {
7099
7336
  async function destinationWritable(dir) {
7100
7337
  let candidate = path38.resolve(dir);
7101
7338
  for (; ; ) {
7102
- if (await fs35.pathExists(candidate)) {
7339
+ if (await fs36.pathExists(candidate)) {
7103
7340
  try {
7104
- await fs35.access(candidate, fs35.constants.W_OK);
7341
+ await fs36.access(candidate, fs36.constants.W_OK);
7105
7342
  return true;
7106
7343
  } catch (err) {
7107
7344
  return err instanceof Error ? err.message : String(err);
@@ -7133,7 +7370,7 @@ async function migrateGlobalConfigDir(options = {}) {
7133
7370
  if (!legacy || !Array.isArray(legacy.files)) {
7134
7371
  return { ...base, reason: "no install manifest under ~/.claude" };
7135
7372
  }
7136
- if (await fs35.pathExists(manifestPathIn(to))) {
7373
+ if (await fs36.pathExists(manifestPathIn(to))) {
7137
7374
  return { ...base, reason: "an install manifest already exists in the resolved config dir" };
7138
7375
  }
7139
7376
  const writable = await destinationWritable(to);
@@ -7156,14 +7393,14 @@ async function migrateGlobalConfigDir(options = {}) {
7156
7393
  continue;
7157
7394
  }
7158
7395
  const newDest = path38.join(to, rel);
7159
- if (!await fs35.pathExists(entry.destPath)) {
7396
+ if (!await fs36.pathExists(entry.destPath)) {
7160
7397
  rewritten.push({ ...entry, destPath: newDest });
7161
7398
  continue;
7162
7399
  }
7163
- if (await fs35.pathExists(newDest)) {
7400
+ if (await fs36.pathExists(newDest)) {
7164
7401
  if (await sameContent(entry.destPath, newDest)) {
7165
7402
  if (!dryRun) {
7166
- await fs35.remove(entry.destPath);
7403
+ await fs36.remove(entry.destPath);
7167
7404
  vacated.push(entry.destPath);
7168
7405
  }
7169
7406
  moved.push(newDest);
@@ -7180,7 +7417,7 @@ async function migrateGlobalConfigDir(options = {}) {
7180
7417
  continue;
7181
7418
  }
7182
7419
  try {
7183
- await fs35.move(entry.destPath, newDest);
7420
+ await fs36.move(entry.destPath, newDest);
7184
7421
  vacated.push(entry.destPath);
7185
7422
  moved.push(newDest);
7186
7423
  rewritten.push({ ...entry, destPath: newDest });
@@ -7205,7 +7442,7 @@ async function migrateGlobalConfigDir(options = {}) {
7205
7442
  }
7206
7443
  try {
7207
7444
  await writeJson(manifestPathIn(to), { ...legacy, files: rewritten });
7208
- await fs35.remove(legacyManifestPath);
7445
+ await fs36.remove(legacyManifestPath);
7209
7446
  vacated.push(legacyManifestPath);
7210
7447
  } catch (err) {
7211
7448
  return {
@@ -7250,8 +7487,8 @@ function printConfigDirMigration(result) {
7250
7487
  }
7251
7488
 
7252
7489
  // src/scanner/read-context.ts
7253
- import path44 from "node:path";
7254
- import fs36 from "fs-extra";
7490
+ import path45 from "node:path";
7491
+ import fs37 from "fs-extra";
7255
7492
 
7256
7493
  // src/commands/workspace/config.ts
7257
7494
  init_fs();
@@ -7381,7 +7618,7 @@ async function readMembers(rootInfo) {
7381
7618
  }
7382
7619
 
7383
7620
  // src/scanner/scan-project.ts
7384
- import path43 from "node:path";
7621
+ import path44 from "node:path";
7385
7622
  init_fs();
7386
7623
 
7387
7624
  // src/utils/git-root.ts
@@ -7445,7 +7682,7 @@ init_paths();
7445
7682
  // src/scanner/detection-registry.ts
7446
7683
  var dep = (value) => ({ kind: "dep", value });
7447
7684
  var depPrefix = (value) => ({ kind: "depPrefix", value });
7448
- var depAbsent = (value) => ({ kind: "depAbsent", value });
7685
+ var depSuffix = (value) => ({ kind: "depSuffix", value });
7449
7686
  var depMajor = (value, major) => ({
7450
7687
  kind: "depMajor",
7451
7688
  value,
@@ -7472,6 +7709,7 @@ var fileStartsWith = (value) => ({
7472
7709
  mode: "startsWith"
7473
7710
  });
7474
7711
  var content = (value) => ({ kind: "content", value });
7712
+ var composerType = (value) => ({ kind: "composerType", value });
7475
7713
  var STACK_BUCKETS = [
7476
7714
  "backend",
7477
7715
  "frontend",
@@ -7488,12 +7726,17 @@ function matchSignal(sig, ctx) {
7488
7726
  case "depPrefix":
7489
7727
  for (const d of ctx.deps) if (d.startsWith(sig.value)) return true;
7490
7728
  return false;
7729
+ case "depSuffix":
7730
+ for (const d of ctx.deps) if (d.endsWith(sig.value)) return true;
7731
+ return false;
7491
7732
  case "depAbsent":
7492
7733
  return !ctx.deps.has(sig.value);
7493
7734
  case "depMajor":
7494
7735
  return ctx.deps.has(sig.value) && resolveMajorVersion(ctx.depVersions.get(sig.value) ?? "") === sig.major;
7495
7736
  case "content":
7496
7737
  return ctx.contentBlob.includes(sig.value);
7738
+ case "composerType":
7739
+ return sig.value.length > 0 && ctx.composerType === sig.value;
7497
7740
  case "file":
7498
7741
  return ctx.files.some((f) => {
7499
7742
  switch (sig.mode) {
@@ -7510,9 +7753,12 @@ function matchSignal(sig, ctx) {
7510
7753
  }
7511
7754
  }
7512
7755
  function matchRule(rule, ctx) {
7513
- if (rule.all) return rule.all.every((s) => matchSignal(s, ctx));
7514
- if (rule.any) return rule.any.some((s) => matchSignal(s, ctx));
7515
- return false;
7756
+ const groups = [];
7757
+ if (rule.all) groups.push(rule.all.every((s) => matchSignal(s, ctx)));
7758
+ if (rule.any) groups.push(rule.any.some((s) => matchSignal(s, ctx)));
7759
+ if (rule.none) groups.push(!rule.none.some((s) => matchSignal(s, ctx)));
7760
+ if (groups.length === 0) return false;
7761
+ return groups.every(Boolean);
7516
7762
  }
7517
7763
  var ROLE_RULES = [
7518
7764
  // Workspace/meta-repo root — same bridge signal `readMembers()`/
@@ -7570,8 +7816,33 @@ var STACK_RULES = [
7570
7816
  },
7571
7817
  { stack: ["tooling", "pm2"], any: [dep("pm2"), fileIncludes("ecosystem.config")] },
7572
7818
  { stack: ["tooling", "sentry"], any: [depPrefix("@sentry/")] },
7573
- { stack: ["tooling", "missing-prettier"], any: [depAbsent("prettier")] },
7574
- { stack: ["tooling", "missing-eslint"], any: [depAbsent("eslint")] },
7819
+ // "Missing" means no evidence of Prettier at all — not merely no direct dependency.
7820
+ // A repo configuring Prettier through a shared config package (`@scope/prettier-config`)
7821
+ // has no `prettier` key of its own, so `depAbsent('prettier')` reported it as missing
7822
+ // even with a `.prettierrc` sitting right there.
7823
+ {
7824
+ stack: ["tooling", "missing-prettier"],
7825
+ none: [
7826
+ dep("prettier"),
7827
+ depSuffix("prettier-config"),
7828
+ fileIncludes(".prettierrc"),
7829
+ fileIncludes("prettier.config.")
7830
+ ]
7831
+ },
7832
+ // Same shape as missing-prettier above (#374). ESLint has TWO shared-config naming
7833
+ // conventions rather than one — `@scope/eslint-config` and the older
7834
+ // `eslint-config-<name>` — so both are evidence, as is a flat (`eslint.config.*`) or
7835
+ // legacy (`.eslintrc*`) config file.
7836
+ {
7837
+ stack: ["tooling", "missing-eslint"],
7838
+ none: [
7839
+ dep("eslint"),
7840
+ depSuffix("eslint-config"),
7841
+ depPrefix("eslint-config-"),
7842
+ fileIncludes("eslint.config."),
7843
+ fileIncludes(".eslintrc")
7844
+ ]
7845
+ },
7575
7846
  {
7576
7847
  stack: ["tooling", "stripe"],
7577
7848
  any: [dep("stripe"), dep("@stripe/stripe-js"), dep("@stripe/react-stripe-js")]
@@ -7603,7 +7874,16 @@ var STACK_RULES = [
7603
7874
  { stack: ["backend", "php"], any: [fileEndsWith("composer.json")] },
7604
7875
  { stack: ["backend", "laravel"], any: [dep("laravel/framework")] },
7605
7876
  { stack: ["backend", "laravel"], any: [fileIncludes("app/Providers/"), fileIncludes("routes/")] },
7606
- { stack: ["backend", "wordpress"], any: [fileEndsWith("wp-config.php"), dep("roots/wordpress")] },
7877
+ // `wp-config.php` and `roots/wordpress` both describe a WordPress *site* — the thing
7878
+ // that runs the install. A WordPress *plugin* repo has neither; it ships source that
7879
+ // gets dropped into someone else's `wp-content/plugins/`, and declares that with
7880
+ // composer's `"type": "wordpress-plugin"`. Both shapes need WordPress context, so
7881
+ // both earn the stack. Site-only roles (`wordpress-*-site`) are unaffected — those
7882
+ // still come from the site signals alone (`finalizeRoles`, src/scanner/detection.ts).
7883
+ {
7884
+ stack: ["backend", "wordpress"],
7885
+ any: [fileEndsWith("wp-config.php"), dep("roots/wordpress"), composerType("wordpress-plugin")]
7886
+ },
7607
7887
  {
7608
7888
  stack: ["backend", "elementor"],
7609
7889
  any: [
@@ -7696,6 +7976,76 @@ async function buildContentBlob(root, files) {
7696
7976
  return parts.join("\n");
7697
7977
  }
7698
7978
 
7979
+ // src/scanner/workspace-packages.ts
7980
+ import path43 from "node:path";
7981
+ import fg5 from "fast-glob";
7982
+ import YAML2 from "yaml";
7983
+ init_fs();
7984
+ var PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
7985
+ var MAX_WORKSPACE_MEMBERS = 200;
7986
+ function isRepoRelativeGlob(glob) {
7987
+ if (glob.length === 0) return false;
7988
+ if (path43.isAbsolute(glob) || glob.startsWith("/")) return false;
7989
+ return !glob.split("/").includes("..");
7990
+ }
7991
+ function stringEntries(value) {
7992
+ if (!Array.isArray(value)) return [];
7993
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
7994
+ }
7995
+ function workspaceGlobsFromPackageJson(pkg) {
7996
+ if (!isRecord(pkg)) return [];
7997
+ const field = pkg.workspaces;
7998
+ if (Array.isArray(field)) return stringEntries(field);
7999
+ if (isRecord(field)) return stringEntries(field.packages);
8000
+ return [];
8001
+ }
8002
+ function workspaceGlobsFromPnpmYaml(text) {
8003
+ try {
8004
+ const doc = YAML2.parse(text);
8005
+ if (!isRecord(doc)) return [];
8006
+ return stringEntries(doc.packages);
8007
+ } catch {
8008
+ return [];
8009
+ }
8010
+ }
8011
+ async function readWorkspaceGlobs(root, pkg) {
8012
+ const fromPkg = workspaceGlobsFromPackageJson(pkg);
8013
+ const pnpmText = await readText(path43.join(root, PNPM_WORKSPACE_FILE));
8014
+ const fromPnpm = pnpmText == null ? [] : workspaceGlobsFromPnpmYaml(pnpmText);
8015
+ return [.../* @__PURE__ */ new Set([...fromPkg, ...fromPnpm])];
8016
+ }
8017
+ async function collectWorkspaceMemberPackages(root, globs) {
8018
+ const safeGlobs = globs.filter(isRepoRelativeGlob);
8019
+ if (safeGlobs.length === 0) return [];
8020
+ const patterns = safeGlobs.map((g) => `${g.replace(/\/+$/, "")}/package.json`);
8021
+ let matches;
8022
+ try {
8023
+ matches = await fg5(patterns, {
8024
+ cwd: root,
8025
+ dot: false,
8026
+ onlyFiles: true,
8027
+ followSymbolicLinks: false,
8028
+ suppressErrors: true,
8029
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
8030
+ });
8031
+ } catch {
8032
+ return [];
8033
+ }
8034
+ const rootManifest = path43.resolve(root, "package.json");
8035
+ const contained = [
8036
+ ...new Set(
8037
+ matches.map((rel) => resolveContainedPath(root, rel)).filter((abs) => abs != null && abs !== rootManifest)
8038
+ )
8039
+ ].sort().slice(0, MAX_WORKSPACE_MEMBERS);
8040
+ const read = await mapWithConcurrency(contained, async (abs) => {
8041
+ const pkg = await readJson(abs);
8042
+ if (!isRecord(pkg)) return null;
8043
+ const rel = path43.relative(root, path43.dirname(abs));
8044
+ return { dir: rel.split(path43.sep).join("/"), pkg };
8045
+ });
8046
+ return read.filter((entry) => entry != null);
8047
+ }
8048
+
7699
8049
  // src/scanner/write-sources-report.ts
7700
8050
  init_manifest_format();
7701
8051
  init_fs();
@@ -7758,6 +8108,20 @@ var SAFE_FILES = [
7758
8108
  "next.config.*",
7759
8109
  "tailwind.config.*",
7760
8110
  "components.json",
8111
+ // Prettier config, in every shape Prettier itself accepts. Presence is what the
8112
+ // `missing-prettier` rule needs: a repo can configure Prettier entirely through a
8113
+ // shared config package, with no `prettier` dependency of its own to detect.
8114
+ ".prettierrc*",
8115
+ "prettier.config.*",
8116
+ "**/.prettierrc*",
8117
+ "**/prettier.config.*",
8118
+ // ESLint config, flat and legacy, for the same reason (#374): a repo can configure
8119
+ // ESLint entirely through a shared config package with no `eslint` dependency of
8120
+ // its own, so presence of the config file is the only local evidence.
8121
+ "eslint.config.*",
8122
+ ".eslintrc*",
8123
+ "**/eslint.config.*",
8124
+ "**/.eslintrc*",
7761
8125
  "playwright.config.*",
7762
8126
  "phpunit.xml",
7763
8127
  "artisan",
@@ -7797,11 +8161,17 @@ var SAFE_FILES = [
7797
8161
  "Gemfile"
7798
8162
  ];
7799
8163
  async function scanProject(root, options = {}) {
7800
- const pkg = await readJson(path43.join(root, "package.json"));
7801
- const composer = await readJson(path43.join(root, "composer.json"));
8164
+ const pkg = await readJson(path44.join(root, "package.json"));
8165
+ const composer = await readJson(path44.join(root, "composer.json"));
7802
8166
  const files = await listFiles(root, SAFE_FILES);
7803
8167
  const safeFiles = files.filter((f) => !blocked(f));
7804
- const depVersions = dependencyVersionMap(pkg, composer);
8168
+ const workspaceGlobs = await readWorkspaceGlobs(root, pkg ?? void 0);
8169
+ const memberPackages = await collectWorkspaceMemberPackages(root, workspaceGlobs);
8170
+ const depVersions = dependencyVersionMap(
8171
+ pkg,
8172
+ composer,
8173
+ memberPackages.map((m) => m.pkg)
8174
+ );
7805
8175
  const deps = [...depVersions.keys()].sort();
7806
8176
  const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
7807
8177
  const contentBlob = await buildContentBlob(root, safeFiles);
@@ -7809,7 +8179,8 @@ async function scanProject(root, options = {}) {
7809
8179
  deps: new Set(deps),
7810
8180
  depVersions,
7811
8181
  files: safeFiles,
7812
- contentBlob
8182
+ contentBlob,
8183
+ composerType: typeof composer?.type === "string" ? composer.type : ""
7813
8184
  });
7814
8185
  const roles = finalizeRoles(detection.roles, deps, safeFiles);
7815
8186
  const stacks = detection.stacks;
@@ -7828,7 +8199,7 @@ async function scanProject(root, options = {}) {
7828
8199
  crossRepoHints.push("Monorepo orchestration detected");
7829
8200
  const unsupportedSignals = collectUnsupportedSignals(safeFiles);
7830
8201
  const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
7831
- const repoNameFallback = pkg?.name ? String(pkg.name) : path43.basename((await resolveRoots(root)).mainRoot);
8202
+ const repoNameFallback = pkg?.name ? String(pkg.name) : path44.basename((await resolveRoots(root)).mainRoot);
7832
8203
  const context = {
7833
8204
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7834
8205
  repoName: repoNameFallback,
@@ -7852,15 +8223,15 @@ async function scanProject(root, options = {}) {
7852
8223
  // src/scanner/read-context.ts
7853
8224
  async function isCacheFresh(root, cachePath) {
7854
8225
  const markerPaths = [
7855
- path44.join(root, "package.json"),
7856
- path44.join(root, "composer.json"),
7857
- path44.join(root, WORKSPACE_FILE),
7858
- path44.join(root, REPOS_MANIFEST_FILE)
8226
+ path45.join(root, "package.json"),
8227
+ path45.join(root, "composer.json"),
8228
+ path45.join(root, WORKSPACE_FILE),
8229
+ path45.join(root, REPOS_MANIFEST_FILE)
7859
8230
  ];
7860
- const cacheStat = await fs36.stat(cachePath);
8231
+ const cacheStat = await fs37.stat(cachePath);
7861
8232
  for (const markerPath of markerPaths) {
7862
- if (!await fs36.pathExists(markerPath)) continue;
7863
- const markerStat = await fs36.stat(markerPath);
8233
+ if (!await fs37.pathExists(markerPath)) continue;
8234
+ const markerStat = await fs37.stat(markerPath);
7864
8235
  if (markerStat.mtimeMs > cacheStat.mtimeMs) return false;
7865
8236
  }
7866
8237
  return true;
@@ -7975,7 +8346,7 @@ async function runDoctor(options) {
7975
8346
  } else {
7976
8347
  ok(`- HOOKS OK: ${hooks.message}`);
7977
8348
  }
7978
- const rootClaudeMdPath = path45.join(root, "CLAUDE.md");
8349
+ const rootClaudeMdPath = path46.join(root, "CLAUDE.md");
7979
8350
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
7980
8351
  if (!rootClaudeMdContent) {
7981
8352
  flag(
@@ -8018,10 +8389,10 @@ async function runDoctor(options) {
8018
8389
  ok("- CLAUDE.md: import block present");
8019
8390
  }
8020
8391
  for (const target of actualTargets) {
8021
- const resolved = path45.resolve(root, target);
8022
- const relFromRoot = path45.relative(root, resolved);
8023
- const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path45.sep}`) || path45.isAbsolute(relFromRoot);
8024
- if (escapesRoot || !await fs37.pathExists(resolved)) {
8392
+ const resolved = path46.resolve(root, target);
8393
+ const relFromRoot = path46.relative(root, resolved);
8394
+ const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path46.sep}`) || path46.isAbsolute(relFromRoot);
8395
+ if (escapesRoot || !await fs38.pathExists(resolved)) {
8025
8396
  flag(
8026
8397
  `- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
8027
8398
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
@@ -8039,7 +8410,7 @@ async function runDoctor(options) {
8039
8410
  );
8040
8411
  }
8041
8412
  const workflowPath = hausPath(root, "WORKFLOW.md");
8042
- const workflowExists = await fs37.pathExists(workflowPath);
8413
+ const workflowExists = await fs38.pathExists(workflowPath);
8043
8414
  if (!workflowExists) {
8044
8415
  flag(
8045
8416
  "- .haus-workflow/WORKFLOW.md: missing (run `haus apply --write`)",
@@ -8054,15 +8425,15 @@ async function runDoctor(options) {
8054
8425
  } else {
8055
8426
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
8056
8427
  const bodyContent = workflowContent.slice(firstLine.length + 1);
8057
- const cachePath = path45.join(getCacheDir(), "templates/agentic-workflow-standard.md");
8058
- const bundledPath = path45.join(
8428
+ const cachePath = path46.join(getCacheDir(), "templates/agentic-workflow-standard.md");
8429
+ const bundledPath = path46.join(
8059
8430
  packageRoot(),
8060
8431
  "library",
8061
8432
  "global",
8062
8433
  "templates",
8063
8434
  "agentic-workflow-standard.md"
8064
8435
  );
8065
- const templatePath = await fs37.pathExists(cachePath) ? cachePath : bundledPath;
8436
+ const templatePath = await fs38.pathExists(cachePath) ? cachePath : bundledPath;
8066
8437
  const templateContent = await readText(templatePath);
8067
8438
  const verdict = checkManagedTamper(bodyContent, storedHashMatch?.[1], templateContent ?? null);
8068
8439
  if (verdict.status === "tampered" || verdict.status === "legacy-diverged") {
@@ -8090,7 +8461,7 @@ async function runDoctor(options) {
8090
8461
  }
8091
8462
  }
8092
8463
  const workflowConfigPath = hausPath(root, "workflow-config.md");
8093
- const workflowConfigExists = await fs37.pathExists(workflowConfigPath);
8464
+ const workflowConfigExists = await fs38.pathExists(workflowConfigPath);
8094
8465
  if (!workflowConfigExists) {
8095
8466
  flag(
8096
8467
  "- .haus-workflow/workflow-config.md: missing (run `haus apply --write`)",
@@ -8098,7 +8469,7 @@ async function runDoctor(options) {
8098
8469
  "haus apply --write"
8099
8470
  );
8100
8471
  } else {
8101
- const cfg = await fs37.readFile(workflowConfigPath, "utf8");
8472
+ const cfg = await fs38.readFile(workflowConfigPath, "utf8");
8102
8473
  const unfilled = cfg.split("\n").filter((l) => l.includes("<!-- fill in")).length;
8103
8474
  if (unfilled > 0) {
8104
8475
  suggest(
@@ -8110,11 +8481,11 @@ async function runDoctor(options) {
8110
8481
  ok("- .haus-workflow/workflow-config.md: OK (project-owned)");
8111
8482
  }
8112
8483
  }
8113
- const docsSkillInstalled = await fs37.pathExists(
8114
- path45.join(root, ".claude", "skills", "writing-documentation", "SKILL.md")
8484
+ const docsSkillInstalled = await fs38.pathExists(
8485
+ path46.join(root, ".claude", "skills", "writing-documentation", "SKILL.md")
8115
8486
  );
8116
8487
  if (docsSkillInstalled) {
8117
- if (await fs37.pathExists(path45.join(root, "docs", "SUMMARY.md"))) {
8488
+ if (await fs38.pathExists(path46.join(root, "docs", "SUMMARY.md"))) {
8118
8489
  ok("- PROJECT DOCS: OK (docs/SUMMARY.md present)");
8119
8490
  } else {
8120
8491
  suggest(
@@ -8125,7 +8496,7 @@ async function runDoctor(options) {
8125
8496
  }
8126
8497
  }
8127
8498
  if (workflowExists) {
8128
- const prettierIgnore = await readText(path45.join(root, ".prettierignore")) ?? "";
8499
+ const prettierIgnore = await readText(path46.join(root, ".prettierignore")) ?? "";
8129
8500
  const lines = prettierIgnore.split("\n");
8130
8501
  const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
8131
8502
  if (missing.length > 0) {
@@ -8241,8 +8612,53 @@ async function runDoctor(options) {
8241
8612
  "find its catalog id and run `haus recommend --include <id>` then `haus apply --write`, or remove the reference"
8242
8613
  );
8243
8614
  }
8244
- } else {
8245
- ok("- SKILL REFS: OK (no unresolved cross-skill references)");
8615
+ }
8616
+ const agentCatalogNames = catalogItems === null ? [] : catalogItems.filter((item) => item.type === "agent").map((item) => installTargetFor(item)).flatMap((target) => target.ok ? [path46.basename(target.relPath, ".md")] : []);
8617
+ const agentRefIssues = await findUnresolvedAgentReferences(root, {
8618
+ catalogAgentNames: agentCatalogNames
8619
+ });
8620
+ if (agentRefIssues.length > 0) {
8621
+ const describeAgent = (i) => `${i.file}:${i.line} "${i.agentName}"`;
8622
+ const installable = agentRefIssues.filter((i) => i.inCatalog);
8623
+ const unknown = agentRefIssues.filter((i) => !i.inCatalog);
8624
+ if (installable.length > 0) {
8625
+ const names = [...new Set(installable.map((i) => i.agentName))];
8626
+ flag(
8627
+ `- AGENT REFS: ${installable.length} reference(s) point at an agent the catalog can install but this project has not (${names.join(", ")}) (${installable.slice(0, 3).map(describeAgent).join("; ")}${installable.length > 3 ? "; \u2026" : ""})`,
8628
+ `${installable.length} rule/agent reference(s) send agents after ${names.join(", ")}, which is available but not installed`,
8629
+ "find its catalog id and run `haus recommend --include <id>` then `haus apply --write`"
8630
+ );
8631
+ }
8632
+ if (unknown.length > 0) {
8633
+ const names = [...new Set(unknown.map((i) => i.agentName))];
8634
+ suggest(
8635
+ `- AGENT REFS: ${unknown.length} reference(s) name an agent that exists neither here nor in the catalog (${names.join(", ")}) (${unknown.slice(0, 3).map(describeAgent).join("; ")}${unknown.length > 3 ? "; \u2026" : ""})`,
8636
+ `${unknown.length} rule/agent reference(s) send agents after ${names.join(", ")}, which was never shipped`,
8637
+ "correct or drop the reference upstream (catalog-owned prose), so agents stop being sent after a tool that does not exist"
8638
+ );
8639
+ }
8640
+ }
8641
+ if (skillRefIssues.length === 0 && agentRefIssues.length === 0) {
8642
+ ok("- SKILL REFS: OK (no unresolved cross-skill or agent references)");
8643
+ }
8644
+ const recommendedIds = new Set(
8645
+ (recommendation?.recommended ?? []).map((entry) => entry.id).filter((id) => typeof id === "string")
8646
+ );
8647
+ if (catalogItems !== null && recommendedIds.size > 0) {
8648
+ const catalogIds = new Set(catalogItems.map((item) => item.id));
8649
+ const excludedIds = new Set(await readProjectExcludedIds(root));
8650
+ const activePinIds = activePins.map((pin) => pin.id);
8651
+ const lockedIds = (await readJson(hausPath(root, "haus.lock.json")) ?? []).map((entry) => entry.id).filter((id) => typeof id === "string");
8652
+ const drifted = lockedIds.filter(
8653
+ (id) => catalogIds.has(id) && !recommendedIds.has(id) && !excludedIds.has(id) && !isSupersededByPlugin(id, activePinIds)
8654
+ );
8655
+ if (drifted.length > 0) {
8656
+ suggest(
8657
+ `- ELIGIBILITY DRIFT: ${drifted.length} installed item(s) are no longer recommended by the current signals (${drifted.slice(0, 3).join(", ")}${drifted.length > 3 ? ", \u2026" : ""})`,
8658
+ `${drifted.length} installed item(s) no longer match anything haus can detect in this repo \u2014 often a role that only \`deep-context.json\` knew about, which is not tracked in git`,
8659
+ "declare the role or stack in .haus-workflow/haus.config.json (declaredRoles / declaredStacks), re-run the docs pass to regenerate deep-context.json, or `haus remove <id>` if the item is genuinely no longer wanted"
8660
+ );
8661
+ }
8246
8662
  }
8247
8663
  if (claudeConfigDirIsRelocated()) {
8248
8664
  const configMigration = await probeGlobalConfigDirMigration();
@@ -8321,10 +8737,10 @@ async function runDoctor(options) {
8321
8737
  );
8322
8738
  } else {
8323
8739
  const lock = await readJson(hausPath(root, "haus.lock.json"));
8324
- const recommendedIds = new Set(
8740
+ const recommendedIds2 = new Set(
8325
8741
  recommendation.recommended.map((r) => r.id).filter((id) => typeof id === "string")
8326
8742
  );
8327
- const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds).map((entry) => entry.id);
8743
+ const orphaned = findOrphanedLockEntries(lock ?? [], recommendedIds2).map((entry) => entry.id);
8328
8744
  if (orphaned.length > 0) {
8329
8745
  suggest(
8330
8746
  `- CATALOG ITEMS: ${orphaned.length} installed item(s) no longer in the current recommendation (${orphaned.join(", ")})`,
@@ -8380,7 +8796,7 @@ async function runDoctor(options) {
8380
8796
  let onDisk = false;
8381
8797
  for (const rel of row.paths ?? []) {
8382
8798
  const abs = resolveContainedPath(root, rel);
8383
- if (abs !== null && await fs37.pathExists(abs)) {
8799
+ if (abs !== null && await fs38.pathExists(abs)) {
8384
8800
  onDisk = true;
8385
8801
  break;
8386
8802
  }
@@ -8396,7 +8812,7 @@ async function runDoctor(options) {
8396
8812
  );
8397
8813
  }
8398
8814
  }
8399
- const pkgJson = await readJson(path45.join(packageRoot(), "package.json"));
8815
+ const pkgJson = await readJson(path46.join(packageRoot(), "package.json"));
8400
8816
  const currentVersion = pkgJson?.version ?? "0.0.0";
8401
8817
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
8402
8818
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -8447,15 +8863,15 @@ async function runDoctor(options) {
8447
8863
  }
8448
8864
 
8449
8865
  // src/commands/update.ts
8450
- import path48 from "node:path";
8866
+ import path49 from "node:path";
8451
8867
  init_github_rate_limit();
8452
8868
 
8453
8869
  // src/claude/refresh-project.ts
8454
- import fs38 from "fs-extra";
8870
+ import fs39 from "fs-extra";
8455
8871
  init_fs();
8456
8872
  init_paths();
8457
8873
  async function isHausProject(root) {
8458
- if (await fs38.pathExists(hausPath(root, "recommendation.json"))) return true;
8874
+ if (await fs39.pathExists(hausPath(root, "recommendation.json"))) return true;
8459
8875
  const result = await readJsonDetailed(claudePath(root, "settings.json"));
8460
8876
  if (result.status === "ok") return result.value._haus != null;
8461
8877
  return false;
@@ -8471,8 +8887,8 @@ async function refreshProjectApply(root) {
8471
8887
 
8472
8888
  // src/install/apply.ts
8473
8889
  import crypto2 from "node:crypto";
8474
- import path46 from "node:path";
8475
- import fs39 from "fs-extra";
8890
+ import path47 from "node:path";
8891
+ import fs40 from "fs-extra";
8476
8892
  init_fs();
8477
8893
  init_logger();
8478
8894
  init_paths();
@@ -8561,64 +8977,64 @@ function hashContent(content2) {
8561
8977
  }
8562
8978
  function destFileMissing(destPath) {
8563
8979
  try {
8564
- return !fs39.statSync(destPath).isFile();
8980
+ return !fs40.statSync(destPath).isFile();
8565
8981
  } catch {
8566
8982
  return true;
8567
8983
  }
8568
8984
  }
8569
8985
  function sourceVersion() {
8570
8986
  try {
8571
- const pkgPath = path46.join(packageRoot(), "package.json");
8572
- const pkg = JSON.parse(fs39.readFileSync(pkgPath, "utf8"));
8987
+ const pkgPath = path47.join(packageRoot(), "package.json");
8988
+ const pkg = JSON.parse(fs40.readFileSync(pkgPath, "utf8"));
8573
8989
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
8574
8990
  } catch {
8575
8991
  return "haus@0.0.0";
8576
8992
  }
8577
8993
  }
8578
8994
  function globalSrcDir() {
8579
- return path46.join(packageRoot(), "library", "global");
8995
+ return path47.join(packageRoot(), "library", "global");
8580
8996
  }
8581
8997
  function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
8582
- const skillDir = path46.join(skillsDir, skillName);
8998
+ const skillDir = path47.join(skillsDir, skillName);
8583
8999
  const entries = [];
8584
- for (const rel of fs39.readdirSync(skillDir, { recursive: true })) {
9000
+ for (const rel of fs40.readdirSync(skillDir, { recursive: true })) {
8585
9001
  if (rel === "SKILL.md") continue;
8586
- const abs = path46.join(skillDir, rel);
8587
- if (!fs39.statSync(abs).isFile()) continue;
8588
- const posixRel = rel.split(path46.sep).join("/");
9002
+ const abs = path47.join(skillDir, rel);
9003
+ if (!fs40.statSync(abs).isFile()) continue;
9004
+ const posixRel = rel.split(path47.sep).join("/");
8589
9005
  entries.push({
8590
9006
  stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
8591
- srcRelPath: path46.join("library", "global", "skills", skillName, rel),
8592
- destPath: path46.join(claudeDir, "skills", skillName, rel)
9007
+ srcRelPath: path47.join("library", "global", "skills", skillName, rel),
9008
+ destPath: path47.join(claudeDir, "skills", skillName, rel)
8593
9009
  });
8594
9010
  }
8595
9011
  return entries;
8596
9012
  }
8597
9013
  function collectSourceFiles(srcDir, claudeDir) {
8598
9014
  const entries = [];
8599
- const skillsDir = path46.join(srcDir, "skills");
8600
- if (fs39.pathExistsSync(skillsDir)) {
8601
- for (const skillName of fs39.readdirSync(skillsDir)) {
8602
- const skillFile = path46.join(skillsDir, skillName, "SKILL.md");
8603
- if (fs39.pathExistsSync(skillFile)) {
9015
+ const skillsDir = path47.join(srcDir, "skills");
9016
+ if (fs40.pathExistsSync(skillsDir)) {
9017
+ for (const skillName of fs40.readdirSync(skillsDir)) {
9018
+ const skillFile = path47.join(skillsDir, skillName, "SKILL.md");
9019
+ if (fs40.pathExistsSync(skillFile)) {
8604
9020
  entries.push({
8605
9021
  stableId: `skill.${skillName}`,
8606
- srcRelPath: path46.join("library", "global", "skills", skillName, "SKILL.md"),
8607
- destPath: path46.join(claudeDir, "skills", skillName, "SKILL.md")
9022
+ srcRelPath: path47.join("library", "global", "skills", skillName, "SKILL.md"),
9023
+ destPath: path47.join(claudeDir, "skills", skillName, "SKILL.md")
8608
9024
  });
8609
9025
  entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
8610
9026
  }
8611
9027
  }
8612
9028
  }
8613
- const commandsDir = path46.join(srcDir, "commands");
8614
- if (fs39.pathExistsSync(commandsDir)) {
8615
- for (const fileName of fs39.readdirSync(commandsDir)) {
9029
+ const commandsDir = path47.join(srcDir, "commands");
9030
+ if (fs40.pathExistsSync(commandsDir)) {
9031
+ for (const fileName of fs40.readdirSync(commandsDir)) {
8616
9032
  if (!fileName.endsWith(".md")) continue;
8617
9033
  const commandName = fileName.slice(0, -".md".length);
8618
9034
  entries.push({
8619
9035
  stableId: `command.${commandName}`,
8620
- srcRelPath: path46.join("library", "global", "commands", fileName),
8621
- destPath: path46.join(claudeDir, "commands", fileName)
9036
+ srcRelPath: path47.join("library", "global", "commands", fileName),
9037
+ destPath: path47.join(claudeDir, "commands", fileName)
8622
9038
  });
8623
9039
  }
8624
9040
  }
@@ -8650,7 +9066,7 @@ async function applyInstall(options = {}) {
8650
9066
  };
8651
9067
  const manifestFiles = [];
8652
9068
  for (const entry of sourceFiles) {
8653
- const srcPath = path46.join(packageRoot(), entry.srcRelPath);
9069
+ const srcPath = path47.join(packageRoot(), entry.srcRelPath);
8654
9070
  const rawContent = await readText(srcPath);
8655
9071
  if (rawContent === void 0) {
8656
9072
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -8671,7 +9087,7 @@ async function applyInstall(options = {}) {
8671
9087
  }
8672
9088
  continue;
8673
9089
  }
8674
- const destExists = fs39.pathExistsSync(entry.destPath);
9090
+ const destExists = fs40.pathExistsSync(entry.destPath);
8675
9091
  if (destExists) {
8676
9092
  const currentContent = await readText(entry.destPath);
8677
9093
  if (currentContent !== void 0) {
@@ -8711,7 +9127,7 @@ async function applyInstall(options = {}) {
8711
9127
  }
8712
9128
  let mergedSettings;
8713
9129
  if (!check) {
8714
- const fragmentPath = path46.join(srcDir, "settings-fragments", "hooks.json");
9130
+ const fragmentPath = path47.join(srcDir, "settings-fragments", "hooks.json");
8715
9131
  const fragments = await loadHooksFragment(fragmentPath);
8716
9132
  const settings = await readSettings();
8717
9133
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -8730,13 +9146,13 @@ async function applyInstall(options = {}) {
8730
9146
  manifestFiles.push(entry);
8731
9147
  continue;
8732
9148
  }
8733
- if (!fs39.pathExistsSync(entry.destPath)) continue;
9149
+ if (!fs40.pathExistsSync(entry.destPath)) continue;
8734
9150
  const content2 = await readText(entry.destPath);
8735
9151
  if (!content2) continue;
8736
9152
  const hasHeader = parseMarkdownHeader(content2) !== void 0;
8737
9153
  const currentHash = hashContent(content2);
8738
9154
  if (hasHeader && currentHash === entry.hash) {
8739
- if (!dryRun) await fs39.remove(entry.destPath);
9155
+ if (!dryRun) await fs40.remove(entry.destPath);
8740
9156
  result.deleted.push(entry.destPath);
8741
9157
  } else {
8742
9158
  warn(`Orphaned file ${entry.destPath} was user-modified \u2014 leaving in place`);
@@ -8810,7 +9226,7 @@ function summarizeLockDiff(before, after) {
8810
9226
  // src/update/lockfile.ts
8811
9227
  import { constants as fsConstants } from "node:fs";
8812
9228
  import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
8813
- import path47 from "node:path";
9229
+ import path48 from "node:path";
8814
9230
  init_fs();
8815
9231
  init_paths();
8816
9232
  async function readLockSummary(root) {
@@ -8850,7 +9266,7 @@ async function backupLockfile(root) {
8850
9266
  await mkdir(backupDir, { recursive: true });
8851
9267
  let stamp = Date.now();
8852
9268
  for (; ; ) {
8853
- const dest = path47.join(backupDir, lockBackupFileName(stamp));
9269
+ const dest = path48.join(backupDir, lockBackupFileName(stamp));
8854
9270
  try {
8855
9271
  await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
8856
9272
  return;
@@ -8901,7 +9317,7 @@ function diffLock(before, after) {
8901
9317
  }
8902
9318
  async function hasLocalOverrides(root) {
8903
9319
  try {
8904
- await readFile2(path47.join(root, ".claude", "settings.json"), "utf8");
9320
+ await readFile2(path48.join(root, ".claude", "settings.json"), "utf8");
8905
9321
  return true;
8906
9322
  } catch {
8907
9323
  return false;
@@ -8946,7 +9362,7 @@ async function runUpdate(options) {
8946
9362
  return;
8947
9363
  }
8948
9364
  if (options.check) {
8949
- const pkgJson2 = await readJson(path48.join(packageRoot(), "package.json"));
9365
+ const pkgJson2 = await readJson(path49.join(packageRoot(), "package.json"));
8950
9366
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
8951
9367
  const [
8952
9368
  status,
@@ -8998,7 +9414,7 @@ async function runUpdate(options) {
8998
9414
  }
8999
9415
  return;
9000
9416
  }
9001
- const pkgJson = await readJson(path48.join(packageRoot(), "package.json"));
9417
+ const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
9002
9418
  const currentVersion = pkgJson?.version ?? "0.0.0";
9003
9419
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
9004
9420
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -9055,7 +9471,7 @@ async function runFromHookCheck(root) {
9055
9471
  try {
9056
9472
  const summary = await readLockSummary(root);
9057
9473
  if (summary.count === 0) return;
9058
- const pkgJson = await readJson(path48.join(packageRoot(), "package.json"));
9474
+ const pkgJson = await readJson(path49.join(packageRoot(), "package.json"));
9059
9475
  const currentVersion = pkgJson?.version ?? "0.0.0";
9060
9476
  const pin = readProjectCatalogPin(root);
9061
9477
  const honoursPin = pin.kind === "pinned" && summary.catalogRef === pin.ref;
@@ -9239,7 +9655,7 @@ init_exec();
9239
9655
  init_logger();
9240
9656
  import { existsSync as existsSync2 } from "node:fs";
9241
9657
  import { readdir } from "node:fs/promises";
9242
- import path49 from "node:path";
9658
+ import path50 from "node:path";
9243
9659
  var GIT_LOCATION_VARS = [
9244
9660
  "GIT_DIR",
9245
9661
  "GIT_WORK_TREE",
@@ -9299,11 +9715,11 @@ async function runClone(url, opts = {}) {
9299
9715
  process.exitCode = 1;
9300
9716
  return;
9301
9717
  }
9302
- const target = path49.resolve(opts.dir?.trim() || repoNameFromUrl(url));
9718
+ const target = path50.resolve(opts.dir?.trim() || repoNameFromUrl(url));
9303
9719
  if (existsSync2(target) && !await isEmptyDirectory(target)) {
9304
9720
  const check = await checkExistingTarget(target, url);
9305
9721
  if (check.kind === "matches") {
9306
- log(`\u2022 ${path49.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
9722
+ log(`\u2022 ${path50.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
9307
9723
  return;
9308
9724
  }
9309
9725
  const message2 = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
@@ -9330,8 +9746,8 @@ async function runClone(url, opts = {}) {
9330
9746
 
9331
9747
  // src/install/scaffold.ts
9332
9748
  init_logger();
9333
- import path50 from "node:path";
9334
- import fs40 from "fs-extra";
9749
+ import path51 from "node:path";
9750
+ import fs41 from "fs-extra";
9335
9751
  async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
9336
9752
  const scaffolded = [];
9337
9753
  const skipped = [];
@@ -9344,27 +9760,27 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
9344
9760
  skipped.push(name);
9345
9761
  if (outcome === "skipped-exists") skippedExisting.push(name);
9346
9762
  };
9347
- const realRoot = await fs40.realpath(catalogRoot).catch(() => null);
9763
+ const realRoot = await fs41.realpath(catalogRoot).catch(() => null);
9348
9764
  for (const item of items) {
9349
9765
  if (item.type !== "config") continue;
9350
- const sourcePath = path50.resolve(catalogRoot, item.path);
9351
- const rel = path50.relative(catalogRoot, sourcePath);
9352
- const escapes = rel === "" || path50.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
9766
+ const sourcePath = path51.resolve(catalogRoot, item.path);
9767
+ const rel = path51.relative(catalogRoot, sourcePath);
9768
+ const escapes = rel === "" || path51.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
9353
9769
  if (escapes) {
9354
9770
  warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
9355
9771
  continue;
9356
9772
  }
9357
- const realSource = await fs40.realpath(sourcePath).catch(() => null);
9773
+ const realSource = await fs41.realpath(sourcePath).catch(() => null);
9358
9774
  if (!realRoot || !realSource) {
9359
9775
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
9360
9776
  continue;
9361
9777
  }
9362
- const realRel = path50.relative(realRoot, realSource);
9363
- if (realRel !== "" && (path50.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
9778
+ const realRel = path51.relative(realRoot, realSource);
9779
+ if (realRel !== "" && (path51.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
9364
9780
  warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
9365
9781
  continue;
9366
9782
  }
9367
- const stat = await fs40.lstat(sourcePath).catch(() => null);
9783
+ const stat = await fs41.lstat(sourcePath).catch(() => null);
9368
9784
  if (!stat) {
9369
9785
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
9370
9786
  continue;
@@ -9374,14 +9790,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
9374
9790
  continue;
9375
9791
  }
9376
9792
  if (stat.isFile()) {
9377
- const filename = path50.basename(sourcePath);
9378
- const dest = path50.join(projectRoot, filename);
9793
+ const filename = path51.basename(sourcePath);
9794
+ const dest = path51.join(projectRoot, filename);
9379
9795
  record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
9380
9796
  } else if (stat.isDirectory()) {
9381
- const entries = await fs40.readdir(sourcePath);
9797
+ const entries = await fs41.readdir(sourcePath);
9382
9798
  for (const entry of entries) {
9383
- const src = path50.join(sourcePath, entry);
9384
- const dest = path50.join(projectRoot, entry);
9799
+ const src = path51.join(sourcePath, entry);
9800
+ const dest = path51.join(projectRoot, entry);
9385
9801
  record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
9386
9802
  }
9387
9803
  }
@@ -9389,42 +9805,42 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
9389
9805
  return { scaffolded, skipped, skippedExisting };
9390
9806
  }
9391
9807
  async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
9392
- const destDisplay = path50.relative(projectRoot, dest) || path50.basename(dest);
9393
- const srcStat = await fs40.lstat(src).catch(() => null);
9808
+ const destDisplay = path51.relative(projectRoot, dest) || path51.basename(dest);
9809
+ const srcStat = await fs41.lstat(src).catch(() => null);
9394
9810
  if (!srcStat) {
9395
- warn(`Skipping ${path50.basename(src)}: source not found`);
9811
+ warn(`Skipping ${path51.basename(src)}: source not found`);
9396
9812
  return "skipped-refused";
9397
9813
  }
9398
9814
  if (srcStat.isSymbolicLink()) {
9399
- warn(`Skipping ${path50.basename(src)}: source is a symlink`);
9815
+ warn(`Skipping ${path51.basename(src)}: source is a symlink`);
9400
9816
  return "skipped-refused";
9401
9817
  }
9402
- const exists = await fs40.pathExists(dest);
9818
+ const exists = await fs41.pathExists(dest);
9403
9819
  if (exists && !opts.force) {
9404
9820
  warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
9405
9821
  return "skipped-exists";
9406
9822
  }
9407
9823
  if (opts.dryRun) {
9408
- log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path50.basename(dest)} (${itemId})`);
9824
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path51.basename(dest)} (${itemId})`);
9409
9825
  return "scaffolded";
9410
9826
  }
9411
9827
  if (exists && opts.force) {
9412
- await fs40.remove(dest);
9828
+ await fs41.remove(dest);
9413
9829
  }
9414
- await fs40.ensureDir(path50.dirname(dest));
9415
- await fs40.copy(src, dest, {
9830
+ await fs41.ensureDir(path51.dirname(dest));
9831
+ await fs41.copy(src, dest, {
9416
9832
  overwrite: true,
9417
9833
  dereference: false,
9418
9834
  // A vanished/unreadable entry mid-walk must skip that path, not crash the copy.
9419
9835
  filter: (s) => {
9420
9836
  try {
9421
- return !fs40.lstatSync(s).isSymbolicLink();
9837
+ return !fs41.lstatSync(s).isSymbolicLink();
9422
9838
  } catch {
9423
9839
  return false;
9424
9840
  }
9425
9841
  }
9426
9842
  });
9427
- if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path50.basename(dest)} (${itemId})`);
9843
+ if (!opts.quiet) log(`${exists ? "Overwrote" : "Created"} ${path51.basename(dest)} (${itemId})`);
9428
9844
  return "scaffolded";
9429
9845
  }
9430
9846
 
@@ -9553,7 +9969,7 @@ async function runDecisionsGuard(options = {}) {
9553
9969
  }
9554
9970
 
9555
9971
  // src/commands/decisions.ts
9556
- import path51 from "node:path";
9972
+ import path52 from "node:path";
9557
9973
 
9558
9974
  // src/decisions/next-number.ts
9559
9975
  async function nextDecisionNumber(root) {
@@ -9664,7 +10080,7 @@ async function runDecisions(sub, arg, options = {}) {
9664
10080
  return;
9665
10081
  }
9666
10082
  if (sub === "validate") {
9667
- const target = arg ? path51.resolve(root, arg) : void 0;
10083
+ const target = arg ? path52.resolve(root, arg) : void 0;
9668
10084
  if (!target) {
9669
10085
  error("usage: haus decisions validate <path-to-decision.md>");
9670
10086
  process.exitCode = 1;
@@ -9940,61 +10356,6 @@ async function runGuard(kind, _options) {
9940
10356
  // src/claude/setup-core.ts
9941
10357
  init_github_rate_limit();
9942
10358
 
9943
- // src/claude/project-exclusions.ts
9944
- import fs41 from "fs-extra";
9945
- init_project_pin();
9946
- init_fs();
9947
- init_logger();
9948
- init_paths();
9949
- function normalizeExcludedIds(value) {
9950
- if (!Array.isArray(value)) return [];
9951
- return [
9952
- ...new Set(
9953
- value.filter((entry) => typeof entry === "string" && entry.length > 0)
9954
- )
9955
- ];
9956
- }
9957
- async function readProjectExcludedIds(root) {
9958
- try {
9959
- const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
9960
- if (!data) return [];
9961
- return normalizeExcludedIds(data.excludedIds);
9962
- } catch (err) {
9963
- if (err.code === "ENOENT") return [];
9964
- warn(
9965
- `${PROJECT_CONFIG_REL} could not be read for excludedIds (${err instanceof Error ? err.message : String(err)}) \u2014 treating as none`
9966
- );
9967
- return [];
9968
- }
9969
- }
9970
- function isItemExcluded(item, excluded) {
9971
- if (excluded.has(item.id)) return true;
9972
- return normalizeFormerIds(item.formerIds).some((formerId) => excluded.has(formerId));
9973
- }
9974
- async function addProjectExcludedIds(root, ids) {
9975
- const toAdd = [...new Set(ids.filter((id) => id.length > 0))];
9976
- if (toAdd.length === 0) return;
9977
- const file = hausPath(root, PROJECT_CONFIG_FILE);
9978
- let existing = {};
9979
- if (await fs41.pathExists(file)) {
9980
- try {
9981
- const parsed = JSON.parse(await fs41.readFile(file, "utf8"));
9982
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
9983
- existing = { ...parsed };
9984
- } else {
9985
- warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with excludedIds only`);
9986
- }
9987
- } catch (err) {
9988
- warn(
9989
- `${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with excludedIds only`
9990
- );
9991
- }
9992
- }
9993
- const merged = normalizeExcludedIds(existing.excludedIds);
9994
- const next = [.../* @__PURE__ */ new Set([...merged, ...toAdd])].sort();
9995
- await writeJson(file, { ...existing, excludedIds: next });
9996
- }
9997
-
9998
10359
  // src/recommender/recommend.ts
9999
10360
  init_fs();
10000
10361
  init_paths();
@@ -10055,8 +10416,11 @@ async function recommend(root, context, opts = {}) {
10055
10416
  (x) => x.toLowerCase()
10056
10417
  )
10057
10418
  );
10058
- const roleSet = /* @__PURE__ */ new Set([...scannerRoles, ...deepRoles]);
10059
- const stackSet = /* @__PURE__ */ new Set([...scannerStacks, ...deepStacks]);
10419
+ const declared = await readDeclaredContext(root);
10420
+ const declaredRoles = new Set(declared.roles);
10421
+ const declaredStacks = new Set(declared.stacks);
10422
+ const roleSet = /* @__PURE__ */ new Set([...scannerRoles, ...deepRoles, ...declaredRoles]);
10423
+ const stackSet = /* @__PURE__ */ new Set([...scannerStacks, ...deepStacks, ...declaredStacks]);
10060
10424
  const depSet = scannerDeps;
10061
10425
  const recommended = [];
10062
10426
  const skipped = [];
@@ -10071,8 +10435,32 @@ async function recommend(root, context, opts = {}) {
10071
10435
  ...gates ? { gates: gates.map(({ name, passed }) => ({ name, passed })) } : {}
10072
10436
  });
10073
10437
  };
10074
- const roleSignal = (name) => scannerRoles.has(name.toLowerCase()) ? `role:${name}` : `deep:role:${name}`;
10075
- const stackSignal = (name) => scannerStacks.has(name.toLowerCase()) ? `tag:${name}` : `deep:tag:${name}`;
10438
+ const roleSignal = (name) => {
10439
+ const key = name.toLowerCase();
10440
+ if (scannerRoles.has(key)) return `role:${name}`;
10441
+ if (declaredRoles.has(key)) return `declared:role:${name}`;
10442
+ return `deep:role:${name}`;
10443
+ };
10444
+ const stackSignal = (name) => {
10445
+ const key = name.toLowerCase();
10446
+ if (scannerStacks.has(key)) return `tag:${name}`;
10447
+ if (declaredStacks.has(key)) return `declared:tag:${name}`;
10448
+ return `deep:tag:${name}`;
10449
+ };
10450
+ const markDeclaredProvenance = (signal) => {
10451
+ if (signal == null) return signal;
10452
+ const separator = signal.indexOf(":");
10453
+ if (separator === -1) return signal;
10454
+ const kind = signal.slice(0, separator);
10455
+ const value = signal.slice(separator + 1).toLowerCase();
10456
+ if (kind === "role" && !scannerRoles.has(value) && declaredRoles.has(value)) {
10457
+ return `declared:${signal}`;
10458
+ }
10459
+ if (kind === "stack" && !scannerStacks.has(value) && declaredStacks.has(value)) {
10460
+ return `declared:${signal}`;
10461
+ }
10462
+ return signal;
10463
+ };
10076
10464
  const buildGateChecks = (item, itemTags, normSource) => {
10077
10465
  const gates = [];
10078
10466
  const add = (name, passed, code, message2, signal) => {
@@ -10220,7 +10608,11 @@ async function recommend(root, context, opts = {}) {
10220
10608
  if (changedMatch)
10221
10609
  push("changed-file-match", "changed file match", `changedFile:${changedMatch}`);
10222
10610
  if (requiresAnySatisfied && !reasons.some((r) => r.code === "stack-match")) {
10223
- push("requires-any-match", "requires-any signal match", requiresAnySatisfied.signal);
10611
+ push(
10612
+ "requires-any-match",
10613
+ "requires-any signal match",
10614
+ markDeclaredProvenance(requiresAnySatisfied.signal)
10615
+ );
10224
10616
  }
10225
10617
  const gateSummary = gates.map(({ name, passed }) => ({ name, passed }));
10226
10618
  const hasEvidence = reasons.some((r) => r.code !== "default-baseline");
@@ -10252,7 +10644,15 @@ async function recommend(root, context, opts = {}) {
10252
10644
  }
10253
10645
  applyCoInstallSuppression(recommended, skipped);
10254
10646
  applyManualExcludes(excludedIds, items, recommended, skipped);
10255
- const includeWarnings = applyManualIncludes(opts.include ?? [], items, recommended, skipped);
10647
+ const persistedIncludes = (await readProjectIncludedIds(root)).filter(
10648
+ (id) => !excludedIds.includes(id)
10649
+ );
10650
+ const includeWarnings = applyManualIncludes(
10651
+ [.../* @__PURE__ */ new Set([...opts.include ?? [], ...persistedIncludes])],
10652
+ items,
10653
+ recommended,
10654
+ skipped
10655
+ );
10256
10656
  const optInEligible = buildOptInEligible(items, skipped);
10257
10657
  recommended.sort((a, b) => a.id.localeCompare(b.id));
10258
10658
  skipped.sort((a, b) => a.id.localeCompare(b.id));
@@ -10463,9 +10863,9 @@ function buildOptInEligible(items, skipped) {
10463
10863
  }
10464
10864
 
10465
10865
  // src/commands/workspace/discover.ts
10466
- import path52 from "node:path";
10467
- import fg5 from "fast-glob";
10468
- import YAML2 from "yaml";
10866
+ import path53 from "node:path";
10867
+ import fg6 from "fast-glob";
10868
+ import YAML3 from "yaml";
10469
10869
  init_fs();
10470
10870
  init_logger();
10471
10871
  var DEFAULT_MAX_DEPTH = 3;
@@ -10500,16 +10900,16 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
10500
10900
  suppressErrors: true
10501
10901
  };
10502
10902
  const [gitMatches, manifestMatches] = await Promise.all([
10503
- fg5(GIT_MARKER, { ...commonOpts, ignore: IGNORE }),
10504
- fg5(MANIFEST_MARKERS, { ...commonOpts, ignore: [...IGNORE, "**/.git/**"] })
10903
+ fg6(GIT_MARKER, { ...commonOpts, ignore: IGNORE }),
10904
+ fg6(MANIFEST_MARKERS, { ...commonOpts, ignore: [...IGNORE, "**/.git/**"] })
10505
10905
  ]);
10506
10906
  const gitDirs = /* @__PURE__ */ new Set();
10507
10907
  const manifestDirs = /* @__PURE__ */ new Set();
10508
10908
  for (const match of gitMatches) {
10509
- gitDirs.add(path52.posix.dirname(match));
10909
+ gitDirs.add(path53.posix.dirname(match));
10510
10910
  }
10511
10911
  for (const match of manifestMatches) {
10512
- manifestDirs.add(path52.posix.dirname(match));
10912
+ manifestDirs.add(path53.posix.dirname(match));
10513
10913
  }
10514
10914
  const repoRoots = [...gitDirs];
10515
10915
  const manifestSorted = [...manifestDirs].sort(
@@ -10526,9 +10926,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
10526
10926
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
10527
10927
  const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
10528
10928
  return mapWithConcurrency(repoRoots, async (relDir) => {
10529
- const absDir = path52.resolve(workspaceRoot, relDir);
10530
- const pkg = await readJson(path52.join(absDir, "package.json"));
10531
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path52.basename(absDir);
10929
+ const absDir = path53.resolve(workspaceRoot, relDir);
10930
+ const pkg = await readJson(path53.join(absDir, "package.json"));
10931
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path53.basename(absDir);
10532
10932
  let role = "auto";
10533
10933
  try {
10534
10934
  const scan = await scanProject(absDir);
@@ -10564,14 +10964,14 @@ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
10564
10964
  };
10565
10965
  }
10566
10966
  function renderWorkspaceYaml(config) {
10567
- return YAML2.stringify({
10967
+ return YAML3.stringify({
10568
10968
  client: config.client,
10569
10969
  repos: config.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
10570
10970
  relationships: config.relationships
10571
10971
  });
10572
10972
  }
10573
10973
  async function runDiscover(workspaceRoot, opts = {}) {
10574
- const yamlPath = path52.join(workspaceRoot, "haus.workspace.yaml");
10974
+ const yamlPath = path53.join(workspaceRoot, "haus.workspace.yaml");
10575
10975
  const existingText = await readText(yamlPath);
10576
10976
  const existing = parseWorkspaceConfig(existingText);
10577
10977
  if (existingText && !existing) {
@@ -10978,7 +11378,7 @@ async function runInstall(options, deps = {}) {
10978
11378
  }
10979
11379
 
10980
11380
  // src/new-project/create-project.ts
10981
- import path53 from "node:path";
11381
+ import path54 from "node:path";
10982
11382
  import fs42 from "fs-extra";
10983
11383
  init_exec();
10984
11384
  init_logger();
@@ -11142,7 +11542,7 @@ var defaultDeps = {
11142
11542
  var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
11143
11543
  async function createProject(opts, deps = defaultDeps) {
11144
11544
  const steps = [];
11145
- const projectPath = path53.resolve(opts.parentDir, opts.name);
11545
+ const projectPath = path54.resolve(opts.parentDir, opts.name);
11146
11546
  const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
11147
11547
  const fail = (step, detail) => {
11148
11548
  steps.push({ step, status: "failed", detail });
@@ -11150,7 +11550,7 @@ async function createProject(opts, deps = defaultDeps) {
11150
11550
  };
11151
11551
  const stack = getStack(opts.stackId);
11152
11552
  if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
11153
- if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path53.dirname(projectPath) !== path53.resolve(opts.parentDir)) {
11553
+ if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path54.dirname(projectPath) !== path54.resolve(opts.parentDir)) {
11154
11554
  return fail(
11155
11555
  "preflight",
11156
11556
  `Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
@@ -11234,15 +11634,15 @@ async function createProject(opts, deps = defaultDeps) {
11234
11634
  steps.push({ step: "scaffold", status: "done" });
11235
11635
  phase = "git";
11236
11636
  if (!opts.json) log("\u25B6 git: init/branch/commit");
11237
- if (!await fs42.pathExists(path53.join(projectPath, ".git"))) {
11637
+ if (!await fs42.pathExists(path54.join(projectPath, ".git"))) {
11238
11638
  const init = await deps.git(["init"], { cwd: projectPath });
11239
11639
  if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
11240
11640
  }
11241
11641
  const branch = await deps.git(["branch", "-M", "main"], { cwd: projectPath });
11242
11642
  if (branch.exitCode !== 0) return fail("git", `git branch -M main failed: ${branch.stderr}`);
11243
11643
  const lockfile = opts.pm === "yarn" ? "yarn.lock" : opts.pm === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
11244
- if (!await fs42.pathExists(path53.join(projectPath, lockfile))) {
11245
- await fs42.writeFile(path53.join(projectPath, lockfile), "");
11644
+ if (!await fs42.pathExists(path54.join(projectPath, lockfile))) {
11645
+ await fs42.writeFile(path54.join(projectPath, lockfile), "");
11246
11646
  }
11247
11647
  const add = await deps.git(["add", "-A"], { cwd: projectPath });
11248
11648
  if (add.exitCode !== 0) return fail("git", `git add failed: ${add.stderr}`);
@@ -11491,6 +11891,7 @@ async function runRecommend(options) {
11491
11891
  const root = process.cwd();
11492
11892
  const context = await readContextOrScan(root);
11493
11893
  const include = parseIdList(options.include);
11894
+ if (include.length > 0) await addProjectIncludedIds(root, include);
11494
11895
  const activePluginIds = await activeGlobalPluginIds(root);
11495
11896
  const result = await recommend(root, context, { include, activePluginIds });
11496
11897
  await writeJson(hausPath(root, "recommendation.json"), result);
@@ -11511,7 +11912,7 @@ async function runRecommend(options) {
11511
11912
  }
11512
11913
 
11513
11914
  // src/commands/remove.ts
11514
- import path54 from "node:path";
11915
+ import path55 from "node:path";
11515
11916
  init_fs();
11516
11917
  init_logger();
11517
11918
  init_paths();
@@ -11616,7 +12017,7 @@ async function runRemove(ids, options = {}) {
11616
12017
  }
11617
12018
  const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
11618
12019
  const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
11619
- report.backupDir = backupRoot === null ? null : path54.basename(backupRoot);
12020
+ report.backupDir = backupRoot === null ? null : path55.basename(backupRoot);
11620
12021
  for (const { id, existing } of cleared) {
11621
12022
  await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
11622
12023
  }
@@ -11704,7 +12105,7 @@ async function runScan(options) {
11704
12105
  }
11705
12106
 
11706
12107
  // src/commands/undo.ts
11707
- import path55 from "node:path";
12108
+ import path56 from "node:path";
11708
12109
  import fs43 from "fs-extra";
11709
12110
 
11710
12111
  // src/claude/managed-paths.ts
@@ -11754,7 +12155,7 @@ async function settingsHasHausContent(root) {
11754
12155
  return settings._haus != null;
11755
12156
  }
11756
12157
  async function claudeMdHasHausBlock(root) {
11757
- const filePath = path55.join(root, "CLAUDE.md");
12158
+ const filePath = path56.join(root, "CLAUDE.md");
11758
12159
  if (!await fs43.pathExists(filePath)) return false;
11759
12160
  const text = await fs43.readFile(filePath, "utf8");
11760
12161
  return text.includes(BLOCK_BEGIN);
@@ -11767,15 +12168,15 @@ async function stripProjectSettings(root) {
11767
12168
  const hasContent = Object.keys(settings).length > 0;
11768
12169
  if (hasContent) {
11769
12170
  await writeProjectSettings(root, settings);
11770
- log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
12171
+ log(`Stripped haus rules from ${path56.relative(root, settingsPath)} (user settings preserved).`);
11771
12172
  return true;
11772
12173
  }
11773
12174
  await fs43.remove(settingsPath);
11774
- log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
12175
+ log(`Removed ${path56.relative(root, settingsPath)} (no user-owned settings remained).`);
11775
12176
  return true;
11776
12177
  }
11777
12178
  async function stripRootClaudeMd(root) {
11778
- const filePath = path55.join(root, "CLAUDE.md");
12179
+ const filePath = path56.join(root, "CLAUDE.md");
11779
12180
  if (!await fs43.pathExists(filePath)) return false;
11780
12181
  const prev = await fs43.readFile(filePath, "utf8");
11781
12182
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -11803,12 +12204,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
11803
12204
  const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
11804
12205
  for (const abs of managedAbsPaths) {
11805
12206
  if (!await fs43.pathExists(abs)) continue;
11806
- const rel = path55.relative(root, abs);
11807
- const backupPath = path55.join(backupRoot, rel);
11808
- await fs43.ensureDir(path55.dirname(backupPath));
12207
+ const rel = path56.relative(root, abs);
12208
+ const backupPath = path56.join(backupRoot, rel);
12209
+ await fs43.ensureDir(path56.dirname(backupPath));
11809
12210
  await fs43.copy(abs, backupPath);
11810
12211
  }
11811
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
12212
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path56.relative(root, backupRoot)}.`);
11812
12213
  }
11813
12214
  async function runUndo(options) {
11814
12215
  const root = options.root ?? process.cwd();
@@ -11822,7 +12223,7 @@ async function runUndo(options) {
11822
12223
  log("Nothing to remove: no haus-managed files found in this directory.");
11823
12224
  return;
11824
12225
  }
11825
- const relTargets = managed.map((p) => path55.relative(root, p));
12226
+ const relTargets = managed.map((p) => path56.relative(root, p));
11826
12227
  const summaryParts = [...relTargets];
11827
12228
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
11828
12229
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -11850,7 +12251,7 @@ User-owned .claude/ files will be preserved.`
11850
12251
  for (const abs of coreManaged) {
11851
12252
  if (!await fs43.pathExists(abs)) continue;
11852
12253
  await fs43.remove(abs);
11853
- log(`Removed ${path55.relative(root, abs)}`);
12254
+ log(`Removed ${path56.relative(root, abs)}`);
11854
12255
  }
11855
12256
  for (const { id, existing } of lockRemovable) {
11856
12257
  await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
@@ -11867,7 +12268,7 @@ init_fs();
11867
12268
  init_logger();
11868
12269
  init_paths();
11869
12270
  import crypto3 from "node:crypto";
11870
- import path56 from "node:path";
12271
+ import path57 from "node:path";
11871
12272
  import fs44 from "fs-extra";
11872
12273
  async function runUninstall(options = {}) {
11873
12274
  const { force = false, removePlugins = removeGlobalPlugins } = options;
@@ -11924,14 +12325,14 @@ async function runUninstall(options = {}) {
11924
12325
  continue;
11925
12326
  }
11926
12327
  await fs44.remove(entry.destPath);
11927
- await pruneEmptyDir(path56.dirname(entry.destPath));
12328
+ await pruneEmptyDir(path57.dirname(entry.destPath));
11928
12329
  result.deleted.push(entry.destPath);
11929
12330
  }
11930
12331
  const settings = await readSettings();
11931
12332
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
11932
12333
  await writeSettings(stripped);
11933
12334
  result.hooksStripped = true;
11934
- const hausDir = path56.join(globalClaudeDir(), "haus");
12335
+ const hausDir = path57.join(globalClaudeDir(), "haus");
11935
12336
  const manifestPath2 = hausManifestPath();
11936
12337
  if (fs44.pathExistsSync(manifestPath2)) {
11937
12338
  await fs44.remove(manifestPath2);
@@ -12008,7 +12409,7 @@ async function runUninstallCommand(options) {
12008
12409
  }
12009
12410
 
12010
12411
  // src/commands/validate-catalog.ts
12011
- import path58 from "node:path";
12412
+ import path59 from "node:path";
12012
12413
 
12013
12414
  // src/catalog/validate-core.ts
12014
12415
  init_forbidden_content();
@@ -12016,7 +12417,7 @@ init_manifest_item_fields();
12016
12417
  init_path_safety();
12017
12418
  init_validation_rules2();
12018
12419
  import fs45 from "node:fs";
12019
- import path57 from "node:path";
12420
+ import path58 from "node:path";
12020
12421
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
12021
12422
  var ITEM_ID_PATTERN = /^haus\./;
12022
12423
  function auditForbiddenStacks(items) {
@@ -12167,7 +12568,7 @@ function readForAudit(absPath, label, failures) {
12167
12568
  }
12168
12569
  }
12169
12570
  function auditTemplateContent(manifestDir, absPath, itemId) {
12170
- const rel = path57.relative(manifestDir, absPath);
12571
+ const rel = path58.relative(manifestDir, absPath);
12171
12572
  const failures = [];
12172
12573
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
12173
12574
  if (text === null) return failures;
@@ -12189,25 +12590,25 @@ function auditShippedFiles(manifestDir, items) {
12189
12590
  const failures = [];
12190
12591
  for (const item of items) {
12191
12592
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
12192
- const absPath = path57.join(manifestDir, item.path);
12593
+ const absPath = path58.join(manifestDir, item.path);
12193
12594
  if (item.type === "skill") {
12194
- const skillMd = path57.join(absPath, "SKILL.md");
12595
+ const skillMd = path58.join(absPath, "SKILL.md");
12195
12596
  if (!fs45.existsSync(skillMd)) {
12196
- failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
12597
+ failures.push(`${item.id}: missing ${path58.relative(manifestDir, skillMd)}`);
12197
12598
  continue;
12198
12599
  }
12199
12600
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
12200
12601
  if (text === null) continue;
12201
12602
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
12202
12603
  failures.push(
12203
- ...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
12604
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path58.relative(manifestDir, skillMd)}`)
12204
12605
  );
12205
12606
  } else if (item.type === "agent") {
12206
12607
  if (!fs45.existsSync(absPath)) {
12207
12608
  failures.push(`${item.id}: missing agent file ${item.path}`);
12208
12609
  continue;
12209
12610
  }
12210
- const rel = path57.relative(manifestDir, absPath);
12611
+ const rel = path58.relative(manifestDir, absPath);
12211
12612
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
12212
12613
  if (text === null) continue;
12213
12614
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -12223,7 +12624,7 @@ function auditShippedFiles(manifestDir, items) {
12223
12624
  failures.push(`${item.id}: missing command file ${item.path}`);
12224
12625
  continue;
12225
12626
  }
12226
- const rel = path57.relative(manifestDir, absPath);
12627
+ const rel = path58.relative(manifestDir, absPath);
12227
12628
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
12228
12629
  if (text === null) continue;
12229
12630
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -12281,7 +12682,7 @@ function walkAllFiles(dir, fn, failures) {
12281
12682
  }
12282
12683
  for (const entry of entries) {
12283
12684
  if (entry.name === ".DS_Store") continue;
12284
- const full = path57.join(dir, entry.name);
12685
+ const full = path58.join(dir, entry.name);
12285
12686
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
12286
12687
  else fn(full);
12287
12688
  }
@@ -12305,12 +12706,12 @@ function auditDiskOrphans(manifestDir, items) {
12305
12706
  const failures = [];
12306
12707
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
12307
12708
  for (const dir of CONTENT_ROOTS) {
12308
- const abs = path57.join(manifestDir, dir);
12709
+ const abs = path58.join(manifestDir, dir);
12309
12710
  if (!fs45.existsSync(abs)) continue;
12310
12711
  walkAllFiles(
12311
12712
  abs,
12312
12713
  (file) => {
12313
- const relPath = path57.relative(manifestDir, file).replace(/\\/g, "/");
12714
+ const relPath = path58.relative(manifestDir, file).replace(/\\/g, "/");
12314
12715
  if (!isPathClaimed(relPath, claimedPaths)) {
12315
12716
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
12316
12717
  }
@@ -12331,7 +12732,7 @@ function walkMd(dir, fn, failures) {
12331
12732
  return;
12332
12733
  }
12333
12734
  for (const entry of entries) {
12334
- const full = path57.join(dir, entry.name);
12735
+ const full = path58.join(dir, entry.name);
12335
12736
  if (entry.isDirectory()) walkMd(full, fn, failures);
12336
12737
  else if (entry.name.endsWith(".md")) fn(full);
12337
12738
  }
@@ -12341,13 +12742,13 @@ function auditMarkdownContent(manifestDir, items) {
12341
12742
  const pathSourceMap = buildItemPathSourceMap(items);
12342
12743
  const pathTypeMap = buildItemPathTypeMap(items);
12343
12744
  for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
12344
- const abs = path57.join(manifestDir, dir);
12745
+ const abs = path58.join(manifestDir, dir);
12345
12746
  if (!fs45.existsSync(abs)) continue;
12346
12747
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
12347
12748
  walkMd(
12348
12749
  abs,
12349
12750
  (file) => {
12350
- const rel = path57.relative(manifestDir, file);
12751
+ const rel = path58.relative(manifestDir, file);
12351
12752
  const text = readForAudit(file, rel, failures);
12352
12753
  if (text === null) return;
12353
12754
  const norm = rel.replace(/\\/g, "/");
@@ -12395,8 +12796,8 @@ async function runValidateCatalog(manifestPath2) {
12395
12796
  process.exitCode = 1;
12396
12797
  return;
12397
12798
  }
12398
- const abs = path58.resolve(process.cwd(), manifestPath2);
12399
- const manifestDir = path58.dirname(abs);
12799
+ const abs = path59.resolve(process.cwd(), manifestPath2);
12800
+ const manifestDir = path59.dirname(abs);
12400
12801
  const data = await readJson(abs);
12401
12802
  if (!data?.items) {
12402
12803
  error(`Could not read catalog manifest at ${abs}`);
@@ -12417,7 +12818,7 @@ init_logger();
12417
12818
 
12418
12819
  // src/workspace/worktree/add.ts
12419
12820
  init_fs();
12420
- import path65 from "node:path";
12821
+ import path66 from "node:path";
12421
12822
  import fs49 from "fs-extra";
12422
12823
 
12423
12824
  // src/workspace/worktree/git-worktree.ts
@@ -12538,12 +12939,12 @@ async function hasUnpushedWork(cwd) {
12538
12939
  }
12539
12940
 
12540
12941
  // src/workspace/worktree/hydrate.ts
12541
- import path61 from "node:path";
12942
+ import path62 from "node:path";
12542
12943
  import fs47 from "fs-extra";
12543
12944
 
12544
12945
  // src/workspace/worktree/cow-copy.ts
12545
12946
  init_exec();
12546
- import path59 from "node:path";
12947
+ import path60 from "node:path";
12547
12948
  import fs46 from "fs-extra";
12548
12949
  async function getMacFilesystemType(dir) {
12549
12950
  const df = await runCommand("df", ["-P", dir]);
@@ -12578,11 +12979,11 @@ async function cowCopyDir(src, dest) {
12578
12979
  const srcExists = await fs46.pathExists(src);
12579
12980
  if (!srcExists)
12580
12981
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
12581
- const strategy = await detectCowStrategy(path59.dirname(dest));
12982
+ const strategy = await detectCowStrategy(path60.dirname(dest));
12582
12983
  if (strategy === "unsupported" || strategy === "unknown-platform") {
12583
12984
  return { strategy, attempted: false, ok: false };
12584
12985
  }
12585
- await fs46.ensureDir(path59.dirname(dest));
12986
+ await fs46.ensureDir(path60.dirname(dest));
12586
12987
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
12587
12988
  const result = await runCommand("cp", args);
12588
12989
  return {
@@ -12596,8 +12997,8 @@ async function cowCopyDir(src, dest) {
12596
12997
  // src/workspace/worktree/install.ts
12597
12998
  init_exec();
12598
12999
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
12599
- import path60 from "node:path";
12600
- import fg6 from "fast-glob";
13000
+ import path61 from "node:path";
13001
+ import fg7 from "fast-glob";
12601
13002
  function detectInstallPlan(signals) {
12602
13003
  if (signals.hasYarnLock) {
12603
13004
  return signals.hasPackageManagerField ? { manager: "yarn", command: "corepack", args: ["yarn", "install"] } : { manager: "yarn", command: "yarn", args: ["install"] };
@@ -12617,11 +13018,11 @@ function detectInstallPlan(signals) {
12617
13018
  return { manager: "none" };
12618
13019
  }
12619
13020
  async function computeLockfileSignals(dir) {
12620
- const hasYarnLock = existsSync3(path60.join(dir, "yarn.lock"));
12621
- const hasPnpmLock = existsSync3(path60.join(dir, "pnpm-lock.yaml"));
12622
- const hasPackageLock = existsSync3(path60.join(dir, "package-lock.json"));
12623
- const hasComposerLock = existsSync3(path60.join(dir, "composer.lock"));
12624
- const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
13021
+ const hasYarnLock = existsSync3(path61.join(dir, "yarn.lock"));
13022
+ const hasPnpmLock = existsSync3(path61.join(dir, "pnpm-lock.yaml"));
13023
+ const hasPackageLock = existsSync3(path61.join(dir, "package-lock.json"));
13024
+ const hasComposerLock = existsSync3(path61.join(dir, "composer.lock"));
13025
+ const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
12625
13026
  cwd: dir,
12626
13027
  onlyFiles: true,
12627
13028
  suppressErrors: true,
@@ -12634,7 +13035,7 @@ async function computeLockfileSignals(dir) {
12634
13035
  });
12635
13036
  const hasDotnetProject = dotnetFiles.length > 0;
12636
13037
  let hasPackageManagerField = false;
12637
- const pkgPath = path60.join(dir, "package.json");
13038
+ const pkgPath = path61.join(dir, "package.json");
12638
13039
  if (existsSync3(pkgPath)) {
12639
13040
  try {
12640
13041
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -12677,7 +13078,7 @@ function missingYarnrcEnvVars(content2, env) {
12677
13078
  return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
12678
13079
  }
12679
13080
  function missingYarnrcEnvVarsInDir(dir, env) {
12680
- const yarnrcPath = path60.join(dir, ".yarnrc.yml");
13081
+ const yarnrcPath = path61.join(dir, ".yarnrc.yml");
12681
13082
  if (!existsSync3(yarnrcPath)) return [];
12682
13083
  try {
12683
13084
  return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
@@ -12767,8 +13168,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
12767
13168
  const start = Date.now();
12768
13169
  const targets = [];
12769
13170
  for (const target of HYDRATION_TARGETS) {
12770
- const src = path61.join(mainMemberPath, target);
12771
- const dest = path61.join(worktreeMemberPath, target);
13171
+ const src = path62.join(mainMemberPath, target);
13172
+ const dest = path62.join(worktreeMemberPath, target);
12772
13173
  if (!await fs47.pathExists(src)) continue;
12773
13174
  const destExists = await fs47.pathExists(dest);
12774
13175
  if (!opts.force && destExists) continue;
@@ -12786,11 +13187,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
12786
13187
 
12787
13188
  // src/workspace/worktree/local-files.ts
12788
13189
  init_exec();
12789
- import path62 from "node:path";
13190
+ import path63 from "node:path";
12790
13191
  import fs48 from "fs-extra";
12791
13192
  var MACHINE_LOCAL_FILES = [
12792
13193
  "docker-compose.local.yml",
12793
- path62.join(".claude", "settings.local.json")
13194
+ path63.join(".claude", "settings.local.json")
12794
13195
  ];
12795
13196
  async function isTracked(repoRoot, relFile) {
12796
13197
  const gitPath = relFile.replace(/\\/g, "/");
@@ -12800,11 +13201,11 @@ async function isTracked(repoRoot, relFile) {
12800
13201
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
12801
13202
  const copied = [];
12802
13203
  for (const rel of MACHINE_LOCAL_FILES) {
12803
- const src = path62.join(mainMemberPath, rel);
13204
+ const src = path63.join(mainMemberPath, rel);
12804
13205
  if (!await fs48.pathExists(src)) continue;
12805
13206
  if (await isTracked(mainMemberPath, rel)) continue;
12806
- const dest = path62.join(worktreeMemberPath, rel);
12807
- await fs48.ensureDir(path62.dirname(dest));
13207
+ const dest = path63.join(worktreeMemberPath, rel);
13208
+ await fs48.ensureDir(path63.dirname(dest));
12808
13209
  await fs48.copy(src, dest, { overwrite: true });
12809
13210
  copied.push(rel);
12810
13211
  }
@@ -12813,15 +13214,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
12813
13214
 
12814
13215
  // src/workspace/worktree/root.ts
12815
13216
  import { existsSync as existsSync4 } from "node:fs";
12816
- import path63 from "node:path";
13217
+ import path64 from "node:path";
12817
13218
  async function resolveWorkspaceForWorktree(start) {
12818
13219
  const rootInfo = await resolveRoots(start);
12819
13220
  if (!rootInfo.isGitRepo) {
12820
13221
  return { ok: false, reason: "Not inside a git repository." };
12821
13222
  }
12822
13223
  const workspaceRoot = rootInfo.mainRoot;
12823
- const hasYaml = existsSync4(path63.join(workspaceRoot, WORKSPACE_FILE));
12824
- const hasManifest = existsSync4(path63.join(workspaceRoot, REPOS_MANIFEST_FILE));
13224
+ const hasYaml = existsSync4(path64.join(workspaceRoot, WORKSPACE_FILE));
13225
+ const hasManifest = existsSync4(path64.join(workspaceRoot, REPOS_MANIFEST_FILE));
12825
13226
  if (!hasYaml && !hasManifest) {
12826
13227
  return {
12827
13228
  ok: false,
@@ -12847,10 +13248,10 @@ function selectMembers(members, only) {
12847
13248
 
12848
13249
  // src/workspace/worktree/state.ts
12849
13250
  init_fs();
12850
- import path64 from "node:path";
13251
+ import path65 from "node:path";
12851
13252
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
12852
13253
  function worktreesDir(mainRoot) {
12853
- return path64.join(mainRoot, ".claude", "worktrees");
13254
+ return path65.join(mainRoot, ".claude", "worktrees");
12854
13255
  }
12855
13256
  function isValidWorktreeSlug(slug) {
12856
13257
  if (slug === "" || slug === "." || slug === "..") return false;
@@ -12863,15 +13264,15 @@ function worktreePath(mainRoot, slug) {
12863
13264
  `Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
12864
13265
  );
12865
13266
  }
12866
- return path64.join(worktreesDir(mainRoot), slug);
13267
+ return path65.join(worktreesDir(mainRoot), slug);
12867
13268
  }
12868
13269
  async function readWorktreeState(worktreeRoot) {
12869
- const state = await readJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE));
13270
+ const state = await readJson(path65.join(worktreeRoot, WORKTREE_STATE_FILE));
12870
13271
  if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
12871
13272
  return state;
12872
13273
  }
12873
13274
  async function writeWorktreeState(worktreeRoot, state) {
12874
- await writeJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE), state);
13275
+ await writeJson(path65.join(worktreeRoot, WORKTREE_STATE_FILE), state);
12875
13276
  }
12876
13277
 
12877
13278
  // src/workspace/worktree/add.ts
@@ -12905,7 +13306,7 @@ async function runAdd(opts) {
12905
13306
  const memberResults = [];
12906
13307
  for (const member of selected) {
12907
13308
  const start = Date.now();
12908
- const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
13309
+ const memberWorktreePath = path66.join(wsWorktreePath, member.folder);
12909
13310
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
12910
13311
  preferDefaultBranchFrom: true
12911
13312
  });
@@ -12935,7 +13336,7 @@ async function runAdd(opts) {
12935
13336
  await mapWithConcurrency(okResults, async (result) => {
12936
13337
  const member = selected.find((m) => m.id === result.member);
12937
13338
  if (!member) return;
12938
- const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
13339
+ const memberWorktreePath = path66.join(wsWorktreePath, member.folder);
12939
13340
  const hydrationStart = Date.now();
12940
13341
  if (opts.hydrate !== false) {
12941
13342
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -13002,25 +13403,25 @@ async function runDryRun(args) {
13002
13403
 
13003
13404
  // src/workspace/worktree/doctor.ts
13004
13405
  import { existsSync as existsSync6 } from "node:fs";
13005
- import path67 from "node:path";
13406
+ import path68 from "node:path";
13006
13407
 
13007
13408
  // src/workspace/worktree/orphans.ts
13008
13409
  import { existsSync as existsSync5 } from "node:fs";
13009
- import path66 from "node:path";
13410
+ import path67 from "node:path";
13010
13411
  var WORKSPACE_REPO_ID = "(workspace)";
13011
13412
  async function findOrphanedRegistrations(repos, wsWorktreesDir) {
13012
13413
  const orphans = [];
13013
13414
  for (const repo of repos) {
13014
13415
  const entries = await listWorktrees(repo.absPath);
13015
13416
  for (const entry of entries) {
13016
- const entryPath = path66.normalize(entry.path);
13017
- const marker = `${path66.sep}.claude${path66.sep}worktrees${path66.sep}`;
13417
+ const entryPath = path67.normalize(entry.path);
13418
+ const marker = `${path67.sep}.claude${path67.sep}worktrees${path67.sep}`;
13018
13419
  const idx = entryPath.indexOf(marker);
13019
13420
  if (idx === -1) continue;
13020
13421
  const rest = entryPath.slice(idx + marker.length);
13021
- const slug = rest.split(path66.sep)[0];
13422
+ const slug = rest.split(path67.sep)[0];
13022
13423
  if (!slug) continue;
13023
- if (existsSync5(path66.join(wsWorktreesDir, slug))) continue;
13424
+ if (existsSync5(path67.join(wsWorktreesDir, slug))) continue;
13024
13425
  orphans.push({
13025
13426
  repoId: repo.id,
13026
13427
  ownerRepo: repo.absPath,
@@ -13035,7 +13436,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
13035
13436
  // src/workspace/worktree/doctor.ts
13036
13437
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
13037
13438
  function looksHydrated(memberPath) {
13038
- return HYDRATION_MARKERS.some((marker) => existsSync6(path67.join(memberPath, marker)));
13439
+ return HYDRATION_MARKERS.some((marker) => existsSync6(path68.join(memberPath, marker)));
13039
13440
  }
13040
13441
  async function runDoctor2() {
13041
13442
  const rootInfo = await resolveRoots();
@@ -13051,7 +13452,7 @@ async function runDoctor2() {
13051
13452
  };
13052
13453
  }
13053
13454
  const workspaceRoot = rootInfo.mainRoot;
13054
- const isWorkspace = existsSync6(path67.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path67.join(workspaceRoot, REPOS_MANIFEST_FILE));
13455
+ const isWorkspace = existsSync6(path68.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path68.join(workspaceRoot, REPOS_MANIFEST_FILE));
13055
13456
  if (!isWorkspace) {
13056
13457
  return {
13057
13458
  isWorkspace: false,
@@ -13066,7 +13467,7 @@ async function runDoctor2() {
13066
13467
  };
13067
13468
  }
13068
13469
  const wsWorktreesDir = worktreesDir(workspaceRoot);
13069
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path67.sep}`);
13470
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path68.sep}`);
13070
13471
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
13071
13472
  const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
13072
13473
  const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
@@ -13077,10 +13478,10 @@ async function runDoctor2() {
13077
13478
  let slug;
13078
13479
  if (inWorkspaceWorktree) {
13079
13480
  const wtPath = rootInfo.repoRoot;
13080
- slug = rootInfo.worktreeName ?? path67.basename(wtPath);
13481
+ slug = rootInfo.worktreeName ?? path68.basename(wtPath);
13081
13482
  const state = await readWorktreeState(wtPath);
13082
13483
  for (const member of members) {
13083
- const memberPath = path67.join(wtPath, member.folder);
13484
+ const memberPath = path68.join(wtPath, member.folder);
13084
13485
  const materialized = existsSync6(memberPath);
13085
13486
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
13086
13487
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -13120,7 +13521,7 @@ async function runDoctor2() {
13120
13521
  wsWorktreesDir
13121
13522
  );
13122
13523
  const orphans = orphanScan.map(
13123
- (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path67.join(wsWorktreesDir, o.slug)} no longer exists.`
13524
+ (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path68.join(wsWorktreesDir, o.slug)} no longer exists.`
13124
13525
  );
13125
13526
  if (orphans.length > 0) {
13126
13527
  problems.push(...orphans);
@@ -13141,7 +13542,7 @@ async function runDoctor2() {
13141
13542
  // src/workspace/worktree/hydrate-command.ts
13142
13543
  init_fs();
13143
13544
  import { existsSync as existsSync7 } from "node:fs";
13144
- import path68 from "node:path";
13545
+ import path69 from "node:path";
13145
13546
  async function runHydrateWorktree(opts) {
13146
13547
  const rootInfo = await resolveRoots();
13147
13548
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -13152,7 +13553,7 @@ async function runHydrateWorktree(opts) {
13152
13553
  }
13153
13554
  const workspaceRoot = rootInfo.mainRoot;
13154
13555
  const wtPath = rootInfo.repoRoot;
13155
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path68.sep}`)) {
13556
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path69.sep}`)) {
13156
13557
  return {
13157
13558
  ok: false,
13158
13559
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -13164,13 +13565,13 @@ async function runHydrateWorktree(opts) {
13164
13565
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
13165
13566
  }
13166
13567
  const state = await readWorktreeState(wtPath);
13167
- const slug = state?.slug ?? path68.basename(wtPath);
13568
+ const slug = state?.slug ?? path69.basename(wtPath);
13168
13569
  const detectedBranch = await currentBranch(wtPath);
13169
13570
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
13170
13571
  if (opts.dryRun) {
13171
13572
  const results2 = await Promise.all(
13172
13573
  selected.map(async (member) => {
13173
- const memberPath = path68.join(wtPath, member.folder);
13574
+ const memberPath = path69.join(wtPath, member.folder);
13174
13575
  if (existsSync7(memberPath)) {
13175
13576
  return { member: member.id, folder: member.folder, status: "planned" };
13176
13577
  }
@@ -13191,7 +13592,7 @@ async function runHydrateWorktree(opts) {
13191
13592
  const results = await mapWithConcurrency(
13192
13593
  selected,
13193
13594
  async (member) => {
13194
- const memberPath = path68.join(wtPath, member.folder);
13595
+ const memberPath = path69.join(wtPath, member.folder);
13195
13596
  let branchAction;
13196
13597
  if (!existsSync7(memberPath)) {
13197
13598
  if (!opts.materialize) {
@@ -13254,7 +13655,7 @@ async function runHydrateWorktree(opts) {
13254
13655
  }
13255
13656
 
13256
13657
  // src/workspace/worktree/list.ts
13257
- import path69 from "node:path";
13658
+ import path70 from "node:path";
13258
13659
  import fs50 from "fs-extra";
13259
13660
  async function runList2() {
13260
13661
  const resolved = await resolveWorkspaceForWorktree();
@@ -13267,12 +13668,12 @@ async function runList2() {
13267
13668
  const configuredMembers = await readMembers(rootInfo);
13268
13669
  const worktrees = [];
13269
13670
  for (const slug of slugs) {
13270
- const wtPath = path69.join(dir, slug);
13671
+ const wtPath = path70.join(dir, slug);
13271
13672
  const state = await readWorktreeState(wtPath);
13272
13673
  const members = [];
13273
13674
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
13274
13675
  for (const m of source) {
13275
- const memberPath = path69.join(wtPath, m.folder);
13676
+ const memberPath = path70.join(wtPath, m.folder);
13276
13677
  const materialized = await fs50.pathExists(memberPath);
13277
13678
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
13278
13679
  members.push({
@@ -13334,7 +13735,7 @@ async function runPrune2(opts) {
13334
13735
  }
13335
13736
 
13336
13737
  // src/workspace/worktree/remove.ts
13337
- import path70 from "node:path";
13738
+ import path71 from "node:path";
13338
13739
  import fs51 from "fs-extra";
13339
13740
  async function runRemove2(opts) {
13340
13741
  const resolved = await resolveWorkspaceForWorktree();
@@ -13361,7 +13762,7 @@ async function runRemove2(opts) {
13361
13762
  targets.push({
13362
13763
  repo: configMember?.id ?? stateMember.id,
13363
13764
  ownerRepo,
13364
- path: path70.join(wsWorktreePath, stateMember.folder)
13765
+ path: path71.join(wsWorktreePath, stateMember.folder)
13365
13766
  });
13366
13767
  }
13367
13768
  const existingTargets = [];
@@ -13699,7 +14100,7 @@ async function runWorktree(action, opts = {}) {
13699
14100
 
13700
14101
  // src/commands/workspace.ts
13701
14102
  import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
13702
- import path79 from "node:path";
14103
+ import path80 from "node:path";
13703
14104
  init_fs();
13704
14105
  init_logger();
13705
14106
 
@@ -13758,7 +14159,7 @@ ${summaries.map(
13758
14159
 
13759
14160
  // src/commands/workspace/doctor.ts
13760
14161
  import { existsSync as existsSync8 } from "node:fs";
13761
- import path72 from "node:path";
14162
+ import path73 from "node:path";
13762
14163
  init_logger();
13763
14164
  init_paths();
13764
14165
 
@@ -13766,14 +14167,14 @@ init_paths();
13766
14167
  init_fs();
13767
14168
  init_paths();
13768
14169
  import { readFileSync as readFileSync4 } from "node:fs";
13769
- import path71 from "node:path";
14170
+ import path72 from "node:path";
13770
14171
  var MANIFEST_FILE2 = "workspace.manifest.json";
13771
14172
  function manifestPath(workspaceRoot) {
13772
14173
  return hausPath(workspaceRoot, MANIFEST_FILE2);
13773
14174
  }
13774
14175
  function hausVersion() {
13775
14176
  try {
13776
- const pkg = JSON.parse(readFileSync4(path71.join(packageRoot(), "package.json"), "utf8"));
14177
+ const pkg = JSON.parse(readFileSync4(path72.join(packageRoot(), "package.json"), "utf8"));
13777
14178
  return pkg.version ?? "0.0.0";
13778
14179
  } catch {
13779
14180
  return "0.0.0";
@@ -13865,7 +14266,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
13865
14266
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
13866
14267
  const catalogRefByRepo = [];
13867
14268
  for (const repo of config.repos) {
13868
- const repoRoot = path72.resolve(workspaceRoot, repo.path);
14269
+ const repoRoot = path73.resolve(workspaceRoot, repo.path);
13869
14270
  const entry = manifestByName.get(repo.name);
13870
14271
  if (!entry) {
13871
14272
  flag({
@@ -13938,7 +14339,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
13938
14339
  });
13939
14340
  continue;
13940
14341
  }
13941
- if (!existsSync8(path72.join(workspaceRoot, entry.path))) {
14342
+ if (!existsSync8(path73.join(workspaceRoot, entry.path))) {
13942
14343
  flag({
13943
14344
  repo: entry.repo,
13944
14345
  kind: "missing-linked-context-copy",
@@ -13984,17 +14385,17 @@ function emit(args) {
13984
14385
  init_logger();
13985
14386
 
13986
14387
  // src/workspace/link-context/link.ts
13987
- import path75 from "node:path";
14388
+ import path76 from "node:path";
13988
14389
  import fs54 from "fs-extra";
13989
14390
  init_fs();
13990
14391
  init_logger();
13991
14392
 
13992
14393
  // src/workspace/link-context/plan.ts
13993
- import path74 from "node:path";
14394
+ import path75 from "node:path";
13994
14395
  import fs53 from "fs-extra";
13995
14396
 
13996
14397
  // src/workspace/link-context/scan-source.ts
13997
- import path73 from "node:path";
14398
+ import path74 from "node:path";
13998
14399
  import fs52 from "fs-extra";
13999
14400
  var ASSET_SUBDIR = {
14000
14401
  skill: "skills",
@@ -14003,36 +14404,36 @@ var ASSET_SUBDIR = {
14003
14404
  };
14004
14405
  async function hasLinkableContext(member) {
14005
14406
  for (const subdir of Object.values(ASSET_SUBDIR)) {
14006
- if (await fs52.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
14407
+ if (await fs52.pathExists(path74.join(member.absPath, ".claude", subdir))) return true;
14007
14408
  }
14008
14409
  return false;
14009
14410
  }
14010
14411
  async function collectSkills(member) {
14011
- const dir = path73.join(member.absPath, ".claude", "skills");
14412
+ const dir = path74.join(member.absPath, ".claude", "skills");
14012
14413
  if (!await fs52.pathExists(dir)) return [];
14013
14414
  const names = await fs52.readdir(dir);
14014
14415
  const out = [];
14015
14416
  for (const name of [...names].sort()) {
14016
- const abs = path73.join(dir, name);
14417
+ const abs = path74.join(dir, name);
14017
14418
  const stat = await fs52.lstat(abs);
14018
14419
  if (!stat.isDirectory()) continue;
14019
- if (!await fs52.pathExists(path73.join(abs, "SKILL.md"))) continue;
14020
- out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
14420
+ if (!await fs52.pathExists(path74.join(abs, "SKILL.md"))) continue;
14421
+ out.push({ type: "skill", name, sourceRelPath: path74.posix.join(".claude", "skills", name) });
14021
14422
  }
14022
14423
  return out;
14023
14424
  }
14024
14425
  async function collectFlatMarkdown(member, type) {
14025
14426
  const subdir = ASSET_SUBDIR[type];
14026
- const dir = path73.join(member.absPath, ".claude", subdir);
14427
+ const dir = path74.join(member.absPath, ".claude", subdir);
14027
14428
  if (!await fs52.pathExists(dir)) return [];
14028
14429
  const names = await fs52.readdir(dir);
14029
14430
  const out = [];
14030
14431
  for (const fileName of [...names].sort()) {
14031
14432
  if (!fileName.endsWith(".md")) continue;
14032
- const abs = path73.join(dir, fileName);
14433
+ const abs = path74.join(dir, fileName);
14033
14434
  if (!(await fs52.lstat(abs)).isFile()) continue;
14034
14435
  const name = fileName.slice(0, -".md".length);
14035
- out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
14436
+ out.push({ type, name, sourceRelPath: path74.posix.join(".claude", subdir, fileName) });
14036
14437
  }
14037
14438
  return out;
14038
14439
  }
@@ -14047,9 +14448,9 @@ async function collectSourceAssets(member) {
14047
14448
 
14048
14449
  // src/workspace/link-context/plan.ts
14049
14450
  function repoFolderPrefix(member) {
14050
- const base = path74.posix.basename(member.folder);
14451
+ const base = path75.posix.basename(member.folder);
14051
14452
  if (base && base !== "." && base !== "/") return base;
14052
- const fromAbs = path74.basename(member.absPath);
14453
+ const fromAbs = path75.basename(member.absPath);
14053
14454
  return fromAbs || member.id;
14054
14455
  }
14055
14456
  function assetKey(type, destKey) {
@@ -14106,7 +14507,7 @@ async function buildLinkPlan(members) {
14106
14507
  name: asset.name,
14107
14508
  destKey,
14108
14509
  sourceRelPath: asset.sourceRelPath,
14109
- sourceAbsPath: path74.join(member.absPath, asset.sourceRelPath),
14510
+ sourceAbsPath: path75.join(member.absPath, asset.sourceRelPath),
14110
14511
  sourceHash
14111
14512
  });
14112
14513
  }
@@ -14117,7 +14518,7 @@ async function buildLinkPlan(members) {
14117
14518
  function destRelPathFor(type, destKey) {
14118
14519
  const subdir = `${type}s`;
14119
14520
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
14120
- return path75.posix.join(".claude", subdir, leaf);
14521
+ return path76.posix.join(".claude", subdir, leaf);
14121
14522
  }
14122
14523
  async function runLinkContext(workspaceRoot, opts = {}) {
14123
14524
  const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
@@ -14143,9 +14544,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
14143
14544
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
14144
14545
  for (const entry of plan.entries) {
14145
14546
  const destRel = destRelPathFor(entry.type, entry.destKey);
14146
- const destAbs = path75.join(workspaceRoot, destRel);
14547
+ const destAbs = path76.join(workspaceRoot, destRel);
14147
14548
  if (write) {
14148
- await fs54.ensureDir(path75.dirname(destAbs));
14549
+ await fs54.ensureDir(path76.dirname(destAbs));
14149
14550
  if (await fs54.pathExists(destAbs)) await fs54.remove(destAbs);
14150
14551
  await fs54.copy(entry.sourceAbsPath, destAbs, {
14151
14552
  overwrite: true,
@@ -14234,10 +14635,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
14234
14635
 
14235
14636
  // src/commands/workspace/setup.ts
14236
14637
  import { existsSync as existsSync9, statSync } from "node:fs";
14237
- import path77 from "node:path";
14638
+ import path78 from "node:path";
14238
14639
 
14239
14640
  // src/claude/write-workspace-claude-md.ts
14240
- import path76 from "node:path";
14641
+ import path77 from "node:path";
14241
14642
  import fs55 from "fs-extra";
14242
14643
  init_fs();
14243
14644
  init_logger();
@@ -14251,16 +14652,19 @@ function buildWorkspaceImportBlock(client, members) {
14251
14652
  `# Workspace: ${client}`,
14252
14653
  "",
14253
14654
  "Member repos:",
14655
+ "",
14254
14656
  ...memberLines
14255
14657
  ].join("\n");
14256
14658
  return `${WORKSPACE_BLOCK_BEGIN}
14659
+
14257
14660
  ${body}
14661
+
14258
14662
  ${WORKSPACE_BLOCK_END}`;
14259
14663
  }
14260
14664
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
14261
14665
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
14262
14666
  const dryRun = opts.dryRun ?? false;
14263
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
14667
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path77.join(workspaceRoot, "CLAUDE.md");
14264
14668
  const say = opts.quiet ? () => {
14265
14669
  } : log;
14266
14670
  const existed = await fs55.pathExists(filePath);
@@ -14293,20 +14697,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
14293
14697
  init_fs();
14294
14698
  init_logger();
14295
14699
  function resolveWorkspaceRoot(start = process.cwd()) {
14296
- let dir = path77.resolve(start);
14700
+ let dir = path78.resolve(start);
14297
14701
  for (; ; ) {
14298
- if (existsSync9(path77.join(dir, WORKSPACE_FILE))) return dir;
14299
- const parent = path77.dirname(dir);
14300
- if (parent === dir) return path77.resolve(start);
14702
+ if (existsSync9(path78.join(dir, WORKSPACE_FILE))) return dir;
14703
+ const parent = path78.dirname(dir);
14704
+ if (parent === dir) return path78.resolve(start);
14301
14705
  dir = parent;
14302
14706
  }
14303
14707
  }
14304
14708
  function isRootRepo(workspaceRoot, repoPath) {
14305
- return path77.resolve(workspaceRoot, repoPath) === path77.resolve(workspaceRoot);
14709
+ return path78.resolve(workspaceRoot, repoPath) === path78.resolve(workspaceRoot);
14306
14710
  }
14307
14711
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
14308
14712
  const apply = options.write ?? false;
14309
- const configText = await readText(path77.join(workspaceRoot, WORKSPACE_FILE));
14713
+ const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
14310
14714
  if (!configText) {
14311
14715
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
14312
14716
  process.exitCode = 1;
@@ -14333,7 +14737,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
14333
14737
  const statuses = [];
14334
14738
  const aggregateInputs = [];
14335
14739
  for (const repo of repos) {
14336
- const repoRoot = path77.resolve(workspaceRoot, repo.path);
14740
+ const repoRoot = path78.resolve(workspaceRoot, repo.path);
14337
14741
  say(`
14338
14742
  \u2192 ${repo.name} (${repo.path})`);
14339
14743
  let res;
@@ -14449,7 +14853,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
14449
14853
  const status = statusByName.get(repo.name);
14450
14854
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
14451
14855
  if (status?.status === "ok") {
14452
- const lock = await checkLock(path77.resolve(workspaceRoot, repo.path));
14856
+ const lock = await checkLock(path78.resolve(workspaceRoot, repo.path));
14453
14857
  manifestRepos.push({
14454
14858
  name: repo.name,
14455
14859
  path: repo.path,
@@ -14505,7 +14909,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
14505
14909
  try {
14506
14910
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
14507
14911
  if (linkResult.ok) {
14508
- written.push(...linkResult.added.map((rel) => path77.join(workspaceRoot, rel)));
14912
+ written.push(...linkResult.added.map((rel) => path78.join(workspaceRoot, rel)));
14509
14913
  if (!options.json) {
14510
14914
  say(
14511
14915
  `Linked ${linkResult.linked.length} cross-repo skill/agent/command copy(ies) into .claude/ (${linkResult.added.length} new, ${linkResult.removed.length} removed)${linkResult.skipped.length > 0 ? ` \u2014 ${linkResult.skipped.length} member(s) skipped, see above` : ""}.`
@@ -14538,7 +14942,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
14538
14942
 
14539
14943
  // src/commands/workspace/undo.ts
14540
14944
  import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
14541
- import path78 from "node:path";
14945
+ import path79 from "node:path";
14542
14946
  import fs56 from "fs-extra";
14543
14947
  init_fs();
14544
14948
  init_logger();
@@ -14569,7 +14973,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
14569
14973
  }
14570
14974
  if (options.dryRun) {
14571
14975
  for (const repo of config.repos) {
14572
- const repoRoot = path78.resolve(workspaceRoot, repo.path);
14976
+ const repoRoot = path79.resolve(workspaceRoot, repo.path);
14573
14977
  log(`
14574
14978
  \u2192 ${repo.name} (${repo.path})`);
14575
14979
  try {
@@ -14601,11 +15005,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
14601
15005
  ];
14602
15006
  for (const abs of workspaceArtifacts) {
14603
15007
  if (await fs56.pathExists(abs)) {
14604
- log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
15008
+ log(`[dry-run] Would remove ${path79.relative(workspaceRoot, abs)}`);
14605
15009
  }
14606
15010
  }
14607
15011
  if (!await fs56.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
14608
- const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
15012
+ const claudeMd = await readText(path79.join(workspaceRoot, "CLAUDE.md"));
14609
15013
  if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
14610
15014
  log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
14611
15015
  }
@@ -14624,7 +15028,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
14624
15028
  }
14625
15029
  let anyRepoFailed = false;
14626
15030
  for (const repo of config.repos) {
14627
- const repoRoot = path78.resolve(workspaceRoot, repo.path);
15031
+ const repoRoot = path79.resolve(workspaceRoot, repo.path);
14628
15032
  log(`
14629
15033
  \u2192 ${repo.name} (${repo.path})`);
14630
15034
  try {
@@ -14653,20 +15057,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
14653
15057
  const abs = hausPath(workspaceRoot, rel);
14654
15058
  if (await fs56.pathExists(abs)) {
14655
15059
  await fs56.remove(abs);
14656
- log(`Removed ${path78.relative(workspaceRoot, abs)}`);
15060
+ log(`Removed ${path79.relative(workspaceRoot, abs)}`);
14657
15061
  }
14658
15062
  }
14659
15063
  const manifestFile = manifestPath(workspaceRoot);
14660
15064
  if (await fs56.pathExists(manifestFile)) {
14661
15065
  await fs56.remove(manifestFile);
14662
- log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
15066
+ log(`Removed ${path79.relative(workspaceRoot, manifestFile)}`);
14663
15067
  }
14664
15068
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
14665
15069
  if (await fs56.pathExists(workspaceMdPath)) {
14666
15070
  await fs56.remove(workspaceMdPath);
14667
- log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
15071
+ log(`Removed ${path79.relative(workspaceRoot, workspaceMdPath)}`);
14668
15072
  } else {
14669
- const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
15073
+ const claudeMdPath = path79.join(workspaceRoot, "CLAUDE.md");
14670
15074
  if (existsSync10(claudeMdPath)) {
14671
15075
  const prev = await readText(claudeMdPath);
14672
15076
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
@@ -14735,7 +15139,7 @@ relationships: []
14735
15139
  log("Workspace initialized.");
14736
15140
  }
14737
15141
  async function scanWorkspace(workspaceRoot, opts) {
14738
- const configText = await readText(path79.join(workspaceRoot, WORKSPACE_FILE));
15142
+ const configText = await readText(path80.join(workspaceRoot, WORKSPACE_FILE));
14739
15143
  if (!configText) {
14740
15144
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
14741
15145
  process.exitCode = 1;
@@ -14757,7 +15161,7 @@ async function scanWorkspace(workspaceRoot, opts) {
14757
15161
  const inputs = [];
14758
15162
  const failures = [];
14759
15163
  for (const repo of config.repos) {
14760
- const repoRoot = path79.resolve(workspaceRoot, repo.path);
15164
+ const repoRoot = path80.resolve(workspaceRoot, repo.path);
14761
15165
  try {
14762
15166
  if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
14763
15167
  throw new Error("path is not a directory");
@@ -14826,7 +15230,7 @@ init_logger();
14826
15230
  init_paths();
14827
15231
  function cliVersion() {
14828
15232
  try {
14829
- const pkgPath = path80.join(packageRoot(), "package.json");
15233
+ const pkgPath = path81.join(packageRoot(), "package.json");
14830
15234
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
14831
15235
  return pkg.version ?? "0.0.0";
14832
15236
  } catch {
@@ -14836,7 +15240,7 @@ function cliVersion() {
14836
15240
  var program = new Command();
14837
15241
  function validateRuntimeNodeVersion() {
14838
15242
  try {
14839
- const pkgPath = path80.join(packageRoot(), "package.json");
15243
+ const pkgPath = path81.join(packageRoot(), "package.json");
14840
15244
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
14841
15245
  const requiredRange = pkg.engines?.node;
14842
15246
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {