@haus-tech/haus-workflow 1.5.0 → 1.5.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
@@ -60,7 +60,7 @@ var init_exec = __esm({
60
60
 
61
61
  // src/cli.ts
62
62
  import { readFileSync as readFileSync5 } from "node:fs";
63
- import path70 from "node:path";
63
+ import path71 from "node:path";
64
64
  import { Command } from "commander";
65
65
 
66
66
  // src/commands/apply.ts
@@ -2103,8 +2103,8 @@ function buildDenyRules() {
2103
2103
  for (const command of DENY_COMMANDS) {
2104
2104
  rules.push(`Bash(${command}:*)`);
2105
2105
  }
2106
- for (const path71 of DENY_PATHS) {
2107
- const pattern = DENY_DIRS.has(path71) ? `${path71}/**` : path71;
2106
+ for (const path72 of DENY_PATHS) {
2107
+ const pattern = DENY_DIRS.has(path72) ? `${path72}/**` : path72;
2108
2108
  for (const tool of FILE_TOOLS) {
2109
2109
  rules.push(`${tool}(${pattern})`);
2110
2110
  }
@@ -4151,19 +4151,133 @@ async function runCatalogAudit() {
4151
4151
  }
4152
4152
 
4153
4153
  // src/commands/doctor.ts
4154
- import path37 from "node:path";
4154
+ import path38 from "node:path";
4155
4155
  import fs29 from "fs-extra";
4156
4156
 
4157
4157
  // src/decisions/doctor.ts
4158
- import path28 from "node:path";
4158
+ import path29 from "node:path";
4159
4159
  import fs26 from "fs-extra";
4160
4160
 
4161
4161
  // src/decisions/check.ts
4162
- import path27 from "node:path";
4162
+ import path28 from "node:path";
4163
4163
  import fs25 from "fs-extra";
4164
4164
 
4165
+ // src/decisions/dep-bump.ts
4166
+ var DEP_SECTIONS = [
4167
+ "dependencies",
4168
+ "devDependencies",
4169
+ "peerDependencies",
4170
+ "optionalDependencies"
4171
+ ];
4172
+ function isRecord(value) {
4173
+ return value != null && typeof value === "object" && !Array.isArray(value);
4174
+ }
4175
+ function extractDepMap(pkg) {
4176
+ const map = /* @__PURE__ */ new Map();
4177
+ if (!isRecord(pkg)) return map;
4178
+ for (const section of DEP_SECTIONS) {
4179
+ const block = pkg[section];
4180
+ if (!isRecord(block)) continue;
4181
+ for (const [name, ver] of Object.entries(block)) {
4182
+ if (typeof ver === "string") map.set(`${section}:${name}`, ver);
4183
+ }
4184
+ }
4185
+ return map;
4186
+ }
4187
+ function stripDepSections(pkg) {
4188
+ const out = { ...pkg };
4189
+ for (const section of DEP_SECTIONS) delete out[section];
4190
+ return out;
4191
+ }
4192
+ function mapsEqualKeys(a, b) {
4193
+ if (a.size !== b.size) return false;
4194
+ for (const k of a.keys()) if (!b.has(k)) return false;
4195
+ return true;
4196
+ }
4197
+ function isDependencyBumpOnly(beforeText, afterText) {
4198
+ let before;
4199
+ let after;
4200
+ try {
4201
+ before = JSON.parse(beforeText);
4202
+ after = JSON.parse(afterText);
4203
+ } catch {
4204
+ return false;
4205
+ }
4206
+ if (!isRecord(before) || !isRecord(after)) return false;
4207
+ const beforeDeps = extractDepMap(before);
4208
+ const afterDeps = extractDepMap(after);
4209
+ if (beforeDeps.size === 0 && afterDeps.size === 0) return false;
4210
+ if (!mapsEqualKeys(beforeDeps, afterDeps)) return false;
4211
+ if (JSON.stringify(stripDepSections(before)) !== JSON.stringify(stripDepSections(after))) {
4212
+ return false;
4213
+ }
4214
+ for (const [key, ver] of beforeDeps) {
4215
+ if (afterDeps.get(key) !== ver) return true;
4216
+ }
4217
+ return false;
4218
+ }
4219
+
4165
4220
  // src/decisions/diff.ts
4166
4221
  init_exec();
4222
+ import path25 from "node:path";
4223
+ function isPackageJsonPath(file) {
4224
+ return file === "package.json" || file.endsWith("/package.json");
4225
+ }
4226
+ async function readBlob(root, spec) {
4227
+ const result = await runGit(["show", spec], { cwd: root });
4228
+ return result.exitCode === 0 ? result.stdout : null;
4229
+ }
4230
+ function parseDiffRange(range) {
4231
+ const tripleIdx = range.indexOf("...");
4232
+ if (tripleIdx !== -1) {
4233
+ return {
4234
+ left: range.slice(0, tripleIdx) || "HEAD",
4235
+ right: range.slice(tripleIdx + 3) || "HEAD",
4236
+ triple: true
4237
+ };
4238
+ }
4239
+ const idx = range.indexOf("..");
4240
+ if (idx !== -1) {
4241
+ return {
4242
+ left: range.slice(0, idx) || "HEAD",
4243
+ right: range.slice(idx + 2) || "HEAD",
4244
+ triple: false
4245
+ };
4246
+ }
4247
+ return null;
4248
+ }
4249
+ async function getCurrentBranch(root) {
4250
+ const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
4251
+ if (result.exitCode !== 0) return void 0;
4252
+ const name = result.stdout.trim();
4253
+ if (name && name !== "HEAD") return name;
4254
+ const gha = process.env.GITHUB_HEAD_REF?.trim();
4255
+ if (gha) return gha;
4256
+ return void 0;
4257
+ }
4258
+ async function resolvePackageJsonPair(root, file, opts) {
4259
+ const empty = "{}";
4260
+ if (opts.staged) {
4261
+ const before2 = await readBlob(root, `HEAD:${file}`) ?? empty;
4262
+ const after2 = await readBlob(root, `:${file}`) ?? empty;
4263
+ return { before: before2, after: after2 };
4264
+ }
4265
+ const range = opts.range ?? "HEAD";
4266
+ const parsed = parseDiffRange(range);
4267
+ if (parsed) {
4268
+ let leftRev = parsed.left;
4269
+ if (parsed.triple) {
4270
+ const mb = await runGit(["merge-base", parsed.left, parsed.right], { cwd: root });
4271
+ leftRev = mb.exitCode === 0 ? mb.stdout.trim() : parsed.left;
4272
+ }
4273
+ const before2 = await readBlob(root, `${leftRev}:${file}`) ?? empty;
4274
+ const after2 = await readBlob(root, `${parsed.right}:${file}`) ?? empty;
4275
+ return { before: before2, after: after2 };
4276
+ }
4277
+ const before = await readBlob(root, `${range}:${file}`) ?? empty;
4278
+ const after = await readText(path25.join(root, file)) ?? empty;
4279
+ return { before, after };
4280
+ }
4167
4281
  async function collectDiffStats(root, opts) {
4168
4282
  const gitArgs = opts.staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only", opts.range ?? "HEAD"];
4169
4283
  const names = await runGit(gitArgs, { cwd: root });
@@ -4180,9 +4294,23 @@ async function collectDiffStats(root, opts) {
4180
4294
  if (!Number.isNaN(added)) linesAdded += added;
4181
4295
  if (!Number.isNaN(removed)) linesRemoved += removed;
4182
4296
  }
4183
- return { files, linesAdded, linesRemoved };
4297
+ const packageJsonPairs = {};
4298
+ for (const file of files) {
4299
+ if (!isPackageJsonPath(file)) continue;
4300
+ packageJsonPairs[file] = await resolvePackageJsonPair(root, file, opts);
4301
+ }
4302
+ return {
4303
+ files,
4304
+ linesAdded,
4305
+ linesRemoved,
4306
+ ...Object.keys(packageJsonPairs).length > 0 ? { packageJsonPairs } : {}
4307
+ };
4184
4308
  }
4185
4309
 
4310
+ // src/decisions/local-exemptions.ts
4311
+ var LOCAL_EXEMPT_GLOBS = ["library/catalog/**"];
4312
+ var LOCAL_EXEMPT_BRANCH_PATTERN = /^chore\/sync-catalog/;
4313
+
4186
4314
  // src/decisions/match.ts
4187
4315
  function segmentToRegex(segment) {
4188
4316
  if (segment === "**") return ".*";
@@ -4210,26 +4338,29 @@ function matchesAnyGlob(filePath, patterns) {
4210
4338
  return patterns.some((p) => matchesPathGlob(filePath, p));
4211
4339
  }
4212
4340
  function matchesAnyRegex(filePath, patterns) {
4341
+ return firstMatchingRegex(filePath, patterns) != null;
4342
+ }
4343
+ function firstMatchingRegex(filePath, patterns) {
4213
4344
  const norm = filePath.replace(/\\/g, "/");
4214
- return patterns.some((p) => new RegExp(p).test(norm));
4345
+ return patterns.find((p) => new RegExp(p).test(norm));
4215
4346
  }
4216
4347
 
4217
4348
  // src/decisions/paths.ts
4218
- import path25 from "node:path";
4349
+ import path26 from "node:path";
4219
4350
  import fs23 from "fs-extra";
4220
4351
  async function resolveDecisionsDir(root) {
4221
- const primary = path25.join(root, DECISIONS_TRIGGERS.decisionsDir);
4352
+ const primary = path26.join(root, DECISIONS_TRIGGERS.decisionsDir);
4222
4353
  if (await fs23.pathExists(primary)) return primary;
4223
- const legacy = path25.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4354
+ const legacy = path26.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4224
4355
  if (await fs23.pathExists(legacy)) return legacy;
4225
4356
  return primary;
4226
4357
  }
4227
4358
  function relativeDecisionPath(decisionsDir, root) {
4228
- return path25.relative(root, decisionsDir).replace(/\\/g, "/");
4359
+ return path26.relative(root, decisionsDir).replace(/\\/g, "/");
4229
4360
  }
4230
4361
 
4231
4362
  // src/decisions/validate.ts
4232
- import path26 from "node:path";
4363
+ import path27 from "node:path";
4233
4364
  import fs24 from "fs-extra";
4234
4365
  function validateDecisionContent(content2) {
4235
4366
  const errors = [];
@@ -4259,14 +4390,14 @@ function decisionNumberFromFile(name) {
4259
4390
  return match?.[1] ?? null;
4260
4391
  }
4261
4392
  async function validateIndexSync(decisionsDir, decisionFiles) {
4262
- const readmePath = path26.join(decisionsDir, "README.md");
4393
+ const readmePath = path27.join(decisionsDir, "README.md");
4263
4394
  if (!await fs24.pathExists(readmePath)) {
4264
4395
  return ["docs/decisions/README.md is missing"];
4265
4396
  }
4266
4397
  const readme = await readText(readmePath) ?? "";
4267
4398
  const errors = [];
4268
4399
  for (const file of decisionFiles) {
4269
- const num = decisionNumberFromFile(path26.basename(file));
4400
+ const num = decisionNumberFromFile(path27.basename(file));
4270
4401
  if (num && !indexReferencesNumber(readme, num)) {
4271
4402
  errors.push(`README.md missing index row for ADR-${num}`);
4272
4403
  }
@@ -4275,8 +4406,35 @@ async function validateIndexSync(decisionsDir, decisionFiles) {
4275
4406
  }
4276
4407
 
4277
4408
  // src/decisions/check.ts
4409
+ var MANIFEST_LOCK_BASENAMES = /* @__PURE__ */ new Set([
4410
+ "package.json",
4411
+ "yarn.lock",
4412
+ "pnpm-lock.yaml",
4413
+ "package-lock.json"
4414
+ ]);
4278
4415
  function isExempt(file) {
4279
- return matchesAnyGlob(file, DECISIONS_TRIGGERS.exemptGlobs);
4416
+ return matchesAnyGlob(file, DECISIONS_TRIGGERS.exemptGlobs) || matchesAnyGlob(file, [...LOCAL_EXEMPT_GLOBS]);
4417
+ }
4418
+ function isManifestOrLockfile(file) {
4419
+ const base = file.split("/").pop() ?? file;
4420
+ return MANIFEST_LOCK_BASENAMES.has(base);
4421
+ }
4422
+ function isPackageJsonPath2(file) {
4423
+ return file === "package.json" || file.endsWith("/package.json");
4424
+ }
4425
+ function isPureDependencyBumpDiff(stats) {
4426
+ const relevant = stats.files.filter((f) => !isExempt(f));
4427
+ if (relevant.length === 0) return false;
4428
+ if (relevant.some((f) => !isManifestOrLockfile(f))) return false;
4429
+ const pkgJsons = relevant.filter(isPackageJsonPath2);
4430
+ if (pkgJsons.length === 0) {
4431
+ return true;
4432
+ }
4433
+ const pairs = stats.packageJsonPairs ?? {};
4434
+ return pkgJsons.every((f) => {
4435
+ const pair = pairs[f];
4436
+ return pair != null && isDependencyBumpOnly(pair.before, pair.after);
4437
+ });
4280
4438
  }
4281
4439
  function countTopLevelDirs(files) {
4282
4440
  const dirs = /* @__PURE__ */ new Set();
@@ -4286,21 +4444,35 @@ function countTopLevelDirs(files) {
4286
4444
  }
4287
4445
  return dirs.size;
4288
4446
  }
4289
- function isDecisionTriggered(stats) {
4290
- const relevant = stats.files.filter((f) => !isExempt(f));
4291
- if (relevant.length === 0) return false;
4447
+ function firstMatchingGlob(file, globs) {
4448
+ return globs.find((g) => matchesPathGlob(file, g));
4449
+ }
4450
+ function explainDecisionTrigger(stats) {
4451
+ let relevant = stats.files.filter((f) => !isExempt(f));
4452
+ if (relevant.length === 0) return [];
4453
+ if (isPureDependencyBumpDiff(stats)) {
4454
+ relevant = relevant.filter((f) => !isManifestOrLockfile(f));
4455
+ if (relevant.length === 0) return [];
4456
+ }
4292
4457
  for (const file of relevant) {
4293
- if (matchesAnyGlob(file, DECISIONS_TRIGGERS.pathGlobs)) return true;
4294
- if (matchesAnyRegex(file, DECISIONS_TRIGGERS.pathRegex)) return true;
4458
+ const glob = firstMatchingGlob(file, DECISIONS_TRIGGERS.pathGlobs);
4459
+ if (glob) {
4460
+ return [`matched pathGlobs: ${glob}`];
4461
+ }
4462
+ const regex = firstMatchingRegex(file, DECISIONS_TRIGGERS.pathRegex);
4463
+ if (regex) {
4464
+ return [`matched pathRegex: ${regex}`];
4465
+ }
4295
4466
  }
4296
4467
  const totalLines = stats.linesAdded + stats.linesRemoved;
4297
- if (relevant.length >= DECISIONS_TRIGGERS.minFilesChanged && countTopLevelDirs(relevant) >= 2) {
4298
- return true;
4468
+ const dirs = countTopLevelDirs(relevant);
4469
+ if (relevant.length >= DECISIONS_TRIGGERS.minFilesChanged && dirs >= 2) {
4470
+ return [`size threshold: ${relevant.length} files across ${dirs} top-level dirs`];
4299
4471
  }
4300
4472
  if (totalLines >= DECISIONS_TRIGGERS.minLinesChanged && relevant.some((f) => !f.startsWith("docs/"))) {
4301
- return true;
4473
+ return [`size threshold: ${totalLines} lines changed (non-docs)`];
4302
4474
  }
4303
- return false;
4475
+ return [];
4304
4476
  }
4305
4477
  function isSecurityTriggered(stats) {
4306
4478
  const relevant = stats.files.filter((f) => !isExempt(f));
@@ -4320,26 +4492,27 @@ async function checkSatisfaction(input2) {
4320
4492
  const errors = [];
4321
4493
  const decisionsPrefix = `${input2.decisionsRel}/`;
4322
4494
  const newDecisionFiles = input2.changedFiles.filter(
4323
- (f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path27.basename(f))
4495
+ (f) => f.startsWith(decisionsPrefix) && isDecisionFileName(path28.basename(f))
4324
4496
  );
4325
4497
  if (newDecisionFiles.length === 0) {
4326
4498
  errors.push(`no new or updated decision under ${input2.decisionsRel}/`);
4327
4499
  return errors;
4328
4500
  }
4329
- const decisionsDir = path27.join(input2.root, input2.decisionsRel);
4501
+ const decisionsDir = path28.join(input2.root, input2.decisionsRel);
4330
4502
  for (const rel of newDecisionFiles) {
4331
- const abs = path27.join(input2.root, rel);
4503
+ const abs = path28.join(input2.root, rel);
4332
4504
  const content2 = await readText(abs) ?? "";
4333
4505
  const valid = validateDecisionContent(content2);
4334
4506
  if (!valid.ok) errors.push(...valid.errors.map((e) => `${rel}: ${e}`));
4335
4507
  }
4336
4508
  const indexErrors = await validateIndexSync(
4337
4509
  decisionsDir,
4338
- newDecisionFiles.map((f) => path27.basename(f))
4510
+ newDecisionFiles.map((f) => path28.basename(f))
4339
4511
  );
4340
4512
  errors.push(...indexErrors);
4341
4513
  return errors;
4342
4514
  }
4515
+ var WAYS_OUT = "Write an ADR under docs/decisions/ (haus decisions suggest), or use [adr-skip] for non-security changes.";
4343
4516
  async function runDecisionsCheck(root, opts = {}) {
4344
4517
  const reasons = [];
4345
4518
  if (await readGateMode(root) === "off") {
@@ -4351,8 +4524,18 @@ async function runDecisionsCheck(root, opts = {}) {
4351
4524
  };
4352
4525
  }
4353
4526
  const stats = await collectDiffStats(root, opts);
4354
- const triggered = isDecisionTriggered(stats);
4355
4527
  const securityTriggered = isSecurityTriggered(stats);
4528
+ const branch = await getCurrentBranch(root);
4529
+ if (branch != null && LOCAL_EXEMPT_BRANCH_PATTERN.test(branch) && !securityTriggered) {
4530
+ return {
4531
+ triggered: false,
4532
+ satisfied: true,
4533
+ reasons: ["catalog-sync branch \u2014 exempt"],
4534
+ securityTriggered: false
4535
+ };
4536
+ }
4537
+ const triggerReasons = explainDecisionTrigger(stats);
4538
+ const triggered = triggerReasons.length > 0;
4356
4539
  if (!triggered) {
4357
4540
  return {
4358
4541
  triggered: false,
@@ -4361,6 +4544,7 @@ async function runDecisionsCheck(root, opts = {}) {
4361
4544
  securityTriggered
4362
4545
  };
4363
4546
  }
4547
+ reasons.push(...triggerReasons);
4364
4548
  const skipText = `${opts.prBody ?? ""}
4365
4549
  ${opts.commitMessages ?? ""}`;
4366
4550
  if (hasSkipToken(skipText) && !securityTriggered) {
@@ -4379,7 +4563,7 @@ ${opts.commitMessages ?? ""}`;
4379
4563
  changedFiles: stats.files
4380
4564
  });
4381
4565
  if (satisfactionErrors.length > 0 && decisionsRel !== legacyRel) {
4382
- const legacyExists = await fs25.pathExists(path27.join(root, legacyRel));
4566
+ const legacyExists = await fs25.pathExists(path28.join(root, legacyRel));
4383
4567
  if (legacyExists && !await fs25.pathExists(decisionsDir)) {
4384
4568
  satisfactionErrors = await checkSatisfaction({
4385
4569
  root,
@@ -4389,14 +4573,16 @@ ${opts.commitMessages ?? ""}`;
4389
4573
  }
4390
4574
  }
4391
4575
  if (satisfactionErrors.length === 0) {
4576
+ reasons.push("decision record present");
4392
4577
  return {
4393
4578
  triggered: true,
4394
4579
  satisfied: true,
4395
- reasons: ["decision record present"],
4580
+ reasons,
4396
4581
  securityTriggered
4397
4582
  };
4398
4583
  }
4399
4584
  reasons.push(...satisfactionErrors);
4585
+ reasons.push(WAYS_OUT);
4400
4586
  return { triggered: true, satisfied: false, reasons, securityTriggered };
4401
4587
  }
4402
4588
  async function listDecisionNumbers(decisionsDir) {
@@ -4408,8 +4594,8 @@ async function listDecisionNumbers(decisionsDir) {
4408
4594
  // src/decisions/doctor.ts
4409
4595
  async function auditDecisionsLayout(root) {
4410
4596
  const findings = [];
4411
- const primary = path28.join(root, DECISIONS_TRIGGERS.decisionsDir);
4412
- const legacy = path28.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4597
+ const primary = path29.join(root, DECISIONS_TRIGGERS.decisionsDir);
4598
+ const legacy = path29.join(root, DECISIONS_TRIGGERS.legacyDecisionsDir ?? "docs/adr");
4413
4599
  const hasPrimary = await fs26.pathExists(primary);
4414
4600
  const hasLegacy = await fs26.pathExists(legacy);
4415
4601
  if (!hasPrimary && hasLegacy) {
@@ -4424,7 +4610,7 @@ async function auditDecisionsLayout(root) {
4424
4610
  if (!hasPrimary) return findings;
4425
4611
  const decisionsDir = await resolveDecisionsDir(root);
4426
4612
  const rel = relativeDecisionPath(decisionsDir, root);
4427
- const readmePath = path28.join(decisionsDir, "README.md");
4613
+ const readmePath = path29.join(decisionsDir, "README.md");
4428
4614
  if (!await fs26.pathExists(readmePath)) {
4429
4615
  findings.push({
4430
4616
  level: "advisory",
@@ -4449,7 +4635,7 @@ async function auditDecisionsLayout(root) {
4449
4635
  });
4450
4636
  }
4451
4637
  }
4452
- const claude = await readText(path28.join(root, "CLAUDE.md"));
4638
+ const claude = await readText(path29.join(root, "CLAUDE.md"));
4453
4639
  if (claude && !claude.includes(`@${rel}/README.md`)) {
4454
4640
  findings.push({
4455
4641
  level: "advisory",
@@ -4480,11 +4666,11 @@ async function auditDecisionsLayout(root) {
4480
4666
  }
4481
4667
 
4482
4668
  // src/scanner/read-context.ts
4483
- import path36 from "node:path";
4669
+ import path37 from "node:path";
4484
4670
  import fs28 from "fs-extra";
4485
4671
 
4486
4672
  // src/commands/workspace/config.ts
4487
- import path29 from "node:path";
4673
+ import path30 from "node:path";
4488
4674
  import YAML from "yaml";
4489
4675
  var WORKSPACE_FILE = "haus.workspace.yaml";
4490
4676
  function parseWorkspaceConfig(text) {
@@ -4507,11 +4693,11 @@ function parseWorkspaceConfig(text) {
4507
4693
  };
4508
4694
  }
4509
4695
  async function readWorkspaceConfig(workspaceRoot) {
4510
- return parseWorkspaceConfig(await readText(path29.join(workspaceRoot, WORKSPACE_FILE)));
4696
+ return parseWorkspaceConfig(await readText(path30.join(workspaceRoot, WORKSPACE_FILE)));
4511
4697
  }
4512
4698
 
4513
4699
  // src/workspace/members.ts
4514
- import path30 from "node:path";
4700
+ import path31 from "node:path";
4515
4701
  var REPOS_MANIFEST_FILE = "repos.manifest.json";
4516
4702
  var REPOS_LOCAL_FILE = "repos.local.json";
4517
4703
  var MemberConfigError = class extends Error {
@@ -4524,11 +4710,11 @@ var MemberConfigError = class extends Error {
4524
4710
  };
4525
4711
  function resolveAbsPath(workspaceRoot, folder, overrides) {
4526
4712
  const override = overrides[folder];
4527
- if (override) return path30.resolve(workspaceRoot, override);
4528
- return path30.resolve(workspaceRoot, folder);
4713
+ if (override) return path31.resolve(workspaceRoot, override);
4714
+ return path31.resolve(workspaceRoot, folder);
4529
4715
  }
4530
4716
  async function readPathOverrides(workspaceRoot) {
4531
- const filePath = path30.join(workspaceRoot, REPOS_LOCAL_FILE);
4717
+ const filePath = path31.join(workspaceRoot, REPOS_LOCAL_FILE);
4532
4718
  const result = await readJsonDetailed(filePath);
4533
4719
  if (result.status === "missing") return {};
4534
4720
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -4544,7 +4730,7 @@ async function readPathOverrides(workspaceRoot) {
4544
4730
  return overrides;
4545
4731
  }
4546
4732
  async function readFromWorkspaceYaml(workspaceRoot, overrides) {
4547
- const filePath = path30.join(workspaceRoot, WORKSPACE_FILE);
4733
+ const filePath = path31.join(workspaceRoot, WORKSPACE_FILE);
4548
4734
  const text = await readText(filePath);
4549
4735
  if (text === void 0) return void 0;
4550
4736
  const config = parseWorkspaceConfig(text);
@@ -4557,7 +4743,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
4557
4743
  }));
4558
4744
  }
4559
4745
  async function readFromManifest(workspaceRoot, overrides) {
4560
- const filePath = path30.join(workspaceRoot, REPOS_MANIFEST_FILE);
4746
+ const filePath = path31.join(workspaceRoot, REPOS_MANIFEST_FILE);
4561
4747
  const result = await readJsonDetailed(filePath);
4562
4748
  if (result.status === "missing") return void 0;
4563
4749
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -4599,16 +4785,16 @@ async function readMembers(rootInfo) {
4599
4785
  }
4600
4786
 
4601
4787
  // src/scanner/scan-project.ts
4602
- import path35 from "node:path";
4788
+ import path36 from "node:path";
4603
4789
 
4604
4790
  // src/utils/audit-checks.ts
4605
- function isRecord(v) {
4791
+ function isRecord2(v) {
4606
4792
  return typeof v === "object" && v !== null;
4607
4793
  }
4608
4794
 
4609
4795
  // src/utils/git-root.ts
4610
4796
  init_exec();
4611
- import path31 from "node:path";
4797
+ import path32 from "node:path";
4612
4798
  function fallback(start) {
4613
4799
  return {
4614
4800
  cwd: start,
@@ -4624,11 +4810,11 @@ function fallback(start) {
4624
4810
  async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
4625
4811
  const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
4626
4812
  const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
4627
- if (raw && path31.isAbsolute(raw)) return path31.resolve(raw);
4813
+ if (raw && path32.isAbsolute(raw)) return path32.resolve(raw);
4628
4814
  const plain = await runGit(["rev-parse", flag], { cwd });
4629
4815
  const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
4630
4816
  if (!plainRaw) return "";
4631
- return path31.isAbsolute(plainRaw) ? path31.resolve(plainRaw) : path31.resolve(repoRoot, plainRaw);
4817
+ return path32.isAbsolute(plainRaw) ? path32.resolve(plainRaw) : path32.resolve(repoRoot, plainRaw);
4632
4818
  }
4633
4819
  async function resolveRoots(start) {
4634
4820
  const cwd = start ?? process.cwd();
@@ -4637,15 +4823,15 @@ async function resolveRoots(start) {
4637
4823
  if (toplevel.exitCode !== 0) return fallback(cwd);
4638
4824
  const toplevelRaw = toplevel.stdout.trim();
4639
4825
  if (!toplevelRaw) return fallback(cwd);
4640
- const repoRoot = path31.resolve(toplevelRaw);
4826
+ const repoRoot = path32.resolve(toplevelRaw);
4641
4827
  const [gitDir, gitCommonDir] = await Promise.all([
4642
4828
  resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
4643
4829
  resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
4644
4830
  ]);
4645
4831
  if (!gitDir || !gitCommonDir) return fallback(cwd);
4646
4832
  const isLinkedWorktree = gitDir !== gitCommonDir;
4647
- const mainRoot = isLinkedWorktree && path31.basename(gitCommonDir) === ".git" ? path31.dirname(gitCommonDir) : repoRoot;
4648
- const worktreeName = isLinkedWorktree ? path31.basename(gitDir) : null;
4833
+ const mainRoot = isLinkedWorktree && path32.basename(gitCommonDir) === ".git" ? path32.dirname(gitCommonDir) : repoRoot;
4834
+ const worktreeName = isLinkedWorktree ? path32.basename(gitDir) : null;
4649
4835
  return {
4650
4836
  cwd,
4651
4837
  repoRoot,
@@ -4681,7 +4867,7 @@ function compareVersions(a, b) {
4681
4867
  }
4682
4868
 
4683
4869
  // src/scanner/detect-package-manager.ts
4684
- import path32 from "node:path";
4870
+ import path33 from "node:path";
4685
4871
  import fs27 from "fs-extra";
4686
4872
  function detectPackageManager(root, packageManagerField) {
4687
4873
  const field = String(packageManagerField ?? "").trim();
@@ -4700,9 +4886,9 @@ function detectPackageManager(root, packageManagerField) {
4700
4886
  if (satisfiesVersion(version, ">=9")) return "npm";
4701
4887
  return "unknown";
4702
4888
  }
4703
- if (fs27.existsSync(path32.join(root, "yarn.lock"))) return "yarn";
4704
- if (fs27.existsSync(path32.join(root, "pnpm-lock.yaml"))) return "pnpm";
4705
- if (fs27.existsSync(path32.join(root, "package-lock.json"))) return "npm";
4889
+ if (fs27.existsSync(path33.join(root, "yarn.lock"))) return "yarn";
4890
+ if (fs27.existsSync(path33.join(root, "pnpm-lock.yaml"))) return "pnpm";
4891
+ if (fs27.existsSync(path33.join(root, "package-lock.json"))) return "npm";
4706
4892
  return "unknown";
4707
4893
  }
4708
4894
 
@@ -4912,7 +5098,7 @@ function runDetection(ctx, rules = STACK_RULES) {
4912
5098
  }
4913
5099
 
4914
5100
  // src/scanner/detection.ts
4915
- import path33 from "node:path";
5101
+ import path34 from "node:path";
4916
5102
  var UNSUPPORTED_MARKERS = {
4917
5103
  "requirements.txt": "python",
4918
5104
  "pyproject.toml": "python",
@@ -4944,7 +5130,7 @@ function blocked(rel) {
4944
5130
  function dependencySet(pkg, composer) {
4945
5131
  const depNames = /* @__PURE__ */ new Set();
4946
5132
  const pushObj = (obj) => {
4947
- if (!isRecord(obj)) return;
5133
+ if (!isRecord2(obj)) return;
4948
5134
  for (const key of Object.keys(obj)) depNames.add(key);
4949
5135
  };
4950
5136
  pushObj(pkg?.dependencies);
@@ -4972,14 +5158,14 @@ function finalizeRoles(registryRoles, deps, files) {
4972
5158
  function collectUnsupportedSignals(files) {
4973
5159
  return [
4974
5160
  ...new Set(
4975
- files.map((f) => UNSUPPORTED_MARKERS[path33.basename(f)]).filter((s) => Boolean(s))
5161
+ files.map((f) => UNSUPPORTED_MARKERS[path34.basename(f)]).filter((s) => Boolean(s))
4976
5162
  )
4977
5163
  ].sort();
4978
5164
  }
4979
5165
 
4980
5166
  // src/scanner/render.ts
4981
5167
  import { readFile } from "node:fs/promises";
4982
- import path34 from "node:path";
5168
+ import path35 from "node:path";
4983
5169
  async function buildContentBlob(root, files) {
4984
5170
  const candidates = files.filter(
4985
5171
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -4987,7 +5173,7 @@ async function buildContentBlob(root, files) {
4987
5173
  const slice = candidates.slice(0, 300);
4988
5174
  const parts = await mapWithConcurrency(slice, async (rel) => {
4989
5175
  try {
4990
- return await readFile(path34.join(root, rel), "utf8");
5176
+ return await readFile(path35.join(root, rel), "utf8");
4991
5177
  } catch {
4992
5178
  return "";
4993
5179
  }
@@ -5079,8 +5265,8 @@ var SAFE_FILES = [
5079
5265
  "Gemfile"
5080
5266
  ];
5081
5267
  async function scanProject(root) {
5082
- const pkg = await readJson(path35.join(root, "package.json"));
5083
- const composer = await readJson(path35.join(root, "composer.json"));
5268
+ const pkg = await readJson(path36.join(root, "package.json"));
5269
+ const composer = await readJson(path36.join(root, "composer.json"));
5084
5270
  const files = await listFiles(root, SAFE_FILES);
5085
5271
  const safeFiles = files.filter((f) => !blocked(f));
5086
5272
  const deps = dependencySet(pkg, composer);
@@ -5094,7 +5280,7 @@ async function scanProject(root) {
5094
5280
  const warnings = [];
5095
5281
  const securityRisks = [];
5096
5282
  const crossRepoHints = [];
5097
- const nodeEngine = isRecord(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
5283
+ const nodeEngine = isRecord2(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
5098
5284
  if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
5099
5285
  warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
5100
5286
  }
@@ -5104,7 +5290,7 @@ async function scanProject(root) {
5104
5290
  crossRepoHints.push("Monorepo orchestration detected");
5105
5291
  const unsupportedSignals = collectUnsupportedSignals(safeFiles);
5106
5292
  const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
5107
- const repoNameFallback = pkg?.name ? String(pkg.name) : path35.basename((await resolveRoots(root)).mainRoot);
5293
+ const repoNameFallback = pkg?.name ? String(pkg.name) : path36.basename((await resolveRoots(root)).mainRoot);
5108
5294
  const context = {
5109
5295
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5110
5296
  repoName: repoNameFallback,
@@ -5126,9 +5312,9 @@ async function scanProject(root) {
5126
5312
  // src/scanner/read-context.ts
5127
5313
  async function isCacheFresh(root, cachePath) {
5128
5314
  const markerPaths = [
5129
- path36.join(root, "package.json"),
5130
- path36.join(root, WORKSPACE_FILE),
5131
- path36.join(root, REPOS_MANIFEST_FILE)
5315
+ path37.join(root, "package.json"),
5316
+ path37.join(root, WORKSPACE_FILE),
5317
+ path37.join(root, REPOS_MANIFEST_FILE)
5132
5318
  ];
5133
5319
  const cacheStat = await fs28.stat(cachePath);
5134
5320
  for (const markerPath of markerPaths) {
@@ -5234,7 +5420,7 @@ async function runDoctor(options) {
5234
5420
  } else {
5235
5421
  ok(`- HOOKS OK: ${hooks.message}`);
5236
5422
  }
5237
- const rootClaudeMdPath = path37.join(root, "CLAUDE.md");
5423
+ const rootClaudeMdPath = path38.join(root, "CLAUDE.md");
5238
5424
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
5239
5425
  if (!rootClaudeMdContent) {
5240
5426
  flag(
@@ -5288,8 +5474,8 @@ async function runDoctor(options) {
5288
5474
  } else {
5289
5475
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
5290
5476
  const bodyContent = workflowContent.slice(firstLine.length + 1);
5291
- const cachePath = path37.join(getCacheDir(), "templates/agentic-workflow-standard.md");
5292
- const bundledPath = path37.join(
5477
+ const cachePath = path38.join(getCacheDir(), "templates/agentic-workflow-standard.md");
5478
+ const bundledPath = path38.join(
5293
5479
  packageRoot(),
5294
5480
  "library",
5295
5481
  "global",
@@ -5338,7 +5524,7 @@ async function runDoctor(options) {
5338
5524
  }
5339
5525
  }
5340
5526
  if (workflowExists) {
5341
- const prettierIgnore = await readText(path37.join(root, ".prettierignore")) ?? "";
5527
+ const prettierIgnore = await readText(path38.join(root, ".prettierignore")) ?? "";
5342
5528
  const lines = prettierIgnore.split("\n");
5343
5529
  const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
5344
5530
  if (missing.length > 0) {
@@ -5411,7 +5597,7 @@ async function runDoctor(options) {
5411
5597
  );
5412
5598
  }
5413
5599
  }
5414
- const pkgJson = await readJson(path37.join(packageRoot(), "package.json"));
5600
+ const pkgJson = await readJson(path38.join(packageRoot(), "package.json"));
5415
5601
  const currentVersion = pkgJson?.version ?? "0.0.0";
5416
5602
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
5417
5603
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -5444,7 +5630,7 @@ async function runDoctor(options) {
5444
5630
  }
5445
5631
 
5446
5632
  // src/commands/update.ts
5447
- import path40 from "node:path";
5633
+ import path41 from "node:path";
5448
5634
 
5449
5635
  // src/claude/refresh-project.ts
5450
5636
  import fs30 from "fs-extra";
@@ -5461,7 +5647,7 @@ async function refreshProjectApply(root) {
5461
5647
 
5462
5648
  // src/install/apply.ts
5463
5649
  import crypto2 from "node:crypto";
5464
- import path38 from "node:path";
5650
+ import path39 from "node:path";
5465
5651
  import fs31 from "fs-extra";
5466
5652
 
5467
5653
  // src/install/header.ts
@@ -5548,7 +5734,7 @@ function hashContent(content2) {
5548
5734
  }
5549
5735
  function sourceVersion() {
5550
5736
  try {
5551
- const pkgPath = path38.join(packageRoot(), "package.json");
5737
+ const pkgPath = path39.join(packageRoot(), "package.json");
5552
5738
  const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf8"));
5553
5739
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
5554
5740
  } catch {
@@ -5556,49 +5742,49 @@ function sourceVersion() {
5556
5742
  }
5557
5743
  }
5558
5744
  function globalSrcDir() {
5559
- return path38.join(packageRoot(), "library", "global");
5745
+ return path39.join(packageRoot(), "library", "global");
5560
5746
  }
5561
5747
  function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
5562
- const skillDir = path38.join(skillsDir, skillName);
5748
+ const skillDir = path39.join(skillsDir, skillName);
5563
5749
  const entries = [];
5564
5750
  for (const rel of fs31.readdirSync(skillDir, { recursive: true })) {
5565
5751
  if (rel === "SKILL.md") continue;
5566
- const abs = path38.join(skillDir, rel);
5752
+ const abs = path39.join(skillDir, rel);
5567
5753
  if (!fs31.statSync(abs).isFile()) continue;
5568
- const posixRel = rel.split(path38.sep).join("/");
5754
+ const posixRel = rel.split(path39.sep).join("/");
5569
5755
  entries.push({
5570
5756
  stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
5571
- srcRelPath: path38.join("library", "global", "skills", skillName, rel),
5572
- destPath: path38.join(claudeDir, "skills", skillName, rel)
5757
+ srcRelPath: path39.join("library", "global", "skills", skillName, rel),
5758
+ destPath: path39.join(claudeDir, "skills", skillName, rel)
5573
5759
  });
5574
5760
  }
5575
5761
  return entries;
5576
5762
  }
5577
5763
  function collectSourceFiles(srcDir, claudeDir) {
5578
5764
  const entries = [];
5579
- const skillsDir = path38.join(srcDir, "skills");
5765
+ const skillsDir = path39.join(srcDir, "skills");
5580
5766
  if (fs31.pathExistsSync(skillsDir)) {
5581
5767
  for (const skillName of fs31.readdirSync(skillsDir)) {
5582
- const skillFile = path38.join(skillsDir, skillName, "SKILL.md");
5768
+ const skillFile = path39.join(skillsDir, skillName, "SKILL.md");
5583
5769
  if (fs31.pathExistsSync(skillFile)) {
5584
5770
  entries.push({
5585
5771
  stableId: `skill.${skillName}`,
5586
- srcRelPath: path38.join("library", "global", "skills", skillName, "SKILL.md"),
5587
- destPath: path38.join(claudeDir, "skills", skillName, "SKILL.md")
5772
+ srcRelPath: path39.join("library", "global", "skills", skillName, "SKILL.md"),
5773
+ destPath: path39.join(claudeDir, "skills", skillName, "SKILL.md")
5588
5774
  });
5589
5775
  entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
5590
5776
  }
5591
5777
  }
5592
5778
  }
5593
- const commandsDir = path38.join(srcDir, "commands");
5779
+ const commandsDir = path39.join(srcDir, "commands");
5594
5780
  if (fs31.pathExistsSync(commandsDir)) {
5595
5781
  for (const fileName of fs31.readdirSync(commandsDir)) {
5596
5782
  if (!fileName.endsWith(".md")) continue;
5597
5783
  const commandName = fileName.slice(0, -".md".length);
5598
5784
  entries.push({
5599
5785
  stableId: `command.${commandName}`,
5600
- srcRelPath: path38.join("library", "global", "commands", fileName),
5601
- destPath: path38.join(claudeDir, "commands", fileName)
5786
+ srcRelPath: path39.join("library", "global", "commands", fileName),
5787
+ destPath: path39.join(claudeDir, "commands", fileName)
5602
5788
  });
5603
5789
  }
5604
5790
  }
@@ -5624,7 +5810,7 @@ async function applyInstall(options = {}) {
5624
5810
  };
5625
5811
  const manifestFiles = [];
5626
5812
  for (const entry of sourceFiles) {
5627
- const srcPath = path38.join(packageRoot(), entry.srcRelPath);
5813
+ const srcPath = path39.join(packageRoot(), entry.srcRelPath);
5628
5814
  const rawContent = await readText(srcPath);
5629
5815
  if (rawContent === void 0) {
5630
5816
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -5684,7 +5870,7 @@ async function applyInstall(options = {}) {
5684
5870
  }
5685
5871
  let mergedSettings;
5686
5872
  if (!check) {
5687
- const fragmentPath = path38.join(srcDir, "settings-fragments", "hooks.json");
5873
+ const fragmentPath = path39.join(srcDir, "settings-fragments", "hooks.json");
5688
5874
  const fragments = await loadHooksFragment(fragmentPath);
5689
5875
  const settings = await readSettings();
5690
5876
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -5773,7 +5959,7 @@ function summarizeLockDiff(before, after) {
5773
5959
 
5774
5960
  // src/update/lockfile.ts
5775
5961
  import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
5776
- import path39 from "node:path";
5962
+ import path40 from "node:path";
5777
5963
  async function readLockSummary(root) {
5778
5964
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
5779
5965
  const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
@@ -5809,7 +5995,7 @@ async function applyLock(root) {
5809
5995
  try {
5810
5996
  const backupDir = hausPath(root, "backups");
5811
5997
  await mkdir(backupDir, { recursive: true });
5812
- await copyFile(lockPath, path39.join(backupDir, `haus.lock.${Date.now()}.json`));
5998
+ await copyFile(lockPath, path40.join(backupDir, `haus.lock.${Date.now()}.json`));
5813
5999
  } catch {
5814
6000
  }
5815
6001
  const enriched = await Promise.all(
@@ -5831,7 +6017,7 @@ function diffLock(before, after) {
5831
6017
  }
5832
6018
  async function hasLocalOverrides(root) {
5833
6019
  try {
5834
- await readFile2(path39.join(root, ".claude", "settings.json"), "utf8");
6020
+ await readFile2(path40.join(root, ".claude", "settings.json"), "utf8");
5835
6021
  return true;
5836
6022
  } catch {
5837
6023
  return false;
@@ -5847,7 +6033,7 @@ async function runUpdate(options) {
5847
6033
  return;
5848
6034
  }
5849
6035
  if (options.check) {
5850
- const pkgJson2 = await readJson(path40.join(packageRoot(), "package.json"));
6036
+ const pkgJson2 = await readJson(path41.join(packageRoot(), "package.json"));
5851
6037
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
5852
6038
  const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
5853
6039
  options.fast ? fastLockStatus(root) : checkLock(root),
@@ -5882,7 +6068,7 @@ async function runUpdate(options) {
5882
6068
  if (hasHashDrift || formerIdMigrations.length > 0) process.exitCode = 1;
5883
6069
  return;
5884
6070
  }
5885
- const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
6071
+ const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
5886
6072
  const currentVersion = pkgJson?.version ?? "0.0.0";
5887
6073
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
5888
6074
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -5924,7 +6110,7 @@ async function runFromHookCheck(root) {
5924
6110
  try {
5925
6111
  const summary = await readLockSummary(root);
5926
6112
  if (summary.count === 0) return;
5927
- const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
6113
+ const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
5928
6114
  const currentVersion = pkgJson?.version ?? "0.0.0";
5929
6115
  const [npmVersion, latestCatalogTag] = await Promise.all([
5930
6116
  fetchNpmVersionStatus(currentVersion),
@@ -6074,7 +6260,7 @@ async function runCiGate(options = {}) {
6074
6260
  // src/commands/clone.ts
6075
6261
  init_exec();
6076
6262
  import { existsSync as existsSync2 } from "node:fs";
6077
- import path41 from "node:path";
6263
+ import path42 from "node:path";
6078
6264
  var GIT_LOCATION_VARS = [
6079
6265
  "GIT_DIR",
6080
6266
  "GIT_WORK_TREE",
@@ -6124,11 +6310,11 @@ async function runClone(url, opts = {}) {
6124
6310
  process.exitCode = 1;
6125
6311
  return;
6126
6312
  }
6127
- const target = path41.resolve(opts.dir?.trim() || repoNameFromUrl(url));
6313
+ const target = path42.resolve(opts.dir?.trim() || repoNameFromUrl(url));
6128
6314
  if (existsSync2(target)) {
6129
6315
  const check = await checkExistingTarget(target, url);
6130
6316
  if (check.kind === "matches") {
6131
- log(`\u2022 ${path41.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
6317
+ log(`\u2022 ${path42.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
6132
6318
  return;
6133
6319
  }
6134
6320
  const message = check.kind === "not-a-repo" ? `${target} already exists and is not a git repository \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].` : `${target} already exists as a different repository (origin: ${check.origin ?? "(no origin remote)"}) \u2014 refusing to clone ${url} there. Remove or rename it, or pass a different [dir].`;
@@ -6168,7 +6354,7 @@ function readHookPayload() {
6168
6354
  if (!raw) return { payload: {}, malformed: false };
6169
6355
  try {
6170
6356
  const parsed = JSON.parse(raw);
6171
- return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
6357
+ return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
6172
6358
  } catch {
6173
6359
  return { payload: {}, malformed: true };
6174
6360
  }
@@ -6207,7 +6393,7 @@ function readPayload(options) {
6207
6393
  if (options.stdinPayload != null) {
6208
6394
  try {
6209
6395
  const parsed = JSON.parse(options.stdinPayload);
6210
- return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
6396
+ return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
6211
6397
  } catch {
6212
6398
  return { payload: {}, malformed: true };
6213
6399
  }
@@ -6220,7 +6406,7 @@ async function runDecisionsGuard(options = {}) {
6220
6406
  emitPreToolUseDeny("Malformed hook payload");
6221
6407
  return;
6222
6408
  }
6223
- const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
6409
+ const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
6224
6410
  const command = String(toolInput.command ?? "");
6225
6411
  if (!PR_CREATE_PATTERN.test(command)) return;
6226
6412
  const root = process.cwd();
@@ -6232,13 +6418,13 @@ async function runDecisionsGuard(options = {}) {
6232
6418
  const result = await runDecisionsCheck(root, { range, commitMessages });
6233
6419
  if (result.triggered && !result.satisfied) {
6234
6420
  emitPreToolUseDeny(
6235
- `I didn't run that \u2014 this change needs an ADR before opening a PR (${result.reasons.join("; ")}). Run haus decisions suggest, add the decision under docs/decisions/, then retry.`
6421
+ `I didn't run that \u2014 this change needs an ADR before opening a PR (${result.reasons.join("; ")}). Run haus decisions suggest, add the decision under docs/decisions/, then retry \u2014 or use [adr-skip] for non-security changes.`
6236
6422
  );
6237
6423
  }
6238
6424
  }
6239
6425
 
6240
6426
  // src/commands/decisions.ts
6241
- import path42 from "node:path";
6427
+ import path43 from "node:path";
6242
6428
 
6243
6429
  // src/decisions/next-number.ts
6244
6430
  async function nextDecisionNumber(root) {
@@ -6281,10 +6467,9 @@ _(Why this over alternatives \u2014 the line agents read for "why".)_
6281
6467
  `;
6282
6468
  }
6283
6469
  async function runDecisionsSuggest(root, opts = {}) {
6284
- const stats = await collectDiffStats(root, { staged: true });
6285
6470
  const stagedCheck = await runDecisionsCheck(root, { staged: true });
6286
- const unstagedStats = await collectDiffStats(root, {});
6287
- const triggered = isDecisionTriggered(stats) || isDecisionTriggered(unstagedStats);
6471
+ const unstagedCheck = await runDecisionsCheck(root, {});
6472
+ const triggered = stagedCheck.triggered || unstagedCheck.triggered;
6288
6473
  if (!triggered) return;
6289
6474
  if (stagedCheck.triggered && stagedCheck.satisfied) return;
6290
6475
  const number = await nextDecisionNumber(root);
@@ -6335,7 +6520,7 @@ async function runDecisions(sub, arg, options = {}) {
6335
6520
  }
6336
6521
  for (const reason of result.reasons) warn(reason);
6337
6522
  error(
6338
- "decisions gate: decision-worthy change without a valid ADR under docs/decisions/. Run `haus decisions suggest`, add docs/decisions/NNNN-*.md + README row, or use [adr-skip] with justification."
6523
+ `decisions gate: decision-worthy change without a valid ADR under docs/decisions/. Why: ${result.reasons.filter((r) => !r.startsWith("no new")).join("; ") || "see above"}. Run \`haus decisions suggest\`, add docs/decisions/NNNN-*.md + README row, or use [adr-skip] with justification (ignored for security paths).`
6339
6524
  );
6340
6525
  process.exitCode = 1;
6341
6526
  return;
@@ -6349,7 +6534,7 @@ async function runDecisions(sub, arg, options = {}) {
6349
6534
  return;
6350
6535
  }
6351
6536
  if (sub === "validate") {
6352
- const target = arg ? path42.resolve(root, arg) : void 0;
6537
+ const target = arg ? path43.resolve(root, arg) : void 0;
6353
6538
  if (!target) {
6354
6539
  error("usage: haus decisions validate <path-to-decision.md>");
6355
6540
  process.exitCode = 1;
@@ -6601,7 +6786,7 @@ async function runGuard(kind, _options) {
6601
6786
  emitPreToolUseDeny("Malformed hook payload");
6602
6787
  return;
6603
6788
  }
6604
- const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
6789
+ const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
6605
6790
  if (kind === "file-access") {
6606
6791
  const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
6607
6792
  const reason2 = guardFileAccess(candidate);
@@ -6618,7 +6803,7 @@ async function runGuard(kind, _options) {
6618
6803
  }
6619
6804
 
6620
6805
  // src/commands/init.ts
6621
- import path44 from "node:path";
6806
+ import path45 from "node:path";
6622
6807
  import fs32 from "fs-extra";
6623
6808
 
6624
6809
  // src/recommender/git-signal.ts
@@ -7086,7 +7271,7 @@ function buildOptInEligible(items, skipped) {
7086
7271
  }
7087
7272
 
7088
7273
  // src/commands/workspace/discover.ts
7089
- import path43 from "node:path";
7274
+ import path44 from "node:path";
7090
7275
  import fg4 from "fast-glob";
7091
7276
  import YAML2 from "yaml";
7092
7277
  var DEFAULT_MAX_DEPTH = 3;
@@ -7127,10 +7312,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7127
7312
  const gitDirs = /* @__PURE__ */ new Set();
7128
7313
  const manifestDirs = /* @__PURE__ */ new Set();
7129
7314
  for (const match of gitMatches) {
7130
- gitDirs.add(path43.posix.dirname(match));
7315
+ gitDirs.add(path44.posix.dirname(match));
7131
7316
  }
7132
7317
  for (const match of manifestMatches) {
7133
- manifestDirs.add(path43.posix.dirname(match));
7318
+ manifestDirs.add(path44.posix.dirname(match));
7134
7319
  }
7135
7320
  const repoRoots = [...gitDirs];
7136
7321
  const manifestSorted = [...manifestDirs].sort(
@@ -7147,9 +7332,9 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7147
7332
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7148
7333
  const repoRoots = await findRepoRoots(workspaceRoot, maxDepth);
7149
7334
  return mapWithConcurrency(repoRoots, async (relDir) => {
7150
- const absDir = path43.resolve(workspaceRoot, relDir);
7151
- const pkg = await readJson(path43.join(absDir, "package.json"));
7152
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path43.basename(relDir === "." ? workspaceRoot : absDir);
7335
+ const absDir = path44.resolve(workspaceRoot, relDir);
7336
+ const pkg = await readJson(path44.join(absDir, "package.json"));
7337
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path44.basename(relDir === "." ? workspaceRoot : absDir);
7153
7338
  let role = "auto";
7154
7339
  try {
7155
7340
  const scan = await scanProject(absDir);
@@ -7192,7 +7377,7 @@ function renderWorkspaceYaml(config) {
7192
7377
  });
7193
7378
  }
7194
7379
  async function runDiscover(workspaceRoot, opts = {}) {
7195
- const yamlPath = path43.join(workspaceRoot, "haus.workspace.yaml");
7380
+ const yamlPath = path44.join(workspaceRoot, "haus.workspace.yaml");
7196
7381
  const existingText = await readText(yamlPath);
7197
7382
  const existing = parseWorkspaceConfig(existingText);
7198
7383
  if (existingText && !existing) {
@@ -7364,7 +7549,7 @@ async function runSetupProject(options) {
7364
7549
  // src/commands/init.ts
7365
7550
  async function runInit(options) {
7366
7551
  const root = process.cwd();
7367
- const hausDir = path44.join(root, ".haus-workflow");
7552
+ const hausDir = path45.join(root, ".haus-workflow");
7368
7553
  const alreadyInit = await fs32.pathExists(hausDir);
7369
7554
  if (alreadyInit) {
7370
7555
  log("Haus AI already initialized in this project.");
@@ -7500,7 +7685,7 @@ async function runRefresh() {
7500
7685
  }
7501
7686
 
7502
7687
  // src/install/scaffold.ts
7503
- import path45 from "node:path";
7688
+ import path46 from "node:path";
7504
7689
  import fs33 from "fs-extra";
7505
7690
  async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7506
7691
  const scaffolded = [];
@@ -7517,9 +7702,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7517
7702
  const realRoot = await fs33.realpath(catalogRoot).catch(() => null);
7518
7703
  for (const item of items) {
7519
7704
  if (item.type !== "config") continue;
7520
- const sourcePath = path45.resolve(catalogRoot, item.path);
7521
- const rel = path45.relative(catalogRoot, sourcePath);
7522
- const escapes = rel === "" || path45.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
7705
+ const sourcePath = path46.resolve(catalogRoot, item.path);
7706
+ const rel = path46.relative(catalogRoot, sourcePath);
7707
+ const escapes = rel === "" || path46.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
7523
7708
  if (escapes) {
7524
7709
  warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
7525
7710
  continue;
@@ -7529,8 +7714,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7529
7714
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
7530
7715
  continue;
7531
7716
  }
7532
- const realRel = path45.relative(realRoot, realSource);
7533
- if (realRel !== "" && (path45.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
7717
+ const realRel = path46.relative(realRoot, realSource);
7718
+ if (realRel !== "" && (path46.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
7534
7719
  warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
7535
7720
  continue;
7536
7721
  }
@@ -7544,14 +7729,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7544
7729
  continue;
7545
7730
  }
7546
7731
  if (stat.isFile()) {
7547
- const filename = path45.basename(sourcePath);
7548
- const dest = path45.join(projectRoot, filename);
7732
+ const filename = path46.basename(sourcePath);
7733
+ const dest = path46.join(projectRoot, filename);
7549
7734
  record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
7550
7735
  } else if (stat.isDirectory()) {
7551
7736
  const entries = await fs33.readdir(sourcePath);
7552
7737
  for (const entry of entries) {
7553
- const src = path45.join(sourcePath, entry);
7554
- const dest = path45.join(projectRoot, entry);
7738
+ const src = path46.join(sourcePath, entry);
7739
+ const dest = path46.join(projectRoot, entry);
7555
7740
  record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
7556
7741
  }
7557
7742
  }
@@ -7559,14 +7744,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7559
7744
  return { scaffolded, skipped, skippedExisting };
7560
7745
  }
7561
7746
  async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
7562
- const destDisplay = path45.relative(projectRoot, dest) || path45.basename(dest);
7747
+ const destDisplay = path46.relative(projectRoot, dest) || path46.basename(dest);
7563
7748
  const srcStat = await fs33.lstat(src).catch(() => null);
7564
7749
  if (!srcStat) {
7565
- warn(`Skipping ${path45.basename(src)}: source not found`);
7750
+ warn(`Skipping ${path46.basename(src)}: source not found`);
7566
7751
  return "skipped-refused";
7567
7752
  }
7568
7753
  if (srcStat.isSymbolicLink()) {
7569
- warn(`Skipping ${path45.basename(src)}: source is a symlink`);
7754
+ warn(`Skipping ${path46.basename(src)}: source is a symlink`);
7570
7755
  return "skipped-refused";
7571
7756
  }
7572
7757
  const exists = await fs33.pathExists(dest);
@@ -7575,13 +7760,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
7575
7760
  return "skipped-exists";
7576
7761
  }
7577
7762
  if (opts.dryRun) {
7578
- log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path45.basename(dest)} (${itemId})`);
7763
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path46.basename(dest)} (${itemId})`);
7579
7764
  return "scaffolded";
7580
7765
  }
7581
7766
  if (exists && opts.force) {
7582
7767
  await fs33.remove(dest);
7583
7768
  }
7584
- await fs33.ensureDir(path45.dirname(dest));
7769
+ await fs33.ensureDir(path46.dirname(dest));
7585
7770
  await fs33.copy(src, dest, {
7586
7771
  overwrite: true,
7587
7772
  dereference: false,
@@ -7594,7 +7779,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
7594
7779
  }
7595
7780
  }
7596
7781
  });
7597
- log(`${exists ? "Overwrote" : "Created"} ${path45.basename(dest)} (${itemId})`);
7782
+ log(`${exists ? "Overwrote" : "Created"} ${path46.basename(dest)} (${itemId})`);
7598
7783
  return "scaffolded";
7599
7784
  }
7600
7785
 
@@ -7659,7 +7844,7 @@ async function runScan(options) {
7659
7844
  }
7660
7845
 
7661
7846
  // src/commands/undo.ts
7662
- import path46 from "node:path";
7847
+ import path47 from "node:path";
7663
7848
  import fs34 from "fs-extra";
7664
7849
 
7665
7850
  // src/claude/managed-paths.ts
@@ -7687,18 +7872,18 @@ async function collectLockTrackedPaths(root) {
7687
7872
  const relPaths = row.paths ?? [];
7688
7873
  const existingRel = [];
7689
7874
  for (const rel of relPaths) {
7690
- if (await fs34.pathExists(path46.resolve(root, rel))) existingRel.push(rel);
7875
+ if (await fs34.pathExists(path47.resolve(root, rel))) existingRel.push(rel);
7691
7876
  }
7692
7877
  if (existingRel.length === 0) continue;
7693
7878
  if (row.hash === void 0) {
7694
- removable.push(...existingRel.map((rel) => path46.resolve(root, rel)));
7879
+ removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
7695
7880
  continue;
7696
7881
  }
7697
7882
  const currentHash = await hashInstalledPaths(root, existingRel);
7698
7883
  if (currentHash === row.hash) {
7699
- removable.push(...existingRel.map((rel) => path46.resolve(root, rel)));
7884
+ removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
7700
7885
  } else {
7701
- preserved.push(...existingRel.map((rel) => path46.resolve(root, rel)));
7886
+ preserved.push(...existingRel.map((rel) => path47.resolve(root, rel)));
7702
7887
  log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
7703
7888
  }
7704
7889
  }
@@ -7711,7 +7896,7 @@ async function settingsHasHausContent(root) {
7711
7896
  return settings._haus != null;
7712
7897
  }
7713
7898
  async function claudeMdHasHausBlock(root) {
7714
- const filePath = path46.join(root, "CLAUDE.md");
7899
+ const filePath = path47.join(root, "CLAUDE.md");
7715
7900
  if (!await fs34.pathExists(filePath)) return false;
7716
7901
  const text = await fs34.readFile(filePath, "utf8");
7717
7902
  return text.includes(BLOCK_BEGIN);
@@ -7724,15 +7909,15 @@ async function stripProjectSettings(root) {
7724
7909
  const hasContent = Object.keys(settings).length > 0;
7725
7910
  if (hasContent) {
7726
7911
  await writeProjectSettings(root, settings);
7727
- log(`Stripped haus rules from ${path46.relative(root, settingsPath)} (user settings preserved).`);
7912
+ log(`Stripped haus rules from ${path47.relative(root, settingsPath)} (user settings preserved).`);
7728
7913
  return true;
7729
7914
  }
7730
7915
  await fs34.remove(settingsPath);
7731
- log(`Removed ${path46.relative(root, settingsPath)} (no user-owned settings remained).`);
7916
+ log(`Removed ${path47.relative(root, settingsPath)} (no user-owned settings remained).`);
7732
7917
  return true;
7733
7918
  }
7734
7919
  async function stripRootClaudeMd(root) {
7735
- const filePath = path46.join(root, "CLAUDE.md");
7920
+ const filePath = path47.join(root, "CLAUDE.md");
7736
7921
  if (!await fs34.pathExists(filePath)) return false;
7737
7922
  const prev = await fs34.readFile(filePath, "utf8");
7738
7923
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -7760,12 +7945,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
7760
7945
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
7761
7946
  for (const abs of managedAbsPaths) {
7762
7947
  if (!await fs34.pathExists(abs)) continue;
7763
- const rel = path46.relative(root, abs);
7764
- const backupPath = path46.join(backupRoot, rel);
7765
- await fs34.ensureDir(path46.dirname(backupPath));
7948
+ const rel = path47.relative(root, abs);
7949
+ const backupPath = path47.join(backupRoot, rel);
7950
+ await fs34.ensureDir(path47.dirname(backupPath));
7766
7951
  await fs34.copy(abs, backupPath);
7767
7952
  }
7768
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path46.relative(root, backupRoot)}.`);
7953
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path47.relative(root, backupRoot)}.`);
7769
7954
  }
7770
7955
  async function runUndo(options) {
7771
7956
  const root = options.root ?? process.cwd();
@@ -7778,7 +7963,7 @@ async function runUndo(options) {
7778
7963
  log("Nothing to remove: no haus-managed files found in this directory.");
7779
7964
  return;
7780
7965
  }
7781
- const relTargets = managed.map((p) => path46.relative(root, p));
7966
+ const relTargets = managed.map((p) => path47.relative(root, p));
7782
7967
  const summaryParts = [...relTargets];
7783
7968
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
7784
7969
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -7802,7 +7987,7 @@ User-owned .claude/ files will be preserved.`
7802
7987
  for (const abs of managed) {
7803
7988
  if (!await fs34.pathExists(abs)) continue;
7804
7989
  await fs34.remove(abs);
7805
- log(`Removed ${path46.relative(root, abs)}`);
7990
+ log(`Removed ${path47.relative(root, abs)}`);
7806
7991
  }
7807
7992
  if (stripSettings) await stripProjectSettings(root);
7808
7993
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -7813,7 +7998,7 @@ User-owned .claude/ files will be preserved.`
7813
7998
 
7814
7999
  // src/install/uninstall.ts
7815
8000
  import crypto3 from "node:crypto";
7816
- import path47 from "node:path";
8001
+ import path48 from "node:path";
7817
8002
  import fs35 from "fs-extra";
7818
8003
  async function runUninstall(options = {}) {
7819
8004
  const { force = false } = options;
@@ -7843,14 +8028,14 @@ async function runUninstall(options = {}) {
7843
8028
  continue;
7844
8029
  }
7845
8030
  await fs35.remove(entry.destPath);
7846
- await pruneEmptyDir(path47.dirname(entry.destPath));
8031
+ await pruneEmptyDir(path48.dirname(entry.destPath));
7847
8032
  result.deleted.push(entry.destPath);
7848
8033
  }
7849
8034
  const settings = await readSettings();
7850
8035
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
7851
8036
  await writeSettings(stripped);
7852
8037
  result.hooksStripped = true;
7853
- const hausDir = path47.join(globalClaudeDir(), "haus");
8038
+ const hausDir = path48.join(globalClaudeDir(), "haus");
7854
8039
  const manifestPath2 = hausManifestPath();
7855
8040
  if (fs35.pathExistsSync(manifestPath2)) {
7856
8041
  await fs35.remove(manifestPath2);
@@ -7891,12 +8076,13 @@ async function runUninstallCommand(options) {
7891
8076
  }
7892
8077
 
7893
8078
  // src/commands/validate-catalog.ts
7894
- import path49 from "node:path";
8079
+ import path50 from "node:path";
7895
8080
 
7896
8081
  // src/catalog/validate-core.ts
7897
8082
  import fs36 from "node:fs";
7898
- import path48 from "node:path";
8083
+ import path49 from "node:path";
7899
8084
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
8085
+ var ITEM_ID_PATTERN = /^haus\./;
7900
8086
  function auditForbiddenStacks(items) {
7901
8087
  const failures = [];
7902
8088
  for (const item of items) {
@@ -7982,6 +8168,9 @@ function auditManifestStructure(manifestVersion, items) {
7982
8168
  failures.push(`item[${i}]: missing id`);
7983
8169
  continue;
7984
8170
  }
8171
+ if (!ITEM_ID_PATTERN.test(item.id)) {
8172
+ failures.push(`${item.id}: id must start with "haus."`);
8173
+ }
7985
8174
  const provenanceError = validateCuratedProvenance(item);
7986
8175
  if (provenanceError) failures.push(provenanceError);
7987
8176
  const referencesError = validateReferences(item.id, item.references);
@@ -8040,7 +8229,7 @@ function readForAudit(absPath, label, failures) {
8040
8229
  }
8041
8230
  }
8042
8231
  function auditTemplateContent(manifestDir, absPath, itemId) {
8043
- const rel = path48.relative(manifestDir, absPath);
8232
+ const rel = path49.relative(manifestDir, absPath);
8044
8233
  const failures = [];
8045
8234
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
8046
8235
  if (text === null) return failures;
@@ -8062,25 +8251,25 @@ function auditShippedFiles(manifestDir, items) {
8062
8251
  const failures = [];
8063
8252
  for (const item of items) {
8064
8253
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
8065
- const absPath = path48.join(manifestDir, item.path);
8254
+ const absPath = path49.join(manifestDir, item.path);
8066
8255
  if (item.type === "skill") {
8067
- const skillMd = path48.join(absPath, "SKILL.md");
8256
+ const skillMd = path49.join(absPath, "SKILL.md");
8068
8257
  if (!fs36.existsSync(skillMd)) {
8069
- failures.push(`${item.id}: missing ${path48.relative(manifestDir, skillMd)}`);
8258
+ failures.push(`${item.id}: missing ${path49.relative(manifestDir, skillMd)}`);
8070
8259
  continue;
8071
8260
  }
8072
8261
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
8073
8262
  if (text === null) continue;
8074
8263
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
8075
8264
  failures.push(
8076
- ...auditForbiddenTagsInText(text, `${item.id}: ${path48.relative(manifestDir, skillMd)}`)
8265
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path49.relative(manifestDir, skillMd)}`)
8077
8266
  );
8078
8267
  } else if (item.type === "agent") {
8079
8268
  if (!fs36.existsSync(absPath)) {
8080
8269
  failures.push(`${item.id}: missing agent file ${item.path}`);
8081
8270
  continue;
8082
8271
  }
8083
- const rel = path48.relative(manifestDir, absPath);
8272
+ const rel = path49.relative(manifestDir, absPath);
8084
8273
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8085
8274
  if (text === null) continue;
8086
8275
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8096,7 +8285,7 @@ function auditShippedFiles(manifestDir, items) {
8096
8285
  failures.push(`${item.id}: missing command file ${item.path}`);
8097
8286
  continue;
8098
8287
  }
8099
- const rel = path48.relative(manifestDir, absPath);
8288
+ const rel = path49.relative(manifestDir, absPath);
8100
8289
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8101
8290
  if (text === null) continue;
8102
8291
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8128,7 +8317,7 @@ function walkAllFiles(dir, fn, failures) {
8128
8317
  }
8129
8318
  for (const entry of entries) {
8130
8319
  if (entry.name === ".DS_Store") continue;
8131
- const full = path48.join(dir, entry.name);
8320
+ const full = path49.join(dir, entry.name);
8132
8321
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
8133
8322
  else fn(full);
8134
8323
  }
@@ -8146,12 +8335,12 @@ function auditDiskOrphans(manifestDir, items) {
8146
8335
  const failures = [];
8147
8336
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
8148
8337
  for (const dir of CONTENT_ROOTS) {
8149
- const abs = path48.join(manifestDir, dir);
8338
+ const abs = path49.join(manifestDir, dir);
8150
8339
  if (!fs36.existsSync(abs)) continue;
8151
8340
  walkAllFiles(
8152
8341
  abs,
8153
8342
  (file) => {
8154
- const relPath = path48.relative(manifestDir, file).replace(/\\/g, "/");
8343
+ const relPath = path49.relative(manifestDir, file).replace(/\\/g, "/");
8155
8344
  if (!isPathClaimed(relPath, claimedPaths)) {
8156
8345
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
8157
8346
  }
@@ -8172,7 +8361,7 @@ function walkMd(dir, fn, failures) {
8172
8361
  return;
8173
8362
  }
8174
8363
  for (const entry of entries) {
8175
- const full = path48.join(dir, entry.name);
8364
+ const full = path49.join(dir, entry.name);
8176
8365
  if (entry.isDirectory()) walkMd(full, fn, failures);
8177
8366
  else if (entry.name.endsWith(".md")) fn(full);
8178
8367
  }
@@ -8181,13 +8370,13 @@ function auditMarkdownContent(manifestDir, items) {
8181
8370
  const failures = [];
8182
8371
  const pathSourceMap = buildItemPathSourceMap(items);
8183
8372
  for (const dir of ["skills", "agents", "templates", "commands"]) {
8184
- const abs = path48.join(manifestDir, dir);
8373
+ const abs = path49.join(manifestDir, dir);
8185
8374
  if (!fs36.existsSync(abs)) continue;
8186
8375
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
8187
8376
  walkMd(
8188
8377
  abs,
8189
8378
  (file) => {
8190
- const rel = path48.relative(manifestDir, file);
8379
+ const rel = path49.relative(manifestDir, file);
8191
8380
  const text = readForAudit(file, rel, failures);
8192
8381
  if (text === null) return;
8193
8382
  const norm = rel.replace(/\\/g, "/");
@@ -8232,8 +8421,8 @@ async function runValidateCatalog(manifestPath2) {
8232
8421
  process.exitCode = 1;
8233
8422
  return;
8234
8423
  }
8235
- const abs = path49.resolve(process.cwd(), manifestPath2);
8236
- const manifestDir = path49.dirname(abs);
8424
+ const abs = path50.resolve(process.cwd(), manifestPath2);
8425
+ const manifestDir = path50.dirname(abs);
8237
8426
  const data = await readJson(abs);
8238
8427
  if (!data?.items) {
8239
8428
  error(`Could not read catalog manifest at ${abs}`);
@@ -8250,7 +8439,7 @@ async function runValidateCatalog(manifestPath2) {
8250
8439
  }
8251
8440
 
8252
8441
  // src/workspace/worktree/add.ts
8253
- import path56 from "node:path";
8442
+ import path57 from "node:path";
8254
8443
  import fs40 from "fs-extra";
8255
8444
 
8256
8445
  // src/workspace/worktree/git-worktree.ts
@@ -8370,12 +8559,12 @@ async function hasUnpushedWork(cwd) {
8370
8559
  }
8371
8560
 
8372
8561
  // src/workspace/worktree/hydrate.ts
8373
- import path52 from "node:path";
8562
+ import path53 from "node:path";
8374
8563
  import fs38 from "fs-extra";
8375
8564
 
8376
8565
  // src/workspace/worktree/cow-copy.ts
8377
8566
  init_exec();
8378
- import path50 from "node:path";
8567
+ import path51 from "node:path";
8379
8568
  import fs37 from "fs-extra";
8380
8569
  async function getMacFilesystemType(dir) {
8381
8570
  const df = await runCommand("df", ["-P", dir]);
@@ -8410,11 +8599,11 @@ async function cowCopyDir(src, dest) {
8410
8599
  const srcExists = await fs37.pathExists(src);
8411
8600
  if (!srcExists)
8412
8601
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
8413
- const strategy = await detectCowStrategy(path50.dirname(dest));
8602
+ const strategy = await detectCowStrategy(path51.dirname(dest));
8414
8603
  if (strategy === "unsupported" || strategy === "unknown-platform") {
8415
8604
  return { strategy, attempted: false, ok: false };
8416
8605
  }
8417
- await fs37.ensureDir(path50.dirname(dest));
8606
+ await fs37.ensureDir(path51.dirname(dest));
8418
8607
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
8419
8608
  const result = await runCommand("cp", args);
8420
8609
  return {
@@ -8428,7 +8617,7 @@ async function cowCopyDir(src, dest) {
8428
8617
  // src/workspace/worktree/install.ts
8429
8618
  init_exec();
8430
8619
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8431
- import path51 from "node:path";
8620
+ import path52 from "node:path";
8432
8621
  import fg5 from "fast-glob";
8433
8622
  function detectInstallPlan(signals) {
8434
8623
  if (signals.hasYarnLock) {
@@ -8449,10 +8638,10 @@ function detectInstallPlan(signals) {
8449
8638
  return { manager: "none" };
8450
8639
  }
8451
8640
  async function computeLockfileSignals(dir) {
8452
- const hasYarnLock = existsSync3(path51.join(dir, "yarn.lock"));
8453
- const hasPnpmLock = existsSync3(path51.join(dir, "pnpm-lock.yaml"));
8454
- const hasPackageLock = existsSync3(path51.join(dir, "package-lock.json"));
8455
- const hasComposerLock = existsSync3(path51.join(dir, "composer.lock"));
8641
+ const hasYarnLock = existsSync3(path52.join(dir, "yarn.lock"));
8642
+ const hasPnpmLock = existsSync3(path52.join(dir, "pnpm-lock.yaml"));
8643
+ const hasPackageLock = existsSync3(path52.join(dir, "package-lock.json"));
8644
+ const hasComposerLock = existsSync3(path52.join(dir, "composer.lock"));
8456
8645
  const dotnetFiles = await fg5(["**/*.csproj", "**/*.sln"], {
8457
8646
  cwd: dir,
8458
8647
  onlyFiles: true,
@@ -8466,7 +8655,7 @@ async function computeLockfileSignals(dir) {
8466
8655
  });
8467
8656
  const hasDotnetProject = dotnetFiles.length > 0;
8468
8657
  let hasPackageManagerField = false;
8469
- const pkgPath = path51.join(dir, "package.json");
8658
+ const pkgPath = path52.join(dir, "package.json");
8470
8659
  if (existsSync3(pkgPath)) {
8471
8660
  try {
8472
8661
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -8507,8 +8696,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
8507
8696
  const start = Date.now();
8508
8697
  const targets = [];
8509
8698
  for (const target of HYDRATION_TARGETS) {
8510
- const src = path52.join(mainMemberPath, target);
8511
- const dest = path52.join(worktreeMemberPath, target);
8699
+ const src = path53.join(mainMemberPath, target);
8700
+ const dest = path53.join(worktreeMemberPath, target);
8512
8701
  if (!await fs38.pathExists(src)) continue;
8513
8702
  const destExists = await fs38.pathExists(dest);
8514
8703
  if (!opts.force && destExists) continue;
@@ -8526,11 +8715,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
8526
8715
 
8527
8716
  // src/workspace/worktree/local-files.ts
8528
8717
  init_exec();
8529
- import path53 from "node:path";
8718
+ import path54 from "node:path";
8530
8719
  import fs39 from "fs-extra";
8531
8720
  var MACHINE_LOCAL_FILES = [
8532
8721
  "docker-compose.local.yml",
8533
- path53.join(".claude", "settings.local.json")
8722
+ path54.join(".claude", "settings.local.json")
8534
8723
  ];
8535
8724
  async function isTracked(repoRoot, relFile) {
8536
8725
  const gitPath = relFile.replace(/\\/g, "/");
@@ -8540,11 +8729,11 @@ async function isTracked(repoRoot, relFile) {
8540
8729
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
8541
8730
  const copied = [];
8542
8731
  for (const rel of MACHINE_LOCAL_FILES) {
8543
- const src = path53.join(mainMemberPath, rel);
8732
+ const src = path54.join(mainMemberPath, rel);
8544
8733
  if (!await fs39.pathExists(src)) continue;
8545
8734
  if (await isTracked(mainMemberPath, rel)) continue;
8546
- const dest = path53.join(worktreeMemberPath, rel);
8547
- await fs39.ensureDir(path53.dirname(dest));
8735
+ const dest = path54.join(worktreeMemberPath, rel);
8736
+ await fs39.ensureDir(path54.dirname(dest));
8548
8737
  await fs39.copy(src, dest, { overwrite: true });
8549
8738
  copied.push(rel);
8550
8739
  }
@@ -8553,15 +8742,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
8553
8742
 
8554
8743
  // src/workspace/worktree/root.ts
8555
8744
  import { existsSync as existsSync4 } from "node:fs";
8556
- import path54 from "node:path";
8745
+ import path55 from "node:path";
8557
8746
  async function resolveWorkspaceForWorktree(start) {
8558
8747
  const rootInfo = await resolveRoots(start);
8559
8748
  if (!rootInfo.isGitRepo) {
8560
8749
  return { ok: false, reason: "Not inside a git repository." };
8561
8750
  }
8562
8751
  const workspaceRoot = rootInfo.mainRoot;
8563
- const hasYaml = existsSync4(path54.join(workspaceRoot, WORKSPACE_FILE));
8564
- const hasManifest = existsSync4(path54.join(workspaceRoot, REPOS_MANIFEST_FILE));
8752
+ const hasYaml = existsSync4(path55.join(workspaceRoot, WORKSPACE_FILE));
8753
+ const hasManifest = existsSync4(path55.join(workspaceRoot, REPOS_MANIFEST_FILE));
8565
8754
  if (!hasYaml && !hasManifest) {
8566
8755
  return {
8567
8756
  ok: false,
@@ -8586,19 +8775,19 @@ function selectMembers(members, only) {
8586
8775
  }
8587
8776
 
8588
8777
  // src/workspace/worktree/state.ts
8589
- import path55 from "node:path";
8778
+ import path56 from "node:path";
8590
8779
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
8591
8780
  function worktreesDir(mainRoot) {
8592
- return path55.join(mainRoot, ".claude", "worktrees");
8781
+ return path56.join(mainRoot, ".claude", "worktrees");
8593
8782
  }
8594
8783
  function worktreePath(mainRoot, slug) {
8595
- return path55.join(worktreesDir(mainRoot), slug);
8784
+ return path56.join(worktreesDir(mainRoot), slug);
8596
8785
  }
8597
8786
  async function readWorktreeState(worktreeRoot) {
8598
- return readJson(path55.join(worktreeRoot, WORKTREE_STATE_FILE));
8787
+ return readJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE));
8599
8788
  }
8600
8789
  async function writeWorktreeState(worktreeRoot, state) {
8601
- await writeJson(path55.join(worktreeRoot, WORKTREE_STATE_FILE), state);
8790
+ await writeJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE), state);
8602
8791
  }
8603
8792
 
8604
8793
  // src/workspace/worktree/add.ts
@@ -8616,7 +8805,7 @@ async function runAdd(opts) {
8616
8805
  if (await fs40.pathExists(wsWorktreePath)) {
8617
8806
  return {
8618
8807
  ok: false,
8619
- error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate\` or \`remove\` first.`
8808
+ error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate --materialize\` from inside it to check out any missing members (e.g. a worktree created by plain \`git worktree add\`), or \`remove\` first to start over.`
8620
8809
  };
8621
8810
  }
8622
8811
  if (opts.dryRun) {
@@ -8632,7 +8821,7 @@ async function runAdd(opts) {
8632
8821
  const memberResults = [];
8633
8822
  for (const member of selected) {
8634
8823
  const start = Date.now();
8635
- const memberWorktreePath = path56.join(wsWorktreePath, member.folder);
8824
+ const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
8636
8825
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
8637
8826
  preferDefaultBranchFrom: true
8638
8827
  });
@@ -8662,7 +8851,7 @@ async function runAdd(opts) {
8662
8851
  await mapWithConcurrency(okResults, async (result) => {
8663
8852
  const member = selected.find((m) => m.id === result.member);
8664
8853
  if (!member) return;
8665
- const memberWorktreePath = path56.join(wsWorktreePath, member.folder);
8854
+ const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
8666
8855
  const hydrationStart = Date.now();
8667
8856
  if (opts.hydrate !== false) {
8668
8857
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -8729,10 +8918,10 @@ async function runDryRun(args) {
8729
8918
 
8730
8919
  // src/workspace/worktree/doctor.ts
8731
8920
  import { existsSync as existsSync5 } from "node:fs";
8732
- import path57 from "node:path";
8921
+ import path58 from "node:path";
8733
8922
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
8734
8923
  function looksHydrated(memberPath) {
8735
- return HYDRATION_MARKERS.some((marker) => existsSync5(path57.join(memberPath, marker)));
8924
+ return HYDRATION_MARKERS.some((marker) => existsSync5(path58.join(memberPath, marker)));
8736
8925
  }
8737
8926
  async function runDoctor2() {
8738
8927
  const rootInfo = await resolveRoots();
@@ -8746,7 +8935,7 @@ async function runDoctor2() {
8746
8935
  };
8747
8936
  }
8748
8937
  const workspaceRoot = rootInfo.mainRoot;
8749
- const isWorkspace = existsSync5(path57.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path57.join(workspaceRoot, REPOS_MANIFEST_FILE));
8938
+ const isWorkspace = existsSync5(path58.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path58.join(workspaceRoot, REPOS_MANIFEST_FILE));
8750
8939
  if (!isWorkspace) {
8751
8940
  return {
8752
8941
  isWorkspace: false,
@@ -8759,7 +8948,7 @@ async function runDoctor2() {
8759
8948
  };
8760
8949
  }
8761
8950
  const wsWorktreesDir = worktreesDir(workspaceRoot);
8762
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path57.sep}`);
8951
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path58.sep}`);
8763
8952
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
8764
8953
  const members = await readMembers(rootInfo);
8765
8954
  const problems = [];
@@ -8767,10 +8956,10 @@ async function runDoctor2() {
8767
8956
  let slug;
8768
8957
  if (inWorkspaceWorktree) {
8769
8958
  const wtPath = rootInfo.repoRoot;
8770
- slug = rootInfo.worktreeName ?? path57.basename(wtPath);
8959
+ slug = rootInfo.worktreeName ?? path58.basename(wtPath);
8771
8960
  const state = await readWorktreeState(wtPath);
8772
8961
  for (const member of members) {
8773
- const memberPath = path57.join(wtPath, member.folder);
8962
+ const memberPath = path58.join(wtPath, member.folder);
8774
8963
  const materialized = existsSync5(memberPath);
8775
8964
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
8776
8965
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -8801,16 +8990,16 @@ async function runDoctor2() {
8801
8990
  for (const member of members) {
8802
8991
  const entries = await listWorktrees(member.absPath);
8803
8992
  for (const entry of entries) {
8804
- const entryPath = path57.normalize(entry.path);
8805
- const marker = `${path57.sep}.claude${path57.sep}worktrees${path57.sep}`;
8993
+ const entryPath = path58.normalize(entry.path);
8994
+ const marker = `${path58.sep}.claude${path58.sep}worktrees${path58.sep}`;
8806
8995
  const idx = entryPath.indexOf(marker);
8807
8996
  if (idx === -1) continue;
8808
8997
  const rest = entryPath.slice(idx + marker.length);
8809
- const slugName = rest.split(path57.sep)[0];
8998
+ const slugName = rest.split(path58.sep)[0];
8810
8999
  if (!slugName) continue;
8811
- if (!existsSync5(path57.join(wsWorktreesDir, slugName))) {
9000
+ if (!existsSync5(path58.join(wsWorktreesDir, slugName))) {
8812
9001
  orphans.push(
8813
- `${member.id}: worktree registered at ${entry.path} but ${path57.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
9002
+ `${member.id}: worktree registered at ${entry.path} but ${path58.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
8814
9003
  );
8815
9004
  }
8816
9005
  }
@@ -8821,7 +9010,7 @@ async function runDoctor2() {
8821
9010
 
8822
9011
  // src/workspace/worktree/hydrate-command.ts
8823
9012
  import { existsSync as existsSync6 } from "node:fs";
8824
- import path58 from "node:path";
9013
+ import path59 from "node:path";
8825
9014
  async function runHydrateWorktree(opts) {
8826
9015
  const rootInfo = await resolveRoots();
8827
9016
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -8832,7 +9021,7 @@ async function runHydrateWorktree(opts) {
8832
9021
  }
8833
9022
  const workspaceRoot = rootInfo.mainRoot;
8834
9023
  const wtPath = rootInfo.repoRoot;
8835
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path58.sep}`)) {
9024
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path59.sep}`)) {
8836
9025
  return {
8837
9026
  ok: false,
8838
9027
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -8844,49 +9033,97 @@ async function runHydrateWorktree(opts) {
8844
9033
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
8845
9034
  }
8846
9035
  const state = await readWorktreeState(wtPath);
8847
- const slug = state?.slug ?? path58.basename(wtPath);
9036
+ const slug = state?.slug ?? path59.basename(wtPath);
9037
+ const detectedBranch = await currentBranch(wtPath);
9038
+ const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
8848
9039
  if (opts.dryRun) {
8849
- const results2 = selected.map((member) => {
8850
- const memberPath = path58.join(wtPath, member.folder);
8851
- return {
8852
- member: member.id,
8853
- folder: member.folder,
8854
- status: existsSync6(memberPath) ? "planned" : "skipped",
8855
- error: existsSync6(memberPath) ? void 0 : "not materialized in this worktree"
8856
- };
8857
- });
9040
+ const results2 = await Promise.all(
9041
+ selected.map(async (member) => {
9042
+ const memberPath = path59.join(wtPath, member.folder);
9043
+ if (existsSync6(memberPath)) {
9044
+ return { member: member.id, folder: member.folder, status: "planned" };
9045
+ }
9046
+ if (!opts.materialize) {
9047
+ return {
9048
+ member: member.id,
9049
+ folder: member.folder,
9050
+ status: "skipped",
9051
+ error: "not materialized in this worktree"
9052
+ };
9053
+ }
9054
+ const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
9055
+ return { member: member.id, folder: member.folder, status: "planned", branchAction };
9056
+ })
9057
+ );
8858
9058
  return { ok: true, dryRun: true, slug, results: results2 };
8859
9059
  }
8860
9060
  const results = await mapWithConcurrency(
8861
9061
  selected,
8862
9062
  async (member) => {
8863
- const memberPath = path58.join(wtPath, member.folder);
9063
+ const memberPath = path59.join(wtPath, member.folder);
9064
+ let branchAction;
8864
9065
  if (!existsSync6(memberPath)) {
8865
- return {
8866
- member: member.id,
8867
- folder: member.folder,
8868
- status: "skipped",
8869
- error: "not materialized in this worktree"
8870
- };
9066
+ if (!opts.materialize) {
9067
+ return {
9068
+ member: member.id,
9069
+ folder: member.folder,
9070
+ status: "skipped",
9071
+ error: "not materialized in this worktree"
9072
+ };
9073
+ }
9074
+ const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
9075
+ preferDefaultBranchFrom: true
9076
+ });
9077
+ if (!outcome.ok) {
9078
+ return {
9079
+ member: member.id,
9080
+ folder: member.folder,
9081
+ status: "failed",
9082
+ error: outcome.checkedOutElsewhere ? `Branch "${targetBranch}" is already checked out elsewhere in ${member.id} \u2014 remove that worktree or resolve the branch conflict first.` : outcome.stderr ?? "unknown error creating the member worktree"
9083
+ };
9084
+ }
9085
+ branchAction = outcome.branchAction;
8871
9086
  }
8872
9087
  try {
8873
9088
  const hydration = await hydrateMember(member.absPath, memberPath, { force: opts.force });
8874
- return { member: member.id, folder: member.folder, status: "ok", hydration };
9089
+ return { member: member.id, folder: member.folder, status: "ok", hydration, branchAction };
8875
9090
  } catch (err) {
8876
9091
  return {
8877
9092
  member: member.id,
8878
9093
  folder: member.folder,
8879
9094
  status: "failed",
8880
- error: err instanceof Error ? err.message : String(err)
9095
+ error: err instanceof Error ? err.message : String(err),
9096
+ branchAction
8881
9097
  };
8882
9098
  }
8883
9099
  }
8884
9100
  );
9101
+ const newlyMaterialized = results.filter((r) => r.branchAction && r.status === "ok");
9102
+ if (newlyMaterialized.length > 0) {
9103
+ const byId = new Map((state?.members ?? []).map((m) => [m.id, m]));
9104
+ for (const r of newlyMaterialized) {
9105
+ const member = selected.find((m) => m.id === r.member);
9106
+ if (!member) continue;
9107
+ const entry = {
9108
+ id: member.id,
9109
+ folder: member.folder,
9110
+ branch: targetBranch,
9111
+ absPath: member.absPath
9112
+ };
9113
+ byId.set(member.id, entry);
9114
+ }
9115
+ await writeWorktreeState(wtPath, {
9116
+ slug,
9117
+ branch: state?.branch ?? targetBranch,
9118
+ createdAt: state?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
9119
+ members: Array.from(byId.values())
9120
+ });
9121
+ }
8885
9122
  return { ok: results.every((r) => r.status !== "failed"), dryRun: false, slug, results };
8886
9123
  }
8887
9124
 
8888
9125
  // src/workspace/worktree/list.ts
8889
- import path59 from "node:path";
9126
+ import path60 from "node:path";
8890
9127
  import fs41 from "fs-extra";
8891
9128
  async function runList2() {
8892
9129
  const resolved = await resolveWorkspaceForWorktree();
@@ -8899,12 +9136,12 @@ async function runList2() {
8899
9136
  const configuredMembers = await readMembers(rootInfo);
8900
9137
  const worktrees = [];
8901
9138
  for (const slug of slugs) {
8902
- const wtPath = path59.join(dir, slug);
9139
+ const wtPath = path60.join(dir, slug);
8903
9140
  const state = await readWorktreeState(wtPath);
8904
9141
  const members = [];
8905
9142
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
8906
9143
  for (const m of source) {
8907
- const memberPath = path59.join(wtPath, m.folder);
9144
+ const memberPath = path60.join(wtPath, m.folder);
8908
9145
  const materialized = await fs41.pathExists(memberPath);
8909
9146
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
8910
9147
  members.push({
@@ -8926,7 +9163,7 @@ async function runList2() {
8926
9163
  }
8927
9164
 
8928
9165
  // src/workspace/worktree/remove.ts
8929
- import path60 from "node:path";
9166
+ import path61 from "node:path";
8930
9167
  import fs42 from "fs-extra";
8931
9168
  async function runRemove(opts) {
8932
9169
  const resolved = await resolveWorkspaceForWorktree();
@@ -8953,7 +9190,7 @@ async function runRemove(opts) {
8953
9190
  targets.push({
8954
9191
  repo: configMember?.id ?? stateMember.id,
8955
9192
  ownerRepo,
8956
- path: path60.join(wsWorktreePath, stateMember.folder)
9193
+ path: path61.join(wsWorktreePath, stateMember.folder)
8957
9194
  });
8958
9195
  }
8959
9196
  const existingTargets = [];
@@ -9081,7 +9318,8 @@ async function handleHydrate(opts) {
9081
9318
  const hydrateOpts = {
9082
9319
  only: normalizeOnly(opts.only),
9083
9320
  force: Boolean(opts.force),
9084
- dryRun: Boolean(opts.dryRun)
9321
+ dryRun: Boolean(opts.dryRun),
9322
+ materialize: Boolean(opts.materialize)
9085
9323
  };
9086
9324
  const result = await runHydrateWorktree(hydrateOpts);
9087
9325
  if (!result.ok && "error" in result) {
@@ -9091,14 +9329,20 @@ async function handleHydrate(opts) {
9091
9329
  }
9092
9330
  if (result.dryRun) {
9093
9331
  log(`[dry-run] Would hydrate "${result.slug}":`);
9094
- for (const r of result.results)
9095
- log(`[dry-run] ${r.member}: ${r.status}${r.error ? ` (${r.error})` : ""}`);
9332
+ for (const r of result.results) {
9333
+ const action = r.branchAction ? ` (${r.branchAction} member worktree first)` : "";
9334
+ log(`[dry-run] ${r.member}: ${r.status}${action}${r.error ? ` (${r.error})` : ""}`);
9335
+ }
9096
9336
  return;
9097
9337
  }
9098
9338
  log(`Hydrated "${result.slug}":`);
9099
9339
  for (const r of result.results) {
9100
- if (r.status === "ok" && r.hydration) log(` ${r.member}: ${describeInstall(r.hydration)}`);
9101
- else warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
9340
+ if (r.status === "ok" && r.hydration) {
9341
+ const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
9342
+ log(` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`);
9343
+ } else {
9344
+ warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
9345
+ }
9102
9346
  }
9103
9347
  if (!result.ok) process.exitCode = 1;
9104
9348
  }
@@ -9157,6 +9401,13 @@ async function handleList() {
9157
9401
  }
9158
9402
  }
9159
9403
  }
9404
+ function emitFromHookContext(additionalContext) {
9405
+ log(
9406
+ JSON.stringify({
9407
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext }
9408
+ })
9409
+ );
9410
+ }
9160
9411
  async function handleDoctor(opts) {
9161
9412
  try {
9162
9413
  const report = await runDoctor2();
@@ -9168,17 +9419,26 @@ async function handleDoctor(opts) {
9168
9419
  return;
9169
9420
  }
9170
9421
  if (report.problems.length === 0) {
9171
- log(
9172
- report.inWorkspaceWorktree ? `Workspace worktree "${report.slug}" looks healthy.` : "No issues found."
9422
+ if (!opts.fromHook) {
9423
+ log(
9424
+ report.inWorkspaceWorktree ? `Workspace worktree "${report.slug}" looks healthy.` : "No issues found."
9425
+ );
9426
+ }
9427
+ return;
9428
+ }
9429
+ if (opts.fromHook) {
9430
+ const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
9431
+ emitFromHookContext(
9432
+ `${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
9173
9433
  );
9174
9434
  return;
9175
9435
  }
9176
9436
  for (const p of report.problems) warn(p);
9177
- if (!opts.fromHook) process.exitCode = 1;
9437
+ process.exitCode = 1;
9178
9438
  } catch (err) {
9179
9439
  const message = err instanceof Error ? err.message : String(err);
9180
9440
  if (opts.fromHook) {
9181
- warn(`worktree doctor: ${message}`);
9441
+ emitFromHookContext(`\`haus workspace worktree doctor\` failed: ${message}`);
9182
9442
  } else {
9183
9443
  error(message);
9184
9444
  process.exitCode = 1;
@@ -9202,7 +9462,7 @@ async function runWorktree(action, opts = {}) {
9202
9462
 
9203
9463
  // src/commands/workspace.ts
9204
9464
  import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
9205
- import path69 from "node:path";
9465
+ import path70 from "node:path";
9206
9466
 
9207
9467
  // src/commands/workspace/aggregate.ts
9208
9468
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -9257,18 +9517,18 @@ ${summaries.map(
9257
9517
 
9258
9518
  // src/commands/workspace/doctor.ts
9259
9519
  import { existsSync as existsSync7 } from "node:fs";
9260
- import path62 from "node:path";
9520
+ import path63 from "node:path";
9261
9521
 
9262
9522
  // src/commands/workspace/manifest.ts
9263
9523
  import { readFileSync as readFileSync4 } from "node:fs";
9264
- import path61 from "node:path";
9524
+ import path62 from "node:path";
9265
9525
  var MANIFEST_FILE = "workspace.manifest.json";
9266
9526
  function manifestPath(workspaceRoot) {
9267
9527
  return hausPath(workspaceRoot, MANIFEST_FILE);
9268
9528
  }
9269
9529
  function hausVersion() {
9270
9530
  try {
9271
- const pkg = JSON.parse(readFileSync4(path61.join(packageRoot(), "package.json"), "utf8"));
9531
+ const pkg = JSON.parse(readFileSync4(path62.join(packageRoot(), "package.json"), "utf8"));
9272
9532
  return pkg.version ?? "0.0.0";
9273
9533
  } catch {
9274
9534
  return "0.0.0";
@@ -9352,7 +9612,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
9352
9612
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
9353
9613
  const catalogRefByRepo = [];
9354
9614
  for (const repo of config.repos) {
9355
- const repoRoot = path62.resolve(workspaceRoot, repo.path);
9615
+ const repoRoot = path63.resolve(workspaceRoot, repo.path);
9356
9616
  const entry = manifestByName.get(repo.name);
9357
9617
  if (!entry) {
9358
9618
  flag({
@@ -9425,7 +9685,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
9425
9685
  });
9426
9686
  continue;
9427
9687
  }
9428
- if (!existsSync7(path62.join(workspaceRoot, entry.path))) {
9688
+ if (!existsSync7(path63.join(workspaceRoot, entry.path))) {
9429
9689
  flag({
9430
9690
  repo: entry.repo,
9431
9691
  kind: "missing-linked-context-copy",
@@ -9468,15 +9728,15 @@ function emit(args) {
9468
9728
  }
9469
9729
 
9470
9730
  // src/workspace/link-context/link.ts
9471
- import path65 from "node:path";
9731
+ import path66 from "node:path";
9472
9732
  import fs45 from "fs-extra";
9473
9733
 
9474
9734
  // src/workspace/link-context/plan.ts
9475
- import path64 from "node:path";
9735
+ import path65 from "node:path";
9476
9736
  import fs44 from "fs-extra";
9477
9737
 
9478
9738
  // src/workspace/link-context/scan-source.ts
9479
- import path63 from "node:path";
9739
+ import path64 from "node:path";
9480
9740
  import fs43 from "fs-extra";
9481
9741
  var ASSET_SUBDIR = {
9482
9742
  skill: "skills",
@@ -9485,36 +9745,36 @@ var ASSET_SUBDIR = {
9485
9745
  };
9486
9746
  async function hasLinkableContext(member) {
9487
9747
  for (const subdir of Object.values(ASSET_SUBDIR)) {
9488
- if (await fs43.pathExists(path63.join(member.absPath, ".claude", subdir))) return true;
9748
+ if (await fs43.pathExists(path64.join(member.absPath, ".claude", subdir))) return true;
9489
9749
  }
9490
9750
  return false;
9491
9751
  }
9492
9752
  async function collectSkills(member) {
9493
- const dir = path63.join(member.absPath, ".claude", "skills");
9753
+ const dir = path64.join(member.absPath, ".claude", "skills");
9494
9754
  if (!await fs43.pathExists(dir)) return [];
9495
9755
  const names = await fs43.readdir(dir);
9496
9756
  const out = [];
9497
9757
  for (const name of [...names].sort()) {
9498
- const abs = path63.join(dir, name);
9758
+ const abs = path64.join(dir, name);
9499
9759
  const stat = await fs43.lstat(abs);
9500
9760
  if (!stat.isDirectory()) continue;
9501
- if (!await fs43.pathExists(path63.join(abs, "SKILL.md"))) continue;
9502
- out.push({ type: "skill", name, sourceRelPath: path63.posix.join(".claude", "skills", name) });
9761
+ if (!await fs43.pathExists(path64.join(abs, "SKILL.md"))) continue;
9762
+ out.push({ type: "skill", name, sourceRelPath: path64.posix.join(".claude", "skills", name) });
9503
9763
  }
9504
9764
  return out;
9505
9765
  }
9506
9766
  async function collectFlatMarkdown(member, type) {
9507
9767
  const subdir = ASSET_SUBDIR[type];
9508
- const dir = path63.join(member.absPath, ".claude", subdir);
9768
+ const dir = path64.join(member.absPath, ".claude", subdir);
9509
9769
  if (!await fs43.pathExists(dir)) return [];
9510
9770
  const names = await fs43.readdir(dir);
9511
9771
  const out = [];
9512
9772
  for (const fileName of [...names].sort()) {
9513
9773
  if (!fileName.endsWith(".md")) continue;
9514
- const abs = path63.join(dir, fileName);
9774
+ const abs = path64.join(dir, fileName);
9515
9775
  if (!(await fs43.lstat(abs)).isFile()) continue;
9516
9776
  const name = fileName.slice(0, -".md".length);
9517
- out.push({ type, name, sourceRelPath: path63.posix.join(".claude", subdir, fileName) });
9777
+ out.push({ type, name, sourceRelPath: path64.posix.join(".claude", subdir, fileName) });
9518
9778
  }
9519
9779
  return out;
9520
9780
  }
@@ -9529,9 +9789,9 @@ async function collectSourceAssets(member) {
9529
9789
 
9530
9790
  // src/workspace/link-context/plan.ts
9531
9791
  function repoFolderPrefix(member) {
9532
- const base = path64.posix.basename(member.folder);
9792
+ const base = path65.posix.basename(member.folder);
9533
9793
  if (base && base !== "." && base !== "/") return base;
9534
- const fromAbs = path64.basename(member.absPath);
9794
+ const fromAbs = path65.basename(member.absPath);
9535
9795
  return fromAbs || member.id;
9536
9796
  }
9537
9797
  function assetKey(type, destKey) {
@@ -9588,7 +9848,7 @@ async function buildLinkPlan(members) {
9588
9848
  name: asset.name,
9589
9849
  destKey,
9590
9850
  sourceRelPath: asset.sourceRelPath,
9591
- sourceAbsPath: path64.join(member.absPath, asset.sourceRelPath),
9851
+ sourceAbsPath: path65.join(member.absPath, asset.sourceRelPath),
9592
9852
  sourceHash
9593
9853
  });
9594
9854
  }
@@ -9599,7 +9859,7 @@ async function buildLinkPlan(members) {
9599
9859
  function destRelPathFor(type, destKey) {
9600
9860
  const subdir = `${type}s`;
9601
9861
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
9602
- return path65.posix.join(".claude", subdir, leaf);
9862
+ return path66.posix.join(".claude", subdir, leaf);
9603
9863
  }
9604
9864
  async function runLinkContext(workspaceRoot, opts = {}) {
9605
9865
  const write = opts.write ?? false;
@@ -9625,9 +9885,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
9625
9885
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
9626
9886
  for (const entry of plan.entries) {
9627
9887
  const destRel = destRelPathFor(entry.type, entry.destKey);
9628
- const destAbs = path65.join(workspaceRoot, destRel);
9888
+ const destAbs = path66.join(workspaceRoot, destRel);
9629
9889
  if (write) {
9630
- await fs45.ensureDir(path65.dirname(destAbs));
9890
+ await fs45.ensureDir(path66.dirname(destAbs));
9631
9891
  if (await fs45.pathExists(destAbs)) await fs45.remove(destAbs);
9632
9892
  await fs45.copy(entry.sourceAbsPath, destAbs, {
9633
9893
  overwrite: true,
@@ -9659,7 +9919,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
9659
9919
  if (nextPaths.has(old.path)) continue;
9660
9920
  removed.push(old.path);
9661
9921
  if (write) {
9662
- const abs = path65.join(workspaceRoot, old.path);
9922
+ const abs = path66.join(workspaceRoot, old.path);
9663
9923
  if (await fs45.pathExists(abs)) await fs45.remove(abs);
9664
9924
  }
9665
9925
  }
@@ -9712,10 +9972,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
9712
9972
 
9713
9973
  // src/commands/workspace/setup.ts
9714
9974
  import { existsSync as existsSync8, statSync } from "node:fs";
9715
- import path67 from "node:path";
9975
+ import path68 from "node:path";
9716
9976
 
9717
9977
  // src/claude/write-workspace-claude-md.ts
9718
- import path66 from "node:path";
9978
+ import path67 from "node:path";
9719
9979
  import fs46 from "fs-extra";
9720
9980
  function buildWorkspaceImportBlock(client, members) {
9721
9981
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -9734,7 +9994,7 @@ ${BLOCK_END}`;
9734
9994
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
9735
9995
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
9736
9996
  const dryRun = opts.dryRun ?? false;
9737
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path66.join(workspaceRoot, "CLAUDE.md");
9997
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path67.join(workspaceRoot, "CLAUDE.md");
9738
9998
  const say = opts.quiet ? () => {
9739
9999
  } : log;
9740
10000
  const existed = await fs46.pathExists(filePath);
@@ -9765,20 +10025,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
9765
10025
 
9766
10026
  // src/commands/workspace/setup.ts
9767
10027
  function resolveWorkspaceRoot(start = process.cwd()) {
9768
- let dir = path67.resolve(start);
10028
+ let dir = path68.resolve(start);
9769
10029
  for (; ; ) {
9770
- if (existsSync8(path67.join(dir, WORKSPACE_FILE))) return dir;
9771
- const parent = path67.dirname(dir);
9772
- if (parent === dir) return path67.resolve(start);
10030
+ if (existsSync8(path68.join(dir, WORKSPACE_FILE))) return dir;
10031
+ const parent = path68.dirname(dir);
10032
+ if (parent === dir) return path68.resolve(start);
9773
10033
  dir = parent;
9774
10034
  }
9775
10035
  }
9776
10036
  function isRootRepo(workspaceRoot, repoPath) {
9777
- return path67.resolve(workspaceRoot, repoPath) === path67.resolve(workspaceRoot);
10037
+ return path68.resolve(workspaceRoot, repoPath) === path68.resolve(workspaceRoot);
9778
10038
  }
9779
10039
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
9780
10040
  const apply = options.write ?? false;
9781
- const configText = await readText(path67.join(workspaceRoot, WORKSPACE_FILE));
10041
+ const configText = await readText(path68.join(workspaceRoot, WORKSPACE_FILE));
9782
10042
  if (!configText) {
9783
10043
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
9784
10044
  process.exitCode = 1;
@@ -9805,7 +10065,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
9805
10065
  const statuses = [];
9806
10066
  const aggregateInputs = [];
9807
10067
  for (const repo of repos) {
9808
- const repoRoot = path67.resolve(workspaceRoot, repo.path);
10068
+ const repoRoot = path68.resolve(workspaceRoot, repo.path);
9809
10069
  say(`
9810
10070
  \u2192 ${repo.name} (${repo.path})`);
9811
10071
  try {
@@ -9876,7 +10136,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
9876
10136
  const status = statusByName.get(repo.name);
9877
10137
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
9878
10138
  if (status?.status === "ok") {
9879
- const lock = await checkLock(path67.resolve(workspaceRoot, repo.path));
10139
+ const lock = await checkLock(path68.resolve(workspaceRoot, repo.path));
9880
10140
  manifestRepos.push({
9881
10141
  name: repo.name,
9882
10142
  path: repo.path,
@@ -9932,7 +10192,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
9932
10192
  try {
9933
10193
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
9934
10194
  if (linkResult.ok) {
9935
- written.push(...linkResult.added.map((rel) => path67.join(workspaceRoot, rel)));
10195
+ written.push(...linkResult.added.map((rel) => path68.join(workspaceRoot, rel)));
9936
10196
  if (!options.json) {
9937
10197
  say(
9938
10198
  `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` : ""}.`
@@ -9964,7 +10224,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
9964
10224
 
9965
10225
  // src/commands/workspace/undo.ts
9966
10226
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
9967
- import path68 from "node:path";
10227
+ import path69 from "node:path";
9968
10228
  import fs47 from "fs-extra";
9969
10229
  var WORKSPACE_AGGREGATE_FILES = [
9970
10230
  "workspace-summary.json",
@@ -9993,7 +10253,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
9993
10253
  }
9994
10254
  let anyRepoFailed = false;
9995
10255
  for (const repo of config.repos) {
9996
- const repoRoot = path68.resolve(workspaceRoot, repo.path);
10256
+ const repoRoot = path69.resolve(workspaceRoot, repo.path);
9997
10257
  log(`
9998
10258
  \u2192 ${repo.name} (${repo.path})`);
9999
10259
  try {
@@ -10008,7 +10268,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10008
10268
  }
10009
10269
  const linkedContext = await readLinkedContext(workspaceRoot);
10010
10270
  for (const entry of linkedContext) {
10011
- const abs = path68.join(workspaceRoot, entry.path);
10271
+ const abs = path69.join(workspaceRoot, entry.path);
10012
10272
  if (await fs47.pathExists(abs)) {
10013
10273
  await fs47.remove(abs);
10014
10274
  log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
@@ -10018,20 +10278,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10018
10278
  const abs = hausPath(workspaceRoot, rel);
10019
10279
  if (await fs47.pathExists(abs)) {
10020
10280
  await fs47.remove(abs);
10021
- log(`Removed ${path68.relative(workspaceRoot, abs)}`);
10281
+ log(`Removed ${path69.relative(workspaceRoot, abs)}`);
10022
10282
  }
10023
10283
  }
10024
10284
  const manifestFile = manifestPath(workspaceRoot);
10025
10285
  if (await fs47.pathExists(manifestFile)) {
10026
10286
  await fs47.remove(manifestFile);
10027
- log(`Removed ${path68.relative(workspaceRoot, manifestFile)}`);
10287
+ log(`Removed ${path69.relative(workspaceRoot, manifestFile)}`);
10028
10288
  }
10029
10289
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
10030
10290
  if (await fs47.pathExists(workspaceMdPath)) {
10031
10291
  await fs47.remove(workspaceMdPath);
10032
- log(`Removed ${path68.relative(workspaceRoot, workspaceMdPath)}`);
10292
+ log(`Removed ${path69.relative(workspaceRoot, workspaceMdPath)}`);
10033
10293
  } else {
10034
- const claudeMdPath = path68.join(workspaceRoot, "CLAUDE.md");
10294
+ const claudeMdPath = path69.join(workspaceRoot, "CLAUDE.md");
10035
10295
  if (existsSync9(claudeMdPath)) {
10036
10296
  const prev = await readText(claudeMdPath);
10037
10297
  if (prev?.includes(BLOCK_BEGIN)) {
@@ -10083,7 +10343,7 @@ relationships: []
10083
10343
  log("Workspace initialized.");
10084
10344
  }
10085
10345
  async function scanWorkspace(workspaceRoot, opts) {
10086
- const configText = await readText(path69.join(workspaceRoot, WORKSPACE_FILE));
10346
+ const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
10087
10347
  if (!configText) {
10088
10348
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
10089
10349
  process.exitCode = 1;
@@ -10105,7 +10365,7 @@ async function scanWorkspace(workspaceRoot, opts) {
10105
10365
  const inputs = [];
10106
10366
  const failures = [];
10107
10367
  for (const repo of config.repos) {
10108
- const repoRoot = path69.resolve(workspaceRoot, repo.path);
10368
+ const repoRoot = path70.resolve(workspaceRoot, repo.path);
10109
10369
  try {
10110
10370
  if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
10111
10371
  throw new Error("path is not a directory");
@@ -10171,7 +10431,7 @@ async function runWorkspace(action, options = {}) {
10171
10431
  // src/cli.ts
10172
10432
  function cliVersion() {
10173
10433
  try {
10174
- const pkgPath = path70.join(packageRoot(), "package.json");
10434
+ const pkgPath = path71.join(packageRoot(), "package.json");
10175
10435
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10176
10436
  return pkg.version ?? "0.0.0";
10177
10437
  } catch {
@@ -10181,7 +10441,7 @@ function cliVersion() {
10181
10441
  var program = new Command();
10182
10442
  function validateRuntimeNodeVersion() {
10183
10443
  try {
10184
- const pkgPath = path70.join(packageRoot(), "package.json");
10444
+ const pkgPath = path71.join(packageRoot(), "package.json");
10185
10445
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10186
10446
  const requiredRange = pkg.engines?.node;
10187
10447
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -10282,8 +10542,16 @@ worktree.command("add <slug>").description(
10282
10542
  dryRun: opts.dryRun
10283
10543
  })
10284
10544
  );
10285
- worktree.command("hydrate").description("Re-run CoW clone + install-reconciliation for the current workspace worktree").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--force", "Re-clone hydration targets even if already present").option("--dry-run", "Preview without writing or running anything").action(
10286
- (opts) => runWorktree("hydrate", { only: opts.only, force: opts.force, dryRun: opts.dryRun })
10545
+ worktree.command("hydrate").description("Re-run CoW clone + install-reconciliation for the current workspace worktree").option("--only <repos>", "Restrict to comma- or space-separated member ids/folders").option("--force", "Re-clone hydration targets even if already present").option(
10546
+ "--materialize",
10547
+ "Also `git worktree add` any member missing entirely (e.g. a worktree created by plain `git worktree add` outside haus) before hydrating it"
10548
+ ).option("--dry-run", "Preview without writing or running anything").action(
10549
+ (opts) => runWorktree("hydrate", {
10550
+ only: opts.only,
10551
+ force: opts.force,
10552
+ materialize: opts.materialize,
10553
+ dryRun: opts.dryRun
10554
+ })
10287
10555
  );
10288
10556
  worktree.command("list").description("List materialized workspace worktrees and their member status").action(() => runWorktree("list"));
10289
10557
  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").action(