@haus-tech/haus-workflow 0.13.2 → 0.14.0

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cli.js +749 -132
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.13.2...v0.14.0) (2026-06-04)
4
+
5
+ ### Features
6
+
7
+ - **workspace:** add repo auto-discovery ([#70](https://github.com/WeAreHausTech/haus-workflow/issues/70)) ([4b57571](https://github.com/WeAreHausTech/haus-workflow/commit/4b57571a32ef733dda0faf474d593b7fbb297a8e))
8
+ - **workspace:** extract shared setup core for workspace configuration ([#68](https://github.com/WeAreHausTech/haus-workflow/issues/68)) ([5e9b1ea](https://github.com/WeAreHausTech/haus-workflow/commit/5e9b1eaf72e8c7a32ec28148611f778367e4f1ef))
9
+ - **workspace:** manifest + drift doctor + command wiring (Tasks 4–5) ([#72](https://github.com/WeAreHausTech/haus-workflow/issues/72)) ([4526005](https://github.com/WeAreHausTech/haus-workflow/commit/4526005e693cc2f254ee6c1c5991a92f356506cc))
10
+ - **workspace:** per-repo setup loop + workspace aggregate layer ([#71](https://github.com/WeAreHausTech/haus-workflow/issues/71)) ([5c7ae9d](https://github.com/WeAreHausTech/haus-workflow/commit/5c7ae9d5691b0bd262c3449bad2fe649fd0271bf))
11
+
3
12
  ## [0.13.2](https://github.com/WeAreHausTech/haus-workflow/compare/v0.13.1...v0.13.2) (2026-06-04)
4
13
 
5
14
  ### Bug Fixes
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync3 } from "fs";
5
- import path28 from "path";
4
+ import { readFileSync as readFileSync4 } from "fs";
5
+ import path34 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
@@ -449,8 +449,8 @@ function buildDenyRules() {
449
449
  for (const command of DANGEROUS_COMMANDS) {
450
450
  rules.push(`Bash(${command}:*)`);
451
451
  }
452
- for (const path29 of SENSITIVE_PATHS) {
453
- const pattern = SENSITIVE_DIRS.has(path29) ? `${path29}/**` : path29;
452
+ for (const path35 of SENSITIVE_PATHS) {
453
+ const pattern = SENSITIVE_DIRS.has(path35) ? `${path35}/**` : path35;
454
454
  for (const tool of FILE_TOOLS) {
455
455
  rules.push(`${tool}(${pattern})`);
456
456
  }
@@ -814,7 +814,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
814
814
  estimatedTokenReductionPct: 0
815
815
  };
816
816
  const pkgRoot = packageRoot();
817
- const hausVersion = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
817
+ const hausVersion2 = (await readJson(path9.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
818
818
  const coreFiles = [
819
819
  claudePath(root, "settings.json"),
820
820
  claudePath(root, "rules", "haus.md"),
@@ -823,7 +823,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
823
823
  claudePath(root, "commands", "haus-review.md")
824
824
  ];
825
825
  const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
826
- const workflowPath = await writeWorkflow(root, hausVersion, dryRun);
826
+ const workflowPath = await writeWorkflow(root, hausVersion2, dryRun);
827
827
  const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
828
828
  refill: opts.refillConfig
829
829
  });
@@ -870,9 +870,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
870
870
  dryRun
871
871
  );
872
872
  const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
873
- const manifestPath = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
874
- const manifestDir = path9.dirname(manifestPath);
875
- const manifest = await readJson(manifestPath) ?? { items: [] };
873
+ const manifestPath2 = fixtureManifestPath ?? path9.join(pkgRoot, "library", "catalog", "manifest.json");
874
+ const manifestDir = path9.dirname(manifestPath2);
875
+ const manifest = await readJson(manifestPath2) ?? { items: [] };
876
876
  const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
877
877
  const cacheManifest = await readJson(
878
878
  path9.join(CACHE_DIR, "manifest.json")
@@ -943,7 +943,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
943
943
  id: r.id,
944
944
  type: r.type,
945
945
  source: isCurated ? "curated" : "haus",
946
- version: hausVersion,
946
+ version: hausVersion2,
947
947
  catalogRef: CATALOG_REF,
948
948
  hash: await hashInstalledPaths(root, relPaths),
949
949
  installMode: "copied",
@@ -2548,6 +2548,24 @@ async function runGuard(kind, _options) {
2548
2548
  import path18 from "path";
2549
2549
  import fs12 from "fs-extra";
2550
2550
 
2551
+ // src/utils/prompts.ts
2552
+ import { stdin as input, stdout as output } from "process";
2553
+ import readline from "readline/promises";
2554
+ async function ask(question) {
2555
+ const rl = readline.createInterface({ input, output });
2556
+ try {
2557
+ const answer = await rl.question(`${question}
2558
+ > `);
2559
+ return answer.trim();
2560
+ } finally {
2561
+ rl.close();
2562
+ }
2563
+ }
2564
+ async function confirm(question) {
2565
+ const answer = (await ask(`${question} [y/N]`)).toLowerCase();
2566
+ return answer === "y" || answer === "yes";
2567
+ }
2568
+
2551
2569
  // src/utils/exec.ts
2552
2570
  import { execa } from "execa";
2553
2571
  async function runCommand(command, args = [], options = {}) {
@@ -2832,64 +2850,11 @@ function buildStackSet(context) {
2832
2850
  );
2833
2851
  }
2834
2852
 
2835
- // src/utils/prompts.ts
2836
- import { stdin as input, stdout as output } from "process";
2837
- import readline from "readline/promises";
2838
- async function ask(question) {
2839
- const rl = readline.createInterface({ input, output });
2840
- try {
2841
- const answer = await rl.question(`${question}
2842
- > `);
2843
- return answer.trim();
2844
- } finally {
2845
- rl.close();
2846
- }
2847
- }
2848
- async function confirm(question) {
2849
- const answer = (await ask(`${question} [y/N]`)).toLowerCase();
2850
- return answer === "y" || answer === "yes";
2851
- }
2852
-
2853
- // src/commands/setup-project.ts
2854
- var GUIDED_QUESTIONS = [
2855
- "What is this project for?",
2856
- "Is it for a client, internal Haus work, or experimentation?",
2857
- "What should Claude help with most?",
2858
- "Is this project connected to other repositories?",
2859
- "Are there parts of the project Claude should avoid touching?",
2860
- "Are there client-specific rules or sensitive areas?",
2861
- "Do you want a minimal, standard, or strict setup?"
2862
- ];
2863
- async function runSetupProject(options) {
2864
- const root = process.cwd();
2865
- let mode = options.guided ? "guided" : "fast";
2866
- if (!options.guided && !options.fast && !options.json) {
2867
- log("How do you want to set this project up?");
2868
- log("1. Guided setup - I'll ask a few simple questions, then scan the project.");
2869
- log("2. Fast setup - I'll only scan the project and recommend defaults.");
2870
- const choice = await ask("Choose 1 or 2");
2871
- mode = choice === "1" ? "guided" : "fast";
2872
- }
2873
- if (mode === "guided") {
2874
- const existing = await readJson(hausPath(root, "setup-answers.json")) ?? {};
2875
- const merged = {};
2876
- for (const question of GUIDED_QUESTIONS) {
2877
- if (options.json) {
2878
- merged[question] = existing[question] ?? "pending-user-answer";
2879
- continue;
2880
- }
2881
- const prefilled = existing[question];
2882
- if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
2883
- merged[question] = prefilled;
2884
- continue;
2885
- }
2886
- const answer = await ask(question);
2887
- merged[question] = answer || prefilled || "no-answer";
2888
- }
2889
- await writeJson(hausPath(root, "setup-answers.json"), merged);
2890
- }
2853
+ // src/commands/setup-core.ts
2854
+ async function runSetupCore(root, opts) {
2855
+ const { mode, json, apply, dryRun, confirm: confirm2 } = opts;
2891
2856
  const scanResult = await scanProject(root, mode);
2892
- if (options.json) {
2857
+ if (json) {
2893
2858
  log(JSON.stringify(scanResult, null, 2));
2894
2859
  } else {
2895
2860
  log("Haus scan complete");
@@ -2905,7 +2870,7 @@ async function runSetupProject(options) {
2905
2870
  { id: "haus.rule.context-minimal", enabled: true },
2906
2871
  { id: "haus.rule.security", enabled: true }
2907
2872
  ]);
2908
- if (options.json) {
2873
+ if (json) {
2909
2874
  log(JSON.stringify(recommendation, null, 2));
2910
2875
  } else {
2911
2876
  log("Haus recommendation ready");
@@ -2916,6 +2881,7 @@ async function runSetupProject(options) {
2916
2881
  const warningLines = [.../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? []])];
2917
2882
  log(`Repo: ${context.repoName}`);
2918
2883
  for (const warning of warningLines) log(`- WARN: ${warning}`);
2884
+ const hooksOk = hooks.ok;
2919
2885
  if (hooks.skipped) {
2920
2886
  log(`- HOOKS: (skipped) ${hooks.message}`);
2921
2887
  } else if (!hooks.ok) {
@@ -2924,17 +2890,29 @@ async function runSetupProject(options) {
2924
2890
  } else {
2925
2891
  log(`- HOOKS OK: ${hooks.message}`);
2926
2892
  }
2927
- if (options.json) return;
2928
- const approved = await confirm("Approve and write Claude files now?");
2929
- if (!approved) {
2930
- log("Setup reviewed. No files written.");
2931
- log("Next step: run `haus apply --write` when ready.");
2932
- return;
2893
+ const baseResult = {
2894
+ root,
2895
+ repoName: context.repoName,
2896
+ roles: scanResult.repoRoles,
2897
+ recommendedCount: recommendation.recommended.length,
2898
+ warnings: warningLines,
2899
+ hooksOk,
2900
+ written: []
2901
+ };
2902
+ if (!apply) return baseResult;
2903
+ if (confirm2) {
2904
+ const approved = await confirm2();
2905
+ if (!approved) {
2906
+ log("Setup reviewed. No files written.");
2907
+ log("Next step: run `haus apply --write` when ready.");
2908
+ return baseResult;
2909
+ }
2933
2910
  }
2934
- const files = await writeClaudeFiles(root, false);
2911
+ const files = await writeClaudeFiles(root, dryRun ?? false);
2935
2912
  log("Applied files:");
2936
2913
  files.forEach((f) => log(`- ${displayPath(root, f)}`));
2937
2914
  const hooksAfter = await verifyProjectSettingsHooksContract(root);
2915
+ const hooksOkAfter = hooksAfter.ok;
2938
2916
  if (hooksAfter.skipped) {
2939
2917
  log(`- HOOKS: (skipped) ${hooksAfter.message}`);
2940
2918
  } else if (!hooksAfter.ok) {
@@ -2943,6 +2921,54 @@ async function runSetupProject(options) {
2943
2921
  } else {
2944
2922
  log(`- HOOKS OK: ${hooksAfter.message}`);
2945
2923
  }
2924
+ return { ...baseResult, hooksOk: hooksOkAfter, written: files };
2925
+ }
2926
+
2927
+ // src/commands/setup-project.ts
2928
+ var GUIDED_QUESTIONS = [
2929
+ "What is this project for?",
2930
+ "Is it for a client, internal Haus work, or experimentation?",
2931
+ "What should Claude help with most?",
2932
+ "Is this project connected to other repositories?",
2933
+ "Are there parts of the project Claude should avoid touching?",
2934
+ "Are there client-specific rules or sensitive areas?",
2935
+ "Do you want a minimal, standard, or strict setup?"
2936
+ ];
2937
+ async function runSetupProject(options) {
2938
+ const root = process.cwd();
2939
+ let mode = options.guided ? "guided" : "fast";
2940
+ if (!options.guided && !options.fast && !options.json) {
2941
+ log("How do you want to set this project up?");
2942
+ log("1. Guided setup - I'll ask a few simple questions, then scan the project.");
2943
+ log("2. Fast setup - I'll only scan the project and recommend defaults.");
2944
+ const choice = await ask("Choose 1 or 2");
2945
+ mode = choice === "1" ? "guided" : "fast";
2946
+ }
2947
+ if (mode === "guided") {
2948
+ const existing = await readJson(hausPath(root, "setup-answers.json")) ?? {};
2949
+ const merged = {};
2950
+ for (const question of GUIDED_QUESTIONS) {
2951
+ if (options.json) {
2952
+ merged[question] = existing[question] ?? "pending-user-answer";
2953
+ continue;
2954
+ }
2955
+ const prefilled = existing[question];
2956
+ if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
2957
+ merged[question] = prefilled;
2958
+ continue;
2959
+ }
2960
+ const answer = await ask(question);
2961
+ merged[question] = answer || prefilled || "no-answer";
2962
+ }
2963
+ await writeJson(hausPath(root, "setup-answers.json"), merged);
2964
+ }
2965
+ await runSetupCore(root, {
2966
+ mode,
2967
+ json: options.json,
2968
+ apply: !options.json,
2969
+ dryRun: false,
2970
+ confirm: () => confirm("Approve and write Claude files now?")
2971
+ });
2946
2972
  }
2947
2973
 
2948
2974
  // src/commands/init.ts
@@ -3512,9 +3538,9 @@ async function runUninstall(options = {}) {
3512
3538
  await writeSettings(stripped);
3513
3539
  result.hooksStripped = true;
3514
3540
  const hausDir = path23.join(globalClaudeDir(), "haus");
3515
- const manifestPath = hausManifestPath();
3516
- if (fs16.pathExistsSync(manifestPath)) {
3517
- await fs16.remove(manifestPath);
3541
+ const manifestPath2 = hausManifestPath();
3542
+ if (fs16.pathExistsSync(manifestPath2)) {
3543
+ await fs16.remove(manifestPath2);
3518
3544
  }
3519
3545
  if (fs16.pathExistsSync(hausDir)) {
3520
3546
  const remaining = await fs16.readdir(hausDir);
@@ -3831,13 +3857,13 @@ function walkMd(dir, fn) {
3831
3857
  else if (entry.name.endsWith(".md")) fn(full);
3832
3858
  }
3833
3859
  }
3834
- async function runValidateCatalog(manifestPath) {
3835
- if (!manifestPath) {
3860
+ async function runValidateCatalog(manifestPath2) {
3861
+ if (!manifestPath2) {
3836
3862
  error("Usage: haus validate-catalog <path/to/manifest.json>");
3837
3863
  process.exitCode = 1;
3838
3864
  return;
3839
3865
  }
3840
- const abs = path26.resolve(process.cwd(), manifestPath);
3866
+ const abs = path26.resolve(process.cwd(), manifestPath2);
3841
3867
  const manifestDir = path26.dirname(abs);
3842
3868
  const data = await readJson(abs);
3843
3869
  if (!data?.items) {
@@ -3867,77 +3893,665 @@ async function runValidateCatalog(manifestPath) {
3867
3893
  }
3868
3894
 
3869
3895
  // src/commands/workspace.ts
3896
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
3897
+ import path33 from "path";
3898
+
3899
+ // src/commands/workspace/aggregate.ts
3900
+ async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
3901
+ const summaries = repos.map((repo) => ({
3902
+ name: repo.name,
3903
+ path: repo.path,
3904
+ roles: repo.context.repoRoles ?? [],
3905
+ packageManager: repo.context.packageManager,
3906
+ deps: repo.context.dependencies ?? []
3907
+ }));
3908
+ const ownership = {};
3909
+ for (const repo of summaries) {
3910
+ for (const dep2 of repo.deps) {
3911
+ ownership[dep2] ??= [];
3912
+ ownership[dep2].push(repo.name);
3913
+ }
3914
+ }
3915
+ const roles = [...new Set(summaries.flatMap((r) => r.roles))].sort();
3916
+ const crossRepoHints = [...new Set(repos.flatMap((r) => r.context.crossRepoHints ?? []))].sort();
3917
+ const summaryPath = hausPath(workspaceRoot, "workspace-summary.json");
3918
+ const ownershipPath = hausPath(workspaceRoot, "dependency-ownership-map.json");
3919
+ const crossRepoPath = hausPath(workspaceRoot, "cross-repo-summary.md");
3920
+ const contextMapPath = hausPath(workspaceRoot, "workspace-context-map.json");
3921
+ await writeJson(summaryPath, {
3922
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3923
+ repos: summaries
3924
+ });
3925
+ await writeJson(ownershipPath, ownership);
3926
+ await writeText(
3927
+ crossRepoPath,
3928
+ `# Cross Repo Summary
3929
+
3930
+ ${summaries.map(
3931
+ (repo) => `- ${repo.name} (${repo.path}) roles: ${repo.roles.join(", ") || "unknown"}; package manager: ${repo.packageManager}`
3932
+ ).join("\n")}
3933
+ `
3934
+ );
3935
+ await writeJson(contextMapPath, {
3936
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3937
+ roles,
3938
+ crossRepoHints,
3939
+ repos: summaries.map((r) => ({
3940
+ name: r.name,
3941
+ path: r.path,
3942
+ roles: r.roles,
3943
+ packageManager: r.packageManager
3944
+ })),
3945
+ relationships
3946
+ });
3947
+ return [summaryPath, ownershipPath, crossRepoPath, contextMapPath];
3948
+ }
3949
+
3950
+ // src/commands/workspace/config.ts
3870
3951
  import path27 from "path";
3871
3952
  import YAML from "yaml";
3872
- async function runWorkspace(action) {
3873
- if (action === "init") {
3874
- await writeText(
3875
- "haus.workspace.yaml",
3876
- `client: unknown
3877
- repos:
3878
- - name: current
3879
- path: .
3880
- role: auto
3881
- relationships: []
3882
- `
3953
+ var WORKSPACE_FILE = "haus.workspace.yaml";
3954
+ function parseWorkspaceConfig(text) {
3955
+ if (!text) return void 0;
3956
+ let parsed;
3957
+ try {
3958
+ parsed = YAML.parse(text);
3959
+ } catch {
3960
+ return void 0;
3961
+ }
3962
+ if (!parsed || typeof parsed !== "object") return void 0;
3963
+ const obj = parsed;
3964
+ const repos = Array.isArray(obj.repos) ? obj.repos.filter(
3965
+ (r) => typeof r === "object" && r !== null && typeof r.name === "string" && typeof r.path === "string"
3966
+ ) : [];
3967
+ return {
3968
+ client: typeof obj.client === "string" ? obj.client : "unknown",
3969
+ repos,
3970
+ relationships: Array.isArray(obj.relationships) ? obj.relationships : []
3971
+ };
3972
+ }
3973
+ async function readWorkspaceConfig(workspaceRoot) {
3974
+ return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
3975
+ }
3976
+
3977
+ // src/commands/workspace/discover.ts
3978
+ import path28 from "path";
3979
+ import fg3 from "fast-glob";
3980
+ import YAML2 from "yaml";
3981
+ var DEFAULT_MAX_DEPTH = 3;
3982
+ var REPO_MARKERS = ["**/.git", "**/package.json", "**/composer.json"];
3983
+ var IGNORE = [
3984
+ "**/node_modules/**",
3985
+ "**/.git/**",
3986
+ "**/vendor/**",
3987
+ "**/dist/**",
3988
+ "**/.haus-workflow/**"
3989
+ ];
3990
+ function isDescendant(child, ancestor) {
3991
+ if (ancestor === ".") return child !== ".";
3992
+ return child === ancestor ? false : child.startsWith(`${ancestor}/`);
3993
+ }
3994
+ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
3995
+ const matches = await fg3(REPO_MARKERS, {
3996
+ cwd: workspaceRoot,
3997
+ dot: true,
3998
+ onlyFiles: false,
3999
+ deep: maxDepth,
4000
+ followSymbolicLinks: false,
4001
+ ignore: IGNORE
4002
+ });
4003
+ const gitDirs = /* @__PURE__ */ new Set();
4004
+ const manifestDirs = /* @__PURE__ */ new Set();
4005
+ for (const match of matches) {
4006
+ const base = path28.posix.basename(match);
4007
+ const dir = path28.posix.dirname(match);
4008
+ const owner = dir === "." ? "." : dir;
4009
+ if (base === ".git") gitDirs.add(owner);
4010
+ else manifestDirs.add(owner);
4011
+ }
4012
+ const repoRoots = [...gitDirs];
4013
+ const manifestSorted = [...manifestDirs].sort(
4014
+ (a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)
4015
+ );
4016
+ for (const dir of manifestSorted) {
4017
+ if (gitDirs.has(dir)) continue;
4018
+ if (repoRoots.some((root) => isDescendant(dir, root))) continue;
4019
+ repoRoots.push(dir);
4020
+ }
4021
+ repoRoots.sort((a, b) => a.localeCompare(b));
4022
+ return mapWithConcurrency(repoRoots, async (relDir) => {
4023
+ const absDir = path28.resolve(workspaceRoot, relDir);
4024
+ const pkg = await readJson(path28.join(absDir, "package.json"));
4025
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
4026
+ let role = "auto";
4027
+ try {
4028
+ const scan = await scanProject(absDir, "fast");
4029
+ if (scan.repoRoles[0]) role = scan.repoRoles[0];
4030
+ } catch {
4031
+ }
4032
+ return { name, path: relDir === "." ? "." : relDir, role };
4033
+ });
4034
+ }
4035
+ function mergeWorkspaceConfig(existing, discovered, opts = {}) {
4036
+ const existingRepos = existing?.repos ?? [];
4037
+ const byPath = new Map(existingRepos.map((r) => [r.path, r]));
4038
+ for (const repo of discovered) {
4039
+ if (!byPath.has(repo.path)) {
4040
+ byPath.set(repo.path, { name: repo.name, path: repo.path, role: repo.role });
4041
+ }
4042
+ }
4043
+ const ordered = [];
4044
+ const seen = /* @__PURE__ */ new Set();
4045
+ for (const repo of existingRepos) {
4046
+ ordered.push(byPath.get(repo.path));
4047
+ seen.add(repo.path);
4048
+ }
4049
+ for (const repo of discovered) {
4050
+ if (seen.has(repo.path)) continue;
4051
+ ordered.push(byPath.get(repo.path));
4052
+ seen.add(repo.path);
4053
+ }
4054
+ return {
4055
+ client: opts.client ?? existing?.client ?? "unknown",
4056
+ repos: ordered,
4057
+ relationships: existing?.relationships ?? []
4058
+ };
4059
+ }
4060
+ function renderWorkspaceYaml(config2) {
4061
+ return YAML2.stringify({
4062
+ client: config2.client,
4063
+ repos: config2.repos.map((r) => ({ name: r.name, path: r.path, role: r.role ?? "auto" })),
4064
+ relationships: config2.relationships
4065
+ });
4066
+ }
4067
+ async function runDiscover(workspaceRoot, opts = {}) {
4068
+ const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
4069
+ const existingText = await readText(yamlPath);
4070
+ const existing = parseWorkspaceConfig(existingText);
4071
+ if (existingText && !existing) {
4072
+ error(
4073
+ "Existing haus.workspace.yaml is malformed \u2014 fix or remove it before running discover (refusing to overwrite)."
3883
4074
  );
3884
- log("Workspace initialized.");
4075
+ process.exitCode = 1;
3885
4076
  return;
3886
4077
  }
3887
- const configText = await readText("haus.workspace.yaml");
3888
- if (!configText) {
3889
- error("Missing haus.workspace.yaml. Run `haus workspace init` first.");
4078
+ const discovered = await discoverRepos(workspaceRoot, opts.maxDepth ?? DEFAULT_MAX_DEPTH);
4079
+ if (discovered.length === 0) {
4080
+ error("No repos discovered under the workspace root.");
3890
4081
  process.exitCode = 1;
3891
4082
  return;
3892
4083
  }
3893
- const config2 = YAML.parse(configText);
3894
- const repos = config2.repos ?? [];
3895
- if (repos.length === 0) {
3896
- error("No repos configured in haus.workspace.yaml.");
3897
- process.exitCode = 1;
4084
+ const merged = mergeWorkspaceConfig(existing, discovered, { client: opts.client });
4085
+ const yamlText = renderWorkspaceYaml(merged);
4086
+ if (opts.json) {
4087
+ log(JSON.stringify({ discovered, config: merged }, null, 2));
4088
+ }
4089
+ if (opts.write) {
4090
+ await writeText(yamlPath, yamlText);
4091
+ log(`Wrote ${merged.repos.length} repo(s) to haus.workspace.yaml`);
3898
4092
  return;
3899
4093
  }
3900
- const summaries = [];
3901
- const ownership = {};
3902
- for (const repo of repos) {
3903
- const repoRoot = path27.resolve(process.cwd(), repo.path);
3904
- const result = await scanProject(repoRoot, "fast");
3905
- summaries.push({
4094
+ if (!opts.json) {
4095
+ log("Proposed haus.workspace.yaml (run with --write to persist):\n");
4096
+ log(yamlText);
4097
+ }
4098
+ }
4099
+
4100
+ // src/commands/workspace/doctor.ts
4101
+ import { existsSync as existsSync2 } from "fs";
4102
+ import path30 from "path";
4103
+
4104
+ // src/commands/workspace/manifest.ts
4105
+ import { readFileSync as readFileSync3 } from "fs";
4106
+ import path29 from "path";
4107
+ var MANIFEST_FILE = "workspace.manifest.json";
4108
+ function manifestPath(workspaceRoot) {
4109
+ return hausPath(workspaceRoot, MANIFEST_FILE);
4110
+ }
4111
+ function hausVersion() {
4112
+ try {
4113
+ const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
4114
+ return pkg.version ?? "0.0.0";
4115
+ } catch {
4116
+ return "0.0.0";
4117
+ }
4118
+ }
4119
+ function buildManifest2(opts) {
4120
+ const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
4121
+ const version = opts.version ?? hausVersion();
4122
+ return {
4123
+ version: 1,
4124
+ generatedAt: now,
4125
+ hausVersion: version,
4126
+ client: opts.client,
4127
+ repos: opts.repos.map((repo) => ({
3906
4128
  name: repo.name,
3907
4129
  path: repo.path,
3908
- roles: result.repoRoles,
3909
- packageManager: result.packageManager,
3910
- deps: result.dependencies
4130
+ role: repo.role,
4131
+ lastSetupAt: repo.lastSetupAt !== void 0 ? repo.lastSetupAt : repo.status === "ok" ? now : null,
4132
+ hausVersionAtSetup: repo.hausVersionAtSetup !== void 0 ? repo.hausVersionAtSetup : repo.status === "ok" ? version : null,
4133
+ lockItemCount: repo.lockItemCount,
4134
+ catalogRef: repo.catalogRef,
4135
+ status: repo.status,
4136
+ ...repo.error ? { error: repo.error } : {}
4137
+ }))
4138
+ };
4139
+ }
4140
+ async function readManifest2(workspaceRoot) {
4141
+ return readJson(manifestPath(workspaceRoot));
4142
+ }
4143
+ async function writeWorkspaceManifest(workspaceRoot, manifest) {
4144
+ const target = manifestPath(workspaceRoot);
4145
+ await writeJson(target, manifest);
4146
+ return target;
4147
+ }
4148
+
4149
+ // src/commands/workspace/doctor.ts
4150
+ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4151
+ const config2 = await readWorkspaceConfig(workspaceRoot);
4152
+ const manifest = await readManifest2(workspaceRoot);
4153
+ const currentVersion = hausVersion();
4154
+ const drift = [];
4155
+ const detail = [];
4156
+ const ok = (text) => detail.push({ stream: "log", text });
4157
+ const flag = (item) => {
4158
+ drift.push(item);
4159
+ detail.push({ stream: "warn", text: `- ${item.repo}: ${item.detail}` });
4160
+ };
4161
+ if (!config2) {
4162
+ flag({
4163
+ repo: "(workspace)",
4164
+ kind: "no-config",
4165
+ detail: "Missing or malformed haus.workspace.yaml \u2014 run `haus workspace discover --write` or `init`."
3911
4166
  });
3912
- for (const dep2 of result.dependencies) {
3913
- ownership[dep2] ??= [];
3914
- ownership[dep2].push(repo.name);
4167
+ return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
4168
+ }
4169
+ if (!manifest) {
4170
+ flag({
4171
+ repo: "(workspace)",
4172
+ kind: "no-manifest",
4173
+ detail: "No workspace.manifest.json \u2014 run `haus workspace setup --write` first."
4174
+ });
4175
+ return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
4176
+ }
4177
+ const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
4178
+ for (const repo of config2.repos) {
4179
+ const repoRoot = path30.resolve(workspaceRoot, repo.path);
4180
+ const entry = manifestByName.get(repo.name);
4181
+ if (!entry) {
4182
+ flag({
4183
+ repo: repo.name,
4184
+ kind: "missing-from-manifest",
4185
+ detail: "Configured in yaml but absent from the manifest \u2014 run `haus workspace setup --write`."
4186
+ });
4187
+ continue;
4188
+ }
4189
+ const driftBefore = drift.length;
4190
+ if (entry.status === "failed") {
4191
+ flag({
4192
+ repo: repo.name,
4193
+ kind: "failed",
4194
+ detail: `Last setup failed${entry.error ? `: ${entry.error}` : ""}.`
4195
+ });
4196
+ }
4197
+ if (entry.hausVersionAtSetup && entry.hausVersionAtSetup !== currentVersion) {
4198
+ flag({
4199
+ repo: repo.name,
4200
+ kind: "version-mismatch",
4201
+ detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
4202
+ });
4203
+ }
4204
+ if (!existsSync2(claudePath(repoRoot))) {
4205
+ flag({
4206
+ repo: repo.name,
4207
+ kind: "missing-claude",
4208
+ detail: "Missing .claude/ \u2014 run `haus workspace setup --write`."
4209
+ });
4210
+ }
4211
+ const lock = await checkLock(repoRoot);
4212
+ if (!existsSync2(hausPath(repoRoot, "haus.lock.json"))) {
4213
+ flag({
4214
+ repo: repo.name,
4215
+ kind: "missing-lock",
4216
+ detail: "Missing .haus-workflow/haus.lock.json \u2014 run `haus workspace setup --write`."
4217
+ });
4218
+ } else if (lock.count > 0 && !lock.ok) {
4219
+ flag({
4220
+ repo: repo.name,
4221
+ kind: "invalid-lock",
4222
+ detail: "haus.lock.json present but invalid \u2014 re-run `haus workspace setup --write`."
4223
+ });
4224
+ }
4225
+ if (drift.length === driftBefore) {
4226
+ ok(`- ${repo.name}: OK (${lock.count} lock item(s))`);
3915
4227
  }
3916
4228
  }
3917
- await writeJson(".haus-workflow/workspace-summary.json", {
3918
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3919
- repos: summaries
3920
- });
3921
- await writeJson(".haus-workflow/dependency-ownership-map.json", ownership);
3922
- await writeText(
3923
- ".haus-workflow/cross-repo-summary.md",
3924
- `# Cross Repo Summary
4229
+ return emit({ workspaceRoot, manifest, drift, detail, json: opts.json });
4230
+ }
4231
+ function emit(args) {
4232
+ const { workspaceRoot, manifest, drift, detail } = args;
4233
+ if (args.json) {
4234
+ log(JSON.stringify({ manifest: manifest ?? null, drift }, null, 2));
4235
+ } else {
4236
+ if (drift.length === 0) {
4237
+ log("\u2705 Workspace is set up and healthy.");
4238
+ } else {
4239
+ log(`\u26A0\uFE0F ${drift.length} workspace drift item(s) need attention:`);
4240
+ }
4241
+ log("Haus Workspace Doctor");
4242
+ for (const line2 of detail) {
4243
+ if (line2.stream === "warn") warn(line2.text);
4244
+ else log(line2.text);
4245
+ }
4246
+ }
4247
+ if (drift.length > 0) process.exitCode = 1;
4248
+ return { workspaceRoot, manifest, drift };
4249
+ }
3925
4250
 
3926
- ${summaries.map(
3927
- (repo) => `- ${repo.name} (${repo.path}) roles: ${repo.roles.join(", ") || "unknown"}; package manager: ${repo.packageManager}`
3928
- ).join("\n")}
4251
+ // src/commands/workspace/setup.ts
4252
+ import { existsSync as existsSync3, statSync } from "fs";
4253
+ import path32 from "path";
4254
+
4255
+ // src/claude/write-workspace-claude-md.ts
4256
+ import path31 from "path";
4257
+ import fs18 from "fs-extra";
4258
+ function buildWorkspaceImportBlock(client, members) {
4259
+ const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
4260
+ const body = [
4261
+ "@.haus-workflow/cross-repo-summary.md",
4262
+ "",
4263
+ `# Workspace: ${client}`,
4264
+ "",
4265
+ "Member repos:",
4266
+ ...memberLines
4267
+ ].join("\n");
4268
+ return `${BLOCK_BEGIN}
4269
+ ${body}
4270
+ ${BLOCK_END}`;
4271
+ }
4272
+ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4273
+ const block = buildWorkspaceImportBlock(opts.client, opts.members);
4274
+ const dryRun = opts.dryRun ?? false;
4275
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
4276
+ const prev = await fs18.pathExists(filePath) ? await fs18.readFile(filePath, "utf8") : "";
4277
+ const next = opts.collision ? `${block}
4278
+ ` : injectHausBlock(prev, block);
4279
+ const printable = displayPath(workspaceRoot, filePath);
4280
+ if (dryRun) {
4281
+ if (!prev) {
4282
+ log(createUnifiedDiff(printable, "", next));
4283
+ } else if (hasTextChanged(prev, next)) {
4284
+ log(createUnifiedDiff(printable, prev, next));
4285
+ } else {
4286
+ log(`${printable}: unchanged`);
4287
+ }
4288
+ return filePath;
4289
+ }
4290
+ if (hasTextChanged(prev, next) && prev.length > 0) {
4291
+ const diffText = createUnifiedDiff(printable, prev, next);
4292
+ const summary = summarizeDiff(diffText);
4293
+ log(`Overwriting ${printable} (diff +${summary.additions} -${summary.deletions})`);
4294
+ }
4295
+ await writeText(filePath, next);
4296
+ return filePath;
4297
+ }
4298
+
4299
+ // src/commands/workspace/setup.ts
4300
+ function resolveWorkspaceRoot(start = process.cwd()) {
4301
+ let dir = path32.resolve(start);
4302
+ for (; ; ) {
4303
+ if (existsSync3(path32.join(dir, WORKSPACE_FILE))) return dir;
4304
+ const parent = path32.dirname(dir);
4305
+ if (parent === dir) return path32.resolve(start);
4306
+ dir = parent;
4307
+ }
4308
+ }
4309
+ function isRootRepo(workspaceRoot, repoPath) {
4310
+ return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
4311
+ }
4312
+ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4313
+ const mode = options.mode ?? "fast";
4314
+ const apply = options.write ?? false;
4315
+ const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
4316
+ if (!configText) {
4317
+ error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4318
+ process.exitCode = 1;
4319
+ return { workspaceRoot, statuses: [], written: [] };
4320
+ }
4321
+ const config2 = parseWorkspaceConfig(configText);
4322
+ if (!config2 || config2.repos.length === 0) {
4323
+ error(`No repos configured in ${WORKSPACE_FILE}.`);
4324
+ process.exitCode = 1;
4325
+ return { workspaceRoot, statuses: [], written: [] };
4326
+ }
4327
+ const onlySet = options.only && options.only.length > 0 ? new Set(options.only) : void 0;
4328
+ const repos = onlySet ? config2.repos.filter((r) => onlySet.has(r.name)) : config2.repos;
4329
+ const statuses = [];
4330
+ const aggregateInputs = [];
4331
+ for (const repo of repos) {
4332
+ const repoRoot = path32.resolve(workspaceRoot, repo.path);
4333
+ log(`
4334
+ \u2192 ${repo.name} (${repo.path})`);
4335
+ try {
4336
+ if (!existsSync3(repoRoot) || !statSync(repoRoot).isDirectory()) {
4337
+ throw new Error(`Repo path is not a directory: ${repo.path}`);
4338
+ }
4339
+ const res = await runSetupCore(repoRoot, {
4340
+ mode,
4341
+ json: options.json,
4342
+ apply,
4343
+ dryRun: options.dryRun
4344
+ });
4345
+ statuses.push({
4346
+ name: repo.name,
4347
+ path: repo.path,
4348
+ root: repoRoot,
4349
+ status: "ok",
4350
+ roles: res.roles,
4351
+ recommendedCount: res.recommendedCount
4352
+ });
4353
+ const context = await readContextOrScan(repoRoot);
4354
+ aggregateInputs.push({ name: repo.name, path: repo.path, context });
4355
+ } catch (err) {
4356
+ const message = err instanceof Error ? err.message : String(err);
4357
+ statuses.push({
4358
+ name: repo.name,
4359
+ path: repo.path,
4360
+ root: repoRoot,
4361
+ status: "failed",
4362
+ error: message
4363
+ });
4364
+ if (!options.continueOnError) throw err;
4365
+ error(`Setup failed for ${repo.name}: ${message}`);
4366
+ }
4367
+ }
4368
+ const written = [];
4369
+ if (apply && aggregateInputs.length > 0) {
4370
+ const collision = config2.repos.some((r) => isRootRepo(workspaceRoot, r.path));
4371
+ if (!options.dryRun) {
4372
+ const artifacts = await writeWorkspaceArtifacts(
4373
+ workspaceRoot,
4374
+ aggregateInputs,
4375
+ config2.relationships
4376
+ );
4377
+ written.push(...artifacts);
4378
+ }
4379
+ const docPath = await writeWorkspaceClaudeMd(workspaceRoot, {
4380
+ client: config2.client,
4381
+ members: config2.repos.map((r) => ({ name: r.name, path: r.path })),
4382
+ collision,
4383
+ dryRun: options.dryRun
4384
+ });
4385
+ written.push(docPath);
4386
+ }
4387
+ if (apply && !options.dryRun) {
4388
+ const statusByName = new Map(statuses.map((s) => [s.name, s]));
4389
+ const prior = await readManifest2(workspaceRoot);
4390
+ if (!prior && existsSync3(manifestPath(workspaceRoot))) {
4391
+ warn(
4392
+ "Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
4393
+ );
4394
+ }
4395
+ const priorByName = new Map((prior?.repos ?? []).map((r) => [r.name, r]));
4396
+ const manifestRepos = [];
4397
+ for (const repo of config2.repos) {
4398
+ const status = statusByName.get(repo.name);
4399
+ const role = repo.role ?? status?.roles?.[0] ?? "auto";
4400
+ if (status?.status === "ok") {
4401
+ const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
4402
+ manifestRepos.push({
4403
+ name: repo.name,
4404
+ path: repo.path,
4405
+ role,
4406
+ status: "ok",
4407
+ lockItemCount: lock.count,
4408
+ catalogRef: lock.catalogRef
4409
+ });
4410
+ } else if (status?.status === "failed") {
4411
+ manifestRepos.push({
4412
+ name: repo.name,
4413
+ path: repo.path,
4414
+ role,
4415
+ status: "failed",
4416
+ lockItemCount: 0,
4417
+ catalogRef: null,
4418
+ error: status.error
4419
+ });
4420
+ } else {
4421
+ const carried = priorByName.get(repo.name);
4422
+ manifestRepos.push(
4423
+ carried ? {
4424
+ name: carried.name,
4425
+ path: carried.path,
4426
+ role: carried.role,
4427
+ status: carried.status,
4428
+ lockItemCount: carried.lockItemCount,
4429
+ catalogRef: carried.catalogRef,
4430
+ lastSetupAt: carried.lastSetupAt,
4431
+ hausVersionAtSetup: carried.hausVersionAtSetup,
4432
+ ...carried.error ? { error: carried.error } : {}
4433
+ } : {
4434
+ name: repo.name,
4435
+ path: repo.path,
4436
+ role,
4437
+ status: "pending",
4438
+ lockItemCount: 0,
4439
+ catalogRef: null
4440
+ }
4441
+ );
4442
+ }
4443
+ }
4444
+ const manifest = buildManifest2({ client: config2.client, repos: manifestRepos });
4445
+ const manifestFile = await writeWorkspaceManifest(workspaceRoot, manifest);
4446
+ written.push(manifestFile);
4447
+ }
4448
+ const ok = statuses.filter((s) => s.status === "ok").length;
4449
+ const failed = statuses.length - ok;
4450
+ log(`
4451
+ Workspace setup complete: ${ok} ok, ${failed} failed.`);
4452
+ return { workspaceRoot, statuses, written };
4453
+ }
4454
+
4455
+ // src/commands/workspace.ts
4456
+ function normalizeOnly(only) {
4457
+ if (!only) return void 0;
4458
+ const list = Array.isArray(only) ? only : only.split(/[\s,]+/);
4459
+ const cleaned = list.map((s) => s.trim()).filter(Boolean);
4460
+ return cleaned.length > 0 ? cleaned : void 0;
4461
+ }
4462
+ function normalizeMaxDepth(maxDepth) {
4463
+ if (maxDepth === void 0) return void 0;
4464
+ const n = typeof maxDepth === "number" ? maxDepth : Number.parseInt(maxDepth, 10);
4465
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : void 0;
4466
+ }
4467
+ async function initWorkspace() {
4468
+ await writeText(
4469
+ WORKSPACE_FILE,
4470
+ `client: unknown
4471
+ repos:
4472
+ - name: current
4473
+ path: .
4474
+ role: auto
4475
+ relationships: []
3929
4476
  `
3930
4477
  );
3931
- log(
3932
- "Workspace scan complete. Wrote .haus-workflow/workspace-summary.json, cross-repo-summary.md, dependency-ownership-map.json"
3933
- );
4478
+ log("Workspace initialized.");
4479
+ }
4480
+ async function scanWorkspace(workspaceRoot, opts) {
4481
+ const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
4482
+ if (!configText) {
4483
+ error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4484
+ process.exitCode = 1;
4485
+ return;
4486
+ }
4487
+ const config2 = parseWorkspaceConfig(configText);
4488
+ if (!config2) {
4489
+ error(
4490
+ `Malformed ${WORKSPACE_FILE}. Fix the YAML or re-run \`haus workspace discover --write\`.`
4491
+ );
4492
+ process.exitCode = 1;
4493
+ return;
4494
+ }
4495
+ if (config2.repos.length === 0) {
4496
+ error(`No repos configured in ${WORKSPACE_FILE}.`);
4497
+ process.exitCode = 1;
4498
+ return;
4499
+ }
4500
+ const inputs = [];
4501
+ for (const repo of config2.repos) {
4502
+ const repoRoot = path33.resolve(workspaceRoot, repo.path);
4503
+ if (!existsSync4(repoRoot) || !statSync2(repoRoot).isDirectory()) {
4504
+ throw new Error(`Repo path is not a directory: ${repo.path}`);
4505
+ }
4506
+ const result = await scanProject(repoRoot, "fast");
4507
+ inputs.push({ name: repo.name, path: repo.path, context: result });
4508
+ }
4509
+ const written = await writeWorkspaceArtifacts(workspaceRoot, inputs, config2.relationships);
4510
+ if (opts.json) {
4511
+ log(JSON.stringify({ written }, null, 2));
4512
+ } else {
4513
+ log(`Workspace scan complete. Wrote ${written.length} artifact(s) under .haus-workflow/.`);
4514
+ }
4515
+ }
4516
+ async function runWorkspace(action, options = {}) {
4517
+ if (action === "init") {
4518
+ await initWorkspace();
4519
+ return;
4520
+ }
4521
+ const workspaceRoot = resolveWorkspaceRoot();
4522
+ switch (action) {
4523
+ case "discover":
4524
+ await runDiscover(workspaceRoot, {
4525
+ write: options.write,
4526
+ json: options.json,
4527
+ maxDepth: normalizeMaxDepth(options.maxDepth),
4528
+ client: options.client
4529
+ });
4530
+ return;
4531
+ case "scan":
4532
+ await scanWorkspace(workspaceRoot, { json: options.json });
4533
+ return;
4534
+ case "setup":
4535
+ await runWorkspaceSetup(workspaceRoot, {
4536
+ mode: options.guided ? "guided" : "fast",
4537
+ write: options.write,
4538
+ dryRun: options.dryRun,
4539
+ json: options.json,
4540
+ continueOnError: options.continueOnError,
4541
+ only: normalizeOnly(options.only)
4542
+ });
4543
+ return;
4544
+ case "doctor":
4545
+ await runWorkspaceDoctor(workspaceRoot, { json: options.json });
4546
+ return;
4547
+ }
3934
4548
  }
3935
4549
 
3936
4550
  // src/cli.ts
3937
4551
  function cliVersion() {
3938
4552
  try {
3939
- const pkgPath = path28.join(packageRoot(), "package.json");
3940
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
4553
+ const pkgPath = path34.join(packageRoot(), "package.json");
4554
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
3941
4555
  return pkg.version ?? "0.0.0";
3942
4556
  } catch {
3943
4557
  return "0.0.0";
@@ -3946,8 +4560,8 @@ function cliVersion() {
3946
4560
  var program = new Command();
3947
4561
  function validateRuntimeNodeVersion() {
3948
4562
  try {
3949
- const pkgPath = path28.join(packageRoot(), "package.json");
3950
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
4563
+ const pkgPath = path34.join(packageRoot(), "package.json");
4564
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
3951
4565
  const requiredRange = pkg.engines?.node;
3952
4566
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
3953
4567
  throw new Error(`Node ${process.version} does not satisfy required range ${requiredRange}`);
@@ -3990,7 +4604,10 @@ config.command("disable <key>").description("Disable a hook (hook.context)").act
3990
4604
  config.command("status <key>").description("Show current state of a hook (hook.context)").action((key) => runConfig(key, "status"));
3991
4605
  var workspace = program.command("workspace");
3992
4606
  workspace.command("init").action(() => runWorkspace("init"));
3993
- workspace.command("scan").action(() => runWorkspace("scan"));
4607
+ workspace.command("discover").description("Auto-find member repos and write/merge haus.workspace.yaml").option("--write", "Persist haus.workspace.yaml (default previews only)").option("--json", "Output the discovered repos and proposed config as JSON").option("--max-depth <n>", "Max directory depth to traverse (default 3)").option("--client <name>", "Set the workspace client name").action((opts) => runWorkspace("discover", opts));
4608
+ workspace.command("scan").description("Aggregate a cross-repo summary from a fast scan of each repo").option("--json", "Output the written artifact paths as JSON").action((opts) => runWorkspace("scan", opts));
4609
+ workspace.command("setup").description("Per-repo setup loop + workspace layer + manifest").option("--write", "Apply changes (default previews only)").option("--dry-run", "Preview changes without writing").option("--json", "Emit machine-readable per-repo output").option("--fast", "Skip interactive prompts (default)").option("--guided", "Enable guided Q&A per repo").option("--continue-on-error", "Keep going past a failed repo (default fail-fast)").option("--only <names>", "Restrict to comma-separated repo names").action((opts) => runWorkspace("setup", opts));
4610
+ workspace.command("doctor").description("Report workspace drift against the manifest").option("--json", "Output the manifest and drift array as JSON").action((opts) => runWorkspace("doctor", opts));
3994
4611
  program.parseAsync(process.argv).catch((err) => {
3995
4612
  const message = err instanceof Error ? err.message : String(err);
3996
4613
  error(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {