@haus-tech/haus-workflow 1.5.3 → 1.5.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.4](https://github.com/WeAreHausTech/haus-workflow/compare/v1.5.3...v1.5.4) (2026-08-11)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **workspace:** stop setup from clobbering CLAUDE.md imports and dropping manual lock includes ([#213](https://github.com/WeAreHausTech/haus-workflow/issues/213)) ([b29ad06](https://github.com/WeAreHausTech/haus-workflow/commit/b29ad06c6e5c58e9381c7da300a02e8b486c6ceb))
8
+
3
9
  ## [1.5.3](https://github.com/WeAreHausTech/haus-workflow/compare/v1.5.2...v1.5.3) (2026-08-10)
4
10
 
5
11
  ### Performance
package/dist/cli.js CHANGED
@@ -2479,6 +2479,9 @@ import path16 from "node:path";
2479
2479
  import fs14 from "fs-extra";
2480
2480
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
2481
2481
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
2482
+ var WORKSPACE_BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-workspace-imports v=1 -->";
2483
+ var WORKSPACE_BLOCK_END = "<!-- HAUS:END haus-workspace-imports -->";
2484
+ var DEFAULT_SENTINELS = { begin: BLOCK_BEGIN, end: BLOCK_END };
2482
2485
  var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
2483
2486
  @.haus-workflow/workflow-config.md
2484
2487
  @docs/decisions/README.md`;
@@ -2498,15 +2501,15 @@ function findLineMarker2(content2, marker, from = 0) {
2498
2501
  }
2499
2502
  return null;
2500
2503
  }
2501
- function findImportBlockRange(content2) {
2502
- const begin = findLineMarker2(content2, BLOCK_BEGIN);
2504
+ function findImportBlockRange(content2, sentinels = DEFAULT_SENTINELS) {
2505
+ const begin = findLineMarker2(content2, sentinels.begin);
2503
2506
  if (!begin) return null;
2504
- const end = findLineMarker2(content2, BLOCK_END, begin.end);
2507
+ const end = findLineMarker2(content2, sentinels.end, begin.end);
2505
2508
  if (!end) return null;
2506
2509
  return { beginStart: begin.start, beginEnd: begin.end, endStart: end.start, endEnd: end.end };
2507
2510
  }
2508
- function stripHausBlock(existing) {
2509
- const range = findImportBlockRange(existing);
2511
+ function stripHausBlock(existing, sentinels = DEFAULT_SENTINELS) {
2512
+ const range = findImportBlockRange(existing, sentinels);
2510
2513
  if (range) {
2511
2514
  const before = existing.slice(0, range.beginStart);
2512
2515
  const after = existing.slice(range.endEnd);
@@ -2514,7 +2517,7 @@ function stripHausBlock(existing) {
2514
2517
  return merged.length > 0 ? `${merged}
2515
2518
  ` : "";
2516
2519
  }
2517
- const loneBegin = findLineMarker2(existing, BLOCK_BEGIN);
2520
+ const loneBegin = findLineMarker2(existing, sentinels.begin);
2518
2521
  if (loneBegin) {
2519
2522
  const before = existing.slice(0, loneBegin.start).trimEnd();
2520
2523
  return before.length > 0 ? `${before}
@@ -2522,14 +2525,14 @@ function stripHausBlock(existing) {
2522
2525
  }
2523
2526
  return existing;
2524
2527
  }
2525
- function injectHausBlock(existing, block) {
2526
- const range = findImportBlockRange(existing);
2528
+ function injectHausBlock(existing, block, sentinels = DEFAULT_SENTINELS) {
2529
+ const range = findImportBlockRange(existing, sentinels);
2527
2530
  if (range) {
2528
2531
  const before = existing.slice(0, range.beginStart);
2529
2532
  const after = existing.slice(range.endEnd);
2530
2533
  return `${before}${block}${after}`;
2531
2534
  }
2532
- const loneBegin = findLineMarker2(existing, BLOCK_BEGIN);
2535
+ const loneBegin = findLineMarker2(existing, sentinels.begin);
2533
2536
  if (loneBegin) {
2534
2537
  const before = existing.slice(0, loneBegin.start).trimEnd();
2535
2538
  if (before.length === 0) return `${block}
@@ -3144,8 +3147,18 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
3144
3147
  }
3145
3148
  await pruneOrphanedCatalogItems(root, prevLock, notOrphaned, dryRun, opts.quiet);
3146
3149
  }
3147
- if (dryRun) return [...new Set(files)];
3148
3150
  const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
3151
+ if (dryRun) {
3152
+ const prevIds = new Set(
3153
+ prevLock.map((e) => e.id ? migrationByOldId.get(e.id) ?? e.id : void 0).filter((id) => Boolean(id))
3154
+ );
3155
+ const nextIds = new Set(installedItems.map((r) => r.id));
3156
+ const added = [...nextIds].filter((id) => !prevIds.has(id)).sort();
3157
+ const removed = [...prevIds].filter((id) => !nextIds.has(id)).sort();
3158
+ for (const id of added) say(`[dry-run] haus.lock.json would add: ${id}`);
3159
+ for (const id of removed) say(`[dry-run] haus.lock.json would remove: ${id}`);
3160
+ return [...new Set(files)];
3161
+ }
3149
3162
  const prevRefById = new Map(
3150
3163
  prevLock.filter((e) => e.id && e.catalogRef).map((e) => [migrationByOldId.get(e.id) ?? e.id, e.catalogRef])
3151
3164
  );
@@ -5479,13 +5492,31 @@ async function runDoctor(options) {
5479
5492
  "haus apply --write"
5480
5493
  );
5481
5494
  } else {
5482
- ok("- CLAUDE.md: import block present");
5483
5495
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
5484
- const importTargets = [...block.matchAll(/@\.haus-workflow\/(\S+)/g)].map((m) => m[1]);
5485
- for (const target of importTargets) {
5486
- if (!await fs29.pathExists(hausPath(root, target))) {
5496
+ const importLinePattern = /^@(\S+)\s*$/gm;
5497
+ const actualTargets = [...block.matchAll(importLinePattern)].map((m) => m[1]);
5498
+ const expectedTargets = [...IMPORT_CONTENT.matchAll(importLinePattern)].map((m) => m[1]);
5499
+ const missingTargets = expectedTargets.filter((t) => !actualTargets.includes(t));
5500
+ const foreignTargets = actualTargets.filter((t) => !expectedTargets.includes(t));
5501
+ if (missingTargets.length > 0 || foreignTargets.length > 0) {
5502
+ const parts = [];
5503
+ if (missingTargets.length > 0) parts.push(`missing @${missingTargets.join(", @")}`);
5504
+ if (foreignTargets.length > 0) parts.push(`unexpected @${foreignTargets.join(", @")}`);
5505
+ flag(
5506
+ `- CLAUDE.md: haus import block content does not match expected imports (${parts.join("; ")}) (run \`haus apply --write\` to repair)`,
5507
+ "The haus import block in CLAUDE.md has been altered and no longer imports the expected files",
5508
+ "haus apply --write"
5509
+ );
5510
+ } else {
5511
+ ok("- CLAUDE.md: import block present");
5512
+ }
5513
+ for (const target of actualTargets) {
5514
+ const resolved = path38.resolve(root, target);
5515
+ const relFromRoot = path38.relative(root, resolved);
5516
+ const escapesRoot = relFromRoot === ".." || relFromRoot.startsWith(`..${path38.sep}`) || path38.isAbsolute(relFromRoot);
5517
+ if (escapesRoot || !await fs29.pathExists(resolved)) {
5487
5518
  flag(
5488
- `- CLAUDE.md import: @.haus-workflow/${target} does not resolve (run \`haus apply --write\`)`,
5519
+ `- CLAUDE.md import: @${target} does not resolve (run \`haus apply --write\`)`,
5489
5520
  `A file CLAUDE.md links to (${target}) is missing, so part of the guidance won't load`,
5490
5521
  "haus apply --write"
5491
5522
  );
@@ -5493,6 +5524,13 @@ async function runDoctor(options) {
5493
5524
  }
5494
5525
  }
5495
5526
  }
5527
+ if (rootClaudeMdContent && rootClaudeMdContent.includes(WORKSPACE_BLOCK_BEGIN) && !rootClaudeMdContent.includes(BLOCK_BEGIN)) {
5528
+ flag(
5529
+ "- CLAUDE.md: haus-workspace-imports block present but the per-repo haus-imports block is missing (run `haus apply --write`)",
5530
+ "The workspace CLAUDE.md has the workspace import block but not the per-repo one alongside it",
5531
+ "haus apply --write"
5532
+ );
5533
+ }
5496
5534
  const workflowPath = hausPath(root, "WORKFLOW.md");
5497
5535
  const workflowExists = await fs29.pathExists(workflowPath);
5498
5536
  if (!workflowExists) {
@@ -7459,6 +7497,11 @@ async function hasMultipleSiblingRepos(root) {
7459
7497
  }
7460
7498
 
7461
7499
  // src/claude/setup-core.ts
7500
+ async function readPriorManualIncludes(root) {
7501
+ const prior = await readJson(hausPath(root, "recommendation.json"));
7502
+ if (!prior?.recommended) return [];
7503
+ return prior.recommended.filter((entry) => entry.selectionMode === "manual").map((entry) => entry.id);
7504
+ }
7462
7505
  async function runSetupCore(root, opts) {
7463
7506
  const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
7464
7507
  const say = quiet ? () => {
@@ -7473,7 +7516,8 @@ async function runSetupCore(root, opts) {
7473
7516
  }
7474
7517
  const siblingWarning = await hasMultipleSiblingRepos(root) ? [SIBLING_REPO_WARNING] : [];
7475
7518
  const context = await readContextOrScan(root);
7476
- const recommendation = await recommend(root, context);
7519
+ const priorManualIncludes = await readPriorManualIncludes(root);
7520
+ const recommendation = await recommend(root, context, { include: priorManualIncludes });
7477
7521
  if (recommendation.recommended.length === 0 && !force) {
7478
7522
  const warningLines2 = [
7479
7523
  .../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
@@ -10012,6 +10056,7 @@ import path68 from "node:path";
10012
10056
  // src/claude/write-workspace-claude-md.ts
10013
10057
  import path67 from "node:path";
10014
10058
  import fs46 from "fs-extra";
10059
+ var WORKSPACE_SENTINELS = { begin: WORKSPACE_BLOCK_BEGIN, end: WORKSPACE_BLOCK_END };
10015
10060
  function buildWorkspaceImportBlock(client, members) {
10016
10061
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
10017
10062
  const body = [
@@ -10022,9 +10067,9 @@ function buildWorkspaceImportBlock(client, members) {
10022
10067
  "Member repos:",
10023
10068
  ...memberLines
10024
10069
  ].join("\n");
10025
- return `${BLOCK_BEGIN}
10070
+ return `${WORKSPACE_BLOCK_BEGIN}
10026
10071
  ${body}
10027
- ${BLOCK_END}`;
10072
+ ${WORKSPACE_BLOCK_END}`;
10028
10073
  }
10029
10074
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
10030
10075
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
@@ -10035,7 +10080,7 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
10035
10080
  const existed = await fs46.pathExists(filePath);
10036
10081
  const prev = existed ? await fs46.readFile(filePath, "utf8") : "";
10037
10082
  const next = opts.collision ? `${block}
10038
- ` : injectHausBlock(prev, block);
10083
+ ` : injectHausBlock(prev, block, WORKSPACE_SENTINELS);
10039
10084
  const printable = displayPath(workspaceRoot, filePath);
10040
10085
  if (dryRun) {
10041
10086
  if (!existed) {
@@ -10147,6 +10192,17 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
10147
10192
  config.relationships
10148
10193
  );
10149
10194
  written.push(...artifacts);
10195
+ } else {
10196
+ const sayAggregate = options.json ? () => {
10197
+ } : log;
10198
+ for (const rel of [
10199
+ "workspace-summary.json",
10200
+ "dependency-ownership-map.json",
10201
+ "cross-repo-summary.md",
10202
+ "workspace-context-map.json"
10203
+ ]) {
10204
+ sayAggregate(`[dry-run] would write .haus-workflow/${rel}`);
10205
+ }
10150
10206
  }
10151
10207
  const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
10152
10208
  client: config.client,
@@ -10267,6 +10323,14 @@ var WORKSPACE_AGGREGATE_FILES = [
10267
10323
  "cross-repo-summary.md",
10268
10324
  "workspace-context-map.json"
10269
10325
  ];
10326
+ function hasLegacyWorkspaceBlock(content2) {
10327
+ const beginIdx = content2.indexOf(BLOCK_BEGIN);
10328
+ if (beginIdx < 0) return false;
10329
+ const endIdx = content2.indexOf(BLOCK_END, beginIdx + BLOCK_BEGIN.length);
10330
+ if (endIdx < 0) return false;
10331
+ const block = content2.slice(beginIdx, endIdx + BLOCK_END.length);
10332
+ return block.includes("@.haus-workflow/cross-repo-summary.md") && block.includes("Member repos:");
10333
+ }
10270
10334
  async function runWorkspaceUndo(workspaceRoot, options = {}) {
10271
10335
  const config = await readWorkspaceConfig(workspaceRoot);
10272
10336
  if (!config) {
@@ -10329,8 +10393,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10329
10393
  const claudeMdPath = path69.join(workspaceRoot, "CLAUDE.md");
10330
10394
  if (existsSync9(claudeMdPath)) {
10331
10395
  const prev = await readText(claudeMdPath);
10332
- if (prev?.includes(BLOCK_BEGIN)) {
10333
- const next = stripHausBlock(prev);
10396
+ if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
10397
+ const next = stripHausBlock(prev, {
10398
+ begin: WORKSPACE_BLOCK_BEGIN,
10399
+ end: WORKSPACE_BLOCK_END
10400
+ });
10334
10401
  if (next.length === 0) {
10335
10402
  await fs47.remove(claudeMdPath);
10336
10403
  log("Removed CLAUDE.md (only contained the workspace haus import block).");
@@ -10338,6 +10405,17 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
10338
10405
  await fs47.writeFile(claudeMdPath, next, "utf8");
10339
10406
  log("Removed haus import block from CLAUDE.md (user content preserved).");
10340
10407
  }
10408
+ } else if (prev && hasLegacyWorkspaceBlock(prev)) {
10409
+ const next = stripHausBlock(prev, { begin: BLOCK_BEGIN, end: BLOCK_END });
10410
+ if (next.length === 0) {
10411
+ await fs47.remove(claudeMdPath);
10412
+ log("Removed CLAUDE.md (only contained the pre-upgrade workspace haus import block).");
10413
+ } else {
10414
+ await fs47.writeFile(claudeMdPath, next, "utf8");
10415
+ log(
10416
+ "Removed pre-upgrade workspace haus import block from CLAUDE.md (user content preserved)."
10417
+ );
10418
+ }
10341
10419
  }
10342
10420
  }
10343
10421
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {