@haus-tech/haus-workflow 3.0.0 → 3.1.2

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
@@ -324,7 +324,8 @@ var init_path_safety = __esm({
324
324
  // src/utils/exec.ts
325
325
  var exec_exports = {};
326
326
  __export(exec_exports, {
327
- commandExists: () => commandExists,
327
+ assertNeverCommandProbe: () => assertNeverCommandProbe,
328
+ probeCommand: () => probeCommand,
328
329
  runCommand: () => runCommand,
329
330
  runGit: () => runGit
330
331
  });
@@ -337,14 +338,17 @@ async function runCommand(command, args = [], options = {}) {
337
338
  ...options
338
339
  });
339
340
  const killed = result.timedOut === true || result.isCanceled === true || Boolean(result.signal);
341
+ const capturedStderr = String(result.stderr ?? "");
342
+ const timeoutReason = result.timedOut === true ? `timed out after ${options.timeout}ms` : void 0;
343
+ const stderr = timeoutReason === void 0 ? capturedStderr : capturedStderr.length > 0 ? `${capturedStderr}
344
+ ${timeoutReason}` : timeoutReason;
340
345
  return {
341
346
  command,
342
347
  args,
343
348
  stdout: String(result.stdout ?? ""),
344
- stderr: String(
345
- result.stderr ?? (result.timedOut === true ? `timed out after ${options.timeout}ms` : "")
346
- ),
347
- exitCode: result.exitCode ?? (killed || result.failed === true ? 1 : 0)
349
+ stderr,
350
+ exitCode: result.exitCode ?? (killed || result.failed === true ? 1 : 0),
351
+ killed
348
352
  };
349
353
  } catch (error2) {
350
354
  const message2 = error2 instanceof Error ? error2.message : String(error2);
@@ -354,14 +358,30 @@ async function runCommand(command, args = [], options = {}) {
354
358
  async function runGit(args, options = {}) {
355
359
  return runCommand("git", args, options);
356
360
  }
357
- async function commandExists(command, timeoutMs = 1e4) {
361
+ function assertNeverCommandProbe(probe) {
362
+ throw new Error(`Unhandled CommandProbe: ${JSON.stringify(probe)}`);
363
+ }
364
+ async function probeOnce(command, timeoutMs, run) {
358
365
  try {
359
- const result = await runCommand(command, ["--version"], { timeout: timeoutMs });
360
- return result.exitCode === 0;
361
- } catch {
362
- return false;
366
+ const result = await run(command, ["--version"], { timeout: timeoutMs });
367
+ if (result.exitCode === 0) return { status: "available" };
368
+ if (result.killed) {
369
+ return {
370
+ status: "inconclusive",
371
+ reason: result.stderr.length > 0 ? result.stderr : `probe for "${command} --version" did not complete within ${timeoutMs}ms`
372
+ };
373
+ }
374
+ return { status: "absent" };
375
+ } catch (error2) {
376
+ const message2 = error2 instanceof Error ? error2.message : String(error2);
377
+ return { status: "inconclusive", reason: `probing "${command}" threw: ${message2}` };
363
378
  }
364
379
  }
380
+ async function probeCommand(command, timeoutMs = 1e4, run = runCommand) {
381
+ const first = await probeOnce(command, timeoutMs, run);
382
+ if (first.status !== "inconclusive") return first;
383
+ return probeOnce(command, timeoutMs, run);
384
+ }
365
385
  var init_exec = __esm({
366
386
  "src/utils/exec.ts"() {
367
387
  "use strict";
@@ -481,9 +501,9 @@ var init_github_rate_limit = __esm({
481
501
  // src/catalog/remote-catalog/http.ts
482
502
  import path4 from "node:path";
483
503
  import fs2 from "fs-extra";
484
- async function fetchGuarded(url, extract) {
504
+ async function fetchGuarded(url, extract, headers = {}) {
485
505
  try {
486
- const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
506
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(1e4) });
487
507
  if (!res.ok) {
488
508
  warn(`Catalog fetch HTTP ${res.status}: ${url}`);
489
509
  return null;
@@ -494,11 +514,11 @@ async function fetchGuarded(url, extract) {
494
514
  return null;
495
515
  }
496
516
  }
497
- async function fetchText(url) {
498
- return fetchGuarded(url, (res) => res.text());
517
+ async function fetchText(url, headers = {}) {
518
+ return fetchGuarded(url, (res) => res.text(), headers);
499
519
  }
500
- async function fetchBytes(url) {
501
- return fetchGuarded(url, async (res) => Buffer.from(await res.arrayBuffer()));
520
+ async function fetchBytes(url, headers = {}) {
521
+ return fetchGuarded(url, async (res) => Buffer.from(await res.arrayBuffer()), headers);
502
522
  }
503
523
  async function writeTextIfChanged(dest, text) {
504
524
  if (await fs2.pathExists(dest)) {
@@ -728,6 +748,18 @@ async function resolveCatalogRef(opts) {
728
748
  );
729
749
  return "main";
730
750
  }
751
+ async function catalogContentRequest(base, rel) {
752
+ if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
753
+ return { url: `${base}/${rel}`, headers: {} };
754
+ }
755
+ const ref = base.startsWith(`${CATALOG_REPO_URL}/`) ? base.slice(CATALOG_REPO_URL.length + 1) : getResolvedCatalogRef();
756
+ const headers = await getGithubApiHeaders();
757
+ headers["Accept"] = "application/vnd.github.raw";
758
+ return {
759
+ url: `${CATALOG_GITHUB_API_URL}/contents/${rel}?ref=${encodeURIComponent(ref)}`,
760
+ headers
761
+ };
762
+ }
731
763
  async function remoteBase(projectRoot) {
732
764
  if (isTestMode() && process.env["HAUS_CATALOG_REMOTE_BASE"]) {
733
765
  return process.env["HAUS_CATALOG_REMOTE_BASE"];
@@ -1147,7 +1179,8 @@ var init_manifest_schema = __esm({
1147
1179
  // src/catalog/remote-catalog/manifest.ts
1148
1180
  async function fetchRemoteManifest(projectRoot) {
1149
1181
  const base = await remoteBase(projectRoot);
1150
- const text = await fetchText(`${base}/manifest.json`);
1182
+ const { url, headers } = await catalogContentRequest(base, "manifest.json");
1183
+ const text = await fetchText(url, headers);
1151
1184
  if (!text) return { ok: false, reason: "unavailable" };
1152
1185
  const parsed = parseManifest(text);
1153
1186
  if (!parsed.ok) {
@@ -1533,16 +1566,16 @@ function isMarkdownPath(rel) {
1533
1566
  async function fetchPrefixFiles(catalogPrefix, relFiles, base, label) {
1534
1567
  const fetched = [];
1535
1568
  for (const rel of relFiles) {
1536
- const url = `${base}/${catalogPrefix}/${rel}`;
1569
+ const { url, headers } = await catalogContentRequest(base, `${catalogPrefix}/${rel}`);
1537
1570
  if (isMarkdownPath(rel)) {
1538
- const text = await fetchText(url);
1571
+ const text = await fetchText(url, headers);
1539
1572
  if (text === null) {
1540
1573
  warn(`Failed to fetch ${rel} for ${label}`);
1541
1574
  return null;
1542
1575
  }
1543
1576
  fetched.push({ rel, kind: "text", body: text });
1544
1577
  } else {
1545
- const bytes = await fetchBytes(url);
1578
+ const bytes = await fetchBytes(url, headers);
1546
1579
  if (bytes === null) {
1547
1580
  warn(`Failed to fetch ${rel} for ${label}`);
1548
1581
  return null;
@@ -1681,7 +1714,8 @@ async function syncConfigItem(item, base) {
1681
1714
  return "failed";
1682
1715
  }
1683
1716
  }
1684
- const text = await fetchText(`${base}/${item.path}`);
1717
+ const configReq = await catalogContentRequest(base, item.path);
1718
+ const text = await fetchText(configReq.url, configReq.headers);
1685
1719
  if (text === null) {
1686
1720
  warn(`Failed to fetch content for ${item.id}`);
1687
1721
  return "failed";
@@ -1739,7 +1773,8 @@ async function syncOneItem(item, base) {
1739
1773
  warn(`Skipping ${item.id}: path traversal detected`);
1740
1774
  return "failed";
1741
1775
  }
1742
- const text = await fetchText(`${base}/${item.path}`);
1776
+ const itemReq = await catalogContentRequest(base, item.path);
1777
+ const text = await fetchText(itemReq.url, itemReq.headers);
1743
1778
  if (text === null) {
1744
1779
  warn(`Failed to fetch content for ${item.id}`);
1745
1780
  return "failed";
@@ -1878,7 +1913,7 @@ var init_sync = __esm({
1878
1913
 
1879
1914
  // src/cli.ts
1880
1915
  import { readFileSync as readFileSync5 } from "node:fs";
1881
- import path78 from "node:path";
1916
+ import path79 from "node:path";
1882
1917
  import { Command } from "commander";
1883
1918
 
1884
1919
  // src/commands/apply.ts
@@ -1908,7 +1943,8 @@ var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
1908
1943
  async function readWorkflowTemplate(opts = {}) {
1909
1944
  const dest = path7.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
1910
1945
  const base = await remoteBase(opts.projectRoot);
1911
- const text = await fetchText(`${base}/${WORKFLOW_TEMPLATE_REL}`);
1946
+ const { url, headers } = await catalogContentRequest(base, WORKFLOW_TEMPLATE_REL);
1947
+ const text = await fetchText(url, headers);
1912
1948
  if (text === null) {
1913
1949
  if (await fs6.pathExists(dest)) return fs6.readFile(dest, "utf8");
1914
1950
  return null;
@@ -3014,8 +3050,8 @@ function buildDenyRules() {
3014
3050
  for (const command of DENY_COMMANDS) {
3015
3051
  rules.push(`Bash(${command}:*)`);
3016
3052
  }
3017
- for (const path79 of DENY_PATHS) {
3018
- const pattern = DENY_DIRS.has(path79) ? `${path79}/**` : path79;
3053
+ for (const path80 of DENY_PATHS) {
3054
+ const pattern = DENY_DIRS.has(path80) ? `${path80}/**` : path80;
3019
3055
  for (const tool of FILE_TOOLS) {
3020
3056
  rules.push(`${tool}(${pattern})`);
3021
3057
  }
@@ -3228,9 +3264,20 @@ async function installSkillDependencies(opts, deps = {}) {
3228
3264
  if (!planned.ok) return { status: "skipped", reason: planned.reason };
3229
3265
  const { command, args } = planned.command;
3230
3266
  if (opts.dryRun) return { status: "would-install", command, args };
3231
- const exists = deps.exists ?? commandExists;
3232
- if (!await exists(command)) {
3233
- return { status: "skipped", reason: `${command} not found on PATH` };
3267
+ const exists = deps.exists ?? probeCommand;
3268
+ const probe = await exists(command);
3269
+ switch (probe.status) {
3270
+ case "available":
3271
+ break;
3272
+ case "absent":
3273
+ return { status: "skipped", reason: `${command} not found on PATH` };
3274
+ case "inconclusive":
3275
+ return {
3276
+ status: "skipped",
3277
+ reason: `could not determine whether ${command} is on PATH (${probe.reason}) \u2014 dependency install skipped`
3278
+ };
3279
+ default:
3280
+ return assertNeverCommandProbe(probe);
3234
3281
  }
3235
3282
  if (!await fs14.pathExists(contained.dir)) {
3236
3283
  return { status: "skipped", reason: `install directory disappeared (${contained.dir})` };
@@ -4962,8 +5009,24 @@ async function parseMarketplaces(stdout2, configDir) {
4962
5009
  }
4963
5010
  async function probePluginEnv(deps = {}) {
4964
5011
  const exec = deps.exec ?? realExec;
4965
- const isAvailable = deps.isAvailable ?? (() => commandExists("claude"));
4966
5012
  const warn2 = deps.warn ?? warn;
5013
+ const probeClaude = deps.probeClaude ?? (() => probeCommand("claude"));
5014
+ const isAvailable = deps.isAvailable ?? (async () => {
5015
+ const probe = await probeClaude();
5016
+ switch (probe.status) {
5017
+ case "available":
5018
+ return true;
5019
+ case "absent":
5020
+ return false;
5021
+ case "inconclusive":
5022
+ warn2(
5023
+ `Could not determine whether the \`claude\` CLI is on PATH (${probe.reason}) \u2014 leaving global plugins untouched this run.`
5024
+ );
5025
+ return false;
5026
+ default:
5027
+ return assertNeverCommandProbe(probe);
5028
+ }
5029
+ });
4967
5030
  if (globalPluginsDisabled() && deps.exec === void 0) {
4968
5031
  return { available: false, installed: [], marketplaces: [] };
4969
5032
  }
@@ -6772,10 +6835,13 @@ function parseWorkspaceConfig(text) {
6772
6835
  const repos = Array.isArray(obj.repos) ? obj.repos.filter(
6773
6836
  (r) => typeof r === "object" && r !== null && typeof r.name === "string" && typeof r.path === "string"
6774
6837
  ) : [];
6838
+ const worktreeRaw = parsed.worktree;
6839
+ const worktree2 = typeof worktreeRaw === "object" && worktreeRaw !== null && !Array.isArray(worktreeRaw) ? { autoHydrate: worktreeRaw.autoHydrate === true } : void 0;
6775
6840
  return {
6776
6841
  client: typeof obj.client === "string" ? obj.client : "unknown",
6777
6842
  repos,
6778
- relationships: Array.isArray(obj.relationships) ? obj.relationships : []
6843
+ relationships: Array.isArray(obj.relationships) ? obj.relationships : [],
6844
+ ...worktree2 ? { worktree: worktree2 } : {}
6779
6845
  };
6780
6846
  }
6781
6847
  async function readWorkspaceConfig(workspaceRoot) {
@@ -10139,7 +10205,7 @@ function buildCompanionSuggestions(tools, isInstalled) {
10139
10205
  }
10140
10206
  return lines;
10141
10207
  }
10142
- function commandExists2(bin) {
10208
+ function commandExists(bin) {
10143
10209
  try {
10144
10210
  const r = execaSync("sh", ["-c", 'command -v -- "$1"', "probe", bin], { reject: false });
10145
10211
  return r.exitCode === 0;
@@ -10148,7 +10214,7 @@ function commandExists2(bin) {
10148
10214
  }
10149
10215
  }
10150
10216
  function printCompanionToolSuggestions(deps = {}) {
10151
- const isInstalled = deps.isInstalled ?? commandExists2;
10217
+ const isInstalled = deps.isInstalled ?? commandExists;
10152
10218
  const emit2 = deps.log ?? log;
10153
10219
  const lines = buildCompanionSuggestions(COMPANION_TOOLS, isInstalled);
10154
10220
  for (const line2 of lines) emit2(line2);
@@ -10239,7 +10305,7 @@ async function safeExec(exec, command, args, options) {
10239
10305
  return await exec(command, args, options);
10240
10306
  } catch (error2) {
10241
10307
  const message2 = error2 instanceof Error ? error2.message : String(error2);
10242
- return { command, args, stdout: "", stderr: message2, exitCode: 1 };
10308
+ return { command, args, stdout: "", stderr: message2, exitCode: 1, killed: false };
10243
10309
  }
10244
10310
  }
10245
10311
  async function checkGh(exec = runCommand) {
@@ -10347,7 +10413,7 @@ var SCAFFOLD_TIMEOUT_MS = 3e5;
10347
10413
  var defaultDeps = {
10348
10414
  exec: runCommand,
10349
10415
  git: runGit,
10350
- commandExists,
10416
+ commandExists: probeCommand,
10351
10417
  checkGh: () => checkGh(),
10352
10418
  checkOrg: (org) => checkOrg(org),
10353
10419
  createRepo,
@@ -10405,8 +10471,32 @@ async function createProject(opts, deps = defaultDeps) {
10405
10471
  });
10406
10472
  return { ok: true, projectPath, installHint, steps };
10407
10473
  }
10408
- if (!await deps.commandExists("npx") || !await deps.commandExists(opts.pm)) {
10409
- return fail("preflight", `Required tools missing: need npx and ${opts.pm} on PATH.`);
10474
+ const requiredTools = ["npx", opts.pm];
10475
+ const missingTools = [];
10476
+ const inconclusiveTools = [];
10477
+ for (const tool of requiredTools) {
10478
+ const probe = await deps.commandExists(tool);
10479
+ switch (probe.status) {
10480
+ case "available":
10481
+ break;
10482
+ case "absent":
10483
+ missingTools.push(tool);
10484
+ break;
10485
+ case "inconclusive":
10486
+ inconclusiveTools.push(`${tool} (${probe.reason})`);
10487
+ break;
10488
+ default:
10489
+ return assertNeverCommandProbe(probe);
10490
+ }
10491
+ }
10492
+ if (missingTools.length > 0) {
10493
+ return fail("preflight", `Required tools missing: need ${missingTools.join(" and ")} on PATH.`);
10494
+ }
10495
+ if (inconclusiveTools.length > 0) {
10496
+ return fail(
10497
+ "preflight",
10498
+ `Could not confirm tool availability on PATH: ${inconclusiveTools.join("; ")} \u2014 the probe did not complete (likely transient load). Try again.`
10499
+ );
10410
10500
  }
10411
10501
  if (opts.github) {
10412
10502
  const gh = await deps.checkGh();
@@ -11657,7 +11747,8 @@ async function removeWorktree(ownerRepo, worktreePath2, force = true) {
11657
11747
  return { ok: result.exitCode === 0, stderr: result.exitCode === 0 ? void 0 : result.stderr };
11658
11748
  }
11659
11749
  async function pruneWorktrees(ownerRepo) {
11660
- await runGit(["worktree", "prune"], { cwd: ownerRepo });
11750
+ const result = await runGit(["worktree", "prune"], { cwd: ownerRepo });
11751
+ return { ok: result.exitCode === 0, stderr: result.exitCode === 0 ? void 0 : result.stderr };
11661
11752
  }
11662
11753
  async function listWorktrees(ownerRepo) {
11663
11754
  const result = await runGit(["worktree", "list", "--porcelain"], { cwd: ownerRepo });
@@ -11835,13 +11926,26 @@ async function computeLockfileSignals(dir) {
11835
11926
  hasDotnetProject
11836
11927
  };
11837
11928
  }
11838
- async function runInstallPlan(dir, plan) {
11929
+ async function runInstallPlan(dir, plan, deps = {}) {
11839
11930
  if (plan.manager === "none") {
11840
11931
  return { plan, ran: false, ok: true, skippedReason: "no recognized lockfile" };
11841
11932
  }
11842
- const available = await commandExists(plan.command);
11843
- if (!available) {
11844
- return { plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` };
11933
+ const probe = deps.probe ?? probeCommand;
11934
+ const probed = await probe(plan.command);
11935
+ switch (probed.status) {
11936
+ case "available":
11937
+ break;
11938
+ case "absent":
11939
+ return { plan, ran: false, ok: true, skippedReason: `${plan.command} not found on PATH` };
11940
+ 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
+ };
11947
+ default:
11948
+ return assertNeverCommandProbe(probed);
11845
11949
  }
11846
11950
  const result = await runCommand(plan.command, plan.args, { cwd: dir });
11847
11951
  return {
@@ -12093,11 +12197,41 @@ async function runDryRun(args) {
12093
12197
  }
12094
12198
 
12095
12199
  // src/workspace/worktree/doctor.ts
12200
+ import { existsSync as existsSync6 } from "node:fs";
12201
+ import path66 from "node:path";
12202
+
12203
+ // src/workspace/worktree/orphans.ts
12096
12204
  import { existsSync as existsSync5 } from "node:fs";
12097
12205
  import path65 from "node:path";
12206
+ var WORKSPACE_REPO_ID = "(workspace)";
12207
+ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
12208
+ const orphans = [];
12209
+ for (const repo of repos) {
12210
+ const entries = await listWorktrees(repo.absPath);
12211
+ for (const entry of entries) {
12212
+ const entryPath = path65.normalize(entry.path);
12213
+ const marker = `${path65.sep}.claude${path65.sep}worktrees${path65.sep}`;
12214
+ const idx = entryPath.indexOf(marker);
12215
+ if (idx === -1) continue;
12216
+ const rest = entryPath.slice(idx + marker.length);
12217
+ const slug = rest.split(path65.sep)[0];
12218
+ if (!slug) continue;
12219
+ if (existsSync5(path65.join(wsWorktreesDir, slug))) continue;
12220
+ orphans.push({
12221
+ repoId: repo.id,
12222
+ ownerRepo: repo.absPath,
12223
+ registeredPath: entry.path,
12224
+ slug
12225
+ });
12226
+ }
12227
+ }
12228
+ return orphans;
12229
+ }
12230
+
12231
+ // src/workspace/worktree/doctor.ts
12098
12232
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
12099
12233
  function looksHydrated(memberPath) {
12100
- return HYDRATION_MARKERS.some((marker) => existsSync5(path65.join(memberPath, marker)));
12234
+ return HYDRATION_MARKERS.some((marker) => existsSync6(path66.join(memberPath, marker)));
12101
12235
  }
12102
12236
  async function runDoctor2() {
12103
12237
  const rootInfo = await resolveRoots();
@@ -12107,11 +12241,13 @@ async function runDoctor2() {
12107
12241
  inWorkspaceWorktree: false,
12108
12242
  members: [],
12109
12243
  orphans: [],
12110
- problems: ["Not inside a git repository."]
12244
+ problems: ["Not inside a git repository."],
12245
+ remedies: [],
12246
+ autoHydrate: false
12111
12247
  };
12112
12248
  }
12113
12249
  const workspaceRoot = rootInfo.mainRoot;
12114
- const isWorkspace = existsSync5(path65.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path65.join(workspaceRoot, REPOS_MANIFEST_FILE));
12250
+ const isWorkspace = existsSync6(path66.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path66.join(workspaceRoot, REPOS_MANIFEST_FILE));
12115
12251
  if (!isWorkspace) {
12116
12252
  return {
12117
12253
  isWorkspace: false,
@@ -12120,23 +12256,28 @@ async function runDoctor2() {
12120
12256
  orphans: [],
12121
12257
  problems: [
12122
12258
  `Not a haus workspace \u2014 no ${WORKSPACE_FILE} or ${REPOS_MANIFEST_FILE} at ${workspaceRoot}.`
12123
- ]
12259
+ ],
12260
+ remedies: [],
12261
+ autoHydrate: false
12124
12262
  };
12125
12263
  }
12126
12264
  const wsWorktreesDir = worktreesDir(workspaceRoot);
12127
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path65.sep}`);
12265
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path66.sep}`);
12128
12266
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
12267
+ const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
12268
+ const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
12129
12269
  const members = await readMembers(rootInfo);
12130
12270
  const problems = [];
12271
+ const remedies = [];
12131
12272
  const memberChecks = [];
12132
12273
  let slug;
12133
12274
  if (inWorkspaceWorktree) {
12134
12275
  const wtPath = rootInfo.repoRoot;
12135
- slug = rootInfo.worktreeName ?? path65.basename(wtPath);
12276
+ slug = rootInfo.worktreeName ?? path66.basename(wtPath);
12136
12277
  const state = await readWorktreeState(wtPath);
12137
12278
  for (const member of members) {
12138
- const memberPath = path65.join(wtPath, member.folder);
12139
- const materialized = existsSync5(memberPath);
12279
+ const memberPath = path66.join(wtPath, member.folder);
12280
+ const materialized = existsSync6(memberPath);
12140
12281
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
12141
12282
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
12142
12283
  const branchMismatch = Boolean(
@@ -12161,33 +12302,42 @@ async function runDoctor2() {
12161
12302
  );
12162
12303
  }
12163
12304
  }
12164
- }
12165
- const orphans = [];
12166
- for (const member of members) {
12167
- const entries = await listWorktrees(member.absPath);
12168
- for (const entry of entries) {
12169
- const entryPath = path65.normalize(entry.path);
12170
- const marker = `${path65.sep}.claude${path65.sep}worktrees${path65.sep}`;
12171
- const idx = entryPath.indexOf(marker);
12172
- if (idx === -1) continue;
12173
- const rest = entryPath.slice(idx + marker.length);
12174
- const slugName = rest.split(path65.sep)[0];
12175
- if (!slugName) continue;
12176
- if (!existsSync5(path65.join(wsWorktreesDir, slugName))) {
12177
- orphans.push(
12178
- `${member.id}: worktree registered at ${entry.path} but ${path65.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
12179
- );
12180
- }
12305
+ if (memberChecks.some((m) => !m.materialized)) {
12306
+ remedies.push(
12307
+ "Run `haus workspace worktree hydrate --materialize` to materialize the missing members."
12308
+ );
12181
12309
  }
12182
12310
  }
12183
- if (orphans.length > 0) problems.push(...orphans);
12184
- return { isWorkspace: true, inWorkspaceWorktree, slug, members: memberChecks, orphans, problems };
12311
+ const orphanScan = await findOrphanedRegistrations(
12312
+ [
12313
+ { id: WORKSPACE_REPO_ID, absPath: workspaceRoot },
12314
+ ...members.map((m) => ({ id: m.id, absPath: m.absPath }))
12315
+ ],
12316
+ wsWorktreesDir
12317
+ );
12318
+ const orphans = orphanScan.map(
12319
+ (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path66.join(wsWorktreesDir, o.slug)} no longer exists.`
12320
+ );
12321
+ if (orphans.length > 0) {
12322
+ problems.push(...orphans);
12323
+ remedies.push("Run `haus workspace worktree prune` to clean up stale worktree registrations.");
12324
+ }
12325
+ return {
12326
+ isWorkspace: true,
12327
+ inWorkspaceWorktree,
12328
+ slug,
12329
+ members: memberChecks,
12330
+ orphans,
12331
+ problems,
12332
+ remedies,
12333
+ autoHydrate
12334
+ };
12185
12335
  }
12186
12336
 
12187
12337
  // src/workspace/worktree/hydrate-command.ts
12188
12338
  init_fs();
12189
- import { existsSync as existsSync6 } from "node:fs";
12190
- import path66 from "node:path";
12339
+ import { existsSync as existsSync7 } from "node:fs";
12340
+ import path67 from "node:path";
12191
12341
  async function runHydrateWorktree(opts) {
12192
12342
  const rootInfo = await resolveRoots();
12193
12343
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -12198,7 +12348,7 @@ async function runHydrateWorktree(opts) {
12198
12348
  }
12199
12349
  const workspaceRoot = rootInfo.mainRoot;
12200
12350
  const wtPath = rootInfo.repoRoot;
12201
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path66.sep}`)) {
12351
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path67.sep}`)) {
12202
12352
  return {
12203
12353
  ok: false,
12204
12354
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -12210,14 +12360,14 @@ async function runHydrateWorktree(opts) {
12210
12360
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
12211
12361
  }
12212
12362
  const state = await readWorktreeState(wtPath);
12213
- const slug = state?.slug ?? path66.basename(wtPath);
12363
+ const slug = state?.slug ?? path67.basename(wtPath);
12214
12364
  const detectedBranch = await currentBranch(wtPath);
12215
12365
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
12216
12366
  if (opts.dryRun) {
12217
12367
  const results2 = await Promise.all(
12218
12368
  selected.map(async (member) => {
12219
- const memberPath = path66.join(wtPath, member.folder);
12220
- if (existsSync6(memberPath)) {
12369
+ const memberPath = path67.join(wtPath, member.folder);
12370
+ if (existsSync7(memberPath)) {
12221
12371
  return { member: member.id, folder: member.folder, status: "planned" };
12222
12372
  }
12223
12373
  if (!opts.materialize) {
@@ -12225,7 +12375,7 @@ async function runHydrateWorktree(opts) {
12225
12375
  member: member.id,
12226
12376
  folder: member.folder,
12227
12377
  status: "skipped",
12228
- error: "not materialized in this worktree"
12378
+ error: "not materialized in this worktree \u2014 re-run with --materialize to create it"
12229
12379
  };
12230
12380
  }
12231
12381
  const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
@@ -12237,15 +12387,15 @@ async function runHydrateWorktree(opts) {
12237
12387
  const results = await mapWithConcurrency(
12238
12388
  selected,
12239
12389
  async (member) => {
12240
- const memberPath = path66.join(wtPath, member.folder);
12390
+ const memberPath = path67.join(wtPath, member.folder);
12241
12391
  let branchAction;
12242
- if (!existsSync6(memberPath)) {
12392
+ if (!existsSync7(memberPath)) {
12243
12393
  if (!opts.materialize) {
12244
12394
  return {
12245
12395
  member: member.id,
12246
12396
  folder: member.folder,
12247
12397
  status: "skipped",
12248
- error: "not materialized in this worktree"
12398
+ error: "not materialized in this worktree \u2014 re-run with --materialize to create it"
12249
12399
  };
12250
12400
  }
12251
12401
  const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
@@ -12300,7 +12450,7 @@ async function runHydrateWorktree(opts) {
12300
12450
  }
12301
12451
 
12302
12452
  // src/workspace/worktree/list.ts
12303
- import path67 from "node:path";
12453
+ import path68 from "node:path";
12304
12454
  import fs47 from "fs-extra";
12305
12455
  async function runList2() {
12306
12456
  const resolved = await resolveWorkspaceForWorktree();
@@ -12313,12 +12463,12 @@ async function runList2() {
12313
12463
  const configuredMembers = await readMembers(rootInfo);
12314
12464
  const worktrees = [];
12315
12465
  for (const slug of slugs) {
12316
- const wtPath = path67.join(dir, slug);
12466
+ const wtPath = path68.join(dir, slug);
12317
12467
  const state = await readWorktreeState(wtPath);
12318
12468
  const members = [];
12319
12469
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
12320
12470
  for (const m of source) {
12321
- const memberPath = path67.join(wtPath, m.folder);
12471
+ const memberPath = path68.join(wtPath, m.folder);
12322
12472
  const materialized = await fs47.pathExists(memberPath);
12323
12473
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
12324
12474
  members.push({
@@ -12339,8 +12489,48 @@ async function runList2() {
12339
12489
  return { ok: true, workspaceRoot, worktrees };
12340
12490
  }
12341
12491
 
12492
+ // src/workspace/worktree/prune.ts
12493
+ async function runPrune2(opts) {
12494
+ const resolved = await resolveWorkspaceForWorktree();
12495
+ if (!resolved.ok) return { ok: false, error: resolved.reason };
12496
+ const { rootInfo, workspaceRoot } = resolved;
12497
+ const members = await readMembers(rootInfo);
12498
+ const orphanScan = await findOrphanedRegistrations(
12499
+ [
12500
+ { id: WORKSPACE_REPO_ID, absPath: workspaceRoot },
12501
+ ...members.map((m) => ({ id: m.id, absPath: m.absPath }))
12502
+ ],
12503
+ worktreesDir(workspaceRoot)
12504
+ );
12505
+ const byRepo = /* @__PURE__ */ new Map();
12506
+ for (const orphan of orphanScan) {
12507
+ const bucket = byRepo.get(orphan.repoId) ?? { ownerRepo: orphan.ownerRepo, orphans: [] };
12508
+ bucket.orphans.push(orphan);
12509
+ byRepo.set(orphan.repoId, bucket);
12510
+ }
12511
+ if (opts.dryRun) {
12512
+ const results2 = Array.from(byRepo, ([repo, { orphans }]) => ({
12513
+ repo,
12514
+ orphans: orphans.map((o) => o.registeredPath),
12515
+ pruned: false
12516
+ }));
12517
+ return { ok: true, dryRun: true, results: results2 };
12518
+ }
12519
+ const results = [];
12520
+ for (const [repo, { ownerRepo, orphans }] of byRepo) {
12521
+ const outcome = await pruneWorktrees(ownerRepo);
12522
+ results.push({
12523
+ repo,
12524
+ orphans: orphans.map((o) => o.registeredPath),
12525
+ pruned: outcome.ok,
12526
+ error: outcome.ok ? void 0 : outcome.stderr ?? "git worktree prune failed"
12527
+ });
12528
+ }
12529
+ return { ok: results.every((r) => r.pruned), dryRun: false, results };
12530
+ }
12531
+
12342
12532
  // src/workspace/worktree/remove.ts
12343
- import path68 from "node:path";
12533
+ import path69 from "node:path";
12344
12534
  import fs48 from "fs-extra";
12345
12535
  async function runRemove2(opts) {
12346
12536
  const resolved = await resolveWorkspaceForWorktree();
@@ -12367,7 +12557,7 @@ async function runRemove2(opts) {
12367
12557
  targets.push({
12368
12558
  repo: configMember?.id ?? stateMember.id,
12369
12559
  ownerRepo,
12370
- path: path68.join(wsWorktreePath, stateMember.folder)
12560
+ path: path69.join(wsWorktreePath, stateMember.folder)
12371
12561
  });
12372
12562
  }
12373
12563
  const existingTargets = [];
@@ -12580,6 +12770,32 @@ async function handleRemove(opts) {
12580
12770
  }
12581
12771
  }
12582
12772
  }
12773
+ async function handlePrune(opts) {
12774
+ const result = await runPrune2({
12775
+ dryRun: resolveWriteMode(opts, "mutate-by-default").kind === "preview"
12776
+ });
12777
+ if (!result.ok && "error" in result) {
12778
+ if (opts.json) emitJson(result);
12779
+ else error(result.error);
12780
+ process.exitCode = 1;
12781
+ return;
12782
+ }
12783
+ if (opts.json) {
12784
+ emitJson(result);
12785
+ if (!result.dryRun && !result.ok) process.exitCode = 1;
12786
+ return;
12787
+ }
12788
+ if (result.results.length === 0) {
12789
+ log("No stale worktree registrations found.");
12790
+ return;
12791
+ }
12792
+ for (const r of result.results) {
12793
+ const prefix = result.dryRun ? "[dry-run] Would prune" : r.pruned ? "Pruned" : "FAILED to prune";
12794
+ const detail = r.error ? ` \u2014 ${r.error}` : "";
12795
+ log(`${prefix} ${r.repo}: ${r.orphans.join(", ")}${detail}`);
12796
+ }
12797
+ if (!result.dryRun && !result.ok) process.exitCode = 1;
12798
+ }
12583
12799
  async function handleList(opts) {
12584
12800
  const result = await runList2();
12585
12801
  if (opts.json) {
@@ -12631,12 +12847,17 @@ async function handleDoctor(opts) {
12631
12847
  }
12632
12848
  if (opts.fromHook) {
12633
12849
  const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
12850
+ const needsMaterialize = report.members.some((m) => !m.materialized || !m.hydrated);
12851
+ const directive = report.autoHydrate && needsMaterialize ? "autoHydrate is enabled for this workspace: run `haus workspace worktree hydrate --materialize` now, before any other work, to materialize and hydrate the missing members. " : "";
12852
+ const remedies = directive ? report.remedies.filter((r) => !r.includes("--materialize")) : report.remedies;
12853
+ const remedySuffix = remedies.length > 0 ? ` ${remedies.join(" ")}` : "";
12634
12854
  emitFromHookContext(
12635
- `${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
12855
+ `${directive}${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}${remedySuffix}`
12636
12856
  );
12637
12857
  return;
12638
12858
  }
12639
12859
  for (const p of report.problems) warn(p);
12860
+ for (const r of report.remedies) log(r);
12640
12861
  process.exitCode = 1;
12641
12862
  } catch (err) {
12642
12863
  const message2 = err instanceof Error ? err.message : String(err);
@@ -12660,12 +12881,14 @@ async function runWorktree(action, opts = {}) {
12660
12881
  return handleList(opts);
12661
12882
  case "doctor":
12662
12883
  return handleDoctor(opts);
12884
+ case "prune":
12885
+ return handlePrune(opts);
12663
12886
  }
12664
12887
  }
12665
12888
 
12666
12889
  // src/commands/workspace.ts
12667
- import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
12668
- import path77 from "node:path";
12890
+ import { existsSync as existsSync11, statSync as statSync3 } from "node:fs";
12891
+ import path78 from "node:path";
12669
12892
  init_fs();
12670
12893
  init_logger();
12671
12894
 
@@ -12723,8 +12946,8 @@ ${summaries.map(
12723
12946
  }
12724
12947
 
12725
12948
  // src/commands/workspace/doctor.ts
12726
- import { existsSync as existsSync7 } from "node:fs";
12727
- import path70 from "node:path";
12949
+ import { existsSync as existsSync8 } from "node:fs";
12950
+ import path71 from "node:path";
12728
12951
  init_logger();
12729
12952
  init_paths();
12730
12953
 
@@ -12732,14 +12955,14 @@ init_paths();
12732
12955
  init_fs();
12733
12956
  init_paths();
12734
12957
  import { readFileSync as readFileSync4 } from "node:fs";
12735
- import path69 from "node:path";
12958
+ import path70 from "node:path";
12736
12959
  var MANIFEST_FILE = "workspace.manifest.json";
12737
12960
  function manifestPath(workspaceRoot) {
12738
12961
  return hausPath(workspaceRoot, MANIFEST_FILE);
12739
12962
  }
12740
12963
  function hausVersion() {
12741
12964
  try {
12742
- const pkg = JSON.parse(readFileSync4(path69.join(packageRoot(), "package.json"), "utf8"));
12965
+ const pkg = JSON.parse(readFileSync4(path70.join(packageRoot(), "package.json"), "utf8"));
12743
12966
  return pkg.version ?? "0.0.0";
12744
12967
  } catch {
12745
12968
  return "0.0.0";
@@ -12831,7 +13054,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
12831
13054
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
12832
13055
  const catalogRefByRepo = [];
12833
13056
  for (const repo of config.repos) {
12834
- const repoRoot = path70.resolve(workspaceRoot, repo.path);
13057
+ const repoRoot = path71.resolve(workspaceRoot, repo.path);
12835
13058
  const entry = manifestByName.get(repo.name);
12836
13059
  if (!entry) {
12837
13060
  flag({
@@ -12856,7 +13079,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
12856
13079
  detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
12857
13080
  });
12858
13081
  }
12859
- if (!existsSync7(claudePath(repoRoot))) {
13082
+ if (!existsSync8(claudePath(repoRoot))) {
12860
13083
  flag({
12861
13084
  repo: repo.name,
12862
13085
  kind: "missing-claude",
@@ -12865,7 +13088,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
12865
13088
  }
12866
13089
  const lock = await checkLock(repoRoot);
12867
13090
  if (lock.catalogRef) catalogRefByRepo.push({ repo: repo.name, ref: lock.catalogRef });
12868
- if (!existsSync7(hausPath(repoRoot, "haus.lock.json"))) {
13091
+ if (!existsSync8(hausPath(repoRoot, "haus.lock.json"))) {
12869
13092
  flag({
12870
13093
  repo: repo.name,
12871
13094
  kind: "missing-lock",
@@ -12896,7 +13119,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
12896
13119
  const memberById = new Map(members.map((m) => [m.id, m]));
12897
13120
  for (const entry of manifest.linkedContext) {
12898
13121
  const member = memberById.get(entry.repo);
12899
- if (!member || !existsSync7(member.absPath)) {
13122
+ if (!member || !existsSync8(member.absPath)) {
12900
13123
  flag({
12901
13124
  repo: entry.repo,
12902
13125
  kind: "missing-linked-context-source",
@@ -12904,7 +13127,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
12904
13127
  });
12905
13128
  continue;
12906
13129
  }
12907
- if (!existsSync7(path70.join(workspaceRoot, entry.path))) {
13130
+ if (!existsSync8(path71.join(workspaceRoot, entry.path))) {
12908
13131
  flag({
12909
13132
  repo: entry.repo,
12910
13133
  kind: "missing-linked-context-copy",
@@ -12950,17 +13173,17 @@ function emit(args) {
12950
13173
  init_logger();
12951
13174
 
12952
13175
  // src/workspace/link-context/link.ts
12953
- import path73 from "node:path";
13176
+ import path74 from "node:path";
12954
13177
  import fs51 from "fs-extra";
12955
13178
  init_fs();
12956
13179
  init_logger();
12957
13180
 
12958
13181
  // src/workspace/link-context/plan.ts
12959
- import path72 from "node:path";
13182
+ import path73 from "node:path";
12960
13183
  import fs50 from "fs-extra";
12961
13184
 
12962
13185
  // src/workspace/link-context/scan-source.ts
12963
- import path71 from "node:path";
13186
+ import path72 from "node:path";
12964
13187
  import fs49 from "fs-extra";
12965
13188
  var ASSET_SUBDIR = {
12966
13189
  skill: "skills",
@@ -12969,36 +13192,36 @@ var ASSET_SUBDIR = {
12969
13192
  };
12970
13193
  async function hasLinkableContext(member) {
12971
13194
  for (const subdir of Object.values(ASSET_SUBDIR)) {
12972
- if (await fs49.pathExists(path71.join(member.absPath, ".claude", subdir))) return true;
13195
+ if (await fs49.pathExists(path72.join(member.absPath, ".claude", subdir))) return true;
12973
13196
  }
12974
13197
  return false;
12975
13198
  }
12976
13199
  async function collectSkills(member) {
12977
- const dir = path71.join(member.absPath, ".claude", "skills");
13200
+ const dir = path72.join(member.absPath, ".claude", "skills");
12978
13201
  if (!await fs49.pathExists(dir)) return [];
12979
13202
  const names = await fs49.readdir(dir);
12980
13203
  const out = [];
12981
13204
  for (const name of [...names].sort()) {
12982
- const abs = path71.join(dir, name);
13205
+ const abs = path72.join(dir, name);
12983
13206
  const stat = await fs49.lstat(abs);
12984
13207
  if (!stat.isDirectory()) continue;
12985
- if (!await fs49.pathExists(path71.join(abs, "SKILL.md"))) continue;
12986
- out.push({ type: "skill", name, sourceRelPath: path71.posix.join(".claude", "skills", name) });
13208
+ if (!await fs49.pathExists(path72.join(abs, "SKILL.md"))) continue;
13209
+ out.push({ type: "skill", name, sourceRelPath: path72.posix.join(".claude", "skills", name) });
12987
13210
  }
12988
13211
  return out;
12989
13212
  }
12990
13213
  async function collectFlatMarkdown(member, type) {
12991
13214
  const subdir = ASSET_SUBDIR[type];
12992
- const dir = path71.join(member.absPath, ".claude", subdir);
13215
+ const dir = path72.join(member.absPath, ".claude", subdir);
12993
13216
  if (!await fs49.pathExists(dir)) return [];
12994
13217
  const names = await fs49.readdir(dir);
12995
13218
  const out = [];
12996
13219
  for (const fileName of [...names].sort()) {
12997
13220
  if (!fileName.endsWith(".md")) continue;
12998
- const abs = path71.join(dir, fileName);
13221
+ const abs = path72.join(dir, fileName);
12999
13222
  if (!(await fs49.lstat(abs)).isFile()) continue;
13000
13223
  const name = fileName.slice(0, -".md".length);
13001
- out.push({ type, name, sourceRelPath: path71.posix.join(".claude", subdir, fileName) });
13224
+ out.push({ type, name, sourceRelPath: path72.posix.join(".claude", subdir, fileName) });
13002
13225
  }
13003
13226
  return out;
13004
13227
  }
@@ -13013,9 +13236,9 @@ async function collectSourceAssets(member) {
13013
13236
 
13014
13237
  // src/workspace/link-context/plan.ts
13015
13238
  function repoFolderPrefix(member) {
13016
- const base = path72.posix.basename(member.folder);
13239
+ const base = path73.posix.basename(member.folder);
13017
13240
  if (base && base !== "." && base !== "/") return base;
13018
- const fromAbs = path72.basename(member.absPath);
13241
+ const fromAbs = path73.basename(member.absPath);
13019
13242
  return fromAbs || member.id;
13020
13243
  }
13021
13244
  function assetKey(type, destKey) {
@@ -13072,7 +13295,7 @@ async function buildLinkPlan(members) {
13072
13295
  name: asset.name,
13073
13296
  destKey,
13074
13297
  sourceRelPath: asset.sourceRelPath,
13075
- sourceAbsPath: path72.join(member.absPath, asset.sourceRelPath),
13298
+ sourceAbsPath: path73.join(member.absPath, asset.sourceRelPath),
13076
13299
  sourceHash
13077
13300
  });
13078
13301
  }
@@ -13083,7 +13306,7 @@ async function buildLinkPlan(members) {
13083
13306
  function destRelPathFor(type, destKey) {
13084
13307
  const subdir = `${type}s`;
13085
13308
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
13086
- return path73.posix.join(".claude", subdir, leaf);
13309
+ return path74.posix.join(".claude", subdir, leaf);
13087
13310
  }
13088
13311
  async function runLinkContext(workspaceRoot, opts = {}) {
13089
13312
  const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
@@ -13109,9 +13332,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
13109
13332
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
13110
13333
  for (const entry of plan.entries) {
13111
13334
  const destRel = destRelPathFor(entry.type, entry.destKey);
13112
- const destAbs = path73.join(workspaceRoot, destRel);
13335
+ const destAbs = path74.join(workspaceRoot, destRel);
13113
13336
  if (write) {
13114
- await fs51.ensureDir(path73.dirname(destAbs));
13337
+ await fs51.ensureDir(path74.dirname(destAbs));
13115
13338
  if (await fs51.pathExists(destAbs)) await fs51.remove(destAbs);
13116
13339
  await fs51.copy(entry.sourceAbsPath, destAbs, {
13117
13340
  overwrite: true,
@@ -13199,11 +13422,11 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
13199
13422
  }
13200
13423
 
13201
13424
  // src/commands/workspace/setup.ts
13202
- import { existsSync as existsSync8, statSync } from "node:fs";
13203
- import path75 from "node:path";
13425
+ import { existsSync as existsSync9, statSync } from "node:fs";
13426
+ import path76 from "node:path";
13204
13427
 
13205
13428
  // src/claude/write-workspace-claude-md.ts
13206
- import path74 from "node:path";
13429
+ import path75 from "node:path";
13207
13430
  import fs52 from "fs-extra";
13208
13431
  init_fs();
13209
13432
  init_logger();
@@ -13226,7 +13449,7 @@ ${WORKSPACE_BLOCK_END}`;
13226
13449
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
13227
13450
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
13228
13451
  const dryRun = opts.dryRun ?? false;
13229
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path74.join(workspaceRoot, "CLAUDE.md");
13452
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path75.join(workspaceRoot, "CLAUDE.md");
13230
13453
  const say = opts.quiet ? () => {
13231
13454
  } : log;
13232
13455
  const existed = await fs52.pathExists(filePath);
@@ -13259,20 +13482,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
13259
13482
  init_fs();
13260
13483
  init_logger();
13261
13484
  function resolveWorkspaceRoot(start = process.cwd()) {
13262
- let dir = path75.resolve(start);
13485
+ let dir = path76.resolve(start);
13263
13486
  for (; ; ) {
13264
- if (existsSync8(path75.join(dir, WORKSPACE_FILE))) return dir;
13265
- const parent = path75.dirname(dir);
13266
- if (parent === dir) return path75.resolve(start);
13487
+ if (existsSync9(path76.join(dir, WORKSPACE_FILE))) return dir;
13488
+ const parent = path76.dirname(dir);
13489
+ if (parent === dir) return path76.resolve(start);
13267
13490
  dir = parent;
13268
13491
  }
13269
13492
  }
13270
13493
  function isRootRepo(workspaceRoot, repoPath) {
13271
- return path75.resolve(workspaceRoot, repoPath) === path75.resolve(workspaceRoot);
13494
+ return path76.resolve(workspaceRoot, repoPath) === path76.resolve(workspaceRoot);
13272
13495
  }
13273
13496
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
13274
13497
  const apply = options.write ?? false;
13275
- const configText = await readText(path75.join(workspaceRoot, WORKSPACE_FILE));
13498
+ const configText = await readText(path76.join(workspaceRoot, WORKSPACE_FILE));
13276
13499
  if (!configText) {
13277
13500
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
13278
13501
  process.exitCode = 1;
@@ -13299,11 +13522,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13299
13522
  const statuses = [];
13300
13523
  const aggregateInputs = [];
13301
13524
  for (const repo of repos) {
13302
- const repoRoot = path75.resolve(workspaceRoot, repo.path);
13525
+ const repoRoot = path76.resolve(workspaceRoot, repo.path);
13303
13526
  say(`
13304
13527
  \u2192 ${repo.name} (${repo.path})`);
13305
13528
  try {
13306
- if (!existsSync8(repoRoot) || !statSync(repoRoot).isDirectory()) {
13529
+ if (!existsSync9(repoRoot) || !statSync(repoRoot).isDirectory()) {
13307
13530
  throw new Error(`Repo path is not a directory: ${repo.path}`);
13308
13531
  }
13309
13532
  const res = await runSetupCore(repoRoot, {
@@ -13378,7 +13601,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13378
13601
  if (apply && !options.dryRun) {
13379
13602
  const statusByName = new Map(statuses.map((s) => [s.name, s]));
13380
13603
  const prior = await readManifest2(workspaceRoot);
13381
- if (!prior && existsSync8(manifestPath(workspaceRoot))) {
13604
+ if (!prior && existsSync9(manifestPath(workspaceRoot))) {
13382
13605
  warn(
13383
13606
  "Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
13384
13607
  );
@@ -13389,7 +13612,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13389
13612
  const status = statusByName.get(repo.name);
13390
13613
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
13391
13614
  if (status?.status === "ok") {
13392
- const lock = await checkLock(path75.resolve(workspaceRoot, repo.path));
13615
+ const lock = await checkLock(path76.resolve(workspaceRoot, repo.path));
13393
13616
  manifestRepos.push({
13394
13617
  name: repo.name,
13395
13618
  path: repo.path,
@@ -13445,7 +13668,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
13445
13668
  try {
13446
13669
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
13447
13670
  if (linkResult.ok) {
13448
- written.push(...linkResult.added.map((rel) => path75.join(workspaceRoot, rel)));
13671
+ written.push(...linkResult.added.map((rel) => path76.join(workspaceRoot, rel)));
13449
13672
  if (!options.json) {
13450
13673
  say(
13451
13674
  `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` : ""}.`
@@ -13476,8 +13699,8 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
13476
13699
  }
13477
13700
 
13478
13701
  // src/commands/workspace/undo.ts
13479
- import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
13480
- import path76 from "node:path";
13702
+ import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
13703
+ import path77 from "node:path";
13481
13704
  import fs53 from "fs-extra";
13482
13705
  init_fs();
13483
13706
  init_logger();
@@ -13508,11 +13731,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13508
13731
  }
13509
13732
  if (options.dryRun) {
13510
13733
  for (const repo of config.repos) {
13511
- const repoRoot = path76.resolve(workspaceRoot, repo.path);
13734
+ const repoRoot = path77.resolve(workspaceRoot, repo.path);
13512
13735
  log(`
13513
13736
  \u2192 ${repo.name} (${repo.path})`);
13514
13737
  try {
13515
- if (!existsSync9(repoRoot) || !statSync2(repoRoot).isDirectory()) {
13738
+ if (!existsSync10(repoRoot) || !statSync2(repoRoot).isDirectory()) {
13516
13739
  throw new Error(`Repo path is not a directory: ${repo.path}`);
13517
13740
  }
13518
13741
  await runUndo({ dryRun: true, root: repoRoot });
@@ -13540,11 +13763,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13540
13763
  ];
13541
13764
  for (const abs of workspaceArtifacts) {
13542
13765
  if (await fs53.pathExists(abs)) {
13543
- log(`[dry-run] Would remove ${path76.relative(workspaceRoot, abs)}`);
13766
+ log(`[dry-run] Would remove ${path77.relative(workspaceRoot, abs)}`);
13544
13767
  }
13545
13768
  }
13546
13769
  if (!await fs53.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
13547
- const claudeMd = await readText(path76.join(workspaceRoot, "CLAUDE.md"));
13770
+ const claudeMd = await readText(path77.join(workspaceRoot, "CLAUDE.md"));
13548
13771
  if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
13549
13772
  log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
13550
13773
  }
@@ -13563,11 +13786,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13563
13786
  }
13564
13787
  let anyRepoFailed = false;
13565
13788
  for (const repo of config.repos) {
13566
- const repoRoot = path76.resolve(workspaceRoot, repo.path);
13789
+ const repoRoot = path77.resolve(workspaceRoot, repo.path);
13567
13790
  log(`
13568
13791
  \u2192 ${repo.name} (${repo.path})`);
13569
13792
  try {
13570
- if (!existsSync9(repoRoot) || !statSync2(repoRoot).isDirectory()) {
13793
+ if (!existsSync10(repoRoot) || !statSync2(repoRoot).isDirectory()) {
13571
13794
  throw new Error(`Repo path is not a directory: ${repo.path}`);
13572
13795
  }
13573
13796
  await runUndo({ yes: true, root: repoRoot });
@@ -13592,21 +13815,21 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
13592
13815
  const abs = hausPath(workspaceRoot, rel);
13593
13816
  if (await fs53.pathExists(abs)) {
13594
13817
  await fs53.remove(abs);
13595
- log(`Removed ${path76.relative(workspaceRoot, abs)}`);
13818
+ log(`Removed ${path77.relative(workspaceRoot, abs)}`);
13596
13819
  }
13597
13820
  }
13598
13821
  const manifestFile = manifestPath(workspaceRoot);
13599
13822
  if (await fs53.pathExists(manifestFile)) {
13600
13823
  await fs53.remove(manifestFile);
13601
- log(`Removed ${path76.relative(workspaceRoot, manifestFile)}`);
13824
+ log(`Removed ${path77.relative(workspaceRoot, manifestFile)}`);
13602
13825
  }
13603
13826
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
13604
13827
  if (await fs53.pathExists(workspaceMdPath)) {
13605
13828
  await fs53.remove(workspaceMdPath);
13606
- log(`Removed ${path76.relative(workspaceRoot, workspaceMdPath)}`);
13829
+ log(`Removed ${path77.relative(workspaceRoot, workspaceMdPath)}`);
13607
13830
  } else {
13608
- const claudeMdPath = path76.join(workspaceRoot, "CLAUDE.md");
13609
- if (existsSync9(claudeMdPath)) {
13831
+ const claudeMdPath = path77.join(workspaceRoot, "CLAUDE.md");
13832
+ if (existsSync10(claudeMdPath)) {
13610
13833
  const prev = await readText(claudeMdPath);
13611
13834
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
13612
13835
  const next = stripHausBlock(prev, {
@@ -13652,7 +13875,7 @@ function normalizeMaxDepth(maxDepth) {
13652
13875
  return void 0;
13653
13876
  }
13654
13877
  async function initWorkspace(options = {}) {
13655
- if (existsSync10(WORKSPACE_FILE) && !options.yes) {
13878
+ if (existsSync11(WORKSPACE_FILE) && !options.yes) {
13656
13879
  const ok = await confirm(
13657
13880
  `${WORKSPACE_FILE} already exists \u2014 overwrite with a fresh placeholder? Existing client/repos/relationships will be lost. Run \`haus workspace discover\` instead to merge in newly found repos.`
13658
13881
  );
@@ -13674,7 +13897,7 @@ relationships: []
13674
13897
  log("Workspace initialized.");
13675
13898
  }
13676
13899
  async function scanWorkspace(workspaceRoot, opts) {
13677
- const configText = await readText(path77.join(workspaceRoot, WORKSPACE_FILE));
13900
+ const configText = await readText(path78.join(workspaceRoot, WORKSPACE_FILE));
13678
13901
  if (!configText) {
13679
13902
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
13680
13903
  process.exitCode = 1;
@@ -13696,9 +13919,9 @@ async function scanWorkspace(workspaceRoot, opts) {
13696
13919
  const inputs = [];
13697
13920
  const failures = [];
13698
13921
  for (const repo of config.repos) {
13699
- const repoRoot = path77.resolve(workspaceRoot, repo.path);
13922
+ const repoRoot = path78.resolve(workspaceRoot, repo.path);
13700
13923
  try {
13701
- if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
13924
+ if (!existsSync11(repoRoot) || !statSync3(repoRoot).isDirectory()) {
13702
13925
  throw new Error("path is not a directory");
13703
13926
  }
13704
13927
  const result = await scanProject(repoRoot);
@@ -13764,7 +13987,7 @@ init_logger();
13764
13987
  init_paths();
13765
13988
  function cliVersion() {
13766
13989
  try {
13767
- const pkgPath = path78.join(packageRoot(), "package.json");
13990
+ const pkgPath = path79.join(packageRoot(), "package.json");
13768
13991
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
13769
13992
  return pkg.version ?? "0.0.0";
13770
13993
  } catch {
@@ -13774,7 +13997,7 @@ function cliVersion() {
13774
13997
  var program = new Command();
13775
13998
  function validateRuntimeNodeVersion() {
13776
13999
  try {
13777
- const pkgPath = path78.join(packageRoot(), "package.json");
14000
+ const pkgPath = path79.join(packageRoot(), "package.json");
13778
14001
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
13779
14002
  const requiredRange = pkg.engines?.node;
13780
14003
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -13911,6 +14134,11 @@ worktree.command("list").description("List materialized workspace worktrees and
13911
14134
  worktree.command("remove <slug>").description("Remove a workspace worktree and its member worktrees").option("--force", "Bypass the uncommitted/unpushed-work refusal").option("--dry-run", "Preview without removing anything").option("--json", "Emit the structured remove result as JSON").action(
13912
14135
  (slug, opts) => runWorktree("remove", { slug, force: opts.force, dryRun: opts.dryRun, json: opts.json })
13913
14136
  );
14137
+ worktree.command("prune").description(
14138
+ "Remove stale worktree registrations left by a workspace worktree deleted without `haus workspace worktree remove` (e.g. a natively deleted Claude Code worktree)"
14139
+ ).option("--dry-run", "Preview which registrations would be pruned").option("--json", "Emit the structured prune result as JSON").action(
14140
+ (opts) => runWorktree("prune", { dryRun: opts.dryRun, json: opts.json })
14141
+ );
13914
14142
  worktree.command("doctor").description("Fast, side-effect-free health check for the current workspace worktree").option("--from-hook", "Emit for the SessionStart hook \u2014 always exits 0").action((opts) => runWorktree("doctor", { fromHook: opts.fromHook }));
13915
14143
  program.parseAsync(process.argv).catch((err) => {
13916
14144
  const message2 = err instanceof Error ? err.message : String(err);