@haus-tech/haus-workflow 3.1.2 → 4.0.1

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
@@ -1913,7 +1913,7 @@ var init_sync = __esm({
1913
1913
 
1914
1914
  // src/cli.ts
1915
1915
  import { readFileSync as readFileSync5 } from "node:fs";
1916
- import path79 from "node:path";
1916
+ import path80 from "node:path";
1917
1917
  import { Command } from "commander";
1918
1918
 
1919
1919
  // src/commands/apply.ts
@@ -3050,8 +3050,8 @@ function buildDenyRules() {
3050
3050
  for (const command of DENY_COMMANDS) {
3051
3051
  rules.push(`Bash(${command}:*)`);
3052
3052
  }
3053
- for (const path80 of DENY_PATHS) {
3054
- const pattern = DENY_DIRS.has(path80) ? `${path80}/**` : path80;
3053
+ for (const path81 of DENY_PATHS) {
3054
+ const pattern = DENY_DIRS.has(path81) ? `${path81}/**` : path81;
3055
3055
  for (const tool of FILE_TOOLS) {
3056
3056
  rules.push(`${tool}(${pattern})`);
3057
3057
  }
@@ -4441,7 +4441,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
4441
4441
  const relPaths = entry.paths ?? [];
4442
4442
  const existing = await resolveExistingTrackedPaths(root, migration.oldId, relPaths, warn);
4443
4443
  if (existing.length === 0) continue;
4444
- const gate = await hashGateEntry(root, { id: migration.oldId, hash: entry.hash }, existing);
4444
+ const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
4445
4445
  if (!gate.ok) {
4446
4446
  if (gate.reason === "no-hash") {
4447
4447
  warn(
@@ -4468,7 +4468,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
4468
4468
  return;
4469
4469
  }
4470
4470
  const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
4471
- await backupTrackedPaths(root, allAbsPaths, say, "migrate");
4471
+ void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
4472
4472
  for (const { existing } of toRemove) {
4473
4473
  for (const { abs } of existing) {
4474
4474
  await fs22.remove(abs);
@@ -4487,11 +4487,13 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
4487
4487
  );
4488
4488
  let keepShared = false;
4489
4489
  for (const entry of supersededEntries) {
4490
+ const id = entry.id;
4491
+ if (!id) continue;
4490
4492
  const relPaths = entry.paths ?? [];
4491
4493
  if (relPaths.length === 0) continue;
4492
- const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
4494
+ const existing = await resolveExistingTrackedPaths(root, id, relPaths, warn);
4493
4495
  if (existing.length === 0) continue;
4494
- const gate = await hashGateEntry(root, { id: entry.id, hash: entry.hash }, existing);
4496
+ const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
4495
4497
  if (!gate.ok) keepShared = true;
4496
4498
  }
4497
4499
  for (const entry of prevLock) {
@@ -4503,12 +4505,15 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
4503
4505
  if (!removedFromManifest && !deprecated && !superseded) continue;
4504
4506
  const relPaths = entry.paths ?? [];
4505
4507
  if (relPaths.length === 0) continue;
4506
- if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
4507
4508
  const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
4509
+ if (await hasProtectedUnsafePath(root, relPaths)) {
4510
+ refusedIds.add(entry.id);
4511
+ continue;
4512
+ }
4508
4513
  if (existing.length === 0) continue;
4509
4514
  const pruneReason = superseded ? "superseded" : deprecated ? "deprecated" : "stale";
4510
4515
  const label = superseded ? "Superseded" : deprecated ? "Deprecated" : "Stale";
4511
- const gate = await hashGateEntry(root, { id: entry.id, hash: entry.hash }, existing);
4516
+ const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
4512
4517
  if (!gate.ok) {
4513
4518
  if (gate.reason === "no-hash") {
4514
4519
  warn(
@@ -4533,7 +4538,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
4533
4538
  return true;
4534
4539
  }) : existing;
4535
4540
  if (!dryRun && removable.length > 0) {
4536
- await backupTrackedPaths(
4541
+ void await backupTrackedPaths(
4537
4542
  root,
4538
4543
  removable.map((e) => e.abs),
4539
4544
  say,
@@ -4563,10 +4568,13 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
4563
4568
  if (!entry.id) continue;
4564
4569
  const relPaths = entry.paths ?? [];
4565
4570
  if (relPaths.length === 0) continue;
4566
- if (await hasProtectedUnsafePath(root, relPaths)) refusedIds.add(entry.id);
4567
4571
  const existing = await resolveExistingTrackedPaths(root, entry.id, relPaths, warn);
4572
+ if (await hasProtectedUnsafePath(root, relPaths)) {
4573
+ refusedIds.add(entry.id);
4574
+ continue;
4575
+ }
4568
4576
  if (existing.length === 0) continue;
4569
- const gate = await hashGateEntry(root, { id: entry.id, hash: entry.hash }, existing);
4577
+ const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
4570
4578
  if (!gate.ok) {
4571
4579
  const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
4572
4580
  warn(
@@ -4587,7 +4595,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
4587
4595
  return refusedIds;
4588
4596
  }
4589
4597
  const allAbsPaths = toRemove.flatMap(({ existing }) => existing.map(({ abs }) => abs));
4590
- await backupTrackedPaths(root, allAbsPaths, say, "prune");
4598
+ void await backupTrackedPaths(root, allAbsPaths, say, "prune");
4591
4599
  for (const { id, existing } of toRemove) {
4592
4600
  await deleteTrackedPaths(root, id, existing, { verb: "Pruned", reason: "orphaned" }, say);
4593
4601
  }
@@ -4604,7 +4612,8 @@ var GITIGNORED_ARTIFACT_PATHS = [
4604
4612
  ".haus-workflow/context-map.json",
4605
4613
  ".haus-workflow/recommendation.json",
4606
4614
  ".haus-workflow/sources-report.json",
4607
- ".haus-workflow/deep-context.json"
4615
+ ".haus-workflow/deep-context.json",
4616
+ ".haus-workflow/backups/"
4608
4617
  ];
4609
4618
  function findLineMarker3(content2, marker, from = 0) {
4610
4619
  let idx = content2.indexOf(marker, from);
@@ -4649,7 +4658,7 @@ function buildBlockFor(existing, beginMarker, endMarker, comment, patterns) {
4649
4658
  if (needed.length === 0) return null;
4650
4659
  return [beginMarker, comment, ...needed, endMarker].join("\n");
4651
4660
  }
4652
- var ARTIFACT_BLOCK_COMMENT = "# haus scan artifacts \u2014 machine-local, must never be committed (see ADR-0025)";
4661
+ var ARTIFACT_BLOCK_COMMENT = "# haus machine-local artifacts \u2014 must never be committed (see ADR-0025, ADR-0058)";
4653
4662
  function injectBlockFor(existing, beginMarker, endMarker, comment, patterns) {
4654
4663
  const block = buildBlockFor(existing, beginMarker, endMarker, comment, patterns);
4655
4664
  const range = findBlockRangeFor(existing, beginMarker, endMarker);
@@ -4742,7 +4751,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
4742
4751
  for (const rel of tracked) {
4743
4752
  if (dryRun) {
4744
4753
  notify(
4745
- `Would untrack ${rel} from git \u2014 it contains machine-local scan output and should never have been committed.`
4754
+ `Would untrack ${rel} from git \u2014 it is machine-local haus output and should never have been committed.`
4746
4755
  );
4747
4756
  continue;
4748
4757
  }
@@ -4750,7 +4759,7 @@ async function untrackMachineLocalArtifacts(root, dryRun, notify) {
4750
4759
  if (result.exitCode === 0) {
4751
4760
  untracked.push(rel);
4752
4761
  notify(
4753
- `Untracked ${rel} from git \u2014 it contains machine-local scan output and should never have been committed. Run \`git status\` to review before committing this change.`
4762
+ `Untracked ${rel} from git \u2014 it is machine-local haus output and should never have been committed. Run \`git status\` to review before committing this change.`
4754
4763
  );
4755
4764
  } else {
4756
4765
  notify(`Could not untrack ${rel} from git: ${result.stderr.trim() || result.stdout.trim()}`);
@@ -7665,12 +7674,12 @@ async function runDoctor(options) {
7665
7674
  const trackedArtifacts = await listTrackedArtifactPaths(root);
7666
7675
  if (trackedArtifacts.length > 0) {
7667
7676
  flag(
7668
- `- GITIGNORE: ${trackedArtifacts.length} machine-local scan artifact(s) still tracked in git (${trackedArtifacts.join(", ")})`,
7669
- `${trackedArtifacts.length} machine-local scan artifact(s) are still tracked in git`,
7677
+ `- GITIGNORE: ${trackedArtifacts.length} machine-local artifact(s) still tracked in git (${trackedArtifacts.join(", ")})`,
7678
+ `${trackedArtifacts.length} machine-local artifact(s) are still tracked in git`,
7670
7679
  `git rm --cached ${trackedArtifacts.join(" ")} (or run \`haus apply --write\` to do this and update .gitignore for you)`
7671
7680
  );
7672
7681
  } else {
7673
- ok("- GITIGNORE: OK (no machine-local scan artifacts tracked in git)");
7682
+ ok("- GITIGNORE: OK (no machine-local artifacts tracked in git)");
7674
7683
  }
7675
7684
  const gitignoredHausPaths = await findGitignoredHausPaths(root);
7676
7685
  if (gitignoredHausPaths.length > 0) {
@@ -8291,7 +8300,8 @@ function summarizeLockDiff(before, after) {
8291
8300
  }
8292
8301
 
8293
8302
  // src/update/lockfile.ts
8294
- import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
8303
+ import { constants as fsConstants } from "node:fs";
8304
+ import { copyFile, mkdir, readFile as readFile2 } from "node:fs/promises";
8295
8305
  import path47 from "node:path";
8296
8306
  init_fs();
8297
8307
  init_paths();
@@ -8325,16 +8335,30 @@ async function readLockRaw(root) {
8325
8335
  return "[]";
8326
8336
  }
8327
8337
  }
8328
- async function applyLock(root) {
8338
+ async function backupLockfile(root) {
8329
8339
  const lockPath = hausPath(root, "haus.lock.json");
8330
- const before = await readLockRaw(root);
8331
- const lock = await readJson(lockPath) ?? [];
8332
8340
  try {
8333
8341
  const backupDir = hausPath(root, "backups");
8334
8342
  await mkdir(backupDir, { recursive: true });
8335
- await copyFile(lockPath, path47.join(backupDir, `haus.lock.${Date.now()}.json`));
8343
+ let stamp = Date.now();
8344
+ for (; ; ) {
8345
+ const dest = path47.join(backupDir, `haus.lock.${stamp}.json`);
8346
+ try {
8347
+ await copyFile(lockPath, dest, fsConstants.COPYFILE_EXCL);
8348
+ return;
8349
+ } catch (err) {
8350
+ if (err.code !== "EEXIST") throw err;
8351
+ stamp += 1;
8352
+ }
8353
+ }
8336
8354
  } catch {
8337
8355
  }
8356
+ }
8357
+ async function applyLock(root) {
8358
+ const lockPath = hausPath(root, "haus.lock.json");
8359
+ const before = await readLockRaw(root);
8360
+ const lock = await readJson(lockPath) ?? [];
8361
+ await backupLockfile(root);
8338
8362
  const enriched = await Promise.all(
8339
8363
  lock.map(async (x) => {
8340
8364
  const paths = Array.isArray(x.paths) ? x.paths.map(String) : [];
@@ -8357,12 +8381,7 @@ async function removeLockEntries(root, ids) {
8357
8381
  const removed = [...wanted].filter((id) => present.has(id));
8358
8382
  const missing = [...wanted].filter((id) => !present.has(id));
8359
8383
  if (removed.length === 0) return { removed, missing, before, after: before };
8360
- try {
8361
- const backupDir = hausPath(root, "backups");
8362
- await mkdir(backupDir, { recursive: true });
8363
- await copyFile(lockPath, path47.join(backupDir, `haus.lock.${Date.now()}.json`));
8364
- } catch {
8365
- }
8384
+ await backupLockfile(root);
8366
8385
  const survivors = lock.filter((item) => !wanted.has(item.id));
8367
8386
  await writeJson(lockPath, survivors);
8368
8387
  return { removed, missing, before, after: `${JSON.stringify(survivors, null, 2)}
@@ -10014,6 +10033,9 @@ async function hasMultipleSiblingRepos(root) {
10014
10033
  init_fs();
10015
10034
  init_logger();
10016
10035
  init_paths();
10036
+ function emitJsonEnvelope(json, quiet, envelope) {
10037
+ if (json && !quiet) log(JSON.stringify(envelope, null, 2));
10038
+ }
10017
10039
  async function readPriorManualIncludes(root) {
10018
10040
  const prior = await readJson(hausPath(root, "recommendation.json"));
10019
10041
  if (!prior?.recommended) return [];
@@ -10021,16 +10043,12 @@ async function readPriorManualIncludes(root) {
10021
10043
  }
10022
10044
  async function runSetupCore(root, opts) {
10023
10045
  const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
10024
- const say = quiet ? () => {
10046
+ const say = quiet || json ? () => {
10025
10047
  } : log;
10026
10048
  const scanResult = await scanProject(root);
10027
- if (json) {
10028
- say(JSON.stringify(scanResult, null, 2));
10029
- } else {
10030
- say("Haus scan complete");
10031
- say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
10032
- say(`Package manager: ${scanResult.packageManager}`);
10033
- }
10049
+ say("Haus scan complete");
10050
+ say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
10051
+ say(`Package manager: ${scanResult.packageManager}`);
10034
10052
  const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
10035
10053
  const context = await readContextOrScan(root);
10036
10054
  const priorManualIncludes = await readPriorManualIncludes(root);
@@ -10043,39 +10061,39 @@ async function runSetupCore(root, opts) {
10043
10061
  const warningLines2 = [
10044
10062
  .../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
10045
10063
  ];
10046
- if (json) {
10047
- say(JSON.stringify(recommendation, null, 2));
10048
- } else {
10049
- say("Haus recommendation ready");
10050
- say("Recommended: 0");
10051
- say(`Skipped: ${recommendation.skipped.length}`);
10052
- say(`Repo: ${context.repoName}`);
10053
- for (const warning of warningLines2) say(`- WARN: ${warning}`);
10054
- }
10064
+ say("Haus recommendation ready");
10065
+ say("Recommended: 0");
10066
+ say(`Skipped: ${recommendation.skipped.length}`);
10067
+ say(`Repo: ${context.repoName}`);
10068
+ for (const warning of warningLines2) say(`- WARN: ${warning}`);
10055
10069
  const supersededCount = recommendation.skipped.filter(
10056
10070
  (s) => s.skipReasons?.some((r) => r.code === "superseded-by-global-plugin")
10057
10071
  ).length;
10058
10072
  const message2 = supersededCount > 0 ? `No catalog items matched this project after ${supersededCount} item(s) were gated as already provided by an installed global plugin \u2014 refusing to write recommendation.json/haus.lock.json. Those skills are available globally; re-run with \`haus init --force\` to write anyway.` : "No catalog items matched this project at all (zero signal) \u2014 refusing to write recommendation.json/haus.lock.json. Re-run with `haus init --force` to write anyway.";
10059
10073
  warn(message2);
10060
10074
  process.exitCode = 1;
10075
+ const warnings = [...warningLines2, message2];
10076
+ emitJsonEnvelope(json, quiet, {
10077
+ scan: scanResult,
10078
+ recommendation,
10079
+ hooks: null,
10080
+ repoName: context.repoName,
10081
+ warnings
10082
+ });
10061
10083
  return {
10062
10084
  root,
10063
10085
  repoName: context.repoName,
10064
10086
  roles: scanResult.repoRoles,
10065
10087
  recommendedCount: 0,
10066
- warnings: [...warningLines2, message2],
10088
+ warnings,
10067
10089
  hooksOk: false,
10068
10090
  written: []
10069
10091
  };
10070
10092
  }
10071
10093
  await writeJson(hausPath(root, "recommendation.json"), recommendation);
10072
- if (json) {
10073
- say(JSON.stringify(recommendation, null, 2));
10074
- } else {
10075
- say("Haus recommendation ready");
10076
- say(`Recommended: ${recommendation.recommended.length}`);
10077
- say(`Skipped: ${recommendation.skipped.length}`);
10078
- }
10094
+ say("Haus recommendation ready");
10095
+ say(`Recommended: ${recommendation.recommended.length}`);
10096
+ say(`Skipped: ${recommendation.skipped.length}`);
10079
10097
  const hooks = await verifyProjectSettingsHooksContract(root);
10080
10098
  const warningLines = [
10081
10099
  .../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
@@ -10100,12 +10118,23 @@ async function runSetupCore(root, opts) {
10100
10118
  hooksOk,
10101
10119
  written: []
10102
10120
  };
10103
- if (!apply) return baseResult;
10121
+ const envelope = (hooksResult) => ({
10122
+ scan: scanResult,
10123
+ recommendation,
10124
+ hooks: hooksResult,
10125
+ repoName: context.repoName,
10126
+ warnings: warningLines
10127
+ });
10128
+ if (!apply) {
10129
+ emitJsonEnvelope(json, quiet, envelope(hooks));
10130
+ return baseResult;
10131
+ }
10104
10132
  if (confirm2) {
10105
10133
  const approved = await confirm2();
10106
10134
  if (!approved) {
10107
10135
  say("Setup reviewed. No files written.");
10108
10136
  say("Next step: run `haus apply --write` when ready.");
10137
+ emitJsonEnvelope(json, quiet, envelope(hooks));
10109
10138
  return baseResult;
10110
10139
  }
10111
10140
  }
@@ -10115,6 +10144,7 @@ async function runSetupCore(root, opts) {
10115
10144
  if (sync.formatRefusal) {
10116
10145
  error(sync.formatRefusal);
10117
10146
  process.exitCode = 1;
10147
+ emitJsonEnvelope(json, quiet, envelope(hooks));
10118
10148
  return baseResult;
10119
10149
  }
10120
10150
  if (sync.newItems.length > 0) {
@@ -10142,6 +10172,7 @@ async function runSetupCore(root, opts) {
10142
10172
  } else {
10143
10173
  say(`- HOOKS OK: ${hooksAfter.message}`);
10144
10174
  }
10175
+ emitJsonEnvelope(json, quiet, envelope(hooksAfter));
10145
10176
  return { ...baseResult, hooksOk: hooksOkAfter, written: files };
10146
10177
  }
10147
10178
 
@@ -10787,6 +10818,7 @@ async function runRecommend(options) {
10787
10818
  }
10788
10819
 
10789
10820
  // src/commands/remove.ts
10821
+ import path54 from "node:path";
10790
10822
  init_fs();
10791
10823
  init_logger();
10792
10824
  init_paths();
@@ -10798,7 +10830,7 @@ async function runRemove(ids, options = {}) {
10798
10830
  const dryRun = Boolean(options.dryRun);
10799
10831
  const say = options.json ? () => {
10800
10832
  } : log;
10801
- const wanted = parseIdList(ids);
10833
+ const wanted = [...new Set(parseIdList(ids))];
10802
10834
  if (wanted.length === 0) {
10803
10835
  error("Specify at least one catalog item id \u2014 see `haus doctor` for what is installed.");
10804
10836
  process.exitCode = 1;
@@ -10844,7 +10876,7 @@ async function runRemove(ids, options = {}) {
10844
10876
  cleared.push({ id, existing: [] });
10845
10877
  continue;
10846
10878
  }
10847
- const gate = await hashGateEntry(root, { id, hash: entry.hash }, existing);
10879
+ const gate = await hashGateEntry(root, { hash: entry.hash }, existing);
10848
10880
  if (!gate.ok) {
10849
10881
  const detail = gate.reason === "no-hash" ? "has no lock hash" : "was modified locally";
10850
10882
  warn(
@@ -10890,7 +10922,8 @@ async function runRemove(ids, options = {}) {
10890
10922
  }
10891
10923
  }
10892
10924
  const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
10893
- report.backupDir = await backupTrackedPaths(root, allAbs, say, "remove");
10925
+ const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
10926
+ report.backupDir = backupRoot === null ? null : path54.basename(backupRoot);
10894
10927
  for (const { id, existing } of cleared) {
10895
10928
  await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
10896
10929
  }
@@ -10975,7 +11008,7 @@ async function runScan(options) {
10975
11008
  }
10976
11009
 
10977
11010
  // src/commands/undo.ts
10978
- import path54 from "node:path";
11011
+ import path55 from "node:path";
10979
11012
  import fs40 from "fs-extra";
10980
11013
 
10981
11014
  // src/claude/managed-paths.ts
@@ -11007,18 +11040,18 @@ async function collectLockTrackedPaths(root) {
11007
11040
  const relPaths = row.paths ?? [];
11008
11041
  const existingRel = [];
11009
11042
  for (const rel of relPaths) {
11010
- if (await fs40.pathExists(path54.resolve(root, rel))) existingRel.push(rel);
11043
+ if (await fs40.pathExists(path55.resolve(root, rel))) existingRel.push(rel);
11011
11044
  }
11012
11045
  if (existingRel.length === 0) continue;
11013
11046
  if (row.hash === void 0) {
11014
- removable.push(...existingRel.map((rel) => path54.resolve(root, rel)));
11047
+ removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
11015
11048
  continue;
11016
11049
  }
11017
11050
  const currentHash = await hashInstalledPaths(root, existingRel);
11018
11051
  if (currentHash === row.hash) {
11019
- removable.push(...existingRel.map((rel) => path54.resolve(root, rel)));
11052
+ removable.push(...existingRel.map((rel) => path55.resolve(root, rel)));
11020
11053
  } else {
11021
- preserved.push(...existingRel.map((rel) => path54.resolve(root, rel)));
11054
+ preserved.push(...existingRel.map((rel) => path55.resolve(root, rel)));
11022
11055
  log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
11023
11056
  }
11024
11057
  }
@@ -11031,7 +11064,7 @@ async function settingsHasHausContent(root) {
11031
11064
  return settings._haus != null;
11032
11065
  }
11033
11066
  async function claudeMdHasHausBlock(root) {
11034
- const filePath = path54.join(root, "CLAUDE.md");
11067
+ const filePath = path55.join(root, "CLAUDE.md");
11035
11068
  if (!await fs40.pathExists(filePath)) return false;
11036
11069
  const text = await fs40.readFile(filePath, "utf8");
11037
11070
  return text.includes(BLOCK_BEGIN);
@@ -11044,15 +11077,15 @@ async function stripProjectSettings(root) {
11044
11077
  const hasContent = Object.keys(settings).length > 0;
11045
11078
  if (hasContent) {
11046
11079
  await writeProjectSettings(root, settings);
11047
- log(`Stripped haus rules from ${path54.relative(root, settingsPath)} (user settings preserved).`);
11080
+ log(`Stripped haus rules from ${path55.relative(root, settingsPath)} (user settings preserved).`);
11048
11081
  return true;
11049
11082
  }
11050
11083
  await fs40.remove(settingsPath);
11051
- log(`Removed ${path54.relative(root, settingsPath)} (no user-owned settings remained).`);
11084
+ log(`Removed ${path55.relative(root, settingsPath)} (no user-owned settings remained).`);
11052
11085
  return true;
11053
11086
  }
11054
11087
  async function stripRootClaudeMd(root) {
11055
- const filePath = path54.join(root, "CLAUDE.md");
11088
+ const filePath = path55.join(root, "CLAUDE.md");
11056
11089
  if (!await fs40.pathExists(filePath)) return false;
11057
11090
  const prev = await fs40.readFile(filePath, "utf8");
11058
11091
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -11080,12 +11113,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
11080
11113
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
11081
11114
  for (const abs of managedAbsPaths) {
11082
11115
  if (!await fs40.pathExists(abs)) continue;
11083
- const rel = path54.relative(root, abs);
11084
- const backupPath = path54.join(backupRoot, rel);
11085
- await fs40.ensureDir(path54.dirname(backupPath));
11116
+ const rel = path55.relative(root, abs);
11117
+ const backupPath = path55.join(backupRoot, rel);
11118
+ await fs40.ensureDir(path55.dirname(backupPath));
11086
11119
  await fs40.copy(abs, backupPath);
11087
11120
  }
11088
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path54.relative(root, backupRoot)}.`);
11121
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path55.relative(root, backupRoot)}.`);
11089
11122
  }
11090
11123
  async function runUndo(options) {
11091
11124
  const root = options.root ?? process.cwd();
@@ -11098,7 +11131,7 @@ async function runUndo(options) {
11098
11131
  log("Nothing to remove: no haus-managed files found in this directory.");
11099
11132
  return;
11100
11133
  }
11101
- const relTargets = managed.map((p) => path54.relative(root, p));
11134
+ const relTargets = managed.map((p) => path55.relative(root, p));
11102
11135
  const summaryParts = [...relTargets];
11103
11136
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
11104
11137
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -11128,7 +11161,7 @@ User-owned .claude/ files will be preserved.`
11128
11161
  for (const abs of managed) {
11129
11162
  if (!await fs40.pathExists(abs)) continue;
11130
11163
  await fs40.remove(abs);
11131
- log(`Removed ${path54.relative(root, abs)}`);
11164
+ log(`Removed ${path55.relative(root, abs)}`);
11132
11165
  }
11133
11166
  if (stripSettings) await stripProjectSettings(root);
11134
11167
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -11142,7 +11175,7 @@ init_fs();
11142
11175
  init_logger();
11143
11176
  init_paths();
11144
11177
  import crypto3 from "node:crypto";
11145
- import path55 from "node:path";
11178
+ import path56 from "node:path";
11146
11179
  import fs41 from "fs-extra";
11147
11180
  async function runUninstall(options = {}) {
11148
11181
  const { force = false, removePlugins = removeGlobalPlugins } = options;
@@ -11199,14 +11232,14 @@ async function runUninstall(options = {}) {
11199
11232
  continue;
11200
11233
  }
11201
11234
  await fs41.remove(entry.destPath);
11202
- await pruneEmptyDir(path55.dirname(entry.destPath));
11235
+ await pruneEmptyDir(path56.dirname(entry.destPath));
11203
11236
  result.deleted.push(entry.destPath);
11204
11237
  }
11205
11238
  const settings = await readSettings();
11206
11239
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
11207
11240
  await writeSettings(stripped);
11208
11241
  result.hooksStripped = true;
11209
- const hausDir = path55.join(globalClaudeDir(), "haus");
11242
+ const hausDir = path56.join(globalClaudeDir(), "haus");
11210
11243
  const manifestPath2 = hausManifestPath();
11211
11244
  if (fs41.pathExistsSync(manifestPath2)) {
11212
11245
  await fs41.remove(manifestPath2);
@@ -11283,7 +11316,7 @@ async function runUninstallCommand(options) {
11283
11316
  }
11284
11317
 
11285
11318
  // src/commands/validate-catalog.ts
11286
- import path57 from "node:path";
11319
+ import path58 from "node:path";
11287
11320
 
11288
11321
  // src/catalog/validate-core.ts
11289
11322
  init_forbidden_content();
@@ -11291,7 +11324,7 @@ init_manifest_item_fields();
11291
11324
  init_path_safety();
11292
11325
  init_validation_rules2();
11293
11326
  import fs42 from "node:fs";
11294
- import path56 from "node:path";
11327
+ import path57 from "node:path";
11295
11328
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
11296
11329
  var ITEM_ID_PATTERN = /^haus\./;
11297
11330
  function auditForbiddenStacks(items) {
@@ -11442,7 +11475,7 @@ function readForAudit(absPath, label, failures) {
11442
11475
  }
11443
11476
  }
11444
11477
  function auditTemplateContent(manifestDir, absPath, itemId) {
11445
- const rel = path56.relative(manifestDir, absPath);
11478
+ const rel = path57.relative(manifestDir, absPath);
11446
11479
  const failures = [];
11447
11480
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
11448
11481
  if (text === null) return failures;
@@ -11464,25 +11497,25 @@ function auditShippedFiles(manifestDir, items) {
11464
11497
  const failures = [];
11465
11498
  for (const item of items) {
11466
11499
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
11467
- const absPath = path56.join(manifestDir, item.path);
11500
+ const absPath = path57.join(manifestDir, item.path);
11468
11501
  if (item.type === "skill") {
11469
- const skillMd = path56.join(absPath, "SKILL.md");
11502
+ const skillMd = path57.join(absPath, "SKILL.md");
11470
11503
  if (!fs42.existsSync(skillMd)) {
11471
- failures.push(`${item.id}: missing ${path56.relative(manifestDir, skillMd)}`);
11504
+ failures.push(`${item.id}: missing ${path57.relative(manifestDir, skillMd)}`);
11472
11505
  continue;
11473
11506
  }
11474
11507
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
11475
11508
  if (text === null) continue;
11476
11509
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
11477
11510
  failures.push(
11478
- ...auditForbiddenTagsInText(text, `${item.id}: ${path56.relative(manifestDir, skillMd)}`)
11511
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path57.relative(manifestDir, skillMd)}`)
11479
11512
  );
11480
11513
  } else if (item.type === "agent") {
11481
11514
  if (!fs42.existsSync(absPath)) {
11482
11515
  failures.push(`${item.id}: missing agent file ${item.path}`);
11483
11516
  continue;
11484
11517
  }
11485
- const rel = path56.relative(manifestDir, absPath);
11518
+ const rel = path57.relative(manifestDir, absPath);
11486
11519
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
11487
11520
  if (text === null) continue;
11488
11521
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -11498,7 +11531,7 @@ function auditShippedFiles(manifestDir, items) {
11498
11531
  failures.push(`${item.id}: missing command file ${item.path}`);
11499
11532
  continue;
11500
11533
  }
11501
- const rel = path56.relative(manifestDir, absPath);
11534
+ const rel = path57.relative(manifestDir, absPath);
11502
11535
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
11503
11536
  if (text === null) continue;
11504
11537
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -11556,7 +11589,7 @@ function walkAllFiles(dir, fn, failures) {
11556
11589
  }
11557
11590
  for (const entry of entries) {
11558
11591
  if (entry.name === ".DS_Store") continue;
11559
- const full = path56.join(dir, entry.name);
11592
+ const full = path57.join(dir, entry.name);
11560
11593
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
11561
11594
  else fn(full);
11562
11595
  }
@@ -11580,12 +11613,12 @@ function auditDiskOrphans(manifestDir, items) {
11580
11613
  const failures = [];
11581
11614
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
11582
11615
  for (const dir of CONTENT_ROOTS) {
11583
- const abs = path56.join(manifestDir, dir);
11616
+ const abs = path57.join(manifestDir, dir);
11584
11617
  if (!fs42.existsSync(abs)) continue;
11585
11618
  walkAllFiles(
11586
11619
  abs,
11587
11620
  (file) => {
11588
- const relPath = path56.relative(manifestDir, file).replace(/\\/g, "/");
11621
+ const relPath = path57.relative(manifestDir, file).replace(/\\/g, "/");
11589
11622
  if (!isPathClaimed(relPath, claimedPaths)) {
11590
11623
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
11591
11624
  }
@@ -11606,7 +11639,7 @@ function walkMd(dir, fn, failures) {
11606
11639
  return;
11607
11640
  }
11608
11641
  for (const entry of entries) {
11609
- const full = path56.join(dir, entry.name);
11642
+ const full = path57.join(dir, entry.name);
11610
11643
  if (entry.isDirectory()) walkMd(full, fn, failures);
11611
11644
  else if (entry.name.endsWith(".md")) fn(full);
11612
11645
  }
@@ -11616,13 +11649,13 @@ function auditMarkdownContent(manifestDir, items) {
11616
11649
  const pathSourceMap = buildItemPathSourceMap(items);
11617
11650
  const pathTypeMap = buildItemPathTypeMap(items);
11618
11651
  for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
11619
- const abs = path56.join(manifestDir, dir);
11652
+ const abs = path57.join(manifestDir, dir);
11620
11653
  if (!fs42.existsSync(abs)) continue;
11621
11654
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
11622
11655
  walkMd(
11623
11656
  abs,
11624
11657
  (file) => {
11625
- const rel = path56.relative(manifestDir, file);
11658
+ const rel = path57.relative(manifestDir, file);
11626
11659
  const text = readForAudit(file, rel, failures);
11627
11660
  if (text === null) return;
11628
11661
  const norm = rel.replace(/\\/g, "/");
@@ -11670,8 +11703,8 @@ async function runValidateCatalog(manifestPath2) {
11670
11703
  process.exitCode = 1;
11671
11704
  return;
11672
11705
  }
11673
- const abs = path57.resolve(process.cwd(), manifestPath2);
11674
- const manifestDir = path57.dirname(abs);
11706
+ const abs = path58.resolve(process.cwd(), manifestPath2);
11707
+ const manifestDir = path58.dirname(abs);
11675
11708
  const data = await readJson(abs);
11676
11709
  if (!data?.items) {
11677
11710
  error(`Could not read catalog manifest at ${abs}`);
@@ -11692,7 +11725,7 @@ init_logger();
11692
11725
 
11693
11726
  // src/workspace/worktree/add.ts
11694
11727
  init_fs();
11695
- import path64 from "node:path";
11728
+ import path65 from "node:path";
11696
11729
  import fs46 from "fs-extra";
11697
11730
 
11698
11731
  // src/workspace/worktree/git-worktree.ts
@@ -11813,12 +11846,12 @@ async function hasUnpushedWork(cwd) {
11813
11846
  }
11814
11847
 
11815
11848
  // src/workspace/worktree/hydrate.ts
11816
- import path60 from "node:path";
11849
+ import path61 from "node:path";
11817
11850
  import fs44 from "fs-extra";
11818
11851
 
11819
11852
  // src/workspace/worktree/cow-copy.ts
11820
11853
  init_exec();
11821
- import path58 from "node:path";
11854
+ import path59 from "node:path";
11822
11855
  import fs43 from "fs-extra";
11823
11856
  async function getMacFilesystemType(dir) {
11824
11857
  const df = await runCommand("df", ["-P", dir]);
@@ -11853,11 +11886,11 @@ async function cowCopyDir(src, dest) {
11853
11886
  const srcExists = await fs43.pathExists(src);
11854
11887
  if (!srcExists)
11855
11888
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
11856
- const strategy = await detectCowStrategy(path58.dirname(dest));
11889
+ const strategy = await detectCowStrategy(path59.dirname(dest));
11857
11890
  if (strategy === "unsupported" || strategy === "unknown-platform") {
11858
11891
  return { strategy, attempted: false, ok: false };
11859
11892
  }
11860
- await fs43.ensureDir(path58.dirname(dest));
11893
+ await fs43.ensureDir(path59.dirname(dest));
11861
11894
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
11862
11895
  const result = await runCommand("cp", args);
11863
11896
  return {
@@ -11871,7 +11904,7 @@ async function cowCopyDir(src, dest) {
11871
11904
  // src/workspace/worktree/install.ts
11872
11905
  init_exec();
11873
11906
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
11874
- import path59 from "node:path";
11907
+ import path60 from "node:path";
11875
11908
  import fg6 from "fast-glob";
11876
11909
  function detectInstallPlan(signals) {
11877
11910
  if (signals.hasYarnLock) {
@@ -11892,10 +11925,10 @@ function detectInstallPlan(signals) {
11892
11925
  return { manager: "none" };
11893
11926
  }
11894
11927
  async function computeLockfileSignals(dir) {
11895
- const hasYarnLock = existsSync3(path59.join(dir, "yarn.lock"));
11896
- const hasPnpmLock = existsSync3(path59.join(dir, "pnpm-lock.yaml"));
11897
- const hasPackageLock = existsSync3(path59.join(dir, "package-lock.json"));
11898
- const hasComposerLock = existsSync3(path59.join(dir, "composer.lock"));
11928
+ const hasYarnLock = existsSync3(path60.join(dir, "yarn.lock"));
11929
+ const hasPnpmLock = existsSync3(path60.join(dir, "pnpm-lock.yaml"));
11930
+ const hasPackageLock = existsSync3(path60.join(dir, "package-lock.json"));
11931
+ const hasComposerLock = existsSync3(path60.join(dir, "composer.lock"));
11899
11932
  const dotnetFiles = await fg6(["**/*.csproj", "**/*.sln"], {
11900
11933
  cwd: dir,
11901
11934
  onlyFiles: true,
@@ -11909,7 +11942,7 @@ async function computeLockfileSignals(dir) {
11909
11942
  });
11910
11943
  const hasDotnetProject = dotnetFiles.length > 0;
11911
11944
  let hasPackageManagerField = false;
11912
- const pkgPath = path59.join(dir, "package.json");
11945
+ const pkgPath = path60.join(dir, "package.json");
11913
11946
  if (existsSync3(pkgPath)) {
11914
11947
  try {
11915
11948
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -11926,9 +11959,72 @@ async function computeLockfileSignals(dir) {
11926
11959
  hasDotnetProject
11927
11960
  };
11928
11961
  }
11962
+ var INSTALL_OUTPUT_CAP = 8192;
11963
+ var INSTALL_TAIL_LINES = 8;
11964
+ function capTail(text, max = INSTALL_OUTPUT_CAP) {
11965
+ return text.length <= max ? text : text.slice(-max);
11966
+ }
11967
+ function nonemptyLines(text) {
11968
+ return text.split(/\r?\n/).filter((line2) => line2.trim().length > 0);
11969
+ }
11970
+ function findRequiredYarnrcEnvVars(content2) {
11971
+ const required = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
11972
+ const names = /* @__PURE__ */ new Set();
11973
+ for (const line2 of content2.split(/\r?\n/)) {
11974
+ if (line2.trimStart().startsWith("#")) continue;
11975
+ required.lastIndex = 0;
11976
+ let match;
11977
+ while ((match = required.exec(line2)) !== null) {
11978
+ const name = match[1];
11979
+ if (name !== void 0) names.add(name);
11980
+ }
11981
+ }
11982
+ return [...names];
11983
+ }
11984
+ function missingYarnrcEnvVars(content2, env) {
11985
+ return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
11986
+ }
11987
+ function missingYarnrcEnvVarsInDir(dir, env) {
11988
+ const yarnrcPath = path60.join(dir, ".yarnrc.yml");
11989
+ if (!existsSync3(yarnrcPath)) return [];
11990
+ try {
11991
+ return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
11992
+ } catch {
11993
+ return [];
11994
+ }
11995
+ }
11996
+ function tailInstallOutput(stdout2, stderr) {
11997
+ const errLines = nonemptyLines(stderr).slice(-INSTALL_TAIL_LINES);
11998
+ const remaining = INSTALL_TAIL_LINES - errLines.length;
11999
+ const outLines = remaining > 0 ? nonemptyLines(stdout2).slice(-remaining) : [];
12000
+ return [...errLines, ...outLines].join("\n");
12001
+ }
12002
+ function summarizeInstall(install) {
12003
+ if (install.plan.manager === "none") return "no install step (no recognized lockfile)";
12004
+ const envNotes = (install.missingEnvVars ?? []).map(
12005
+ (name) => `${name} not set in environment \u2014 yarn install will fail`
12006
+ );
12007
+ if (!install.ran) {
12008
+ const skip = `install skipped (${install.skippedReason ?? "unavailable"})`;
12009
+ return envNotes.length > 0 ? [skip, ...envNotes].join("\n ") : skip;
12010
+ }
12011
+ if (install.ok) return `hydrated via ${install.plan.manager}`;
12012
+ const head = `install FAILED via ${install.plan.manager}`;
12013
+ const tail = tailInstallOutput(install.stdout ?? "", install.stderr ?? "");
12014
+ const details = [...envNotes, ...tail.length > 0 ? tail.split("\n") : []];
12015
+ return details.length > 0 ? [head, ...details].join("\n ") : head;
12016
+ }
12017
+ function withMissingEnv(result, missingEnvVars) {
12018
+ return missingEnvVars.length > 0 ? { ...result, missingEnvVars } : result;
12019
+ }
11929
12020
  async function runInstallPlan(dir, plan, deps = {}) {
12021
+ const env = deps.env ?? process.env;
12022
+ const missingEnvVars = plan.manager === "yarn" ? missingYarnrcEnvVarsInDir(dir, env) : [];
11930
12023
  if (plan.manager === "none") {
11931
- return { plan, ran: false, ok: true, skippedReason: "no recognized lockfile" };
12024
+ return withMissingEnv(
12025
+ { plan, ran: false, ok: true, skippedReason: "no recognized lockfile" },
12026
+ missingEnvVars
12027
+ );
11932
12028
  }
11933
12029
  const probe = deps.probe ?? probeCommand;
11934
12030
  const probed = await probe(plan.command);
@@ -11936,25 +12032,41 @@ async function runInstallPlan(dir, plan, deps = {}) {
11936
12032
  case "available":
11937
12033
  break;
11938
12034
  case "absent":
11939
- return { plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` };
12035
+ return withMissingEnv(
12036
+ { plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` },
12037
+ missingEnvVars
12038
+ );
11940
12039
  case "inconclusive":
11941
- return {
11942
- plan,
11943
- ran: false,
11944
- ok: true,
11945
- skippedReason: `could not determine whether ${plan.command} is on PATH (${probed.reason})`
11946
- };
12040
+ return withMissingEnv(
12041
+ {
12042
+ plan,
12043
+ ran: false,
12044
+ ok: true,
12045
+ skippedReason: `could not determine whether ${plan.command} is on PATH (${probed.reason})`
12046
+ },
12047
+ missingEnvVars
12048
+ );
11947
12049
  default:
11948
12050
  return assertNeverCommandProbe(probed);
11949
12051
  }
11950
- const result = await runCommand(plan.command, plan.args, { cwd: dir });
11951
- return {
11952
- plan,
11953
- ran: true,
11954
- ok: result.exitCode === 0,
11955
- exitCode: result.exitCode,
11956
- stderr: result.exitCode === 0 ? void 0 : result.stderr
11957
- };
12052
+ const run = deps.run ?? runCommand;
12053
+ const result = await run(plan.command, plan.args, { cwd: dir });
12054
+ if (result.exitCode === 0) {
12055
+ return withMissingEnv({ plan, ran: true, ok: true, exitCode: 0 }, missingEnvVars);
12056
+ }
12057
+ const stdout2 = capTail(result.stdout);
12058
+ const stderr = capTail(result.stderr);
12059
+ return withMissingEnv(
12060
+ {
12061
+ plan,
12062
+ ran: true,
12063
+ ok: false,
12064
+ exitCode: result.exitCode,
12065
+ stdout: stdout2.length > 0 ? stdout2 : void 0,
12066
+ stderr: stderr.length > 0 ? stderr : void 0
12067
+ },
12068
+ missingEnvVars
12069
+ );
11958
12070
  }
11959
12071
 
11960
12072
  // src/workspace/worktree/hydrate.ts
@@ -11963,8 +12075,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
11963
12075
  const start = Date.now();
11964
12076
  const targets = [];
11965
12077
  for (const target of HYDRATION_TARGETS) {
11966
- const src = path60.join(mainMemberPath, target);
11967
- const dest = path60.join(worktreeMemberPath, target);
12078
+ const src = path61.join(mainMemberPath, target);
12079
+ const dest = path61.join(worktreeMemberPath, target);
11968
12080
  if (!await fs44.pathExists(src)) continue;
11969
12081
  const destExists = await fs44.pathExists(dest);
11970
12082
  if (!opts.force && destExists) continue;
@@ -11982,11 +12094,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
11982
12094
 
11983
12095
  // src/workspace/worktree/local-files.ts
11984
12096
  init_exec();
11985
- import path61 from "node:path";
12097
+ import path62 from "node:path";
11986
12098
  import fs45 from "fs-extra";
11987
12099
  var MACHINE_LOCAL_FILES = [
11988
12100
  "docker-compose.local.yml",
11989
- path61.join(".claude", "settings.local.json")
12101
+ path62.join(".claude", "settings.local.json")
11990
12102
  ];
11991
12103
  async function isTracked(repoRoot, relFile) {
11992
12104
  const gitPath = relFile.replace(/\\/g, "/");
@@ -11996,11 +12108,11 @@ async function isTracked(repoRoot, relFile) {
11996
12108
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
11997
12109
  const copied = [];
11998
12110
  for (const rel of MACHINE_LOCAL_FILES) {
11999
- const src = path61.join(mainMemberPath, rel);
12111
+ const src = path62.join(mainMemberPath, rel);
12000
12112
  if (!await fs45.pathExists(src)) continue;
12001
12113
  if (await isTracked(mainMemberPath, rel)) continue;
12002
- const dest = path61.join(worktreeMemberPath, rel);
12003
- await fs45.ensureDir(path61.dirname(dest));
12114
+ const dest = path62.join(worktreeMemberPath, rel);
12115
+ await fs45.ensureDir(path62.dirname(dest));
12004
12116
  await fs45.copy(src, dest, { overwrite: true });
12005
12117
  copied.push(rel);
12006
12118
  }
@@ -12009,15 +12121,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
12009
12121
 
12010
12122
  // src/workspace/worktree/root.ts
12011
12123
  import { existsSync as existsSync4 } from "node:fs";
12012
- import path62 from "node:path";
12124
+ import path63 from "node:path";
12013
12125
  async function resolveWorkspaceForWorktree(start) {
12014
12126
  const rootInfo = await resolveRoots(start);
12015
12127
  if (!rootInfo.isGitRepo) {
12016
12128
  return { ok: false, reason: "Not inside a git repository." };
12017
12129
  }
12018
12130
  const workspaceRoot = rootInfo.mainRoot;
12019
- const hasYaml = existsSync4(path62.join(workspaceRoot, WORKSPACE_FILE));
12020
- const hasManifest = existsSync4(path62.join(workspaceRoot, REPOS_MANIFEST_FILE));
12131
+ const hasYaml = existsSync4(path63.join(workspaceRoot, WORKSPACE_FILE));
12132
+ const hasManifest = existsSync4(path63.join(workspaceRoot, REPOS_MANIFEST_FILE));
12021
12133
  if (!hasYaml && !hasManifest) {
12022
12134
  return {
12023
12135
  ok: false,
@@ -12043,10 +12155,10 @@ function selectMembers(members, only) {
12043
12155
 
12044
12156
  // src/workspace/worktree/state.ts
12045
12157
  init_fs();
12046
- import path63 from "node:path";
12158
+ import path64 from "node:path";
12047
12159
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
12048
12160
  function worktreesDir(mainRoot) {
12049
- return path63.join(mainRoot, ".claude", "worktrees");
12161
+ return path64.join(mainRoot, ".claude", "worktrees");
12050
12162
  }
12051
12163
  function isValidWorktreeSlug(slug) {
12052
12164
  if (slug === "" || slug === "." || slug === "..") return false;
@@ -12059,15 +12171,15 @@ function worktreePath(mainRoot, slug) {
12059
12171
  `Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
12060
12172
  );
12061
12173
  }
12062
- return path63.join(worktreesDir(mainRoot), slug);
12174
+ return path64.join(worktreesDir(mainRoot), slug);
12063
12175
  }
12064
12176
  async function readWorktreeState(worktreeRoot) {
12065
- const state = await readJson(path63.join(worktreeRoot, WORKTREE_STATE_FILE));
12177
+ const state = await readJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE));
12066
12178
  if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
12067
12179
  return state;
12068
12180
  }
12069
12181
  async function writeWorktreeState(worktreeRoot, state) {
12070
- await writeJson(path63.join(worktreeRoot, WORKTREE_STATE_FILE), state);
12182
+ await writeJson(path64.join(worktreeRoot, WORKTREE_STATE_FILE), state);
12071
12183
  }
12072
12184
 
12073
12185
  // src/workspace/worktree/add.ts
@@ -12101,7 +12213,7 @@ async function runAdd(opts) {
12101
12213
  const memberResults = [];
12102
12214
  for (const member of selected) {
12103
12215
  const start = Date.now();
12104
- const memberWorktreePath = path64.join(wsWorktreePath, member.folder);
12216
+ const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
12105
12217
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
12106
12218
  preferDefaultBranchFrom: true
12107
12219
  });
@@ -12131,7 +12243,7 @@ async function runAdd(opts) {
12131
12243
  await mapWithConcurrency(okResults, async (result) => {
12132
12244
  const member = selected.find((m) => m.id === result.member);
12133
12245
  if (!member) return;
12134
- const memberWorktreePath = path64.join(wsWorktreePath, member.folder);
12246
+ const memberWorktreePath = path65.join(wsWorktreePath, member.folder);
12135
12247
  const hydrationStart = Date.now();
12136
12248
  if (opts.hydrate !== false) {
12137
12249
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -12198,25 +12310,25 @@ async function runDryRun(args) {
12198
12310
 
12199
12311
  // src/workspace/worktree/doctor.ts
12200
12312
  import { existsSync as existsSync6 } from "node:fs";
12201
- import path66 from "node:path";
12313
+ import path67 from "node:path";
12202
12314
 
12203
12315
  // src/workspace/worktree/orphans.ts
12204
12316
  import { existsSync as existsSync5 } from "node:fs";
12205
- import path65 from "node:path";
12317
+ import path66 from "node:path";
12206
12318
  var WORKSPACE_REPO_ID = "(workspace)";
12207
12319
  async function findOrphanedRegistrations(repos, wsWorktreesDir) {
12208
12320
  const orphans = [];
12209
12321
  for (const repo of repos) {
12210
12322
  const entries = await listWorktrees(repo.absPath);
12211
12323
  for (const entry of entries) {
12212
- const entryPath = path65.normalize(entry.path);
12213
- const marker = `${path65.sep}.claude${path65.sep}worktrees${path65.sep}`;
12324
+ const entryPath = path66.normalize(entry.path);
12325
+ const marker = `${path66.sep}.claude${path66.sep}worktrees${path66.sep}`;
12214
12326
  const idx = entryPath.indexOf(marker);
12215
12327
  if (idx === -1) continue;
12216
12328
  const rest = entryPath.slice(idx + marker.length);
12217
- const slug = rest.split(path65.sep)[0];
12329
+ const slug = rest.split(path66.sep)[0];
12218
12330
  if (!slug) continue;
12219
- if (existsSync5(path65.join(wsWorktreesDir, slug))) continue;
12331
+ if (existsSync5(path66.join(wsWorktreesDir, slug))) continue;
12220
12332
  orphans.push({
12221
12333
  repoId: repo.id,
12222
12334
  ownerRepo: repo.absPath,
@@ -12231,7 +12343,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
12231
12343
  // src/workspace/worktree/doctor.ts
12232
12344
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
12233
12345
  function looksHydrated(memberPath) {
12234
- return HYDRATION_MARKERS.some((marker) => existsSync6(path66.join(memberPath, marker)));
12346
+ return HYDRATION_MARKERS.some((marker) => existsSync6(path67.join(memberPath, marker)));
12235
12347
  }
12236
12348
  async function runDoctor2() {
12237
12349
  const rootInfo = await resolveRoots();
@@ -12247,7 +12359,7 @@ async function runDoctor2() {
12247
12359
  };
12248
12360
  }
12249
12361
  const workspaceRoot = rootInfo.mainRoot;
12250
- const isWorkspace = existsSync6(path66.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path66.join(workspaceRoot, REPOS_MANIFEST_FILE));
12362
+ const isWorkspace = existsSync6(path67.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path67.join(workspaceRoot, REPOS_MANIFEST_FILE));
12251
12363
  if (!isWorkspace) {
12252
12364
  return {
12253
12365
  isWorkspace: false,
@@ -12262,7 +12374,7 @@ async function runDoctor2() {
12262
12374
  };
12263
12375
  }
12264
12376
  const wsWorktreesDir = worktreesDir(workspaceRoot);
12265
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path66.sep}`);
12377
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path67.sep}`);
12266
12378
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
12267
12379
  const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
12268
12380
  const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
@@ -12273,10 +12385,10 @@ async function runDoctor2() {
12273
12385
  let slug;
12274
12386
  if (inWorkspaceWorktree) {
12275
12387
  const wtPath = rootInfo.repoRoot;
12276
- slug = rootInfo.worktreeName ?? path66.basename(wtPath);
12388
+ slug = rootInfo.worktreeName ?? path67.basename(wtPath);
12277
12389
  const state = await readWorktreeState(wtPath);
12278
12390
  for (const member of members) {
12279
- const memberPath = path66.join(wtPath, member.folder);
12391
+ const memberPath = path67.join(wtPath, member.folder);
12280
12392
  const materialized = existsSync6(memberPath);
12281
12393
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
12282
12394
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -12316,7 +12428,7 @@ async function runDoctor2() {
12316
12428
  wsWorktreesDir
12317
12429
  );
12318
12430
  const orphans = orphanScan.map(
12319
- (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path66.join(wsWorktreesDir, o.slug)} no longer exists.`
12431
+ (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path67.join(wsWorktreesDir, o.slug)} no longer exists.`
12320
12432
  );
12321
12433
  if (orphans.length > 0) {
12322
12434
  problems.push(...orphans);
@@ -12337,7 +12449,7 @@ async function runDoctor2() {
12337
12449
  // src/workspace/worktree/hydrate-command.ts
12338
12450
  init_fs();
12339
12451
  import { existsSync as existsSync7 } from "node:fs";
12340
- import path67 from "node:path";
12452
+ import path68 from "node:path";
12341
12453
  async function runHydrateWorktree(opts) {
12342
12454
  const rootInfo = await resolveRoots();
12343
12455
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -12348,7 +12460,7 @@ async function runHydrateWorktree(opts) {
12348
12460
  }
12349
12461
  const workspaceRoot = rootInfo.mainRoot;
12350
12462
  const wtPath = rootInfo.repoRoot;
12351
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path67.sep}`)) {
12463
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path68.sep}`)) {
12352
12464
  return {
12353
12465
  ok: false,
12354
12466
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -12360,13 +12472,13 @@ async function runHydrateWorktree(opts) {
12360
12472
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
12361
12473
  }
12362
12474
  const state = await readWorktreeState(wtPath);
12363
- const slug = state?.slug ?? path67.basename(wtPath);
12475
+ const slug = state?.slug ?? path68.basename(wtPath);
12364
12476
  const detectedBranch = await currentBranch(wtPath);
12365
12477
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
12366
12478
  if (opts.dryRun) {
12367
12479
  const results2 = await Promise.all(
12368
12480
  selected.map(async (member) => {
12369
- const memberPath = path67.join(wtPath, member.folder);
12481
+ const memberPath = path68.join(wtPath, member.folder);
12370
12482
  if (existsSync7(memberPath)) {
12371
12483
  return { member: member.id, folder: member.folder, status: "planned" };
12372
12484
  }
@@ -12387,7 +12499,7 @@ async function runHydrateWorktree(opts) {
12387
12499
  const results = await mapWithConcurrency(
12388
12500
  selected,
12389
12501
  async (member) => {
12390
- const memberPath = path67.join(wtPath, member.folder);
12502
+ const memberPath = path68.join(wtPath, member.folder);
12391
12503
  let branchAction;
12392
12504
  if (!existsSync7(memberPath)) {
12393
12505
  if (!opts.materialize) {
@@ -12450,7 +12562,7 @@ async function runHydrateWorktree(opts) {
12450
12562
  }
12451
12563
 
12452
12564
  // src/workspace/worktree/list.ts
12453
- import path68 from "node:path";
12565
+ import path69 from "node:path";
12454
12566
  import fs47 from "fs-extra";
12455
12567
  async function runList2() {
12456
12568
  const resolved = await resolveWorkspaceForWorktree();
@@ -12463,12 +12575,12 @@ async function runList2() {
12463
12575
  const configuredMembers = await readMembers(rootInfo);
12464
12576
  const worktrees = [];
12465
12577
  for (const slug of slugs) {
12466
- const wtPath = path68.join(dir, slug);
12578
+ const wtPath = path69.join(dir, slug);
12467
12579
  const state = await readWorktreeState(wtPath);
12468
12580
  const members = [];
12469
12581
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
12470
12582
  for (const m of source) {
12471
- const memberPath = path68.join(wtPath, m.folder);
12583
+ const memberPath = path69.join(wtPath, m.folder);
12472
12584
  const materialized = await fs47.pathExists(memberPath);
12473
12585
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
12474
12586
  members.push({
@@ -12530,7 +12642,7 @@ async function runPrune2(opts) {
12530
12642
  }
12531
12643
 
12532
12644
  // src/workspace/worktree/remove.ts
12533
- import path69 from "node:path";
12645
+ import path70 from "node:path";
12534
12646
  import fs48 from "fs-extra";
12535
12647
  async function runRemove2(opts) {
12536
12648
  const resolved = await resolveWorkspaceForWorktree();
@@ -12557,7 +12669,7 @@ async function runRemove2(opts) {
12557
12669
  targets.push({
12558
12670
  repo: configMember?.id ?? stateMember.id,
12559
12671
  ownerRepo,
12560
- path: path69.join(wsWorktreePath, stateMember.folder)
12672
+ path: path70.join(wsWorktreePath, stateMember.folder)
12561
12673
  });
12562
12674
  }
12563
12675
  const existingTargets = [];
@@ -12629,10 +12741,11 @@ function emitJson(result) {
12629
12741
  log(JSON.stringify(result, null, 2));
12630
12742
  }
12631
12743
  function describeInstall(hydration) {
12632
- if (hydration.install.plan.manager === "none") return "no install step (no recognized lockfile)";
12633
- if (!hydration.install.ran)
12634
- return `install skipped (${hydration.install.skippedReason ?? "unavailable"})`;
12635
- return hydration.install.ok ? `hydrated via ${hydration.install.plan.manager}` : `install FAILED via ${hydration.install.plan.manager}`;
12744
+ return summarizeInstall(hydration.install);
12745
+ }
12746
+ function emitMemberLine(hydration, line2) {
12747
+ if (hydration?.install.ran && !hydration.install.ok) warn(line2);
12748
+ else log(line2);
12636
12749
  }
12637
12750
  async function handleAdd(opts) {
12638
12751
  if (!opts.slug) {
@@ -12677,7 +12790,10 @@ async function handleAdd(opts) {
12677
12790
  for (const m of result.members) {
12678
12791
  if (m.status === "ok") {
12679
12792
  const hydrationNote = m.hydration ? describeInstall(m.hydration) : "not hydrated (--no-hydrate)";
12680
- log(` ${m.member}: ${m.branchAction} "${m.branch}" \u2014 ${hydrationNote} (${m.durationMs}ms)`);
12793
+ emitMemberLine(
12794
+ m.hydration,
12795
+ ` ${m.member}: ${m.branchAction} "${m.branch}" \u2014 ${hydrationNote} (${m.durationMs}ms)`
12796
+ );
12681
12797
  } else {
12682
12798
  warn(` ${m.member}: FAILED \u2014 ${m.error}`);
12683
12799
  }
@@ -12721,7 +12837,10 @@ async function handleHydrate(opts) {
12721
12837
  for (const r of result.results) {
12722
12838
  if (r.status === "ok" && r.hydration) {
12723
12839
  const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
12724
- log(` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`);
12840
+ emitMemberLine(
12841
+ r.hydration,
12842
+ ` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`
12843
+ );
12725
12844
  } else {
12726
12845
  warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
12727
12846
  }
@@ -12888,7 +13007,7 @@ async function runWorktree(action, opts = {}) {
12888
13007
 
12889
13008
  // src/commands/workspace.ts
12890
13009
  import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
12891
- import path78 from "node:path";
13010
+ import path79 from "node:path";
12892
13011
  init_fs();
12893
13012
  init_logger();
12894
13013
 
@@ -12947,7 +13066,7 @@ ${summaries.map(
12947
13066
 
12948
13067
  // src/commands/workspace/doctor.ts
12949
13068
  import { existsSync as existsSync8 } from "node:fs";
12950
- import path71 from "node:path";
13069
+ import path72 from "node:path";
12951
13070
  init_logger();
12952
13071
  init_paths();
12953
13072
 
@@ -12955,14 +13074,14 @@ init_paths();
12955
13074
  init_fs();
12956
13075
  init_paths();
12957
13076
  import { readFileSync as readFileSync4 } from "node:fs";
12958
- import path70 from "node:path";
13077
+ import path71 from "node:path";
12959
13078
  var MANIFEST_FILE = "workspace.manifest.json";
12960
13079
  function manifestPath(workspaceRoot) {
12961
13080
  return hausPath(workspaceRoot, MANIFEST_FILE);
12962
13081
  }
12963
13082
  function hausVersion() {
12964
13083
  try {
12965
- const pkg = JSON.parse(readFileSync4(path70.join(packageRoot(), "package.json"), "utf8"));
13084
+ const pkg = JSON.parse(readFileSync4(path71.join(packageRoot(), "package.json"), "utf8"));
12966
13085
  return pkg.version ?? "0.0.0";
12967
13086
  } catch {
12968
13087
  return "0.0.0";
@@ -13054,7 +13173,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
13054
13173
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
13055
13174
  const catalogRefByRepo = [];
13056
13175
  for (const repo of config.repos) {
13057
- const repoRoot = path71.resolve(workspaceRoot, repo.path);
13176
+ const repoRoot = path72.resolve(workspaceRoot, repo.path);
13058
13177
  const entry = manifestByName.get(repo.name);
13059
13178
  if (!entry) {
13060
13179
  flag({
@@ -13127,7 +13246,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
13127
13246
  });
13128
13247
  continue;
13129
13248
  }
13130
- if (!existsSync8(path71.join(workspaceRoot, entry.path))) {
13249
+ if (!existsSync8(path72.join(workspaceRoot, entry.path))) {
13131
13250
  flag({
13132
13251
  repo: entry.repo,
13133
13252
  kind: "missing-linked-context-copy",
@@ -13173,17 +13292,17 @@ function emit(args) {
13173
13292
  init_logger();
13174
13293
 
13175
13294
  // src/workspace/link-context/link.ts
13176
- import path74 from "node:path";
13295
+ import path75 from "node:path";
13177
13296
  import fs51 from "fs-extra";
13178
13297
  init_fs();
13179
13298
  init_logger();
13180
13299
 
13181
13300
  // src/workspace/link-context/plan.ts
13182
- import path73 from "node:path";
13301
+ import path74 from "node:path";
13183
13302
  import fs50 from "fs-extra";
13184
13303
 
13185
13304
  // src/workspace/link-context/scan-source.ts
13186
- import path72 from "node:path";
13305
+ import path73 from "node:path";
13187
13306
  import fs49 from "fs-extra";
13188
13307
  var ASSET_SUBDIR = {
13189
13308
  skill: "skills",
@@ -13192,36 +13311,36 @@ var ASSET_SUBDIR = {
13192
13311
  };
13193
13312
  async function hasLinkableContext(member) {
13194
13313
  for (const subdir of Object.values(ASSET_SUBDIR)) {
13195
- if (await fs49.pathExists(path72.join(member.absPath, ".claude", subdir))) return true;
13314
+ if (await fs49.pathExists(path73.join(member.absPath, ".claude", subdir))) return true;
13196
13315
  }
13197
13316
  return false;
13198
13317
  }
13199
13318
  async function collectSkills(member) {
13200
- const dir = path72.join(member.absPath, ".claude", "skills");
13319
+ const dir = path73.join(member.absPath, ".claude", "skills");
13201
13320
  if (!await fs49.pathExists(dir)) return [];
13202
13321
  const names = await fs49.readdir(dir);
13203
13322
  const out = [];
13204
13323
  for (const name of [...names].sort()) {
13205
- const abs = path72.join(dir, name);
13324
+ const abs = path73.join(dir, name);
13206
13325
  const stat = await fs49.lstat(abs);
13207
13326
  if (!stat.isDirectory()) continue;
13208
- if (!await fs49.pathExists(path72.join(abs, "SKILL.md"))) continue;
13209
- out.push({ type: "skill", name, sourceRelPath: path72.posix.join(".claude", "skills", name) });
13327
+ if (!await fs49.pathExists(path73.join(abs, "SKILL.md"))) continue;
13328
+ out.push({ type: "skill", name, sourceRelPath: path73.posix.join(".claude", "skills", name) });
13210
13329
  }
13211
13330
  return out;
13212
13331
  }
13213
13332
  async function collectFlatMarkdown(member, type) {
13214
13333
  const subdir = ASSET_SUBDIR[type];
13215
- const dir = path72.join(member.absPath, ".claude", subdir);
13334
+ const dir = path73.join(member.absPath, ".claude", subdir);
13216
13335
  if (!await fs49.pathExists(dir)) return [];
13217
13336
  const names = await fs49.readdir(dir);
13218
13337
  const out = [];
13219
13338
  for (const fileName of [...names].sort()) {
13220
13339
  if (!fileName.endsWith(".md")) continue;
13221
- const abs = path72.join(dir, fileName);
13340
+ const abs = path73.join(dir, fileName);
13222
13341
  if (!(await fs49.lstat(abs)).isFile()) continue;
13223
13342
  const name = fileName.slice(0, -".md".length);
13224
- out.push({ type, name, sourceRelPath: path72.posix.join(".claude", subdir, fileName) });
13343
+ out.push({ type, name, sourceRelPath: path73.posix.join(".claude", subdir, fileName) });
13225
13344
  }
13226
13345
  return out;
13227
13346
  }
@@ -13236,9 +13355,9 @@ async function collectSourceAssets(member) {
13236
13355
 
13237
13356
  // src/workspace/link-context/plan.ts
13238
13357
  function repoFolderPrefix(member) {
13239
- const base = path73.posix.basename(member.folder);
13358
+ const base = path74.posix.basename(member.folder);
13240
13359
  if (base && base !== "." && base !== "/") return base;
13241
- const fromAbs = path73.basename(member.absPath);
13360
+ const fromAbs = path74.basename(member.absPath);
13242
13361
  return fromAbs || member.id;
13243
13362
  }
13244
13363
  function assetKey(type, destKey) {
@@ -13295,7 +13414,7 @@ async function buildLinkPlan(members) {
13295
13414
  name: asset.name,
13296
13415
  destKey,
13297
13416
  sourceRelPath: asset.sourceRelPath,
13298
- sourceAbsPath: path73.join(member.absPath, asset.sourceRelPath),
13417
+ sourceAbsPath: path74.join(member.absPath, asset.sourceRelPath),
13299
13418
  sourceHash
13300
13419
  });
13301
13420
  }
@@ -13306,7 +13425,7 @@ async function buildLinkPlan(members) {
13306
13425
  function destRelPathFor(type, destKey) {
13307
13426
  const subdir = `${type}s`;
13308
13427
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
13309
- return path74.posix.join(".claude", subdir, leaf);
13428
+ return path75.posix.join(".claude", subdir, leaf);
13310
13429
  }
13311
13430
  async function runLinkContext(workspaceRoot, opts = {}) {
13312
13431
  const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
@@ -13332,9 +13451,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
13332
13451
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
13333
13452
  for (const entry of plan.entries) {
13334
13453
  const destRel = destRelPathFor(entry.type, entry.destKey);
13335
- const destAbs = path74.join(workspaceRoot, destRel);
13454
+ const destAbs = path75.join(workspaceRoot, destRel);
13336
13455
  if (write) {
13337
- await fs51.ensureDir(path74.dirname(destAbs));
13456
+ await fs51.ensureDir(path75.dirname(destAbs));
13338
13457
  if (await fs51.pathExists(destAbs)) await fs51.remove(destAbs);
13339
13458
  await fs51.copy(entry.sourceAbsPath, destAbs, {
13340
13459
  overwrite: true,
@@ -13423,10 +13542,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
13423
13542
 
13424
13543
  // src/commands/workspace/setup.ts
13425
13544
  import { existsSync as existsSync9, statSync } from "node:fs";
13426
- import path76 from "node:path";
13545
+ import path77 from "node:path";
13427
13546
 
13428
13547
  // src/claude/write-workspace-claude-md.ts
13429
- import path75 from "node:path";
13548
+ import path76 from "node:path";
13430
13549
  import fs52 from "fs-extra";
13431
13550
  init_fs();
13432
13551
  init_logger();
@@ -13449,7 +13568,7 @@ ${WORKSPACE_BLOCK_END}`;
13449
13568
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
13450
13569
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
13451
13570
  const dryRun = opts.dryRun ?? false;
13452
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path75.join(workspaceRoot, "CLAUDE.md");
13571
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path76.join(workspaceRoot, "CLAUDE.md");
13453
13572
  const say = opts.quiet ? () => {
13454
13573
  } : log;
13455
13574
  const existed = await fs52.pathExists(filePath);
@@ -13482,20 +13601,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
13482
13601
  init_fs();
13483
13602
  init_logger();
13484
13603
  function resolveWorkspaceRoot(start = process.cwd()) {
13485
- let dir = path76.resolve(start);
13604
+ let dir = path77.resolve(start);
13486
13605
  for (; ; ) {
13487
- if (existsSync9(path76.join(dir, WORKSPACE_FILE))) return dir;
13488
- const parent = path76.dirname(dir);
13489
- if (parent === dir) return path76.resolve(start);
13606
+ if (existsSync9(path77.join(dir, WORKSPACE_FILE))) return dir;
13607
+ const parent = path77.dirname(dir);
13608
+ if (parent === dir) return path77.resolve(start);
13490
13609
  dir = parent;
13491
13610
  }
13492
13611
  }
13493
13612
  function isRootRepo(workspaceRoot, repoPath) {
13494
- return path76.resolve(workspaceRoot, repoPath) === path76.resolve(workspaceRoot);
13613
+ return path77.resolve(workspaceRoot, repoPath) === path77.resolve(workspaceRoot);
13495
13614
  }
13496
13615
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
13497
13616
  const apply = options.write ?? false;
13498
- const configText = await readText(path76.join(workspaceRoot, WORKSPACE_FILE));
13617
+ const configText = await readText(path77.join(workspaceRoot, WORKSPACE_FILE));
13499
13618
  if (!configText) {
13500
13619
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
13501
13620
  process.exitCode = 1;
@@ -13522,7 +13641,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13522
13641
  const statuses = [];
13523
13642
  const aggregateInputs = [];
13524
13643
  for (const repo of repos) {
13525
- const repoRoot = path76.resolve(workspaceRoot, repo.path);
13644
+ const repoRoot = path77.resolve(workspaceRoot, repo.path);
13526
13645
  say(`
13527
13646
  \u2192 ${repo.name} (${repo.path})`);
13528
13647
  try {
@@ -13612,7 +13731,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13612
13731
  const status = statusByName.get(repo.name);
13613
13732
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
13614
13733
  if (status?.status === "ok") {
13615
- const lock = await checkLock(path76.resolve(workspaceRoot, repo.path));
13734
+ const lock = await checkLock(path77.resolve(workspaceRoot, repo.path));
13616
13735
  manifestRepos.push({
13617
13736
  name: repo.name,
13618
13737
  path: repo.path,
@@ -13668,7 +13787,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13668
13787
  try {
13669
13788
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
13670
13789
  if (linkResult.ok) {
13671
- written.push(...linkResult.added.map((rel) => path76.join(workspaceRoot, rel)));
13790
+ written.push(...linkResult.added.map((rel) => path77.join(workspaceRoot, rel)));
13672
13791
  if (!options.json) {
13673
13792
  say(
13674
13793
  `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` : ""}.`
@@ -13700,7 +13819,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
13700
13819
 
13701
13820
  // src/commands/workspace/undo.ts
13702
13821
  import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
13703
- import path77 from "node:path";
13822
+ import path78 from "node:path";
13704
13823
  import fs53 from "fs-extra";
13705
13824
  init_fs();
13706
13825
  init_logger();
@@ -13731,7 +13850,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13731
13850
  }
13732
13851
  if (options.dryRun) {
13733
13852
  for (const repo of config.repos) {
13734
- const repoRoot = path77.resolve(workspaceRoot, repo.path);
13853
+ const repoRoot = path78.resolve(workspaceRoot, repo.path);
13735
13854
  log(`
13736
13855
  \u2192 ${repo.name} (${repo.path})`);
13737
13856
  try {
@@ -13763,11 +13882,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13763
13882
  ];
13764
13883
  for (const abs of workspaceArtifacts) {
13765
13884
  if (await fs53.pathExists(abs)) {
13766
- log(`[dry-run] Would remove ${path77.relative(workspaceRoot, abs)}`);
13885
+ log(`[dry-run] Would remove ${path78.relative(workspaceRoot, abs)}`);
13767
13886
  }
13768
13887
  }
13769
13888
  if (!await fs53.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
13770
- const claudeMd = await readText(path77.join(workspaceRoot, "CLAUDE.md"));
13889
+ const claudeMd = await readText(path78.join(workspaceRoot, "CLAUDE.md"));
13771
13890
  if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
13772
13891
  log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
13773
13892
  }
@@ -13786,7 +13905,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13786
13905
  }
13787
13906
  let anyRepoFailed = false;
13788
13907
  for (const repo of config.repos) {
13789
- const repoRoot = path77.resolve(workspaceRoot, repo.path);
13908
+ const repoRoot = path78.resolve(workspaceRoot, repo.path);
13790
13909
  log(`
13791
13910
  \u2192 ${repo.name} (${repo.path})`);
13792
13911
  try {
@@ -13815,20 +13934,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13815
13934
  const abs = hausPath(workspaceRoot, rel);
13816
13935
  if (await fs53.pathExists(abs)) {
13817
13936
  await fs53.remove(abs);
13818
- log(`Removed ${path77.relative(workspaceRoot, abs)}`);
13937
+ log(`Removed ${path78.relative(workspaceRoot, abs)}`);
13819
13938
  }
13820
13939
  }
13821
13940
  const manifestFile = manifestPath(workspaceRoot);
13822
13941
  if (await fs53.pathExists(manifestFile)) {
13823
13942
  await fs53.remove(manifestFile);
13824
- log(`Removed ${path77.relative(workspaceRoot, manifestFile)}`);
13943
+ log(`Removed ${path78.relative(workspaceRoot, manifestFile)}`);
13825
13944
  }
13826
13945
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
13827
13946
  if (await fs53.pathExists(workspaceMdPath)) {
13828
13947
  await fs53.remove(workspaceMdPath);
13829
- log(`Removed ${path77.relative(workspaceRoot, workspaceMdPath)}`);
13948
+ log(`Removed ${path78.relative(workspaceRoot, workspaceMdPath)}`);
13830
13949
  } else {
13831
- const claudeMdPath = path77.join(workspaceRoot, "CLAUDE.md");
13950
+ const claudeMdPath = path78.join(workspaceRoot, "CLAUDE.md");
13832
13951
  if (existsSync10(claudeMdPath)) {
13833
13952
  const prev = await readText(claudeMdPath);
13834
13953
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
@@ -13897,7 +14016,7 @@ relationships: []
13897
14016
  log("Workspace initialized.");
13898
14017
  }
13899
14018
  async function scanWorkspace(workspaceRoot, opts) {
13900
- const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
14019
+ const configText = await readText(path79.join(workspaceRoot, WORKSPACE_FILE));
13901
14020
  if (!configText) {
13902
14021
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
13903
14022
  process.exitCode = 1;
@@ -13919,7 +14038,7 @@ async function scanWorkspace(workspaceRoot, opts) {
13919
14038
  const inputs = [];
13920
14039
  const failures = [];
13921
14040
  for (const repo of config.repos) {
13922
- const repoRoot = path78.resolve(workspaceRoot, repo.path);
14041
+ const repoRoot = path79.resolve(workspaceRoot, repo.path);
13923
14042
  try {
13924
14043
  if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
13925
14044
  throw new Error("path is not a directory");
@@ -13987,7 +14106,7 @@ init_logger();
13987
14106
  init_paths();
13988
14107
  function cliVersion() {
13989
14108
  try {
13990
- const pkgPath = path79.join(packageRoot(), "package.json");
14109
+ const pkgPath = path80.join(packageRoot(), "package.json");
13991
14110
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
13992
14111
  return pkg.version ?? "0.0.0";
13993
14112
  } catch {
@@ -13997,7 +14116,7 @@ function cliVersion() {
13997
14116
  var program = new Command();
13998
14117
  function validateRuntimeNodeVersion() {
13999
14118
  try {
14000
- const pkgPath = path79.join(packageRoot(), "package.json");
14119
+ const pkgPath = path80.join(packageRoot(), "package.json");
14001
14120
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
14002
14121
  const requiredRange = pkg.engines?.node;
14003
14122
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -14058,7 +14177,7 @@ backups.command("prune").description("Delete old backups by age and/or count (re
14058
14177
  })
14059
14178
  );
14060
14179
  program.command("explain-recommendation").description("Print a per-item breakdown of the current recommendation, with skip reasons").option("--json").action(runExplainRecommendation);
14061
- program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "Machine-readable scan + recommendation documents (requires --dry-run)").option(
14180
+ program.command("init").description("Full setup: scan, recommend, and optionally apply Claude files (safe to re-run)").option("--dry-run", "Preview the `.claude/` write; scan artifacts are still written").option("--json", "Emit one JSON envelope on stdout (requires --dry-run)").option(
14062
14181
  "--force",
14063
14182
  "Write recommendation.json/haus.lock.json even when zero catalog items matched at all"
14064
14183
  ).action(runInit);