@haus-tech/haus-workflow 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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,14 @@ 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);
4715
+ }
4716
+ function isSelfMember(workspaceRoot, member) {
4717
+ return path31.resolve(member.absPath) === path31.resolve(workspaceRoot);
4529
4718
  }
4530
4719
  async function readPathOverrides(workspaceRoot) {
4531
- const filePath = path30.join(workspaceRoot, REPOS_LOCAL_FILE);
4720
+ const filePath = path31.join(workspaceRoot, REPOS_LOCAL_FILE);
4532
4721
  const result = await readJsonDetailed(filePath);
4533
4722
  if (result.status === "missing") return {};
4534
4723
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -4544,7 +4733,7 @@ async function readPathOverrides(workspaceRoot) {
4544
4733
  return overrides;
4545
4734
  }
4546
4735
  async function readFromWorkspaceYaml(workspaceRoot, overrides) {
4547
- const filePath = path30.join(workspaceRoot, WORKSPACE_FILE);
4736
+ const filePath = path31.join(workspaceRoot, WORKSPACE_FILE);
4548
4737
  const text = await readText(filePath);
4549
4738
  if (text === void 0) return void 0;
4550
4739
  const config = parseWorkspaceConfig(text);
@@ -4557,7 +4746,7 @@ async function readFromWorkspaceYaml(workspaceRoot, overrides) {
4557
4746
  }));
4558
4747
  }
4559
4748
  async function readFromManifest(workspaceRoot, overrides) {
4560
- const filePath = path30.join(workspaceRoot, REPOS_MANIFEST_FILE);
4749
+ const filePath = path31.join(workspaceRoot, REPOS_MANIFEST_FILE);
4561
4750
  const result = await readJsonDetailed(filePath);
4562
4751
  if (result.status === "missing") return void 0;
4563
4752
  if (result.status === "invalid") throw new MemberConfigError(filePath, "invalid JSON");
@@ -4592,23 +4781,23 @@ async function readMembers(rootInfo) {
4592
4781
  const workspaceRoot = rootInfo.mainRoot;
4593
4782
  const overrides = await readPathOverrides(workspaceRoot);
4594
4783
  const fromYaml = await readFromWorkspaceYaml(workspaceRoot, overrides);
4595
- if (fromYaml) return fromYaml;
4784
+ if (fromYaml) return fromYaml.filter((m) => !isSelfMember(workspaceRoot, m));
4596
4785
  const fromManifest = await readFromManifest(workspaceRoot, overrides);
4597
- if (fromManifest) return fromManifest;
4786
+ if (fromManifest) return fromManifest.filter((m) => !isSelfMember(workspaceRoot, m));
4598
4787
  return [];
4599
4788
  }
4600
4789
 
4601
4790
  // src/scanner/scan-project.ts
4602
- import path35 from "node:path";
4791
+ import path36 from "node:path";
4603
4792
 
4604
4793
  // src/utils/audit-checks.ts
4605
- function isRecord(v) {
4794
+ function isRecord2(v) {
4606
4795
  return typeof v === "object" && v !== null;
4607
4796
  }
4608
4797
 
4609
4798
  // src/utils/git-root.ts
4610
4799
  init_exec();
4611
- import path31 from "node:path";
4800
+ import path32 from "node:path";
4612
4801
  function fallback(start) {
4613
4802
  return {
4614
4803
  cwd: start,
@@ -4624,11 +4813,11 @@ function fallback(start) {
4624
4813
  async function resolveAbsoluteGitPath(flag, cwd, repoRoot) {
4625
4814
  const withFormat = await runGit(["rev-parse", "--path-format=absolute", flag], { cwd });
4626
4815
  const raw = withFormat.exitCode === 0 ? withFormat.stdout.trim() : "";
4627
- if (raw && path31.isAbsolute(raw)) return path31.resolve(raw);
4816
+ if (raw && path32.isAbsolute(raw)) return path32.resolve(raw);
4628
4817
  const plain = await runGit(["rev-parse", flag], { cwd });
4629
4818
  const plainRaw = plain.exitCode === 0 ? plain.stdout.trim() : "";
4630
4819
  if (!plainRaw) return "";
4631
- return path31.isAbsolute(plainRaw) ? path31.resolve(plainRaw) : path31.resolve(repoRoot, plainRaw);
4820
+ return path32.isAbsolute(plainRaw) ? path32.resolve(plainRaw) : path32.resolve(repoRoot, plainRaw);
4632
4821
  }
4633
4822
  async function resolveRoots(start) {
4634
4823
  const cwd = start ?? process.cwd();
@@ -4637,15 +4826,15 @@ async function resolveRoots(start) {
4637
4826
  if (toplevel.exitCode !== 0) return fallback(cwd);
4638
4827
  const toplevelRaw = toplevel.stdout.trim();
4639
4828
  if (!toplevelRaw) return fallback(cwd);
4640
- const repoRoot = path31.resolve(toplevelRaw);
4829
+ const repoRoot = path32.resolve(toplevelRaw);
4641
4830
  const [gitDir, gitCommonDir] = await Promise.all([
4642
4831
  resolveAbsoluteGitPath("--git-dir", cwd, repoRoot),
4643
4832
  resolveAbsoluteGitPath("--git-common-dir", cwd, repoRoot)
4644
4833
  ]);
4645
4834
  if (!gitDir || !gitCommonDir) return fallback(cwd);
4646
4835
  const isLinkedWorktree = gitDir !== gitCommonDir;
4647
- const mainRoot = isLinkedWorktree && path31.basename(gitCommonDir) === ".git" ? path31.dirname(gitCommonDir) : repoRoot;
4648
- const worktreeName = isLinkedWorktree ? path31.basename(gitDir) : null;
4836
+ const mainRoot = isLinkedWorktree && path32.basename(gitCommonDir) === ".git" ? path32.dirname(gitCommonDir) : repoRoot;
4837
+ const worktreeName = isLinkedWorktree ? path32.basename(gitDir) : null;
4649
4838
  return {
4650
4839
  cwd,
4651
4840
  repoRoot,
@@ -4679,9 +4868,18 @@ function compareVersions(a, b) {
4679
4868
  }
4680
4869
  return semver.compare(normalizedA, normalizedB);
4681
4870
  }
4871
+ function resolveMajorVersion(range) {
4872
+ if (!range) return null;
4873
+ try {
4874
+ const min = semver.minVersion(range);
4875
+ return min ? min.major : null;
4876
+ } catch {
4877
+ return null;
4878
+ }
4879
+ }
4682
4880
 
4683
4881
  // src/scanner/detect-package-manager.ts
4684
- import path32 from "node:path";
4882
+ import path33 from "node:path";
4685
4883
  import fs27 from "fs-extra";
4686
4884
  function detectPackageManager(root, packageManagerField) {
4687
4885
  const field = String(packageManagerField ?? "").trim();
@@ -4700,9 +4898,9 @@ function detectPackageManager(root, packageManagerField) {
4700
4898
  if (satisfiesVersion(version, ">=9")) return "npm";
4701
4899
  return "unknown";
4702
4900
  }
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";
4901
+ if (fs27.existsSync(path33.join(root, "yarn.lock"))) return "yarn";
4902
+ if (fs27.existsSync(path33.join(root, "pnpm-lock.yaml"))) return "pnpm";
4903
+ if (fs27.existsSync(path33.join(root, "package-lock.json"))) return "npm";
4706
4904
  return "unknown";
4707
4905
  }
4708
4906
 
@@ -4710,6 +4908,11 @@ function detectPackageManager(root, packageManagerField) {
4710
4908
  var dep = (value) => ({ kind: "dep", value });
4711
4909
  var depPrefix = (value) => ({ kind: "depPrefix", value });
4712
4910
  var depAbsent = (value) => ({ kind: "depAbsent", value });
4911
+ var depMajor = (value, major) => ({
4912
+ kind: "depMajor",
4913
+ value,
4914
+ major
4915
+ });
4713
4916
  var fileEndsWith = (value) => ({
4714
4917
  kind: "file",
4715
4918
  value,
@@ -4749,6 +4952,8 @@ function matchSignal(sig, ctx) {
4749
4952
  return false;
4750
4953
  case "depAbsent":
4751
4954
  return !ctx.deps.has(sig.value);
4955
+ case "depMajor":
4956
+ return ctx.deps.has(sig.value) && resolveMajorVersion(ctx.depVersions.get(sig.value) ?? "") === sig.major;
4752
4957
  case "content":
4753
4958
  return ctx.contentBlob.includes(sig.value);
4754
4959
  case "file":
@@ -4800,9 +5005,9 @@ var ROLE_RULES = [
4800
5005
  ];
4801
5006
  var STACK_RULES = [
4802
5007
  { stack: ["frontend", "nextjs"], any: [dep("next")] },
4803
- { stack: ["frontend", "react19"], any: [dep("react")] },
5008
+ { stack: ["frontend", "react19"], any: [depMajor("react", 19)] },
4804
5009
  { stack: ["frontend", "vue"], any: [dep("vue")] },
4805
- { stack: ["frontend", "vite8"], any: [dep("vite")] },
5010
+ { stack: ["frontend", "vite8"], any: [depMajor("vite", 8)] },
4806
5011
  { stack: ["frontend", "react-router-v7"], all: [dep("react-router"), dep("@react-router/node")] },
4807
5012
  {
4808
5013
  stack: ["frontend", "tailwindcss"],
@@ -4812,7 +5017,7 @@ var STACK_RULES = [
4812
5017
  stack: ["frontend", "shadcn"],
4813
5018
  all: [fileEndsWith("components.json"), dep("class-variance-authority")]
4814
5019
  },
4815
- { stack: ["tooling", "typescript5"], any: [dep("typescript")] },
5020
+ { stack: ["tooling", "typescript5"], any: [depMajor("typescript", 5)] },
4816
5021
  { stack: ["backend", "sanity"], any: [dep("sanity"), dep("next-sanity"), dep("@sanity/client")] },
4817
5022
  { stack: ["backend", "strapi"], any: [dep("@strapi/strapi"), depPrefix("@strapi/")] },
4818
5023
  { stack: ["backend", "prisma"], any: [dep("prisma"), dep("@prisma/client")] },
@@ -4837,9 +5042,15 @@ var STACK_RULES = [
4837
5042
  stack: ["databases", "supabase"],
4838
5043
  any: [dep("@supabase/supabase-js"), depPrefix("@supabase/")]
4839
5044
  },
4840
- { stack: ["backend", "vendure3"], any: [dep("@vendure/core")] },
5045
+ { stack: ["backend", "vendure3"], any: [depMajor("@vendure/core", 3)] },
4841
5046
  { stack: ["backend", "nestjs"], any: [dep("@nestjs/core")] },
4842
5047
  { stack: ["backend", "nestjs"], any: [content("NestFactory")] },
5048
+ // Deliberately NOT major-gated: this rule catches vendure *plugin* repos that use
5049
+ // the @VendurePlugin decorator without depending on @vendure/core directly (a
5050
+ // peer/transitive dep, common in a plugin package). depMajor requires the name to
5051
+ // be a direct dependency (see matchSignal's depMajor case), which this repo shape
5052
+ // doesn't satisfy — falling back to content detection is the only signal available,
5053
+ // so it stays presence/content-based rather than gated on a major we can't read.
4843
5054
  { stack: ["backend", "vendure3"], any: [content("@VendurePlugin")] },
4844
5055
  { stack: ["backend", "graphql"], any: [dep("graphql"), dep("@nestjs/graphql")] },
4845
5056
  {
@@ -4912,7 +5123,7 @@ function runDetection(ctx, rules = STACK_RULES) {
4912
5123
  }
4913
5124
 
4914
5125
  // src/scanner/detection.ts
4915
- import path33 from "node:path";
5126
+ import path34 from "node:path";
4916
5127
  var UNSUPPORTED_MARKERS = {
4917
5128
  "requirements.txt": "python",
4918
5129
  "pyproject.toml": "python",
@@ -4941,17 +5152,21 @@ function describeUnknownDetection(unsupportedSignals, isLinkedWorktree) {
4941
5152
  function blocked(rel) {
4942
5153
  return SENSITIVE_PATH_REGEXES.some((x) => x.test(rel));
4943
5154
  }
4944
- function dependencySet(pkg, composer) {
4945
- const depNames = /* @__PURE__ */ new Set();
5155
+ function dependencyVersionMap(pkg, composer) {
5156
+ const versions = /* @__PURE__ */ new Map();
4946
5157
  const pushObj = (obj) => {
4947
- if (!isRecord(obj)) return;
4948
- for (const key of Object.keys(obj)) depNames.add(key);
5158
+ if (!isRecord2(obj)) return;
5159
+ for (const key of Object.keys(obj)) {
5160
+ if (versions.has(key)) continue;
5161
+ const value = obj[key];
5162
+ versions.set(key, typeof value === "string" ? value : "");
5163
+ }
4949
5164
  };
4950
5165
  pushObj(pkg?.dependencies);
4951
5166
  pushObj(pkg?.devDependencies);
4952
5167
  pushObj(composer?.require);
4953
5168
  pushObj(composer?.["require-dev"]);
4954
- return [...depNames].sort();
5169
+ return versions;
4955
5170
  }
4956
5171
  function finalizeRoles(registryRoles, deps, files) {
4957
5172
  const roles = new Set(registryRoles);
@@ -4972,14 +5187,14 @@ function finalizeRoles(registryRoles, deps, files) {
4972
5187
  function collectUnsupportedSignals(files) {
4973
5188
  return [
4974
5189
  ...new Set(
4975
- files.map((f) => UNSUPPORTED_MARKERS[path33.basename(f)]).filter((s) => Boolean(s))
5190
+ files.map((f) => UNSUPPORTED_MARKERS[path34.basename(f)]).filter((s) => Boolean(s))
4976
5191
  )
4977
5192
  ].sort();
4978
5193
  }
4979
5194
 
4980
5195
  // src/scanner/render.ts
4981
5196
  import { readFile } from "node:fs/promises";
4982
- import path34 from "node:path";
5197
+ import path35 from "node:path";
4983
5198
  async function buildContentBlob(root, files) {
4984
5199
  const candidates = files.filter(
4985
5200
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
@@ -4987,7 +5202,7 @@ async function buildContentBlob(root, files) {
4987
5202
  const slice = candidates.slice(0, 300);
4988
5203
  const parts = await mapWithConcurrency(slice, async (rel) => {
4989
5204
  try {
4990
- return await readFile(path34.join(root, rel), "utf8");
5205
+ return await readFile(path35.join(root, rel), "utf8");
4991
5206
  } catch {
4992
5207
  return "";
4993
5208
  }
@@ -5079,14 +5294,20 @@ var SAFE_FILES = [
5079
5294
  "Gemfile"
5080
5295
  ];
5081
5296
  async function scanProject(root) {
5082
- const pkg = await readJson(path35.join(root, "package.json"));
5083
- const composer = await readJson(path35.join(root, "composer.json"));
5297
+ const pkg = await readJson(path36.join(root, "package.json"));
5298
+ const composer = await readJson(path36.join(root, "composer.json"));
5084
5299
  const files = await listFiles(root, SAFE_FILES);
5085
5300
  const safeFiles = files.filter((f) => !blocked(f));
5086
- const deps = dependencySet(pkg, composer);
5301
+ const depVersions = dependencyVersionMap(pkg, composer);
5302
+ const deps = [...depVersions.keys()].sort();
5087
5303
  const packageManager = detectPackageManager(root, String(pkg?.packageManager ?? ""));
5088
5304
  const contentBlob = await buildContentBlob(root, safeFiles);
5089
- const detection = runDetection({ deps: new Set(deps), files: safeFiles, contentBlob });
5305
+ const detection = runDetection({
5306
+ deps: new Set(deps),
5307
+ depVersions,
5308
+ files: safeFiles,
5309
+ contentBlob
5310
+ });
5090
5311
  const roles = finalizeRoles(detection.roles, deps, safeFiles);
5091
5312
  const stacks = detection.stacks;
5092
5313
  if (packageManager === "yarn") stacks.packageManagers.push("yarn4");
@@ -5094,7 +5315,7 @@ async function scanProject(root) {
5094
5315
  const warnings = [];
5095
5316
  const securityRisks = [];
5096
5317
  const crossRepoHints = [];
5097
- const nodeEngine = isRecord(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
5318
+ const nodeEngine = isRecord2(pkg?.engines) ? String(pkg.engines.node ?? "") : "";
5098
5319
  if (nodeEngine && !satisfiesVersion(process.version, nodeEngine)) {
5099
5320
  warnings.push(`Current Node ${process.version} does not satisfy package engine ${nodeEngine}`);
5100
5321
  }
@@ -5104,7 +5325,7 @@ async function scanProject(root) {
5104
5325
  crossRepoHints.push("Monorepo orchestration detected");
5105
5326
  const unsupportedSignals = collectUnsupportedSignals(safeFiles);
5106
5327
  const detectionStatus = computeDetectionStatus(roles, stacks, unsupportedSignals);
5107
- const repoNameFallback = pkg?.name ? String(pkg.name) : path35.basename((await resolveRoots(root)).mainRoot);
5328
+ const repoNameFallback = pkg?.name ? String(pkg.name) : path36.basename((await resolveRoots(root)).mainRoot);
5108
5329
  const context = {
5109
5330
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5110
5331
  repoName: repoNameFallback,
@@ -5126,9 +5347,9 @@ async function scanProject(root) {
5126
5347
  // src/scanner/read-context.ts
5127
5348
  async function isCacheFresh(root, cachePath) {
5128
5349
  const markerPaths = [
5129
- path36.join(root, "package.json"),
5130
- path36.join(root, WORKSPACE_FILE),
5131
- path36.join(root, REPOS_MANIFEST_FILE)
5350
+ path37.join(root, "package.json"),
5351
+ path37.join(root, WORKSPACE_FILE),
5352
+ path37.join(root, REPOS_MANIFEST_FILE)
5132
5353
  ];
5133
5354
  const cacheStat = await fs28.stat(cachePath);
5134
5355
  for (const markerPath of markerPaths) {
@@ -5234,7 +5455,7 @@ async function runDoctor(options) {
5234
5455
  } else {
5235
5456
  ok(`- HOOKS OK: ${hooks.message}`);
5236
5457
  }
5237
- const rootClaudeMdPath = path37.join(root, "CLAUDE.md");
5458
+ const rootClaudeMdPath = path38.join(root, "CLAUDE.md");
5238
5459
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
5239
5460
  if (!rootClaudeMdContent) {
5240
5461
  flag(
@@ -5288,8 +5509,8 @@ async function runDoctor(options) {
5288
5509
  } else {
5289
5510
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
5290
5511
  const bodyContent = workflowContent.slice(firstLine.length + 1);
5291
- const cachePath = path37.join(getCacheDir(), "templates/agentic-workflow-standard.md");
5292
- const bundledPath = path37.join(
5512
+ const cachePath = path38.join(getCacheDir(), "templates/agentic-workflow-standard.md");
5513
+ const bundledPath = path38.join(
5293
5514
  packageRoot(),
5294
5515
  "library",
5295
5516
  "global",
@@ -5338,7 +5559,7 @@ async function runDoctor(options) {
5338
5559
  }
5339
5560
  }
5340
5561
  if (workflowExists) {
5341
- const prettierIgnore = await readText(path37.join(root, ".prettierignore")) ?? "";
5562
+ const prettierIgnore = await readText(path38.join(root, ".prettierignore")) ?? "";
5342
5563
  const lines = prettierIgnore.split("\n");
5343
5564
  const missing = IGNORED_PATHS.filter((p) => !prettierIgnoreProtects(lines, p));
5344
5565
  if (missing.length > 0) {
@@ -5411,7 +5632,7 @@ async function runDoctor(options) {
5411
5632
  );
5412
5633
  }
5413
5634
  }
5414
- const pkgJson = await readJson(path37.join(packageRoot(), "package.json"));
5635
+ const pkgJson = await readJson(path38.join(packageRoot(), "package.json"));
5415
5636
  const currentVersion = pkgJson?.version ?? "0.0.0";
5416
5637
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
5417
5638
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -5444,7 +5665,7 @@ async function runDoctor(options) {
5444
5665
  }
5445
5666
 
5446
5667
  // src/commands/update.ts
5447
- import path40 from "node:path";
5668
+ import path41 from "node:path";
5448
5669
 
5449
5670
  // src/claude/refresh-project.ts
5450
5671
  import fs30 from "fs-extra";
@@ -5461,7 +5682,7 @@ async function refreshProjectApply(root) {
5461
5682
 
5462
5683
  // src/install/apply.ts
5463
5684
  import crypto2 from "node:crypto";
5464
- import path38 from "node:path";
5685
+ import path39 from "node:path";
5465
5686
  import fs31 from "fs-extra";
5466
5687
 
5467
5688
  // src/install/header.ts
@@ -5548,7 +5769,7 @@ function hashContent(content2) {
5548
5769
  }
5549
5770
  function sourceVersion() {
5550
5771
  try {
5551
- const pkgPath = path38.join(packageRoot(), "package.json");
5772
+ const pkgPath = path39.join(packageRoot(), "package.json");
5552
5773
  const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf8"));
5553
5774
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
5554
5775
  } catch {
@@ -5556,49 +5777,49 @@ function sourceVersion() {
5556
5777
  }
5557
5778
  }
5558
5779
  function globalSrcDir() {
5559
- return path38.join(packageRoot(), "library", "global");
5780
+ return path39.join(packageRoot(), "library", "global");
5560
5781
  }
5561
5782
  function collectSkillAuxFiles(skillsDir, claudeDir, skillName) {
5562
- const skillDir = path38.join(skillsDir, skillName);
5783
+ const skillDir = path39.join(skillsDir, skillName);
5563
5784
  const entries = [];
5564
5785
  for (const rel of fs31.readdirSync(skillDir, { recursive: true })) {
5565
5786
  if (rel === "SKILL.md") continue;
5566
- const abs = path38.join(skillDir, rel);
5787
+ const abs = path39.join(skillDir, rel);
5567
5788
  if (!fs31.statSync(abs).isFile()) continue;
5568
- const posixRel = rel.split(path38.sep).join("/");
5789
+ const posixRel = rel.split(path39.sep).join("/");
5569
5790
  entries.push({
5570
5791
  stableId: `skill.${skillName}.${posixRel.replace(/\//g, ".")}`,
5571
- srcRelPath: path38.join("library", "global", "skills", skillName, rel),
5572
- destPath: path38.join(claudeDir, "skills", skillName, rel)
5792
+ srcRelPath: path39.join("library", "global", "skills", skillName, rel),
5793
+ destPath: path39.join(claudeDir, "skills", skillName, rel)
5573
5794
  });
5574
5795
  }
5575
5796
  return entries;
5576
5797
  }
5577
5798
  function collectSourceFiles(srcDir, claudeDir) {
5578
5799
  const entries = [];
5579
- const skillsDir = path38.join(srcDir, "skills");
5800
+ const skillsDir = path39.join(srcDir, "skills");
5580
5801
  if (fs31.pathExistsSync(skillsDir)) {
5581
5802
  for (const skillName of fs31.readdirSync(skillsDir)) {
5582
- const skillFile = path38.join(skillsDir, skillName, "SKILL.md");
5803
+ const skillFile = path39.join(skillsDir, skillName, "SKILL.md");
5583
5804
  if (fs31.pathExistsSync(skillFile)) {
5584
5805
  entries.push({
5585
5806
  stableId: `skill.${skillName}`,
5586
- srcRelPath: path38.join("library", "global", "skills", skillName, "SKILL.md"),
5587
- destPath: path38.join(claudeDir, "skills", skillName, "SKILL.md")
5807
+ srcRelPath: path39.join("library", "global", "skills", skillName, "SKILL.md"),
5808
+ destPath: path39.join(claudeDir, "skills", skillName, "SKILL.md")
5588
5809
  });
5589
5810
  entries.push(...collectSkillAuxFiles(skillsDir, claudeDir, skillName));
5590
5811
  }
5591
5812
  }
5592
5813
  }
5593
- const commandsDir = path38.join(srcDir, "commands");
5814
+ const commandsDir = path39.join(srcDir, "commands");
5594
5815
  if (fs31.pathExistsSync(commandsDir)) {
5595
5816
  for (const fileName of fs31.readdirSync(commandsDir)) {
5596
5817
  if (!fileName.endsWith(".md")) continue;
5597
5818
  const commandName = fileName.slice(0, -".md".length);
5598
5819
  entries.push({
5599
5820
  stableId: `command.${commandName}`,
5600
- srcRelPath: path38.join("library", "global", "commands", fileName),
5601
- destPath: path38.join(claudeDir, "commands", fileName)
5821
+ srcRelPath: path39.join("library", "global", "commands", fileName),
5822
+ destPath: path39.join(claudeDir, "commands", fileName)
5602
5823
  });
5603
5824
  }
5604
5825
  }
@@ -5624,7 +5845,7 @@ async function applyInstall(options = {}) {
5624
5845
  };
5625
5846
  const manifestFiles = [];
5626
5847
  for (const entry of sourceFiles) {
5627
- const srcPath = path38.join(packageRoot(), entry.srcRelPath);
5848
+ const srcPath = path39.join(packageRoot(), entry.srcRelPath);
5628
5849
  const rawContent = await readText(srcPath);
5629
5850
  if (rawContent === void 0) {
5630
5851
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -5684,7 +5905,7 @@ async function applyInstall(options = {}) {
5684
5905
  }
5685
5906
  let mergedSettings;
5686
5907
  if (!check) {
5687
- const fragmentPath = path38.join(srcDir, "settings-fragments", "hooks.json");
5908
+ const fragmentPath = path39.join(srcDir, "settings-fragments", "hooks.json");
5688
5909
  const fragments = await loadHooksFragment(fragmentPath);
5689
5910
  const settings = await readSettings();
5690
5911
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -5773,7 +5994,7 @@ function summarizeLockDiff(before, after) {
5773
5994
 
5774
5995
  // src/update/lockfile.ts
5775
5996
  import { mkdir, readFile as readFile2, copyFile } from "node:fs/promises";
5776
- import path39 from "node:path";
5997
+ import path40 from "node:path";
5777
5998
  async function readLockSummary(root) {
5778
5999
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
5779
6000
  const catalogRef = lock.find((item) => item.catalogRef)?.catalogRef ?? null;
@@ -5809,7 +6030,7 @@ async function applyLock(root) {
5809
6030
  try {
5810
6031
  const backupDir = hausPath(root, "backups");
5811
6032
  await mkdir(backupDir, { recursive: true });
5812
- await copyFile(lockPath, path39.join(backupDir, `haus.lock.${Date.now()}.json`));
6033
+ await copyFile(lockPath, path40.join(backupDir, `haus.lock.${Date.now()}.json`));
5813
6034
  } catch {
5814
6035
  }
5815
6036
  const enriched = await Promise.all(
@@ -5831,7 +6052,7 @@ function diffLock(before, after) {
5831
6052
  }
5832
6053
  async function hasLocalOverrides(root) {
5833
6054
  try {
5834
- await readFile2(path39.join(root, ".claude", "settings.json"), "utf8");
6055
+ await readFile2(path40.join(root, ".claude", "settings.json"), "utf8");
5835
6056
  return true;
5836
6057
  } catch {
5837
6058
  return false;
@@ -5847,7 +6068,7 @@ async function runUpdate(options) {
5847
6068
  return;
5848
6069
  }
5849
6070
  if (options.check) {
5850
- const pkgJson2 = await readJson(path40.join(packageRoot(), "package.json"));
6071
+ const pkgJson2 = await readJson(path41.join(packageRoot(), "package.json"));
5851
6072
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
5852
6073
  const [status, npmVersion, latestCatalogTag, globalInstallDrift, catalogItems, lockItems] = await Promise.all([
5853
6074
  options.fast ? fastLockStatus(root) : checkLock(root),
@@ -5882,7 +6103,7 @@ async function runUpdate(options) {
5882
6103
  if (hasHashDrift || formerIdMigrations.length > 0) process.exitCode = 1;
5883
6104
  return;
5884
6105
  }
5885
- const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
6106
+ const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
5886
6107
  const currentVersion = pkgJson?.version ?? "0.0.0";
5887
6108
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
5888
6109
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -5924,7 +6145,7 @@ async function runFromHookCheck(root) {
5924
6145
  try {
5925
6146
  const summary = await readLockSummary(root);
5926
6147
  if (summary.count === 0) return;
5927
- const pkgJson = await readJson(path40.join(packageRoot(), "package.json"));
6148
+ const pkgJson = await readJson(path41.join(packageRoot(), "package.json"));
5928
6149
  const currentVersion = pkgJson?.version ?? "0.0.0";
5929
6150
  const [npmVersion, latestCatalogTag] = await Promise.all([
5930
6151
  fetchNpmVersionStatus(currentVersion),
@@ -6074,7 +6295,7 @@ async function runCiGate(options = {}) {
6074
6295
  // src/commands/clone.ts
6075
6296
  init_exec();
6076
6297
  import { existsSync as existsSync2 } from "node:fs";
6077
- import path41 from "node:path";
6298
+ import path42 from "node:path";
6078
6299
  var GIT_LOCATION_VARS = [
6079
6300
  "GIT_DIR",
6080
6301
  "GIT_WORK_TREE",
@@ -6124,11 +6345,11 @@ async function runClone(url, opts = {}) {
6124
6345
  process.exitCode = 1;
6125
6346
  return;
6126
6347
  }
6127
- const target = path41.resolve(opts.dir?.trim() || repoNameFromUrl(url));
6348
+ const target = path42.resolve(opts.dir?.trim() || repoNameFromUrl(url));
6128
6349
  if (existsSync2(target)) {
6129
6350
  const check = await checkExistingTarget(target, url);
6130
6351
  if (check.kind === "matches") {
6131
- log(`\u2022 ${path41.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
6352
+ log(`\u2022 ${path42.basename(target)} already cloned here, matches ${url} \u2014 skipped`);
6132
6353
  return;
6133
6354
  }
6134
6355
  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 +6389,7 @@ function readHookPayload() {
6168
6389
  if (!raw) return { payload: {}, malformed: false };
6169
6390
  try {
6170
6391
  const parsed = JSON.parse(raw);
6171
- return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
6392
+ return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
6172
6393
  } catch {
6173
6394
  return { payload: {}, malformed: true };
6174
6395
  }
@@ -6207,7 +6428,7 @@ function readPayload(options) {
6207
6428
  if (options.stdinPayload != null) {
6208
6429
  try {
6209
6430
  const parsed = JSON.parse(options.stdinPayload);
6210
- return { payload: isRecord(parsed) ? parsed : {}, malformed: false };
6431
+ return { payload: isRecord2(parsed) ? parsed : {}, malformed: false };
6211
6432
  } catch {
6212
6433
  return { payload: {}, malformed: true };
6213
6434
  }
@@ -6220,7 +6441,7 @@ async function runDecisionsGuard(options = {}) {
6220
6441
  emitPreToolUseDeny("Malformed hook payload");
6221
6442
  return;
6222
6443
  }
6223
- const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
6444
+ const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
6224
6445
  const command = String(toolInput.command ?? "");
6225
6446
  if (!PR_CREATE_PATTERN.test(command)) return;
6226
6447
  const root = process.cwd();
@@ -6232,13 +6453,13 @@ async function runDecisionsGuard(options = {}) {
6232
6453
  const result = await runDecisionsCheck(root, { range, commitMessages });
6233
6454
  if (result.triggered && !result.satisfied) {
6234
6455
  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.`
6456
+ `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
6457
  );
6237
6458
  }
6238
6459
  }
6239
6460
 
6240
6461
  // src/commands/decisions.ts
6241
- import path42 from "node:path";
6462
+ import path43 from "node:path";
6242
6463
 
6243
6464
  // src/decisions/next-number.ts
6244
6465
  async function nextDecisionNumber(root) {
@@ -6281,10 +6502,9 @@ _(Why this over alternatives \u2014 the line agents read for "why".)_
6281
6502
  `;
6282
6503
  }
6283
6504
  async function runDecisionsSuggest(root, opts = {}) {
6284
- const stats = await collectDiffStats(root, { staged: true });
6285
6505
  const stagedCheck = await runDecisionsCheck(root, { staged: true });
6286
- const unstagedStats = await collectDiffStats(root, {});
6287
- const triggered = isDecisionTriggered(stats) || isDecisionTriggered(unstagedStats);
6506
+ const unstagedCheck = await runDecisionsCheck(root, {});
6507
+ const triggered = stagedCheck.triggered || unstagedCheck.triggered;
6288
6508
  if (!triggered) return;
6289
6509
  if (stagedCheck.triggered && stagedCheck.satisfied) return;
6290
6510
  const number = await nextDecisionNumber(root);
@@ -6335,7 +6555,7 @@ async function runDecisions(sub, arg, options = {}) {
6335
6555
  }
6336
6556
  for (const reason of result.reasons) warn(reason);
6337
6557
  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."
6558
+ `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
6559
  );
6340
6560
  process.exitCode = 1;
6341
6561
  return;
@@ -6349,7 +6569,7 @@ async function runDecisions(sub, arg, options = {}) {
6349
6569
  return;
6350
6570
  }
6351
6571
  if (sub === "validate") {
6352
- const target = arg ? path42.resolve(root, arg) : void 0;
6572
+ const target = arg ? path43.resolve(root, arg) : void 0;
6353
6573
  if (!target) {
6354
6574
  error("usage: haus decisions validate <path-to-decision.md>");
6355
6575
  process.exitCode = 1;
@@ -6601,7 +6821,7 @@ async function runGuard(kind, _options) {
6601
6821
  emitPreToolUseDeny("Malformed hook payload");
6602
6822
  return;
6603
6823
  }
6604
- const toolInput = isRecord(payload.tool_input) ? payload.tool_input : {};
6824
+ const toolInput = isRecord2(payload.tool_input) ? payload.tool_input : {};
6605
6825
  if (kind === "file-access") {
6606
6826
  const candidate = String(toolInput.path ?? toolInput.file_path ?? "");
6607
6827
  const reason2 = guardFileAccess(candidate);
@@ -6618,7 +6838,7 @@ async function runGuard(kind, _options) {
6618
6838
  }
6619
6839
 
6620
6840
  // src/commands/init.ts
6621
- import path44 from "node:path";
6841
+ import path45 from "node:path";
6622
6842
  import fs32 from "fs-extra";
6623
6843
 
6624
6844
  // src/recommender/git-signal.ts
@@ -7086,7 +7306,7 @@ function buildOptInEligible(items, skipped) {
7086
7306
  }
7087
7307
 
7088
7308
  // src/commands/workspace/discover.ts
7089
- import path43 from "node:path";
7309
+ import path44 from "node:path";
7090
7310
  import fg4 from "fast-glob";
7091
7311
  import YAML2 from "yaml";
7092
7312
  var DEFAULT_MAX_DEPTH = 3;
@@ -7127,10 +7347,10 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7127
7347
  const gitDirs = /* @__PURE__ */ new Set();
7128
7348
  const manifestDirs = /* @__PURE__ */ new Set();
7129
7349
  for (const match of gitMatches) {
7130
- gitDirs.add(path43.posix.dirname(match));
7350
+ gitDirs.add(path44.posix.dirname(match));
7131
7351
  }
7132
7352
  for (const match of manifestMatches) {
7133
- manifestDirs.add(path43.posix.dirname(match));
7353
+ manifestDirs.add(path44.posix.dirname(match));
7134
7354
  }
7135
7355
  const repoRoots = [...gitDirs];
7136
7356
  const manifestSorted = [...manifestDirs].sort(
@@ -7145,18 +7365,18 @@ async function findRepoRoots(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7145
7365
  return repoRoots;
7146
7366
  }
7147
7367
  async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
7148
- const repoRoots = await findRepoRoots(workspaceRoot, maxDepth);
7368
+ const repoRoots = (await findRepoRoots(workspaceRoot, maxDepth)).filter((dir) => dir !== ".");
7149
7369
  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);
7370
+ const absDir = path44.resolve(workspaceRoot, relDir);
7371
+ const pkg = await readJson(path44.join(absDir, "package.json"));
7372
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path44.basename(absDir);
7153
7373
  let role = "auto";
7154
7374
  try {
7155
7375
  const scan = await scanProject(absDir);
7156
7376
  if (scan.repoRoles.length > 0) role = scan.repoRoles.join("+");
7157
7377
  } catch {
7158
7378
  }
7159
- return { name, path: relDir === "." ? "." : relDir, role };
7379
+ return { name, path: relDir, role };
7160
7380
  });
7161
7381
  }
7162
7382
  function mergeWorkspaceConfig(existing, discovered, opts = {}) {
@@ -7192,7 +7412,7 @@ function renderWorkspaceYaml(config) {
7192
7412
  });
7193
7413
  }
7194
7414
  async function runDiscover(workspaceRoot, opts = {}) {
7195
- const yamlPath = path43.join(workspaceRoot, "haus.workspace.yaml");
7415
+ const yamlPath = path44.join(workspaceRoot, "haus.workspace.yaml");
7196
7416
  const existingText = await readText(yamlPath);
7197
7417
  const existing = parseWorkspaceConfig(existingText);
7198
7418
  if (existingText && !existing) {
@@ -7364,7 +7584,7 @@ async function runSetupProject(options) {
7364
7584
  // src/commands/init.ts
7365
7585
  async function runInit(options) {
7366
7586
  const root = process.cwd();
7367
- const hausDir = path44.join(root, ".haus-workflow");
7587
+ const hausDir = path45.join(root, ".haus-workflow");
7368
7588
  const alreadyInit = await fs32.pathExists(hausDir);
7369
7589
  if (alreadyInit) {
7370
7590
  log("Haus AI already initialized in this project.");
@@ -7500,7 +7720,7 @@ async function runRefresh() {
7500
7720
  }
7501
7721
 
7502
7722
  // src/install/scaffold.ts
7503
- import path45 from "node:path";
7723
+ import path46 from "node:path";
7504
7724
  import fs33 from "fs-extra";
7505
7725
  async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7506
7726
  const scaffolded = [];
@@ -7517,9 +7737,9 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7517
7737
  const realRoot = await fs33.realpath(catalogRoot).catch(() => null);
7518
7738
  for (const item of items) {
7519
7739
  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 === "..");
7740
+ const sourcePath = path46.resolve(catalogRoot, item.path);
7741
+ const rel = path46.relative(catalogRoot, sourcePath);
7742
+ const escapes = rel === "" || path46.isAbsolute(rel) || rel.split(/[/\\]/).some((seg) => seg === "..");
7523
7743
  if (escapes) {
7524
7744
  warn(`Skipping ${item.id}: path '${item.path}' escapes the catalog root`);
7525
7745
  continue;
@@ -7529,8 +7749,8 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7529
7749
  warn(`Skipping ${item.id}: source not found at ${sourcePath}`);
7530
7750
  continue;
7531
7751
  }
7532
- const realRel = path45.relative(realRoot, realSource);
7533
- if (realRel !== "" && (path45.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
7752
+ const realRel = path46.relative(realRoot, realSource);
7753
+ if (realRel !== "" && (path46.isAbsolute(realRel) || realRel.split(/[/\\]/)[0] === "..")) {
7534
7754
  warn(`Skipping ${item.id}: path '${item.path}' resolves outside the catalog root`);
7535
7755
  continue;
7536
7756
  }
@@ -7544,14 +7764,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7544
7764
  continue;
7545
7765
  }
7546
7766
  if (stat.isFile()) {
7547
- const filename = path45.basename(sourcePath);
7548
- const dest = path45.join(projectRoot, filename);
7767
+ const filename = path46.basename(sourcePath);
7768
+ const dest = path46.join(projectRoot, filename);
7549
7769
  record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
7550
7770
  } else if (stat.isDirectory()) {
7551
7771
  const entries = await fs33.readdir(sourcePath);
7552
7772
  for (const entry of entries) {
7553
- const src = path45.join(sourcePath, entry);
7554
- const dest = path45.join(projectRoot, entry);
7773
+ const src = path46.join(sourcePath, entry);
7774
+ const dest = path46.join(projectRoot, entry);
7555
7775
  record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
7556
7776
  }
7557
7777
  }
@@ -7559,14 +7779,14 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
7559
7779
  return { scaffolded, skipped, skippedExisting };
7560
7780
  }
7561
7781
  async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
7562
- const destDisplay = path45.relative(projectRoot, dest) || path45.basename(dest);
7782
+ const destDisplay = path46.relative(projectRoot, dest) || path46.basename(dest);
7563
7783
  const srcStat = await fs33.lstat(src).catch(() => null);
7564
7784
  if (!srcStat) {
7565
- warn(`Skipping ${path45.basename(src)}: source not found`);
7785
+ warn(`Skipping ${path46.basename(src)}: source not found`);
7566
7786
  return "skipped-refused";
7567
7787
  }
7568
7788
  if (srcStat.isSymbolicLink()) {
7569
- warn(`Skipping ${path45.basename(src)}: source is a symlink`);
7789
+ warn(`Skipping ${path46.basename(src)}: source is a symlink`);
7570
7790
  return "skipped-refused";
7571
7791
  }
7572
7792
  const exists = await fs33.pathExists(dest);
@@ -7575,13 +7795,13 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
7575
7795
  return "skipped-exists";
7576
7796
  }
7577
7797
  if (opts.dryRun) {
7578
- log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path45.basename(dest)} (${itemId})`);
7798
+ log(`[dry-run] would ${exists ? "overwrite" : "create"} ${path46.basename(dest)} (${itemId})`);
7579
7799
  return "scaffolded";
7580
7800
  }
7581
7801
  if (exists && opts.force) {
7582
7802
  await fs33.remove(dest);
7583
7803
  }
7584
- await fs33.ensureDir(path45.dirname(dest));
7804
+ await fs33.ensureDir(path46.dirname(dest));
7585
7805
  await fs33.copy(src, dest, {
7586
7806
  overwrite: true,
7587
7807
  dereference: false,
@@ -7594,7 +7814,7 @@ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
7594
7814
  }
7595
7815
  }
7596
7816
  });
7597
- log(`${exists ? "Overwrote" : "Created"} ${path45.basename(dest)} (${itemId})`);
7817
+ log(`${exists ? "Overwrote" : "Created"} ${path46.basename(dest)} (${itemId})`);
7598
7818
  return "scaffolded";
7599
7819
  }
7600
7820
 
@@ -7659,7 +7879,7 @@ async function runScan(options) {
7659
7879
  }
7660
7880
 
7661
7881
  // src/commands/undo.ts
7662
- import path46 from "node:path";
7882
+ import path47 from "node:path";
7663
7883
  import fs34 from "fs-extra";
7664
7884
 
7665
7885
  // src/claude/managed-paths.ts
@@ -7687,18 +7907,18 @@ async function collectLockTrackedPaths(root) {
7687
7907
  const relPaths = row.paths ?? [];
7688
7908
  const existingRel = [];
7689
7909
  for (const rel of relPaths) {
7690
- if (await fs34.pathExists(path46.resolve(root, rel))) existingRel.push(rel);
7910
+ if (await fs34.pathExists(path47.resolve(root, rel))) existingRel.push(rel);
7691
7911
  }
7692
7912
  if (existingRel.length === 0) continue;
7693
7913
  if (row.hash === void 0) {
7694
- removable.push(...existingRel.map((rel) => path46.resolve(root, rel)));
7914
+ removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
7695
7915
  continue;
7696
7916
  }
7697
7917
  const currentHash = await hashInstalledPaths(root, existingRel);
7698
7918
  if (currentHash === row.hash) {
7699
- removable.push(...existingRel.map((rel) => path46.resolve(root, rel)));
7919
+ removable.push(...existingRel.map((rel) => path47.resolve(root, rel)));
7700
7920
  } else {
7701
- preserved.push(...existingRel.map((rel) => path46.resolve(root, rel)));
7921
+ preserved.push(...existingRel.map((rel) => path47.resolve(root, rel)));
7702
7922
  log(`Preserving locally-modified ${row.id ?? "(unknown item)"}: ${existingRel.join(", ")}`);
7703
7923
  }
7704
7924
  }
@@ -7711,7 +7931,7 @@ async function settingsHasHausContent(root) {
7711
7931
  return settings._haus != null;
7712
7932
  }
7713
7933
  async function claudeMdHasHausBlock(root) {
7714
- const filePath = path46.join(root, "CLAUDE.md");
7934
+ const filePath = path47.join(root, "CLAUDE.md");
7715
7935
  if (!await fs34.pathExists(filePath)) return false;
7716
7936
  const text = await fs34.readFile(filePath, "utf8");
7717
7937
  return text.includes(BLOCK_BEGIN);
@@ -7724,15 +7944,15 @@ async function stripProjectSettings(root) {
7724
7944
  const hasContent = Object.keys(settings).length > 0;
7725
7945
  if (hasContent) {
7726
7946
  await writeProjectSettings(root, settings);
7727
- log(`Stripped haus rules from ${path46.relative(root, settingsPath)} (user settings preserved).`);
7947
+ log(`Stripped haus rules from ${path47.relative(root, settingsPath)} (user settings preserved).`);
7728
7948
  return true;
7729
7949
  }
7730
7950
  await fs34.remove(settingsPath);
7731
- log(`Removed ${path46.relative(root, settingsPath)} (no user-owned settings remained).`);
7951
+ log(`Removed ${path47.relative(root, settingsPath)} (no user-owned settings remained).`);
7732
7952
  return true;
7733
7953
  }
7734
7954
  async function stripRootClaudeMd(root) {
7735
- const filePath = path46.join(root, "CLAUDE.md");
7955
+ const filePath = path47.join(root, "CLAUDE.md");
7736
7956
  if (!await fs34.pathExists(filePath)) return false;
7737
7957
  const prev = await fs34.readFile(filePath, "utf8");
7738
7958
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -7760,12 +7980,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
7760
7980
  const backupRoot = hausPath(root, "backups", `undo-${stamp}`);
7761
7981
  for (const abs of managedAbsPaths) {
7762
7982
  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));
7983
+ const rel = path47.relative(root, abs);
7984
+ const backupPath = path47.join(backupRoot, rel);
7985
+ await fs34.ensureDir(path47.dirname(backupPath));
7766
7986
  await fs34.copy(abs, backupPath);
7767
7987
  }
7768
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path46.relative(root, backupRoot)}.`);
7988
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path47.relative(root, backupRoot)}.`);
7769
7989
  }
7770
7990
  async function runUndo(options) {
7771
7991
  const root = options.root ?? process.cwd();
@@ -7778,7 +7998,7 @@ async function runUndo(options) {
7778
7998
  log("Nothing to remove: no haus-managed files found in this directory.");
7779
7999
  return;
7780
8000
  }
7781
- const relTargets = managed.map((p) => path46.relative(root, p));
8001
+ const relTargets = managed.map((p) => path47.relative(root, p));
7782
8002
  const summaryParts = [...relTargets];
7783
8003
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
7784
8004
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -7802,7 +8022,7 @@ User-owned .claude/ files will be preserved.`
7802
8022
  for (const abs of managed) {
7803
8023
  if (!await fs34.pathExists(abs)) continue;
7804
8024
  await fs34.remove(abs);
7805
- log(`Removed ${path46.relative(root, abs)}`);
8025
+ log(`Removed ${path47.relative(root, abs)}`);
7806
8026
  }
7807
8027
  if (stripSettings) await stripProjectSettings(root);
7808
8028
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -7813,7 +8033,7 @@ User-owned .claude/ files will be preserved.`
7813
8033
 
7814
8034
  // src/install/uninstall.ts
7815
8035
  import crypto3 from "node:crypto";
7816
- import path47 from "node:path";
8036
+ import path48 from "node:path";
7817
8037
  import fs35 from "fs-extra";
7818
8038
  async function runUninstall(options = {}) {
7819
8039
  const { force = false } = options;
@@ -7843,14 +8063,14 @@ async function runUninstall(options = {}) {
7843
8063
  continue;
7844
8064
  }
7845
8065
  await fs35.remove(entry.destPath);
7846
- await pruneEmptyDir(path47.dirname(entry.destPath));
8066
+ await pruneEmptyDir(path48.dirname(entry.destPath));
7847
8067
  result.deleted.push(entry.destPath);
7848
8068
  }
7849
8069
  const settings = await readSettings();
7850
8070
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
7851
8071
  await writeSettings(stripped);
7852
8072
  result.hooksStripped = true;
7853
- const hausDir = path47.join(globalClaudeDir(), "haus");
8073
+ const hausDir = path48.join(globalClaudeDir(), "haus");
7854
8074
  const manifestPath2 = hausManifestPath();
7855
8075
  if (fs35.pathExistsSync(manifestPath2)) {
7856
8076
  await fs35.remove(manifestPath2);
@@ -7891,12 +8111,13 @@ async function runUninstallCommand(options) {
7891
8111
  }
7892
8112
 
7893
8113
  // src/commands/validate-catalog.ts
7894
- import path49 from "node:path";
8114
+ import path50 from "node:path";
7895
8115
 
7896
8116
  // src/catalog/validate-core.ts
7897
8117
  import fs36 from "node:fs";
7898
- import path48 from "node:path";
8118
+ import path49 from "node:path";
7899
8119
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
8120
+ var ITEM_ID_PATTERN = /^haus\./;
7900
8121
  function auditForbiddenStacks(items) {
7901
8122
  const failures = [];
7902
8123
  for (const item of items) {
@@ -7982,6 +8203,9 @@ function auditManifestStructure(manifestVersion, items) {
7982
8203
  failures.push(`item[${i}]: missing id`);
7983
8204
  continue;
7984
8205
  }
8206
+ if (!ITEM_ID_PATTERN.test(item.id)) {
8207
+ failures.push(`${item.id}: id must start with "haus."`);
8208
+ }
7985
8209
  const provenanceError = validateCuratedProvenance(item);
7986
8210
  if (provenanceError) failures.push(provenanceError);
7987
8211
  const referencesError = validateReferences(item.id, item.references);
@@ -8040,7 +8264,7 @@ function readForAudit(absPath, label, failures) {
8040
8264
  }
8041
8265
  }
8042
8266
  function auditTemplateContent(manifestDir, absPath, itemId) {
8043
- const rel = path48.relative(manifestDir, absPath);
8267
+ const rel = path49.relative(manifestDir, absPath);
8044
8268
  const failures = [];
8045
8269
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
8046
8270
  if (text === null) return failures;
@@ -8062,25 +8286,25 @@ function auditShippedFiles(manifestDir, items) {
8062
8286
  const failures = [];
8063
8287
  for (const item of items) {
8064
8288
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
8065
- const absPath = path48.join(manifestDir, item.path);
8289
+ const absPath = path49.join(manifestDir, item.path);
8066
8290
  if (item.type === "skill") {
8067
- const skillMd = path48.join(absPath, "SKILL.md");
8291
+ const skillMd = path49.join(absPath, "SKILL.md");
8068
8292
  if (!fs36.existsSync(skillMd)) {
8069
- failures.push(`${item.id}: missing ${path48.relative(manifestDir, skillMd)}`);
8293
+ failures.push(`${item.id}: missing ${path49.relative(manifestDir, skillMd)}`);
8070
8294
  continue;
8071
8295
  }
8072
8296
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
8073
8297
  if (text === null) continue;
8074
8298
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
8075
8299
  failures.push(
8076
- ...auditForbiddenTagsInText(text, `${item.id}: ${path48.relative(manifestDir, skillMd)}`)
8300
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path49.relative(manifestDir, skillMd)}`)
8077
8301
  );
8078
8302
  } else if (item.type === "agent") {
8079
8303
  if (!fs36.existsSync(absPath)) {
8080
8304
  failures.push(`${item.id}: missing agent file ${item.path}`);
8081
8305
  continue;
8082
8306
  }
8083
- const rel = path48.relative(manifestDir, absPath);
8307
+ const rel = path49.relative(manifestDir, absPath);
8084
8308
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8085
8309
  if (text === null) continue;
8086
8310
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8096,7 +8320,7 @@ function auditShippedFiles(manifestDir, items) {
8096
8320
  failures.push(`${item.id}: missing command file ${item.path}`);
8097
8321
  continue;
8098
8322
  }
8099
- const rel = path48.relative(manifestDir, absPath);
8323
+ const rel = path49.relative(manifestDir, absPath);
8100
8324
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
8101
8325
  if (text === null) continue;
8102
8326
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -8128,7 +8352,7 @@ function walkAllFiles(dir, fn, failures) {
8128
8352
  }
8129
8353
  for (const entry of entries) {
8130
8354
  if (entry.name === ".DS_Store") continue;
8131
- const full = path48.join(dir, entry.name);
8355
+ const full = path49.join(dir, entry.name);
8132
8356
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
8133
8357
  else fn(full);
8134
8358
  }
@@ -8146,12 +8370,12 @@ function auditDiskOrphans(manifestDir, items) {
8146
8370
  const failures = [];
8147
8371
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
8148
8372
  for (const dir of CONTENT_ROOTS) {
8149
- const abs = path48.join(manifestDir, dir);
8373
+ const abs = path49.join(manifestDir, dir);
8150
8374
  if (!fs36.existsSync(abs)) continue;
8151
8375
  walkAllFiles(
8152
8376
  abs,
8153
8377
  (file) => {
8154
- const relPath = path48.relative(manifestDir, file).replace(/\\/g, "/");
8378
+ const relPath = path49.relative(manifestDir, file).replace(/\\/g, "/");
8155
8379
  if (!isPathClaimed(relPath, claimedPaths)) {
8156
8380
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
8157
8381
  }
@@ -8172,7 +8396,7 @@ function walkMd(dir, fn, failures) {
8172
8396
  return;
8173
8397
  }
8174
8398
  for (const entry of entries) {
8175
- const full = path48.join(dir, entry.name);
8399
+ const full = path49.join(dir, entry.name);
8176
8400
  if (entry.isDirectory()) walkMd(full, fn, failures);
8177
8401
  else if (entry.name.endsWith(".md")) fn(full);
8178
8402
  }
@@ -8181,13 +8405,13 @@ function auditMarkdownContent(manifestDir, items) {
8181
8405
  const failures = [];
8182
8406
  const pathSourceMap = buildItemPathSourceMap(items);
8183
8407
  for (const dir of ["skills", "agents", "templates", "commands"]) {
8184
- const abs = path48.join(manifestDir, dir);
8408
+ const abs = path49.join(manifestDir, dir);
8185
8409
  if (!fs36.existsSync(abs)) continue;
8186
8410
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
8187
8411
  walkMd(
8188
8412
  abs,
8189
8413
  (file) => {
8190
- const rel = path48.relative(manifestDir, file);
8414
+ const rel = path49.relative(manifestDir, file);
8191
8415
  const text = readForAudit(file, rel, failures);
8192
8416
  if (text === null) return;
8193
8417
  const norm = rel.replace(/\\/g, "/");
@@ -8232,8 +8456,8 @@ async function runValidateCatalog(manifestPath2) {
8232
8456
  process.exitCode = 1;
8233
8457
  return;
8234
8458
  }
8235
- const abs = path49.resolve(process.cwd(), manifestPath2);
8236
- const manifestDir = path49.dirname(abs);
8459
+ const abs = path50.resolve(process.cwd(), manifestPath2);
8460
+ const manifestDir = path50.dirname(abs);
8237
8461
  const data = await readJson(abs);
8238
8462
  if (!data?.items) {
8239
8463
  error(`Could not read catalog manifest at ${abs}`);
@@ -8250,7 +8474,7 @@ async function runValidateCatalog(manifestPath2) {
8250
8474
  }
8251
8475
 
8252
8476
  // src/workspace/worktree/add.ts
8253
- import path56 from "node:path";
8477
+ import path57 from "node:path";
8254
8478
  import fs40 from "fs-extra";
8255
8479
 
8256
8480
  // src/workspace/worktree/git-worktree.ts
@@ -8370,12 +8594,12 @@ async function hasUnpushedWork(cwd) {
8370
8594
  }
8371
8595
 
8372
8596
  // src/workspace/worktree/hydrate.ts
8373
- import path52 from "node:path";
8597
+ import path53 from "node:path";
8374
8598
  import fs38 from "fs-extra";
8375
8599
 
8376
8600
  // src/workspace/worktree/cow-copy.ts
8377
8601
  init_exec();
8378
- import path50 from "node:path";
8602
+ import path51 from "node:path";
8379
8603
  import fs37 from "fs-extra";
8380
8604
  async function getMacFilesystemType(dir) {
8381
8605
  const df = await runCommand("df", ["-P", dir]);
@@ -8410,11 +8634,11 @@ async function cowCopyDir(src, dest) {
8410
8634
  const srcExists = await fs37.pathExists(src);
8411
8635
  if (!srcExists)
8412
8636
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
8413
- const strategy = await detectCowStrategy(path50.dirname(dest));
8637
+ const strategy = await detectCowStrategy(path51.dirname(dest));
8414
8638
  if (strategy === "unsupported" || strategy === "unknown-platform") {
8415
8639
  return { strategy, attempted: false, ok: false };
8416
8640
  }
8417
- await fs37.ensureDir(path50.dirname(dest));
8641
+ await fs37.ensureDir(path51.dirname(dest));
8418
8642
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
8419
8643
  const result = await runCommand("cp", args);
8420
8644
  return {
@@ -8428,7 +8652,7 @@ async function cowCopyDir(src, dest) {
8428
8652
  // src/workspace/worktree/install.ts
8429
8653
  init_exec();
8430
8654
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8431
- import path51 from "node:path";
8655
+ import path52 from "node:path";
8432
8656
  import fg5 from "fast-glob";
8433
8657
  function detectInstallPlan(signals) {
8434
8658
  if (signals.hasYarnLock) {
@@ -8449,10 +8673,10 @@ function detectInstallPlan(signals) {
8449
8673
  return { manager: "none" };
8450
8674
  }
8451
8675
  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"));
8676
+ const hasYarnLock = existsSync3(path52.join(dir, "yarn.lock"));
8677
+ const hasPnpmLock = existsSync3(path52.join(dir, "pnpm-lock.yaml"));
8678
+ const hasPackageLock = existsSync3(path52.join(dir, "package-lock.json"));
8679
+ const hasComposerLock = existsSync3(path52.join(dir, "composer.lock"));
8456
8680
  const dotnetFiles = await fg5(["**/*.csproj", "**/*.sln"], {
8457
8681
  cwd: dir,
8458
8682
  onlyFiles: true,
@@ -8466,7 +8690,7 @@ async function computeLockfileSignals(dir) {
8466
8690
  });
8467
8691
  const hasDotnetProject = dotnetFiles.length > 0;
8468
8692
  let hasPackageManagerField = false;
8469
- const pkgPath = path51.join(dir, "package.json");
8693
+ const pkgPath = path52.join(dir, "package.json");
8470
8694
  if (existsSync3(pkgPath)) {
8471
8695
  try {
8472
8696
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -8507,8 +8731,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
8507
8731
  const start = Date.now();
8508
8732
  const targets = [];
8509
8733
  for (const target of HYDRATION_TARGETS) {
8510
- const src = path52.join(mainMemberPath, target);
8511
- const dest = path52.join(worktreeMemberPath, target);
8734
+ const src = path53.join(mainMemberPath, target);
8735
+ const dest = path53.join(worktreeMemberPath, target);
8512
8736
  if (!await fs38.pathExists(src)) continue;
8513
8737
  const destExists = await fs38.pathExists(dest);
8514
8738
  if (!opts.force && destExists) continue;
@@ -8526,11 +8750,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
8526
8750
 
8527
8751
  // src/workspace/worktree/local-files.ts
8528
8752
  init_exec();
8529
- import path53 from "node:path";
8753
+ import path54 from "node:path";
8530
8754
  import fs39 from "fs-extra";
8531
8755
  var MACHINE_LOCAL_FILES = [
8532
8756
  "docker-compose.local.yml",
8533
- path53.join(".claude", "settings.local.json")
8757
+ path54.join(".claude", "settings.local.json")
8534
8758
  ];
8535
8759
  async function isTracked(repoRoot, relFile) {
8536
8760
  const gitPath = relFile.replace(/\\/g, "/");
@@ -8540,11 +8764,11 @@ async function isTracked(repoRoot, relFile) {
8540
8764
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
8541
8765
  const copied = [];
8542
8766
  for (const rel of MACHINE_LOCAL_FILES) {
8543
- const src = path53.join(mainMemberPath, rel);
8767
+ const src = path54.join(mainMemberPath, rel);
8544
8768
  if (!await fs39.pathExists(src)) continue;
8545
8769
  if (await isTracked(mainMemberPath, rel)) continue;
8546
- const dest = path53.join(worktreeMemberPath, rel);
8547
- await fs39.ensureDir(path53.dirname(dest));
8770
+ const dest = path54.join(worktreeMemberPath, rel);
8771
+ await fs39.ensureDir(path54.dirname(dest));
8548
8772
  await fs39.copy(src, dest, { overwrite: true });
8549
8773
  copied.push(rel);
8550
8774
  }
@@ -8553,15 +8777,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
8553
8777
 
8554
8778
  // src/workspace/worktree/root.ts
8555
8779
  import { existsSync as existsSync4 } from "node:fs";
8556
- import path54 from "node:path";
8780
+ import path55 from "node:path";
8557
8781
  async function resolveWorkspaceForWorktree(start) {
8558
8782
  const rootInfo = await resolveRoots(start);
8559
8783
  if (!rootInfo.isGitRepo) {
8560
8784
  return { ok: false, reason: "Not inside a git repository." };
8561
8785
  }
8562
8786
  const workspaceRoot = rootInfo.mainRoot;
8563
- const hasYaml = existsSync4(path54.join(workspaceRoot, WORKSPACE_FILE));
8564
- const hasManifest = existsSync4(path54.join(workspaceRoot, REPOS_MANIFEST_FILE));
8787
+ const hasYaml = existsSync4(path55.join(workspaceRoot, WORKSPACE_FILE));
8788
+ const hasManifest = existsSync4(path55.join(workspaceRoot, REPOS_MANIFEST_FILE));
8565
8789
  if (!hasYaml && !hasManifest) {
8566
8790
  return {
8567
8791
  ok: false,
@@ -8586,19 +8810,19 @@ function selectMembers(members, only) {
8586
8810
  }
8587
8811
 
8588
8812
  // src/workspace/worktree/state.ts
8589
- import path55 from "node:path";
8813
+ import path56 from "node:path";
8590
8814
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
8591
8815
  function worktreesDir(mainRoot) {
8592
- return path55.join(mainRoot, ".claude", "worktrees");
8816
+ return path56.join(mainRoot, ".claude", "worktrees");
8593
8817
  }
8594
8818
  function worktreePath(mainRoot, slug) {
8595
- return path55.join(worktreesDir(mainRoot), slug);
8819
+ return path56.join(worktreesDir(mainRoot), slug);
8596
8820
  }
8597
8821
  async function readWorktreeState(worktreeRoot) {
8598
- return readJson(path55.join(worktreeRoot, WORKTREE_STATE_FILE));
8822
+ return readJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE));
8599
8823
  }
8600
8824
  async function writeWorktreeState(worktreeRoot, state) {
8601
- await writeJson(path55.join(worktreeRoot, WORKTREE_STATE_FILE), state);
8825
+ await writeJson(path56.join(worktreeRoot, WORKTREE_STATE_FILE), state);
8602
8826
  }
8603
8827
 
8604
8828
  // src/workspace/worktree/add.ts
@@ -8616,7 +8840,7 @@ async function runAdd(opts) {
8616
8840
  if (await fs40.pathExists(wsWorktreePath)) {
8617
8841
  return {
8618
8842
  ok: false,
8619
- error: `Workspace worktree already exists at ${wsWorktreePath} \u2014 run \`haus workspace worktree hydrate\` or \`remove\` first.`
8843
+ 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
8844
  };
8621
8845
  }
8622
8846
  if (opts.dryRun) {
@@ -8632,7 +8856,7 @@ async function runAdd(opts) {
8632
8856
  const memberResults = [];
8633
8857
  for (const member of selected) {
8634
8858
  const start = Date.now();
8635
- const memberWorktreePath = path56.join(wsWorktreePath, member.folder);
8859
+ const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
8636
8860
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
8637
8861
  preferDefaultBranchFrom: true
8638
8862
  });
@@ -8662,7 +8886,7 @@ async function runAdd(opts) {
8662
8886
  await mapWithConcurrency(okResults, async (result) => {
8663
8887
  const member = selected.find((m) => m.id === result.member);
8664
8888
  if (!member) return;
8665
- const memberWorktreePath = path56.join(wsWorktreePath, member.folder);
8889
+ const memberWorktreePath = path57.join(wsWorktreePath, member.folder);
8666
8890
  const hydrationStart = Date.now();
8667
8891
  if (opts.hydrate !== false) {
8668
8892
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -8729,10 +8953,10 @@ async function runDryRun(args) {
8729
8953
 
8730
8954
  // src/workspace/worktree/doctor.ts
8731
8955
  import { existsSync as existsSync5 } from "node:fs";
8732
- import path57 from "node:path";
8956
+ import path58 from "node:path";
8733
8957
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
8734
8958
  function looksHydrated(memberPath) {
8735
- return HYDRATION_MARKERS.some((marker) => existsSync5(path57.join(memberPath, marker)));
8959
+ return HYDRATION_MARKERS.some((marker) => existsSync5(path58.join(memberPath, marker)));
8736
8960
  }
8737
8961
  async function runDoctor2() {
8738
8962
  const rootInfo = await resolveRoots();
@@ -8746,7 +8970,7 @@ async function runDoctor2() {
8746
8970
  };
8747
8971
  }
8748
8972
  const workspaceRoot = rootInfo.mainRoot;
8749
- const isWorkspace = existsSync5(path57.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path57.join(workspaceRoot, REPOS_MANIFEST_FILE));
8973
+ const isWorkspace = existsSync5(path58.join(workspaceRoot, WORKSPACE_FILE)) || existsSync5(path58.join(workspaceRoot, REPOS_MANIFEST_FILE));
8750
8974
  if (!isWorkspace) {
8751
8975
  return {
8752
8976
  isWorkspace: false,
@@ -8759,7 +8983,7 @@ async function runDoctor2() {
8759
8983
  };
8760
8984
  }
8761
8985
  const wsWorktreesDir = worktreesDir(workspaceRoot);
8762
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path57.sep}`);
8986
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path58.sep}`);
8763
8987
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
8764
8988
  const members = await readMembers(rootInfo);
8765
8989
  const problems = [];
@@ -8767,10 +8991,10 @@ async function runDoctor2() {
8767
8991
  let slug;
8768
8992
  if (inWorkspaceWorktree) {
8769
8993
  const wtPath = rootInfo.repoRoot;
8770
- slug = rootInfo.worktreeName ?? path57.basename(wtPath);
8994
+ slug = rootInfo.worktreeName ?? path58.basename(wtPath);
8771
8995
  const state = await readWorktreeState(wtPath);
8772
8996
  for (const member of members) {
8773
- const memberPath = path57.join(wtPath, member.folder);
8997
+ const memberPath = path58.join(wtPath, member.folder);
8774
8998
  const materialized = existsSync5(memberPath);
8775
8999
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
8776
9000
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -8801,16 +9025,16 @@ async function runDoctor2() {
8801
9025
  for (const member of members) {
8802
9026
  const entries = await listWorktrees(member.absPath);
8803
9027
  for (const entry of entries) {
8804
- const entryPath = path57.normalize(entry.path);
8805
- const marker = `${path57.sep}.claude${path57.sep}worktrees${path57.sep}`;
9028
+ const entryPath = path58.normalize(entry.path);
9029
+ const marker = `${path58.sep}.claude${path58.sep}worktrees${path58.sep}`;
8806
9030
  const idx = entryPath.indexOf(marker);
8807
9031
  if (idx === -1) continue;
8808
9032
  const rest = entryPath.slice(idx + marker.length);
8809
- const slugName = rest.split(path57.sep)[0];
9033
+ const slugName = rest.split(path58.sep)[0];
8810
9034
  if (!slugName) continue;
8811
- if (!existsSync5(path57.join(wsWorktreesDir, slugName))) {
9035
+ if (!existsSync5(path58.join(wsWorktreesDir, slugName))) {
8812
9036
  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\`.`
9037
+ `${member.id}: worktree registered at ${entry.path} but ${path58.join(wsWorktreesDir, slugName)} no longer exists \u2014 run \`git -C "${member.absPath}" worktree prune\`.`
8814
9038
  );
8815
9039
  }
8816
9040
  }
@@ -8821,7 +9045,7 @@ async function runDoctor2() {
8821
9045
 
8822
9046
  // src/workspace/worktree/hydrate-command.ts
8823
9047
  import { existsSync as existsSync6 } from "node:fs";
8824
- import path58 from "node:path";
9048
+ import path59 from "node:path";
8825
9049
  async function runHydrateWorktree(opts) {
8826
9050
  const rootInfo = await resolveRoots();
8827
9051
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -8832,7 +9056,7 @@ async function runHydrateWorktree(opts) {
8832
9056
  }
8833
9057
  const workspaceRoot = rootInfo.mainRoot;
8834
9058
  const wtPath = rootInfo.repoRoot;
8835
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path58.sep}`)) {
9059
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path59.sep}`)) {
8836
9060
  return {
8837
9061
  ok: false,
8838
9062
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -8844,49 +9068,97 @@ async function runHydrateWorktree(opts) {
8844
9068
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
8845
9069
  }
8846
9070
  const state = await readWorktreeState(wtPath);
8847
- const slug = state?.slug ?? path58.basename(wtPath);
9071
+ const slug = state?.slug ?? path59.basename(wtPath);
9072
+ const detectedBranch = await currentBranch(wtPath);
9073
+ const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
8848
9074
  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
- });
9075
+ const results2 = await Promise.all(
9076
+ selected.map(async (member) => {
9077
+ const memberPath = path59.join(wtPath, member.folder);
9078
+ if (existsSync6(memberPath)) {
9079
+ return { member: member.id, folder: member.folder, status: "planned" };
9080
+ }
9081
+ if (!opts.materialize) {
9082
+ return {
9083
+ member: member.id,
9084
+ folder: member.folder,
9085
+ status: "skipped",
9086
+ error: "not materialized in this worktree"
9087
+ };
9088
+ }
9089
+ const branchAction = await branchExists(member.absPath, targetBranch) ? "checkout" : "create";
9090
+ return { member: member.id, folder: member.folder, status: "planned", branchAction };
9091
+ })
9092
+ );
8858
9093
  return { ok: true, dryRun: true, slug, results: results2 };
8859
9094
  }
8860
9095
  const results = await mapWithConcurrency(
8861
9096
  selected,
8862
9097
  async (member) => {
8863
- const memberPath = path58.join(wtPath, member.folder);
9098
+ const memberPath = path59.join(wtPath, member.folder);
9099
+ let branchAction;
8864
9100
  if (!existsSync6(memberPath)) {
8865
- return {
8866
- member: member.id,
8867
- folder: member.folder,
8868
- status: "skipped",
8869
- error: "not materialized in this worktree"
8870
- };
9101
+ if (!opts.materialize) {
9102
+ return {
9103
+ member: member.id,
9104
+ folder: member.folder,
9105
+ status: "skipped",
9106
+ error: "not materialized in this worktree"
9107
+ };
9108
+ }
9109
+ const outcome = await addWorktree(member.absPath, memberPath, targetBranch, {
9110
+ preferDefaultBranchFrom: true
9111
+ });
9112
+ if (!outcome.ok) {
9113
+ return {
9114
+ member: member.id,
9115
+ folder: member.folder,
9116
+ status: "failed",
9117
+ 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"
9118
+ };
9119
+ }
9120
+ branchAction = outcome.branchAction;
8871
9121
  }
8872
9122
  try {
8873
9123
  const hydration = await hydrateMember(member.absPath, memberPath, { force: opts.force });
8874
- return { member: member.id, folder: member.folder, status: "ok", hydration };
9124
+ return { member: member.id, folder: member.folder, status: "ok", hydration, branchAction };
8875
9125
  } catch (err) {
8876
9126
  return {
8877
9127
  member: member.id,
8878
9128
  folder: member.folder,
8879
9129
  status: "failed",
8880
- error: err instanceof Error ? err.message : String(err)
9130
+ error: err instanceof Error ? err.message : String(err),
9131
+ branchAction
8881
9132
  };
8882
9133
  }
8883
9134
  }
8884
9135
  );
9136
+ const newlyMaterialized = results.filter((r) => r.branchAction && r.status === "ok");
9137
+ if (newlyMaterialized.length > 0) {
9138
+ const byId = new Map((state?.members ?? []).map((m) => [m.id, m]));
9139
+ for (const r of newlyMaterialized) {
9140
+ const member = selected.find((m) => m.id === r.member);
9141
+ if (!member) continue;
9142
+ const entry = {
9143
+ id: member.id,
9144
+ folder: member.folder,
9145
+ branch: targetBranch,
9146
+ absPath: member.absPath
9147
+ };
9148
+ byId.set(member.id, entry);
9149
+ }
9150
+ await writeWorktreeState(wtPath, {
9151
+ slug,
9152
+ branch: state?.branch ?? targetBranch,
9153
+ createdAt: state?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
9154
+ members: Array.from(byId.values())
9155
+ });
9156
+ }
8885
9157
  return { ok: results.every((r) => r.status !== "failed"), dryRun: false, slug, results };
8886
9158
  }
8887
9159
 
8888
9160
  // src/workspace/worktree/list.ts
8889
- import path59 from "node:path";
9161
+ import path60 from "node:path";
8890
9162
  import fs41 from "fs-extra";
8891
9163
  async function runList2() {
8892
9164
  const resolved = await resolveWorkspaceForWorktree();
@@ -8899,12 +9171,12 @@ async function runList2() {
8899
9171
  const configuredMembers = await readMembers(rootInfo);
8900
9172
  const worktrees = [];
8901
9173
  for (const slug of slugs) {
8902
- const wtPath = path59.join(dir, slug);
9174
+ const wtPath = path60.join(dir, slug);
8903
9175
  const state = await readWorktreeState(wtPath);
8904
9176
  const members = [];
8905
9177
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
8906
9178
  for (const m of source) {
8907
- const memberPath = path59.join(wtPath, m.folder);
9179
+ const memberPath = path60.join(wtPath, m.folder);
8908
9180
  const materialized = await fs41.pathExists(memberPath);
8909
9181
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
8910
9182
  members.push({
@@ -8926,7 +9198,7 @@ async function runList2() {
8926
9198
  }
8927
9199
 
8928
9200
  // src/workspace/worktree/remove.ts
8929
- import path60 from "node:path";
9201
+ import path61 from "node:path";
8930
9202
  import fs42 from "fs-extra";
8931
9203
  async function runRemove(opts) {
8932
9204
  const resolved = await resolveWorkspaceForWorktree();
@@ -8953,7 +9225,7 @@ async function runRemove(opts) {
8953
9225
  targets.push({
8954
9226
  repo: configMember?.id ?? stateMember.id,
8955
9227
  ownerRepo,
8956
- path: path60.join(wsWorktreePath, stateMember.folder)
9228
+ path: path61.join(wsWorktreePath, stateMember.folder)
8957
9229
  });
8958
9230
  }
8959
9231
  const existingTargets = [];
@@ -9081,7 +9353,8 @@ async function handleHydrate(opts) {
9081
9353
  const hydrateOpts = {
9082
9354
  only: normalizeOnly(opts.only),
9083
9355
  force: Boolean(opts.force),
9084
- dryRun: Boolean(opts.dryRun)
9356
+ dryRun: Boolean(opts.dryRun),
9357
+ materialize: Boolean(opts.materialize)
9085
9358
  };
9086
9359
  const result = await runHydrateWorktree(hydrateOpts);
9087
9360
  if (!result.ok && "error" in result) {
@@ -9091,14 +9364,20 @@ async function handleHydrate(opts) {
9091
9364
  }
9092
9365
  if (result.dryRun) {
9093
9366
  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})` : ""}`);
9367
+ for (const r of result.results) {
9368
+ const action = r.branchAction ? ` (${r.branchAction} member worktree first)` : "";
9369
+ log(`[dry-run] ${r.member}: ${r.status}${action}${r.error ? ` (${r.error})` : ""}`);
9370
+ }
9096
9371
  return;
9097
9372
  }
9098
9373
  log(`Hydrated "${result.slug}":`);
9099
9374
  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}` : ""}`);
9375
+ if (r.status === "ok" && r.hydration) {
9376
+ const materializedNote = r.branchAction ? `materialized (${r.branchAction}), ` : "";
9377
+ log(` ${r.member}: ${materializedNote}${describeInstall(r.hydration)}`);
9378
+ } else {
9379
+ warn(` ${r.member}: ${r.status}${r.error ? ` \u2014 ${r.error}` : ""}`);
9380
+ }
9102
9381
  }
9103
9382
  if (!result.ok) process.exitCode = 1;
9104
9383
  }
@@ -9157,6 +9436,13 @@ async function handleList() {
9157
9436
  }
9158
9437
  }
9159
9438
  }
9439
+ function emitFromHookContext(additionalContext) {
9440
+ log(
9441
+ JSON.stringify({
9442
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext }
9443
+ })
9444
+ );
9445
+ }
9160
9446
  async function handleDoctor(opts) {
9161
9447
  try {
9162
9448
  const report = await runDoctor2();
@@ -9168,17 +9454,26 @@ async function handleDoctor(opts) {
9168
9454
  return;
9169
9455
  }
9170
9456
  if (report.problems.length === 0) {
9171
- log(
9172
- report.inWorkspaceWorktree ? `Workspace worktree "${report.slug}" looks healthy.` : "No issues found."
9457
+ if (!opts.fromHook) {
9458
+ log(
9459
+ report.inWorkspaceWorktree ? `Workspace worktree "${report.slug}" looks healthy.` : "No issues found."
9460
+ );
9461
+ }
9462
+ return;
9463
+ }
9464
+ if (opts.fromHook) {
9465
+ const scope = report.slug ? `Workspace worktree "${report.slug}"` : "This haus workspace";
9466
+ emitFromHookContext(
9467
+ `${scope} has ${report.problems.length} issue(s): ${report.problems.join("; ")}`
9173
9468
  );
9174
9469
  return;
9175
9470
  }
9176
9471
  for (const p of report.problems) warn(p);
9177
- if (!opts.fromHook) process.exitCode = 1;
9472
+ process.exitCode = 1;
9178
9473
  } catch (err) {
9179
9474
  const message = err instanceof Error ? err.message : String(err);
9180
9475
  if (opts.fromHook) {
9181
- warn(`worktree doctor: ${message}`);
9476
+ emitFromHookContext(`\`haus workspace worktree doctor\` failed: ${message}`);
9182
9477
  } else {
9183
9478
  error(message);
9184
9479
  process.exitCode = 1;
@@ -9202,7 +9497,7 @@ async function runWorktree(action, opts = {}) {
9202
9497
 
9203
9498
  // src/commands/workspace.ts
9204
9499
  import { existsSync as existsSync10, statSync as statSync3 } from "node:fs";
9205
- import path69 from "node:path";
9500
+ import path70 from "node:path";
9206
9501
 
9207
9502
  // src/commands/workspace/aggregate.ts
9208
9503
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -9257,18 +9552,18 @@ ${summaries.map(
9257
9552
 
9258
9553
  // src/commands/workspace/doctor.ts
9259
9554
  import { existsSync as existsSync7 } from "node:fs";
9260
- import path62 from "node:path";
9555
+ import path63 from "node:path";
9261
9556
 
9262
9557
  // src/commands/workspace/manifest.ts
9263
9558
  import { readFileSync as readFileSync4 } from "node:fs";
9264
- import path61 from "node:path";
9559
+ import path62 from "node:path";
9265
9560
  var MANIFEST_FILE = "workspace.manifest.json";
9266
9561
  function manifestPath(workspaceRoot) {
9267
9562
  return hausPath(workspaceRoot, MANIFEST_FILE);
9268
9563
  }
9269
9564
  function hausVersion() {
9270
9565
  try {
9271
- const pkg = JSON.parse(readFileSync4(path61.join(packageRoot(), "package.json"), "utf8"));
9566
+ const pkg = JSON.parse(readFileSync4(path62.join(packageRoot(), "package.json"), "utf8"));
9272
9567
  return pkg.version ?? "0.0.0";
9273
9568
  } catch {
9274
9569
  return "0.0.0";
@@ -9352,7 +9647,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
9352
9647
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
9353
9648
  const catalogRefByRepo = [];
9354
9649
  for (const repo of config.repos) {
9355
- const repoRoot = path62.resolve(workspaceRoot, repo.path);
9650
+ const repoRoot = path63.resolve(workspaceRoot, repo.path);
9356
9651
  const entry = manifestByName.get(repo.name);
9357
9652
  if (!entry) {
9358
9653
  flag({
@@ -9425,7 +9720,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
9425
9720
  });
9426
9721
  continue;
9427
9722
  }
9428
- if (!existsSync7(path62.join(workspaceRoot, entry.path))) {
9723
+ if (!existsSync7(path63.join(workspaceRoot, entry.path))) {
9429
9724
  flag({
9430
9725
  repo: entry.repo,
9431
9726
  kind: "missing-linked-context-copy",
@@ -9468,15 +9763,15 @@ function emit(args) {
9468
9763
  }
9469
9764
 
9470
9765
  // src/workspace/link-context/link.ts
9471
- import path65 from "node:path";
9766
+ import path66 from "node:path";
9472
9767
  import fs45 from "fs-extra";
9473
9768
 
9474
9769
  // src/workspace/link-context/plan.ts
9475
- import path64 from "node:path";
9770
+ import path65 from "node:path";
9476
9771
  import fs44 from "fs-extra";
9477
9772
 
9478
9773
  // src/workspace/link-context/scan-source.ts
9479
- import path63 from "node:path";
9774
+ import path64 from "node:path";
9480
9775
  import fs43 from "fs-extra";
9481
9776
  var ASSET_SUBDIR = {
9482
9777
  skill: "skills",
@@ -9485,36 +9780,36 @@ var ASSET_SUBDIR = {
9485
9780
  };
9486
9781
  async function hasLinkableContext(member) {
9487
9782
  for (const subdir of Object.values(ASSET_SUBDIR)) {
9488
- if (await fs43.pathExists(path63.join(member.absPath, ".claude", subdir))) return true;
9783
+ if (await fs43.pathExists(path64.join(member.absPath, ".claude", subdir))) return true;
9489
9784
  }
9490
9785
  return false;
9491
9786
  }
9492
9787
  async function collectSkills(member) {
9493
- const dir = path63.join(member.absPath, ".claude", "skills");
9788
+ const dir = path64.join(member.absPath, ".claude", "skills");
9494
9789
  if (!await fs43.pathExists(dir)) return [];
9495
9790
  const names = await fs43.readdir(dir);
9496
9791
  const out = [];
9497
9792
  for (const name of [...names].sort()) {
9498
- const abs = path63.join(dir, name);
9793
+ const abs = path64.join(dir, name);
9499
9794
  const stat = await fs43.lstat(abs);
9500
9795
  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) });
9796
+ if (!await fs43.pathExists(path64.join(abs, "SKILL.md"))) continue;
9797
+ out.push({ type: "skill", name, sourceRelPath: path64.posix.join(".claude", "skills", name) });
9503
9798
  }
9504
9799
  return out;
9505
9800
  }
9506
9801
  async function collectFlatMarkdown(member, type) {
9507
9802
  const subdir = ASSET_SUBDIR[type];
9508
- const dir = path63.join(member.absPath, ".claude", subdir);
9803
+ const dir = path64.join(member.absPath, ".claude", subdir);
9509
9804
  if (!await fs43.pathExists(dir)) return [];
9510
9805
  const names = await fs43.readdir(dir);
9511
9806
  const out = [];
9512
9807
  for (const fileName of [...names].sort()) {
9513
9808
  if (!fileName.endsWith(".md")) continue;
9514
- const abs = path63.join(dir, fileName);
9809
+ const abs = path64.join(dir, fileName);
9515
9810
  if (!(await fs43.lstat(abs)).isFile()) continue;
9516
9811
  const name = fileName.slice(0, -".md".length);
9517
- out.push({ type, name, sourceRelPath: path63.posix.join(".claude", subdir, fileName) });
9812
+ out.push({ type, name, sourceRelPath: path64.posix.join(".claude", subdir, fileName) });
9518
9813
  }
9519
9814
  return out;
9520
9815
  }
@@ -9529,9 +9824,9 @@ async function collectSourceAssets(member) {
9529
9824
 
9530
9825
  // src/workspace/link-context/plan.ts
9531
9826
  function repoFolderPrefix(member) {
9532
- const base = path64.posix.basename(member.folder);
9827
+ const base = path65.posix.basename(member.folder);
9533
9828
  if (base && base !== "." && base !== "/") return base;
9534
- const fromAbs = path64.basename(member.absPath);
9829
+ const fromAbs = path65.basename(member.absPath);
9535
9830
  return fromAbs || member.id;
9536
9831
  }
9537
9832
  function assetKey(type, destKey) {
@@ -9588,7 +9883,7 @@ async function buildLinkPlan(members) {
9588
9883
  name: asset.name,
9589
9884
  destKey,
9590
9885
  sourceRelPath: asset.sourceRelPath,
9591
- sourceAbsPath: path64.join(member.absPath, asset.sourceRelPath),
9886
+ sourceAbsPath: path65.join(member.absPath, asset.sourceRelPath),
9592
9887
  sourceHash
9593
9888
  });
9594
9889
  }
@@ -9599,7 +9894,7 @@ async function buildLinkPlan(members) {
9599
9894
  function destRelPathFor(type, destKey) {
9600
9895
  const subdir = `${type}s`;
9601
9896
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
9602
- return path65.posix.join(".claude", subdir, leaf);
9897
+ return path66.posix.join(".claude", subdir, leaf);
9603
9898
  }
9604
9899
  async function runLinkContext(workspaceRoot, opts = {}) {
9605
9900
  const write = opts.write ?? false;
@@ -9625,9 +9920,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
9625
9920
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
9626
9921
  for (const entry of plan.entries) {
9627
9922
  const destRel = destRelPathFor(entry.type, entry.destKey);
9628
- const destAbs = path65.join(workspaceRoot, destRel);
9923
+ const destAbs = path66.join(workspaceRoot, destRel);
9629
9924
  if (write) {
9630
- await fs45.ensureDir(path65.dirname(destAbs));
9925
+ await fs45.ensureDir(path66.dirname(destAbs));
9631
9926
  if (await fs45.pathExists(destAbs)) await fs45.remove(destAbs);
9632
9927
  await fs45.copy(entry.sourceAbsPath, destAbs, {
9633
9928
  overwrite: true,
@@ -9659,7 +9954,7 @@ async function runLinkContext(workspaceRoot, opts = {}) {
9659
9954
  if (nextPaths.has(old.path)) continue;
9660
9955
  removed.push(old.path);
9661
9956
  if (write) {
9662
- const abs = path65.join(workspaceRoot, old.path);
9957
+ const abs = path66.join(workspaceRoot, old.path);
9663
9958
  if (await fs45.pathExists(abs)) await fs45.remove(abs);
9664
9959
  }
9665
9960
  }
@@ -9712,10 +10007,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
9712
10007
 
9713
10008
  // src/commands/workspace/setup.ts
9714
10009
  import { existsSync as existsSync8, statSync } from "node:fs";
9715
- import path67 from "node:path";
10010
+ import path68 from "node:path";
9716
10011
 
9717
10012
  // src/claude/write-workspace-claude-md.ts
9718
- import path66 from "node:path";
10013
+ import path67 from "node:path";
9719
10014
  import fs46 from "fs-extra";
9720
10015
  function buildWorkspaceImportBlock(client, members) {
9721
10016
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -9734,7 +10029,7 @@ ${BLOCK_END}`;
9734
10029
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
9735
10030
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
9736
10031
  const dryRun = opts.dryRun ?? false;
9737
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path66.join(workspaceRoot, "CLAUDE.md");
10032
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path67.join(workspaceRoot, "CLAUDE.md");
9738
10033
  const say = opts.quiet ? () => {
9739
10034
  } : log;
9740
10035
  const existed = await fs46.pathExists(filePath);
@@ -9765,20 +10060,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
9765
10060
 
9766
10061
  // src/commands/workspace/setup.ts
9767
10062
  function resolveWorkspaceRoot(start = process.cwd()) {
9768
- let dir = path67.resolve(start);
10063
+ let dir = path68.resolve(start);
9769
10064
  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);
10065
+ if (existsSync8(path68.join(dir, WORKSPACE_FILE))) return dir;
10066
+ const parent = path68.dirname(dir);
10067
+ if (parent === dir) return path68.resolve(start);
9773
10068
  dir = parent;
9774
10069
  }
9775
10070
  }
9776
10071
  function isRootRepo(workspaceRoot, repoPath) {
9777
- return path67.resolve(workspaceRoot, repoPath) === path67.resolve(workspaceRoot);
10072
+ return path68.resolve(workspaceRoot, repoPath) === path68.resolve(workspaceRoot);
9778
10073
  }
9779
10074
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
9780
10075
  const apply = options.write ?? false;
9781
- const configText = await readText(path67.join(workspaceRoot, WORKSPACE_FILE));
10076
+ const configText = await readText(path68.join(workspaceRoot, WORKSPACE_FILE));
9782
10077
  if (!configText) {
9783
10078
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
9784
10079
  process.exitCode = 1;
@@ -9805,7 +10100,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
9805
10100
  const statuses = [];
9806
10101
  const aggregateInputs = [];
9807
10102
  for (const repo of repos) {
9808
- const repoRoot = path67.resolve(workspaceRoot, repo.path);
10103
+ const repoRoot = path68.resolve(workspaceRoot, repo.path);
9809
10104
  say(`
9810
10105
  \u2192 ${repo.name} (${repo.path})`);
9811
10106
  try {
@@ -9876,7 +10171,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
9876
10171
  const status = statusByName.get(repo.name);
9877
10172
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
9878
10173
  if (status?.status === "ok") {
9879
- const lock = await checkLock(path67.resolve(workspaceRoot, repo.path));
10174
+ const lock = await checkLock(path68.resolve(workspaceRoot, repo.path));
9880
10175
  manifestRepos.push({
9881
10176
  name: repo.name,
9882
10177
  path: repo.path,
@@ -9932,7 +10227,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
9932
10227
  try {
9933
10228
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
9934
10229
  if (linkResult.ok) {
9935
- written.push(...linkResult.added.map((rel) => path67.join(workspaceRoot, rel)));
10230
+ written.push(...linkResult.added.map((rel) => path68.join(workspaceRoot, rel)));
9936
10231
  if (!options.json) {
9937
10232
  say(
9938
10233
  `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 +10259,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
9964
10259
 
9965
10260
  // src/commands/workspace/undo.ts
9966
10261
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
9967
- import path68 from "node:path";
10262
+ import path69 from "node:path";
9968
10263
  import fs47 from "fs-extra";
9969
10264
  var WORKSPACE_AGGREGATE_FILES = [
9970
10265
  "workspace-summary.json",
@@ -9993,7 +10288,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
9993
10288
  }
9994
10289
  let anyRepoFailed = false;
9995
10290
  for (const repo of config.repos) {
9996
- const repoRoot = path68.resolve(workspaceRoot, repo.path);
10291
+ const repoRoot = path69.resolve(workspaceRoot, repo.path);
9997
10292
  log(`
9998
10293
  \u2192 ${repo.name} (${repo.path})`);
9999
10294
  try {
@@ -10008,7 +10303,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10008
10303
  }
10009
10304
  const linkedContext = await readLinkedContext(workspaceRoot);
10010
10305
  for (const entry of linkedContext) {
10011
- const abs = path68.join(workspaceRoot, entry.path);
10306
+ const abs = path69.join(workspaceRoot, entry.path);
10012
10307
  if (await fs47.pathExists(abs)) {
10013
10308
  await fs47.remove(abs);
10014
10309
  log(`Removed ${entry.path} (linked ${entry.type} from ${entry.repo})`);
@@ -10018,20 +10313,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10018
10313
  const abs = hausPath(workspaceRoot, rel);
10019
10314
  if (await fs47.pathExists(abs)) {
10020
10315
  await fs47.remove(abs);
10021
- log(`Removed ${path68.relative(workspaceRoot, abs)}`);
10316
+ log(`Removed ${path69.relative(workspaceRoot, abs)}`);
10022
10317
  }
10023
10318
  }
10024
10319
  const manifestFile = manifestPath(workspaceRoot);
10025
10320
  if (await fs47.pathExists(manifestFile)) {
10026
10321
  await fs47.remove(manifestFile);
10027
- log(`Removed ${path68.relative(workspaceRoot, manifestFile)}`);
10322
+ log(`Removed ${path69.relative(workspaceRoot, manifestFile)}`);
10028
10323
  }
10029
10324
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
10030
10325
  if (await fs47.pathExists(workspaceMdPath)) {
10031
10326
  await fs47.remove(workspaceMdPath);
10032
- log(`Removed ${path68.relative(workspaceRoot, workspaceMdPath)}`);
10327
+ log(`Removed ${path69.relative(workspaceRoot, workspaceMdPath)}`);
10033
10328
  } else {
10034
- const claudeMdPath = path68.join(workspaceRoot, "CLAUDE.md");
10329
+ const claudeMdPath = path69.join(workspaceRoot, "CLAUDE.md");
10035
10330
  if (existsSync9(claudeMdPath)) {
10036
10331
  const prev = await readText(claudeMdPath);
10037
10332
  if (prev?.includes(BLOCK_BEGIN)) {
@@ -10083,7 +10378,7 @@ relationships: []
10083
10378
  log("Workspace initialized.");
10084
10379
  }
10085
10380
  async function scanWorkspace(workspaceRoot, opts) {
10086
- const configText = await readText(path69.join(workspaceRoot, WORKSPACE_FILE));
10381
+ const configText = await readText(path70.join(workspaceRoot, WORKSPACE_FILE));
10087
10382
  if (!configText) {
10088
10383
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
10089
10384
  process.exitCode = 1;
@@ -10105,7 +10400,7 @@ async function scanWorkspace(workspaceRoot, opts) {
10105
10400
  const inputs = [];
10106
10401
  const failures = [];
10107
10402
  for (const repo of config.repos) {
10108
- const repoRoot = path69.resolve(workspaceRoot, repo.path);
10403
+ const repoRoot = path70.resolve(workspaceRoot, repo.path);
10109
10404
  try {
10110
10405
  if (!existsSync10(repoRoot) || !statSync3(repoRoot).isDirectory()) {
10111
10406
  throw new Error("path is not a directory");
@@ -10171,7 +10466,7 @@ async function runWorkspace(action, options = {}) {
10171
10466
  // src/cli.ts
10172
10467
  function cliVersion() {
10173
10468
  try {
10174
- const pkgPath = path70.join(packageRoot(), "package.json");
10469
+ const pkgPath = path71.join(packageRoot(), "package.json");
10175
10470
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10176
10471
  return pkg.version ?? "0.0.0";
10177
10472
  } catch {
@@ -10181,7 +10476,7 @@ function cliVersion() {
10181
10476
  var program = new Command();
10182
10477
  function validateRuntimeNodeVersion() {
10183
10478
  try {
10184
- const pkgPath = path70.join(packageRoot(), "package.json");
10479
+ const pkgPath = path71.join(packageRoot(), "package.json");
10185
10480
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
10186
10481
  const requiredRange = pkg.engines?.node;
10187
10482
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -10282,8 +10577,16 @@ worktree.command("add <slug>").description(
10282
10577
  dryRun: opts.dryRun
10283
10578
  })
10284
10579
  );
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 })
10580
+ 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(
10581
+ "--materialize",
10582
+ "Also `git worktree add` any member missing entirely (e.g. a worktree created by plain `git worktree add` outside haus) before hydrating it"
10583
+ ).option("--dry-run", "Preview without writing or running anything").action(
10584
+ (opts) => runWorktree("hydrate", {
10585
+ only: opts.only,
10586
+ force: opts.force,
10587
+ materialize: opts.materialize,
10588
+ dryRun: opts.dryRun
10589
+ })
10287
10590
  );
10288
10591
  worktree.command("list").description("List materialized workspace worktrees and their member status").action(() => runWorktree("list"));
10289
10592
  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(