@haus-tech/haus-workflow 5.3.0 → 5.4.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.
package/dist/cli.js CHANGED
@@ -1226,7 +1226,8 @@ function parseManifest(json) {
1226
1226
  version: root.version,
1227
1227
  manifestFormat: format.format,
1228
1228
  items,
1229
- ...root.globalPlugins !== void 0 ? { globalPlugins: root.globalPlugins } : {}
1229
+ ...root.globalPlugins !== void 0 ? { globalPlugins: root.globalPlugins } : {},
1230
+ ...root.profiles !== void 0 ? { profiles: root.profiles } : {}
1230
1231
  },
1231
1232
  warnings
1232
1233
  };
@@ -1623,6 +1624,7 @@ var init_ingest_catalog = __esm({
1623
1624
  // src/catalog/remote-catalog/sync.ts
1624
1625
  var sync_exports = {};
1625
1626
  __export(sync_exports, {
1627
+ buildCacheManifest: () => buildCacheManifest,
1626
1628
  getCacheManifestAge: () => getCacheManifestAge,
1627
1629
  syncRemoteCatalog: () => syncRemoteCatalog
1628
1630
  });
@@ -1859,6 +1861,16 @@ async function syncOneItem(item, base) {
1859
1861
  return "failed";
1860
1862
  }
1861
1863
  }
1864
+ function buildCacheManifest(input2) {
1865
+ return {
1866
+ version: input2.version,
1867
+ manifestFormat: input2.manifestFormat,
1868
+ ...input2.catalogRef !== null ? { catalogRef: input2.catalogRef } : {},
1869
+ items: input2.items,
1870
+ ...input2.globalPlugins !== void 0 ? { globalPlugins: input2.globalPlugins } : {},
1871
+ ...input2.profiles !== void 0 ? { profiles: input2.profiles } : {}
1872
+ };
1873
+ }
1862
1874
  async function syncRemoteCatalog(projectRoot) {
1863
1875
  if (isOffline()) {
1864
1876
  warn("Offline \u2014 skipping catalog sync; using cached or bundled content.");
@@ -1888,7 +1900,7 @@ async function syncRemoteCatalog(projectRoot) {
1888
1900
  ...getPrivateCatalogDenial() ? { accessDenied: getPrivateCatalogDenial() } : {}
1889
1901
  };
1890
1902
  }
1891
- const { version, manifestFormat, items, globalPlugins } = fetched.manifest;
1903
+ const { version, manifestFormat, items, globalPlugins, profiles } = fetched.manifest;
1892
1904
  const cacheDir = getCacheDir();
1893
1905
  const catalogRef = expectedCatalogRef(projectRoot);
1894
1906
  try {
@@ -1911,13 +1923,7 @@ async function syncRemoteCatalog(projectRoot) {
1911
1923
  // per-project pin (#239) means two projects can want two different refs: without
1912
1924
  // this field, whichever synced last silently serves its content to the other.
1913
1925
  `${JSON.stringify(
1914
- {
1915
- version,
1916
- manifestFormat,
1917
- ...catalogRef !== null ? { catalogRef } : {},
1918
- items,
1919
- ...globalPlugins !== void 0 ? { globalPlugins } : {}
1920
- },
1926
+ buildCacheManifest({ version, manifestFormat, catalogRef, items, globalPlugins, profiles }),
1921
1927
  null,
1922
1928
  2
1923
1929
  )}
@@ -1998,7 +2004,7 @@ var init_sync = __esm({
1998
2004
  // src/cli.ts
1999
2005
  init_offline();
2000
2006
  import { readFileSync as readFileSync5 } from "node:fs";
2001
- import path91 from "node:path";
2007
+ import path92 from "node:path";
2002
2008
  import { Command, Option } from "commander";
2003
2009
 
2004
2010
  // src/commands/apply.ts
@@ -2210,13 +2216,14 @@ import path9 from "node:path";
2210
2216
  import fs7 from "fs-extra";
2211
2217
  var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
2212
2218
  async function readWorkflowTemplate(opts = {}) {
2213
- const dest = path9.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
2219
+ const rel = opts.templateRel ?? WORKFLOW_TEMPLATE_REL;
2220
+ const dest = path9.join(getCacheDir(), rel);
2214
2221
  if (isOffline()) {
2215
2222
  if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
2216
2223
  return null;
2217
2224
  }
2218
2225
  const base = await remoteBase(opts.projectRoot);
2219
- const { url, headers } = await catalogContentRequest(base, WORKFLOW_TEMPLATE_REL);
2226
+ const { url, headers } = await catalogContentRequest(base, rel);
2220
2227
  const text = await fetchText(url, headers);
2221
2228
  if (text === null) {
2222
2229
  if (await fs7.pathExists(dest)) return fs7.readFile(dest, "utf8");
@@ -2251,7 +2258,8 @@ async function loadCatalogContext(root) {
2251
2258
  source: "fixture",
2252
2259
  manifestFormat: assertSupportedManifestFormat(data2?.manifestFormat, "The fixture catalog"),
2253
2260
  version: manifestVersion(data2?.version),
2254
- globalPlugins: data2?.globalPlugins
2261
+ globalPlugins: data2?.globalPlugins,
2262
+ profiles: data2?.profiles
2255
2263
  };
2256
2264
  }
2257
2265
  const cacheDir = getCacheDir();
@@ -2273,7 +2281,8 @@ async function loadCatalogContext(root) {
2273
2281
  "The cached catalog"
2274
2282
  ),
2275
2283
  version: manifestVersion(cacheData.version),
2276
- globalPlugins: cacheData.globalPlugins
2284
+ globalPlugins: cacheData.globalPlugins,
2285
+ profiles: cacheData.profiles
2277
2286
  };
2278
2287
  }
2279
2288
  }
@@ -2289,7 +2298,8 @@ async function loadCatalogContext(root) {
2289
2298
  "The project-local catalog"
2290
2299
  ),
2291
2300
  version: manifestVersion(localData.version),
2292
- globalPlugins: localData.globalPlugins
2301
+ globalPlugins: localData.globalPlugins,
2302
+ profiles: localData.profiles
2293
2303
  };
2294
2304
  }
2295
2305
  const packageManifest = path11.join(packageRoot(), "library/catalog/manifest.json");
@@ -2300,7 +2310,8 @@ async function loadCatalogContext(root) {
2300
2310
  source: "bundled",
2301
2311
  manifestFormat: assertSupportedManifestFormat(data?.manifestFormat, "The bundled catalog"),
2302
2312
  version: manifestVersion(data?.version),
2303
- globalPlugins: data?.globalPlugins
2313
+ globalPlugins: data?.globalPlugins,
2314
+ profiles: data?.profiles
2304
2315
  };
2305
2316
  }
2306
2317
  async function catalogFormatRefusal(root) {
@@ -2509,7 +2520,7 @@ async function migrateRequestState(root, dryRun, notify) {
2509
2520
 
2510
2521
  // src/claude/write-claude-files.ts
2511
2522
  import path27 from "node:path";
2512
- import fs25 from "fs-extra";
2523
+ import fs26 from "fs-extra";
2513
2524
  init_ingest_catalog();
2514
2525
 
2515
2526
  // src/catalog/skill-description.ts
@@ -2845,6 +2856,206 @@ function mergeRecommendationWarnings(context, isLinkedWorktree = false) {
2845
2856
  return [.../* @__PURE__ */ new Set([...statusLines, ...context.warnings, ...riskLines])];
2846
2857
  }
2847
2858
 
2859
+ // src/claude/project-profile.ts
2860
+ init_project_pin();
2861
+ init_logger();
2862
+
2863
+ // src/claude/project-intent.ts
2864
+ init_project_pin();
2865
+ init_fs();
2866
+ init_logger();
2867
+ init_paths();
2868
+ import fs10 from "fs-extra";
2869
+ function normalizeIdList(value) {
2870
+ if (!Array.isArray(value)) return [];
2871
+ return [
2872
+ ...new Set(
2873
+ value.filter((entry) => typeof entry === "string" && entry.length > 0)
2874
+ )
2875
+ ];
2876
+ }
2877
+ function normalizeSignalList(value) {
2878
+ return [...new Set(normalizeIdList(value).map((entry) => entry.toLowerCase()))];
2879
+ }
2880
+ async function readProjectConfigObject(root) {
2881
+ try {
2882
+ const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
2883
+ if (data == null || typeof data !== "object" || Array.isArray(data)) return null;
2884
+ return data;
2885
+ } catch (err) {
2886
+ if (err.code === "ENOENT") return null;
2887
+ warn(
2888
+ `${PROJECT_CONFIG_REL} could not be read (${err instanceof Error ? err.message : String(err)}) \u2014 treating project intent as unset`
2889
+ );
2890
+ return null;
2891
+ }
2892
+ }
2893
+ async function updateProjectConfig(root, mutate) {
2894
+ const file = hausPath(root, PROJECT_CONFIG_FILE);
2895
+ let existing = {};
2896
+ if (await fs10.pathExists(file)) {
2897
+ try {
2898
+ const parsed = JSON.parse(await fs10.readFile(file, "utf8"));
2899
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2900
+ existing = { ...parsed };
2901
+ } else {
2902
+ warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with haus-managed keys only`);
2903
+ }
2904
+ } catch (err) {
2905
+ warn(
2906
+ `${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with haus-managed keys only`
2907
+ );
2908
+ }
2909
+ }
2910
+ await writeJson(file, mutate(existing));
2911
+ }
2912
+ async function readProjectIncludedIds(root) {
2913
+ const data = await readProjectConfigObject(root);
2914
+ return data == null ? [] : normalizeIdList(data.includedIds);
2915
+ }
2916
+ async function addProjectIncludedIds(root, ids) {
2917
+ const toAdd = normalizeIdList(ids);
2918
+ if (toAdd.length === 0) return;
2919
+ await updateProjectConfig(root, (existing) => ({
2920
+ ...existing,
2921
+ includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
2922
+ }));
2923
+ }
2924
+ function withdrawIncludedIds(existing, ids) {
2925
+ const drop = new Set(ids);
2926
+ return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
2927
+ }
2928
+ async function readDeclaredContext(root) {
2929
+ const data = await readProjectConfigObject(root);
2930
+ if (data == null) return { roles: [], stacks: [] };
2931
+ return {
2932
+ roles: normalizeSignalList(data.declaredRoles),
2933
+ stacks: normalizeSignalList(data.declaredStacks)
2934
+ };
2935
+ }
2936
+
2937
+ // src/claude/project-profile.ts
2938
+ var PROFILE_IDS = ["production", "poc"];
2939
+ var DEFAULT_PROFILE = "production";
2940
+ var PROFILES = {
2941
+ production: {
2942
+ workflowTemplateRel: WORKFLOW_TEMPLATE_REL,
2943
+ writesCiGate: true,
2944
+ writesDecisionsSeed: true,
2945
+ droppedHookCommands: [],
2946
+ workflowConfig: "standard"
2947
+ },
2948
+ poc: {
2949
+ // Catalog item `haus.agentic-workflow-poc` (catalog ADR-0066); fetched by path for WORKFLOW.md, exactly like the production standard, and kept out of item recommendations by id (Task 3).
2950
+ workflowTemplateRel: "templates/agentic-workflow-poc.md",
2951
+ writesCiGate: false,
2952
+ writesDecisionsSeed: false,
2953
+ droppedHookCommands: ["haus decisions guard --from-hook", "haus decisions suggest --from-hook"],
2954
+ workflowConfig: "poc"
2955
+ }
2956
+ };
2957
+ function isProfileId(value) {
2958
+ return typeof value === "string" && PROFILE_IDS.includes(value);
2959
+ }
2960
+ function parseProfile(value) {
2961
+ if (value === void 0 || value === null) return { profile: DEFAULT_PROFILE, malformed: false };
2962
+ if (isProfileId(value)) return { profile: value, malformed: false };
2963
+ return { profile: DEFAULT_PROFILE, malformed: true };
2964
+ }
2965
+ async function readProjectProfile(root) {
2966
+ const data = await readProjectConfigObject(root);
2967
+ const { profile, malformed } = parseProfile(data?.profile);
2968
+ if (malformed) {
2969
+ warn(
2970
+ `${PROJECT_CONFIG_REL}: unknown profile ${JSON.stringify(data?.profile)} \u2014 expected one of ${PROFILE_IDS.join(", ")}; treating as ${DEFAULT_PROFILE}`
2971
+ );
2972
+ }
2973
+ return profile;
2974
+ }
2975
+ async function writeProjectProfile(root, profile) {
2976
+ await updateProjectConfig(root, (existing) => ({ ...existing, profile }));
2977
+ }
2978
+ function pocLockedMessage() {
2979
+ return `This repo is a PoC (profile "poc" in ${PROJECT_CONFIG_REL}). Production starts from zero in a new repo, with docs/MANUS.md as input (\`/haus-workflow new\` in production mode). To re-run setup here, pass \`haus init --profile poc\`. To leave the PoC profile on purpose, remove the "profile" line from ${PROJECT_CONFIG_REL} by hand. Nothing was written.`;
2980
+ }
2981
+
2982
+ // src/recommender/profile-gate.ts
2983
+ init_logger();
2984
+ var WORKFLOW_TEMPLATE_ITEM_IDS = /* @__PURE__ */ new Set([
2985
+ "haus.agentic-workflow-poc"
2986
+ ]);
2987
+ var KNOWN_CLAUSES = ["tags", "ids"];
2988
+ function stringList(value, label) {
2989
+ if (value === void 0) return [];
2990
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
2991
+ warn(`${label} is not a string array \u2014 ignoring it`);
2992
+ return [];
2993
+ }
2994
+ return value;
2995
+ }
2996
+ function parseProfileRules(raw, profile, knownIds) {
2997
+ if (profile === "production") return { tags: /* @__PURE__ */ new Set(), ids: /* @__PURE__ */ new Set(), keepIds: /* @__PURE__ */ new Set() };
2998
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
2999
+ const block = raw[profile];
3000
+ if (!block || typeof block !== "object" || Array.isArray(block)) return null;
3001
+ const exclude = block.exclude;
3002
+ const clauses = exclude && typeof exclude === "object" && !Array.isArray(exclude) ? exclude : {};
3003
+ for (const key of Object.keys(clauses)) {
3004
+ if (!KNOWN_CLAUSES.includes(key)) {
3005
+ warn(
3006
+ `profiles.${profile}.exclude.${key}: unknown clause (catalog schema evolved ahead of this CLI version) \u2014 ignoring it`
3007
+ );
3008
+ }
3009
+ }
3010
+ const keepIds = stringList(
3011
+ block.keepIds,
3012
+ `profiles.${profile}.keepIds`
3013
+ ).filter((id) => {
3014
+ if (knownIds !== void 0 && !knownIds.has(id)) {
3015
+ warn(
3016
+ `profiles.${profile}.keepIds names an id the catalog does not have (${id}) \u2014 ignoring it`
3017
+ );
3018
+ return false;
3019
+ }
3020
+ return true;
3021
+ });
3022
+ return {
3023
+ tags: new Set(
3024
+ stringList(clauses.tags, `profiles.${profile}.exclude.tags`).map((t) => t.toLowerCase())
3025
+ ),
3026
+ ids: new Set(stringList(clauses.ids, `profiles.${profile}.exclude.ids`)),
3027
+ keepIds: new Set(keepIds)
3028
+ };
3029
+ }
3030
+ function profileVerdict(item, rules) {
3031
+ if (rules.ids.has(item.id)) return { kind: "excluded", clause: `id:${item.id}` };
3032
+ const tag = item.tags.find((t) => rules.tags.has(t.toLowerCase()));
3033
+ if (!tag) return null;
3034
+ const clause = `tag:${tag.toLowerCase()}`;
3035
+ return rules.keepIds.has(item.id) ? { kind: "kept", clause } : { kind: "excluded", clause };
3036
+ }
3037
+ function describeCatalogTier(ctx) {
3038
+ return `${ctx.source}${ctx.version ? ` (v${ctx.version})` : ""}`;
3039
+ }
3040
+ function profileRefusalMessage(profile, catalogLabel) {
3041
+ return `This project is set to profile "${profile}", but the ${catalogLabel} catalog defines no "${profile}" profile (needs haus-workflow-catalog >= 6.5.0). Run \`haus update\` to fetch a newer catalog, or set "profile" back to "production" in .haus-workflow/haus.config.json.`;
3042
+ }
3043
+ var ProfileUnsupportedError = class extends Error {
3044
+ profile;
3045
+ catalogLabel;
3046
+ constructor(profile, catalogLabel) {
3047
+ super(profileRefusalMessage(profile, catalogLabel));
3048
+ this.name = "ProfileUnsupportedError";
3049
+ this.profile = profile;
3050
+ this.catalogLabel = catalogLabel;
3051
+ }
3052
+ };
3053
+ async function profileRefusal(root, profileOverride) {
3054
+ const profile = profileOverride ?? await readProjectProfile(root);
3055
+ const ctx = await loadCatalogContext(root);
3056
+ return parseProfileRules(ctx.profiles, profile) === null ? profileRefusalMessage(profile, describeCatalogTier(ctx)) : null;
3057
+ }
3058
+
2848
3059
  // src/recommender/superseded-by-plugin.ts
2849
3060
  var SUPERSESSION_RULES = [
2850
3061
  {
@@ -2875,7 +3086,7 @@ function isSupersededByPlugin(lockId, activePinIds) {
2875
3086
 
2876
3087
  // src/scanner/detect-package-manager.ts
2877
3088
  import path14 from "node:path";
2878
- import fs10 from "fs-extra";
3089
+ import fs11 from "fs-extra";
2879
3090
  function detectPackageManager(root, packageManagerField) {
2880
3091
  const field = String(packageManagerField ?? "").trim();
2881
3092
  if (field.startsWith("yarn@")) {
@@ -2893,16 +3104,16 @@ function detectPackageManager(root, packageManagerField) {
2893
3104
  if (satisfiesVersion(version, ">=9")) return "npm";
2894
3105
  return "unknown";
2895
3106
  }
2896
- if (fs10.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
2897
- if (fs10.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
2898
- if (fs10.existsSync(path14.join(root, "package-lock.json"))) return "npm";
3107
+ if (fs11.existsSync(path14.join(root, "yarn.lock"))) return "yarn";
3108
+ if (fs11.existsSync(path14.join(root, "pnpm-lock.yaml"))) return "pnpm";
3109
+ if (fs11.existsSync(path14.join(root, "package-lock.json"))) return "npm";
2899
3110
  return "unknown";
2900
3111
  }
2901
3112
 
2902
3113
  // src/update/hash-installed.ts
2903
3114
  import path16 from "node:path";
2904
3115
  import fg3 from "fast-glob";
2905
- import fs12 from "fs-extra";
3116
+ import fs13 from "fs-extra";
2906
3117
 
2907
3118
  // src/claude/haus-managed-header.ts
2908
3119
  function parseHausManagedAttrs(line2) {
@@ -2942,7 +3153,7 @@ function parseHausManagedHeader(line2) {
2942
3153
  // src/claude/superpowers-install.ts
2943
3154
  import path15 from "node:path";
2944
3155
  import fg2 from "fast-glob";
2945
- import fs11 from "fs-extra";
3156
+ import fs12 from "fs-extra";
2946
3157
  init_logger();
2947
3158
  var SKILL_DEP_ARTIFACTS = ["node_modules/**"];
2948
3159
  var SKILL_DEP_ARTIFACT_DIRS = /* @__PURE__ */ new Set(["node_modules"]);
@@ -2965,34 +3176,34 @@ async function previewInstalledSkill(srcFile, opts) {
2965
3176
  return next === text ? void 0 : Buffer.from(next, "utf8");
2966
3177
  }
2967
3178
  async function readRewritableText(file) {
2968
- const raw = await fs11.readFile(file);
3179
+ const raw = await fs12.readFile(file);
2969
3180
  const text = raw.toString("utf8");
2970
3181
  return Buffer.from(text, "utf8").equals(raw) ? text : void 0;
2971
3182
  }
2972
3183
  async function composeInstalledDescription(destination, signal) {
2973
3184
  const skillFile = path15.join(destination, SKILL_ENTRY_FILE);
2974
- if (!await fs11.pathExists(skillFile)) return;
3185
+ if (!await fs12.pathExists(skillFile)) return;
2975
3186
  const text = await readRewritableText(skillFile);
2976
3187
  if (text === void 0) return;
2977
3188
  const composed = rewriteSkillDescription(text, signal);
2978
- if (composed !== text) await fs11.writeFile(skillFile, composed, "utf8");
3189
+ if (composed !== text) await fs12.writeFile(skillFile, composed, "utf8");
2979
3190
  }
2980
3191
  async function installCatalogSkill(sourcePath, destination, opts) {
2981
3192
  if (opts.dryRun) return;
2982
- await fs11.ensureDir(path15.dirname(destination));
2983
- if (await fs11.pathExists(destination)) {
3193
+ await fs12.ensureDir(path15.dirname(destination));
3194
+ if (await fs12.pathExists(destination)) {
2984
3195
  const extra = await extraDestFiles(sourcePath, destination);
2985
3196
  if (extra.length > 0) {
2986
3197
  warn(
2987
3198
  `${destination}: removing ${extra.length} untracked file(s) not present in the catalog source before reinstall: ${extra.join(", ")}`
2988
3199
  );
2989
3200
  }
2990
- for (const entry of await fs11.readdir(destination)) {
3201
+ for (const entry of await fs12.readdir(destination)) {
2991
3202
  if (SKILL_DEP_ARTIFACT_DIRS.has(entry)) continue;
2992
- await fs11.remove(path15.join(destination, entry));
3203
+ await fs12.remove(path15.join(destination, entry));
2993
3204
  }
2994
3205
  }
2995
- await fs11.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
3206
+ await fs12.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
2996
3207
  if (opts.description) await composeInstalledDescription(destination, opts.description);
2997
3208
  }
2998
3209
 
@@ -3016,11 +3227,11 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
3016
3227
  const fileDigests = [];
3017
3228
  for (const rel of normalized) {
3018
3229
  const abs = path16.join(root, rel);
3019
- if (!await fs12.pathExists(abs)) continue;
3020
- const stat = followSymlinks ? await fs12.stat(abs) : await fs12.lstat(abs);
3230
+ if (!await fs13.pathExists(abs)) continue;
3231
+ const stat = followSymlinks ? await fs13.stat(abs) : await fs13.lstat(abs);
3021
3232
  if (!followSymlinks && stat.isSymbolicLink()) continue;
3022
3233
  if (stat.isFile()) {
3023
- const body = await fs12.readFile(abs);
3234
+ const body = await fs13.readFile(abs);
3024
3235
  fileDigests.push({ rel, digest: digestFileContent(body) });
3025
3236
  continue;
3026
3237
  }
@@ -3041,9 +3252,9 @@ async function hashInstalledPaths(root, relPaths, opts = {}) {
3041
3252
  });
3042
3253
  for (const sub of inner.sort()) {
3043
3254
  const absFile = path16.join(abs, sub);
3044
- if (!followSymlinks && (await fs12.lstat(absFile)).isSymbolicLink()) continue;
3255
+ if (!followSymlinks && (await fs13.lstat(absFile)).isSymbolicLink()) continue;
3045
3256
  const relFile = path16.join(rel, sub).replace(/\\/g, "/");
3046
- const body = await fs12.readFile(absFile);
3257
+ const body = await fs13.readFile(absFile);
3047
3258
  fileDigests.push({ rel: relFile, digest: digestFileContent(body) });
3048
3259
  }
3049
3260
  }
@@ -3082,15 +3293,15 @@ init_logger();
3082
3293
  init_paths();
3083
3294
 
3084
3295
  // src/claude/managed-write.ts
3085
- import fs13 from "fs-extra";
3296
+ import fs14 from "fs-extra";
3086
3297
  init_fs();
3087
3298
  init_logger();
3088
3299
  init_paths();
3089
3300
  async function writeManagedText(root, filePath, nextText, dryRun, quiet = false) {
3090
3301
  const say = quiet ? () => {
3091
3302
  } : log;
3092
- const existed = await fs13.pathExists(filePath);
3093
- const prev = existed ? await fs13.readFile(filePath, "utf8") : "";
3303
+ const existed = await fs14.pathExists(filePath);
3304
+ const prev = existed ? await fs14.readFile(filePath, "utf8") : "";
3094
3305
  const printable = displayPath(root, filePath);
3095
3306
  if (dryRun) {
3096
3307
  if (!existed) {
@@ -3177,7 +3388,7 @@ function buildAllowRules() {
3177
3388
  // src/install/settings-merge.ts
3178
3389
  init_fs();
3179
3390
  import path18 from "node:path";
3180
- import fs14 from "fs-extra";
3391
+ import fs15 from "fs-extra";
3181
3392
 
3182
3393
  // src/install/manifest.ts
3183
3394
  init_fs();
@@ -3487,7 +3698,7 @@ function stripHausAsk(settings) {
3487
3698
  async function loadHooksFragment(fragmentPath) {
3488
3699
  let raw;
3489
3700
  try {
3490
- raw = await fs14.readJson(fragmentPath);
3701
+ raw = await fs15.readJson(fragmentPath);
3491
3702
  } catch {
3492
3703
  return [];
3493
3704
  }
@@ -3547,8 +3758,8 @@ function buildDenyRules() {
3547
3758
  if (RUNTIME_ONLY_DENY_COMMANDS.includes(command)) continue;
3548
3759
  rules.push(`Bash(${command}:*)`);
3549
3760
  }
3550
- for (const path92 of DENY_PATHS) {
3551
- const pattern = DENY_DIRS.has(path92) ? `${path92}/**` : path92;
3761
+ for (const path93 of DENY_PATHS) {
3762
+ const pattern = DENY_DIRS.has(path93) ? `${path93}/**` : path93;
3552
3763
  for (const tool of FILE_TOOLS) {
3553
3764
  rules.push(`${tool}(${pattern})`);
3554
3765
  }
@@ -3611,6 +3822,11 @@ var PROJECT_HOOK_FRAGMENTS = [
3611
3822
  command: "haus workspace worktree autohydrate --from-hook"
3612
3823
  }
3613
3824
  ];
3825
+ function projectHookFragmentsFor(profile) {
3826
+ const dropped = new Set(PROFILES[profile].droppedHookCommands);
3827
+ if (dropped.size === 0) return PROJECT_HOOK_FRAGMENTS;
3828
+ return PROJECT_HOOK_FRAGMENTS.filter((fragment) => !dropped.has(fragment.command));
3829
+ }
3614
3830
  async function readProjectSettings(root) {
3615
3831
  const filePath = claudePath(root, "settings.json");
3616
3832
  const result = await readJsonDetailed(filePath);
@@ -3622,16 +3838,17 @@ async function readProjectSettings(root) {
3622
3838
  async function writeProjectSettings(root, settings) {
3623
3839
  await writeJson(claudePath(root, "settings.json"), settings);
3624
3840
  }
3625
- async function mergeProjectSettings(root) {
3841
+ async function mergeProjectSettings(root, profile) {
3842
+ const resolved = profile ?? await readProjectProfile(root);
3626
3843
  const base = await readProjectSettings(root);
3627
- const { settings: withHooks } = mergeHooks(base, PROJECT_HOOK_FRAGMENTS);
3844
+ const { settings: withHooks } = mergeHooks(base, projectHookFragmentsFor(resolved));
3628
3845
  const { settings: withDeny } = mergeDenyRules(withHooks, buildDenyRules());
3629
3846
  const { settings: withAllow } = mergeAllowRules(withDeny, buildAllowRules());
3630
3847
  const { settings: merged } = mergeAskRules(withAllow, buildAskRules());
3631
3848
  return merged;
3632
3849
  }
3633
- async function applyProjectSettingsMerge(root) {
3634
- const merged = await mergeProjectSettings(root);
3850
+ async function applyProjectSettingsMerge(root, profile) {
3851
+ const merged = await mergeProjectSettings(root, profile);
3635
3852
  await writeProjectSettings(root, merged);
3636
3853
  return merged;
3637
3854
  }
@@ -3639,7 +3856,7 @@ async function applyProjectSettingsMerge(root) {
3639
3856
  // src/claude/remove-items.ts
3640
3857
  import crypto2 from "node:crypto";
3641
3858
  import path19 from "node:path";
3642
- import fs15 from "fs-extra";
3859
+ import fs16 from "fs-extra";
3643
3860
  init_fs();
3644
3861
  init_paths();
3645
3862
 
@@ -3669,16 +3886,16 @@ function classifyBackupName(name) {
3669
3886
  async function unsafeTrackedPath(root, rel) {
3670
3887
  const abs = resolveContainedPath(root, rel);
3671
3888
  if (abs === null) return "escapes-root";
3672
- if (!await fs15.pathExists(abs)) {
3889
+ if (!await fs16.pathExists(abs)) {
3673
3890
  try {
3674
- if ((await fs15.lstat(abs)).isSymbolicLink()) return "symlink";
3891
+ if ((await fs16.lstat(abs)).isSymbolicLink()) return "symlink";
3675
3892
  } catch {
3676
3893
  return null;
3677
3894
  }
3678
3895
  return null;
3679
3896
  }
3680
- if ((await fs15.lstat(abs)).isSymbolicLink()) return "symlink";
3681
- const [realAbs, realRoot] = await Promise.all([fs15.realpath(abs), fs15.realpath(root)]);
3897
+ if ((await fs16.lstat(abs)).isSymbolicLink()) return "symlink";
3898
+ const [realAbs, realRoot] = await Promise.all([fs16.realpath(abs), fs16.realpath(root)]);
3682
3899
  const prefix = realRoot.endsWith(path19.sep) ? realRoot : realRoot + path19.sep;
3683
3900
  if (realAbs !== realRoot && !realAbs.startsWith(prefix)) return "symlink";
3684
3901
  return null;
@@ -3704,7 +3921,7 @@ async function resolveExistingTrackedPaths(root, itemId, relPaths, warnFn, unsaf
3704
3921
  continue;
3705
3922
  }
3706
3923
  const abs = path19.resolve(root, rel);
3707
- if (await fs15.pathExists(abs)) existing.push({ rel, abs });
3924
+ if (await fs16.pathExists(abs)) existing.push({ rel, abs });
3708
3925
  }
3709
3926
  return existing;
3710
3927
  }
@@ -3722,11 +3939,11 @@ async function backupTrackedPaths(root, absPaths, say, prefix) {
3722
3939
  const stamp = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${crypto2.randomBytes(3).toString("hex")}`;
3723
3940
  const backupRoot = hausPath(root, "backups", dirBackupDirName(prefix, stamp));
3724
3941
  for (const abs of absPaths) {
3725
- if (!await fs15.pathExists(abs)) continue;
3942
+ if (!await fs16.pathExists(abs)) continue;
3726
3943
  const rel = path19.relative(root, abs);
3727
3944
  const backupPath = path19.join(backupRoot, rel);
3728
- await fs15.ensureDir(path19.dirname(backupPath));
3729
- await fs15.copy(abs, backupPath);
3945
+ await fs16.ensureDir(path19.dirname(backupPath));
3946
+ await fs16.copy(abs, backupPath);
3730
3947
  }
3731
3948
  say(
3732
3949
  `Backed up ${absPaths.length} path(s) to ${path19.relative(root, backupRoot)} before deleting them.`
@@ -3735,7 +3952,7 @@ async function backupTrackedPaths(root, absPaths, say, prefix) {
3735
3952
  }
3736
3953
  async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say) {
3737
3954
  for (const { abs } of existing) {
3738
- await fs15.remove(abs);
3955
+ await fs16.remove(abs);
3739
3956
  await pruneEmptyDir(path19.dirname(abs));
3740
3957
  say([verb, reason, displayPath(root, abs)].filter(Boolean).join(" ") + ` (${itemId})`);
3741
3958
  }
@@ -3744,7 +3961,7 @@ async function deleteTrackedPaths(root, itemId, existing, { verb, reason }, say)
3744
3961
  // src/claude/skill-dep-install.ts
3745
3962
  init_exec();
3746
3963
  import path20 from "node:path";
3747
- import fs16 from "fs-extra";
3964
+ import fs17 from "fs-extra";
3748
3965
  function skillDepInstallCommand(manager) {
3749
3966
  switch (manager) {
3750
3967
  case "npm":
@@ -3781,7 +3998,7 @@ function skillDepInstallCommand(manager) {
3781
3998
  }
3782
3999
  async function hasDeclaredDependencies(dir) {
3783
4000
  try {
3784
- const raw = await fs16.readFile(path20.join(dir, "package.json"), "utf8");
4001
+ const raw = await fs17.readFile(path20.join(dir, "package.json"), "utf8");
3785
4002
  const parsed = JSON.parse(raw);
3786
4003
  if (typeof parsed !== "object" || parsed === null) return false;
3787
4004
  const deps = parsed.dependencies;
@@ -3793,7 +4010,7 @@ async function hasDeclaredDependencies(dir) {
3793
4010
  }
3794
4011
  async function realpathOrSelf(target) {
3795
4012
  try {
3796
- return await fs16.realpath(target);
4013
+ return await fs17.realpath(target);
3797
4014
  } catch {
3798
4015
  return path20.resolve(target);
3799
4016
  }
@@ -3838,7 +4055,7 @@ async function installSkillDependencies(opts, deps = {}) {
3838
4055
  default:
3839
4056
  return assertNeverCommandProbe(probe);
3840
4057
  }
3841
- if (!await fs16.pathExists(contained.dir)) {
4058
+ if (!await fs17.pathExists(contained.dir)) {
3842
4059
  return { status: "skipped", reason: `install directory disappeared (${contained.dir})` };
3843
4060
  }
3844
4061
  const run = deps.run ?? defaultRun;
@@ -3852,7 +4069,7 @@ async function installSkillDependencies(opts, deps = {}) {
3852
4069
  // src/claude/verify-hooks-contract.ts
3853
4070
  init_fs();
3854
4071
  init_paths();
3855
- import fs17 from "fs-extra";
4072
+ import fs18 from "fs-extra";
3856
4073
 
3857
4074
  // src/claude/load-hooks.ts
3858
4075
  var CANONICAL_HOOKS = {
@@ -3897,8 +4114,14 @@ var CANONICAL_HOOKS = {
3897
4114
  ]
3898
4115
  }
3899
4116
  };
3900
- async function loadClaudeHooksSettings() {
3901
- return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
4117
+ async function loadClaudeHooksSettings(profile = "production") {
4118
+ const dropped = new Set(PROFILES[profile].droppedHookCommands);
4119
+ const hooks = {};
4120
+ for (const [event, blocks] of Object.entries(CANONICAL_HOOKS.hooks)) {
4121
+ const kept = (blocks ?? []).filter((block) => !block.hooks.some((h) => dropped.has(h.command)));
4122
+ if (kept.length > 0) hooks[event] = kept;
4123
+ }
4124
+ return { hooks, permissions: { deny: buildDenyRules(), ask: buildAskRules() } };
3902
4125
  }
3903
4126
 
3904
4127
  // src/claude/verify-hooks-contract.ts
@@ -3934,8 +4157,8 @@ function hausHookContractSatisfied(project, canonical) {
3934
4157
  if (!coversRules(project.permissions?.ask, canonical.permissions?.ask)) return false;
3935
4158
  return true;
3936
4159
  }
3937
- async function assertPostApplySettingsHausContract(root) {
3938
- const canonical = await loadClaudeHooksSettings();
4160
+ async function assertPostApplySettingsHausContract(root, profile) {
4161
+ const canonical = await loadClaudeHooksSettings(profile ?? await readProjectProfile(root));
3939
4162
  const written = await readJson(claudePath(root, "settings.json"));
3940
4163
  if (written == null || typeof written !== "object") {
3941
4164
  throw new Error(
@@ -3950,7 +4173,7 @@ async function assertPostApplySettingsHausContract(root) {
3950
4173
  }
3951
4174
  async function verifyProjectSettingsHooksContract(root) {
3952
4175
  const settingsPath = claudePath(root, "settings.json");
3953
- if (!await fs17.pathExists(settingsPath)) {
4176
+ if (!await fs18.pathExists(settingsPath)) {
3954
4177
  return {
3955
4178
  ok: true,
3956
4179
  skipped: true,
@@ -3959,7 +4182,7 @@ async function verifyProjectSettingsHooksContract(root) {
3959
4182
  }
3960
4183
  let canonical;
3961
4184
  try {
3962
- canonical = await loadClaudeHooksSettings();
4185
+ canonical = await loadClaudeHooksSettings(await readProjectProfile(root));
3963
4186
  } catch (err) {
3964
4187
  return { ok: false, message: `Cannot load canonical hooks: ${String(err)}` };
3965
4188
  }
@@ -3979,7 +4202,7 @@ async function verifyProjectSettingsHooksContract(root) {
3979
4202
  // src/claude/write-ci-gate-workflow.ts
3980
4203
  init_logger();
3981
4204
  import path21 from "node:path";
3982
- import fs18 from "fs-extra";
4205
+ import fs19 from "fs-extra";
3983
4206
  import YAML from "yaml";
3984
4207
  var CI_GATE_WORKFLOW_REL = path21.join(".github", "workflows", "haus-gate.yml");
3985
4208
  var WORKFLOW = `# Written by \`haus init\`. Yours to edit \u2014 haus will not rewrite this file.
@@ -4020,16 +4243,20 @@ jobs:
4020
4243
  - name: haus ci-gate
4021
4244
  run: haus ci-gate
4022
4245
  `;
4246
+ var CI_GATE_MARKER_LINE = WORKFLOW.split("\n", 1)[0];
4247
+ function isHausWrittenCiGate(content2) {
4248
+ return content2.startsWith(CI_GATE_MARKER_LINE);
4249
+ }
4023
4250
  async function writeCiGateWorkflow(root, dryRun, quiet = false) {
4024
4251
  const dest = path21.join(root, CI_GATE_WORKFLOW_REL);
4025
- if (await fs18.pathExists(dest)) return void 0;
4252
+ if (await fs19.pathExists(dest)) return void 0;
4026
4253
  if (await projectRunsCiGate(root)) return void 0;
4027
4254
  if (dryRun) {
4028
4255
  if (!quiet) log(`Would write ${CI_GATE_WORKFLOW_REL} (CI gate: haus ci-gate on every PR)`);
4029
4256
  return dest;
4030
4257
  }
4031
- await fs18.ensureDir(path21.dirname(dest));
4032
- await fs18.writeFile(dest, WORKFLOW, "utf8");
4258
+ await fs19.ensureDir(path21.dirname(dest));
4259
+ await fs19.writeFile(dest, WORKFLOW, "utf8");
4033
4260
  if (!quiet) log(`Wrote ${CI_GATE_WORKFLOW_REL} \u2014 runs \`haus ci-gate\` on every pull request`);
4034
4261
  return dest;
4035
4262
  }
@@ -4051,14 +4278,14 @@ async function projectRunsCiGate(root) {
4051
4278
  const dir = path21.join(root, ".github", "workflows");
4052
4279
  let entries;
4053
4280
  try {
4054
- entries = await fs18.readdir(dir);
4281
+ entries = await fs19.readdir(dir);
4055
4282
  } catch {
4056
4283
  return false;
4057
4284
  }
4058
4285
  for (const entry of entries) {
4059
4286
  if (!/\.ya?ml$/i.test(entry)) continue;
4060
4287
  try {
4061
- const text = await fs18.readFile(path21.join(dir, entry), "utf8");
4288
+ const text = await fs19.readFile(path21.join(dir, entry), "utf8");
4062
4289
  if (runScripts(YAML.parse(text)).some((script) => CI_GATE_COMMAND.test(script))) return true;
4063
4290
  } catch {
4064
4291
  }
@@ -4068,7 +4295,7 @@ async function projectRunsCiGate(root) {
4068
4295
 
4069
4296
  // src/claude/write-decisions-seed.ts
4070
4297
  import path22 from "node:path";
4071
- import fs19 from "fs-extra";
4298
+ import fs20 from "fs-extra";
4072
4299
 
4073
4300
  // library/catalog/decisions-triggers.json
4074
4301
  var decisions_triggers_default = {
@@ -4113,15 +4340,15 @@ Why we chose X \u2014 short, dated, write-once decision log for humans and agent
4113
4340
  async function writeDecisionsSeed(root, dryRun, quiet = false) {
4114
4341
  const decisionsDir = path22.join(root, DECISIONS_TRIGGERS.decisionsDir);
4115
4342
  const readmePath = path22.join(decisionsDir, "README.md");
4116
- if (await fs19.pathExists(readmePath)) return null;
4117
- if (!dryRun) await fs19.ensureDir(decisionsDir);
4343
+ if (await fs20.pathExists(readmePath)) return null;
4344
+ if (!dryRun) await fs20.ensureDir(decisionsDir);
4118
4345
  await writeManagedText(root, readmePath, SEED_README, dryRun, quiet);
4119
4346
  return readmePath;
4120
4347
  }
4121
4348
 
4122
4349
  // src/claude/write-prettierignore.ts
4123
4350
  import path23 from "node:path";
4124
- import fs20 from "fs-extra";
4351
+ import fs21 from "fs-extra";
4125
4352
  var PRETTIERIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
4126
4353
  var PRETTIERIGNORE_END = "# HAUS:END haus-managed";
4127
4354
  var IGNORED_PATHS = [".haus-workflow/", ".claude/"];
@@ -4190,7 +4417,7 @@ ${block}
4190
4417
  }
4191
4418
  async function writePrettierIgnore(root, dryRun, quiet = false) {
4192
4419
  const filePath = path23.join(root, ".prettierignore");
4193
- const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
4420
+ const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
4194
4421
  const next = injectPrettierIgnoreBlock(prev, buildPrettierIgnoreBlock());
4195
4422
  await writeManagedText(root, filePath, next, dryRun, quiet);
4196
4423
  return filePath;
@@ -4198,19 +4425,21 @@ async function writePrettierIgnore(root, dryRun, quiet = false) {
4198
4425
 
4199
4426
  // src/claude/write-root-claude-md.ts
4200
4427
  import path24 from "node:path";
4201
- import fs21 from "fs-extra";
4428
+ import fs22 from "fs-extra";
4202
4429
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
4203
4430
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
4204
4431
  var WORKSPACE_BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-workspace-imports v=1 -->";
4205
4432
  var WORKSPACE_BLOCK_END = "<!-- HAUS:END haus-workspace-imports -->";
4206
4433
  var DEFAULT_SENTINELS = { begin: BLOCK_BEGIN, end: BLOCK_END };
4207
- var IMPORT_CONTENT = `@.haus-workflow/WORKFLOW.md
4208
- @.haus-workflow/workflow-config.md
4209
- @docs/decisions/README.md`;
4210
- function buildImportBlock() {
4434
+ function importContentFor(profile) {
4435
+ const lines = ["@.haus-workflow/WORKFLOW.md", "@.haus-workflow/workflow-config.md"];
4436
+ if (PROFILES[profile].writesDecisionsSeed) lines.push("@docs/decisions/README.md");
4437
+ return lines.join("\n");
4438
+ }
4439
+ function buildImportBlock(profile) {
4211
4440
  return `${BLOCK_BEGIN}
4212
4441
 
4213
- ${IMPORT_CONTENT}
4442
+ ${importContentFor(profile)}
4214
4443
  ${BLOCK_END}`;
4215
4444
  }
4216
4445
  function findLineMarker2(content2, marker, from = 0) {
@@ -4275,10 +4504,10 @@ ${block}
4275
4504
  ${block}
4276
4505
  `;
4277
4506
  }
4278
- async function writeRootClaudeMd(root, dryRun, quiet = false) {
4507
+ async function writeRootClaudeMd(root, dryRun, profile, quiet = false) {
4279
4508
  const filePath = path24.join(root, "CLAUDE.md");
4280
- const block = buildImportBlock();
4281
- const prev = await fs21.pathExists(filePath) ? await fs21.readFile(filePath, "utf8") : "";
4509
+ const block = buildImportBlock(profile);
4510
+ const prev = await fs22.pathExists(filePath) ? await fs22.readFile(filePath, "utf8") : "";
4282
4511
  const next = injectHausBlock(prev, block);
4283
4512
  await writeManagedText(root, filePath, next, dryRun, quiet);
4284
4513
  return filePath;
@@ -4289,12 +4518,12 @@ init_fs();
4289
4518
  init_logger();
4290
4519
  init_paths();
4291
4520
  import path26 from "node:path";
4292
- import fs23 from "fs-extra";
4521
+ import fs24 from "fs-extra";
4293
4522
 
4294
4523
  // src/claude/derive-workflow-config.ts
4295
4524
  init_fs();
4296
4525
  import path25 from "node:path";
4297
- import fs22 from "fs-extra";
4526
+ import fs23 from "fs-extra";
4298
4527
  function binCmd(pm, bin, args) {
4299
4528
  const tail = args ? ` ${args}` : "";
4300
4529
  if (pm === "yarn") return `yarn ${bin}${tail}`;
@@ -4313,22 +4542,25 @@ async function deriveWorkflowConfig(root, ctx) {
4313
4542
  return null;
4314
4543
  };
4315
4544
  const hasDep = (name) => deps.has(name);
4316
- const exists2 = (rel) => fs22.pathExistsSync(path25.join(root, rel));
4545
+ const exists2 = (rel) => fs23.pathExistsSync(path25.join(root, rel));
4317
4546
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
4318
4547
  const hasCypress = hasDep("cypress");
4319
4548
  const preCommitTool = exists2("lefthook.yml") || exists2("lefthook.yaml") ? "lefthook" : exists2(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists2(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
4320
4549
  return {
4321
4550
  test: script("test") ?? `${pm} test`,
4322
4551
  testE2E: firstScript("test:e2e", "e2e", "test:integration") ?? (hasPlaywright ? binCmd(pm, "playwright", "test") : null) ?? (hasCypress ? binCmd(pm, "cypress", "run") : null),
4552
+ manus: script("manus") ?? binCmd(pm, "playwright", "test demo/manus.spec.ts"),
4553
+ dev: script("dev"),
4323
4554
  preCommitTool,
4324
4555
  specPath: exists2("docs/SPEC.md") ? "docs/SPEC.md" : null,
4325
4556
  designPath: exists2("docs/DESIGN.md") ? "docs/DESIGN.md" : null,
4326
- uxPath: exists2("docs/UX.md") ? "docs/UX.md" : null
4557
+ uxPath: exists2("docs/UX.md") ? "docs/UX.md" : null,
4558
+ packageManager: pm
4327
4559
  };
4328
4560
  }
4329
4561
 
4330
4562
  // src/claude/write-workflow-config.ts
4331
- function fields(v) {
4563
+ function productionFields(v) {
4332
4564
  return [
4333
4565
  { prefix: "- Spec: ", value: v.specPath, hint: "path, e.g. docs/SPEC.md" },
4334
4566
  { prefix: "- Design: ", value: v.designPath, hint: "path, e.g. docs/DESIGN.md" },
@@ -4343,6 +4575,20 @@ function fields(v) {
4343
4575
  { prefix: "- Tool: ", value: v.preCommitTool, hint: "e.g. lefthook, husky" }
4344
4576
  ];
4345
4577
  }
4578
+ function pocFields(v) {
4579
+ return [
4580
+ { prefix: "- Run the manus: ", value: v.manus, hint: "command", code: true },
4581
+ {
4582
+ prefix: "- Dev server: ",
4583
+ value: v.dev,
4584
+ hint: `command, e.g. ${v.packageManager} run dev`,
4585
+ code: true
4586
+ }
4587
+ ];
4588
+ }
4589
+ function fieldsFor(profile, v) {
4590
+ return PROFILES[profile].workflowConfig === "poc" ? pocFields(v) : productionFields(v);
4591
+ }
4346
4592
  function renderValue(f) {
4347
4593
  if (f.value === null) return `<!-- fill in ${f.hint} -->`;
4348
4594
  return f.code ? `\`${f.value}\`` : f.value;
@@ -4351,12 +4597,17 @@ function line(f) {
4351
4597
  return `${f.prefix}${renderValue(f)}`;
4352
4598
  }
4353
4599
  function buildWorkflowConfig(v) {
4354
- const f = fields(v);
4600
+ const f = productionFields(v);
4355
4601
  const byPrefix = (p) => line(f.find((x) => x.prefix === p));
4356
4602
  return "# How this project works (workflow methodology bindings)\n\n> The few project-specific values the workflow standard (WORKFLOW.md) binds to:\n> where the source-of-truth docs live, the test commands the TDD/verification gate\n> runs, the highest-stakes logic, and the pre-commit tool. This file is yours to\n> edit and haus will not overwrite it.\n>\n> Everyday commands (dev, build, lint, typecheck, format) and project documentation\n> live in `CLAUDE.md` + `docs/` \u2014 run **`/docs`** to generate/refresh them.\n\n## Source-of-truth documents\n" + byPrefix("- Spec: ") + "\n" + byPrefix("- Design: ") + "\n" + byPrefix("- UX flows: ") + "\n\n## Test commands (TDD / verification gate)\n" + byPrefix("- Test (unit + integration): ") + "\n" + byPrefix("- Test (E2E): ") + "\n\n## Highest-stakes logic\n<!-- fill in domain areas requiring TDD-only treatment, e.g. payment flows, auth, medical data -->\n\n## Pre-commit tool\n" + byPrefix("- Tool: ") + "\n";
4357
4603
  }
4358
- function refillContent(existing, v) {
4359
- const f = fields(v);
4604
+ function buildPocWorkflowConfig(v) {
4605
+ const f = pocFields(v);
4606
+ const byPrefix = (p) => line(f.find((x) => x.prefix === p));
4607
+ return "# How this PoC works (workflow methodology bindings)\n\n> The few project-specific values the PoC standard (WORKFLOW.md) binds to: where the\n> manus lives and the two commands the verification gate runs. This file is yours to\n> edit and haus will not overwrite it. A PoC has no test suite, no CI gate and no\n> pre-commit hooks by design (haus-workflow ADR-0125).\n\n## Source-of-truth documents\n- Manus: docs/MANUS.md\n- Plans: docs/plans/<round>.md\n\n## Demo commands (the verification gate)\n" + byPrefix("- Run the manus: ") + "\n" + byPrefix("- Dev server: ") + "\n\n## Highest-stakes logic\nn/a. A PoC has none by definition; the manus is the only gate.\n\n## Pre-commit tool\nn/a. No hooks in a PoC.\n";
4608
+ }
4609
+ function refillContent(existing, v, profile) {
4610
+ const f = fieldsFor(profile, v);
4360
4611
  return existing.split("\n").map((ln) => {
4361
4612
  const field = f.find((x) => ln.startsWith(x.prefix));
4362
4613
  if (!field || field.value === null) return ln;
@@ -4380,9 +4631,10 @@ var FALLBACK_CONTEXT = {
4380
4631
  async function writeWorkflowConfig(root, dryRun, opts = {}) {
4381
4632
  const say = opts.quiet ? () => {
4382
4633
  } : log;
4634
+ const profile = opts.profile ?? "production";
4383
4635
  const destPath = hausPath(root, "workflow-config.md");
4384
4636
  const printable = displayPath(root, destPath);
4385
- const exists2 = await fs23.pathExists(destPath);
4637
+ const exists2 = await fs24.pathExists(destPath);
4386
4638
  if (exists2 && !opts.refill) {
4387
4639
  if (dryRun) say(printable + ": exists (project-owned, skipping)");
4388
4640
  return null;
@@ -4394,8 +4646,8 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
4394
4646
  };
4395
4647
  const values = await deriveWorkflowConfig(root, ctx);
4396
4648
  if (exists2) {
4397
- const current = await fs23.readFile(destPath, "utf8");
4398
- const refilled = refillContent(current, values);
4649
+ const current = await fs24.readFile(destPath, "utf8");
4650
+ const refilled = refillContent(current, values, profile);
4399
4651
  if (refilled === current) {
4400
4652
  if (dryRun) say(printable + ": no blank fields to refill");
4401
4653
  return null;
@@ -4411,13 +4663,16 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
4411
4663
  say(printable + ": would create");
4412
4664
  return destPath;
4413
4665
  }
4414
- await writeText(destPath, buildWorkflowConfig(values));
4666
+ await writeText(
4667
+ destPath,
4668
+ PROFILES[profile].workflowConfig === "poc" ? buildPocWorkflowConfig(values) : buildWorkflowConfig(values)
4669
+ );
4415
4670
  return destPath;
4416
4671
  }
4417
4672
 
4418
4673
  // src/claude/write-workflow.ts
4419
4674
  init_offline();
4420
- import fs24 from "fs-extra";
4675
+ import fs25 from "fs-extra";
4421
4676
  init_fs();
4422
4677
  init_logger();
4423
4678
  init_paths();
@@ -4443,10 +4698,10 @@ function makeWorkflowHeader(pkgVersion, contentHash) {
4443
4698
  const hashAttr = contentHash === void 0 ? "" : ` hash=${contentHash}`;
4444
4699
  return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion}${hashAttr} -->`;
4445
4700
  }
4446
- async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = false) {
4701
+ async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = false, templateRel = WORKFLOW_TEMPLATE_REL) {
4447
4702
  const say = quiet ? () => {
4448
4703
  } : log;
4449
- const templateContent = await readWorkflowTemplate({ dryRun, projectRoot: root });
4704
+ const templateContent = await readWorkflowTemplate({ dryRun, projectRoot: root, templateRel });
4450
4705
  if (templateContent === null) {
4451
4706
  warn(
4452
4707
  isOffline() ? `Workflow template is not in the local catalog cache and this run is offline \u2014 run \`haus catalog bundle import <file>\` (or drop \`--offline\`), then re-run \`haus apply --write\`` : `Workflow template could not be fetched from the catalog \u2014 check your network, then re-run \`haus apply --write\` (or \`haus update\`)`
@@ -4459,8 +4714,8 @@ async function writeWorkflow(root, pkgVersion, dryRun, force = false, quiet = fa
4459
4714
  ${templateContent}`;
4460
4715
  const destPath = hausPath(root, "WORKFLOW.md");
4461
4716
  const printable = displayPath(root, destPath);
4462
- if (await fs24.pathExists(destPath)) {
4463
- const existing = await fs24.readFile(destPath, "utf8");
4717
+ if (await fs25.pathExists(destPath)) {
4718
+ const existing = await fs25.readFile(destPath, "utf8");
4464
4719
  const firstLine = existing.split("\n")[0] ?? "";
4465
4720
  const parsed = parseHausManagedHeader(firstLine);
4466
4721
  if (!parsed) {
@@ -4504,7 +4759,7 @@ ${existingContent}`;
4504
4759
  }
4505
4760
  }
4506
4761
  if (dryRun) {
4507
- const prev = await fs24.pathExists(destPath) ? await fs24.readFile(destPath, "utf8") : "";
4762
+ const prev = await fs25.pathExists(destPath) ? await fs25.readFile(destPath, "utf8") : "";
4508
4763
  if (!prev) {
4509
4764
  say(createUnifiedDiff(printable, "", next));
4510
4765
  } else {
@@ -4559,14 +4814,14 @@ function decodeIfText(buf) {
4559
4814
  return Buffer.from(asText, "utf8").equals(buf) ? asText : null;
4560
4815
  }
4561
4816
  async function listFilesRecursive2(dir) {
4562
- if (!await fs25.pathExists(dir)) return [];
4817
+ if (!await fs26.pathExists(dir)) return [];
4563
4818
  const files = [];
4564
4819
  const walk = async (current, relPrefix) => {
4565
- const names = await fs25.readdir(current);
4820
+ const names = await fs26.readdir(current);
4566
4821
  for (const name of names) {
4567
4822
  const abs = path27.join(current, name);
4568
4823
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
4569
- const stat = await fs25.lstat(abs);
4824
+ const stat = await fs26.lstat(abs);
4570
4825
  if (stat.isSymbolicLink()) {
4571
4826
  warn(`Skipping symlink in dry-run diff preview: ${rel}`);
4572
4827
  continue;
@@ -4583,14 +4838,14 @@ async function listFilesRecursive2(dir) {
4583
4838
  }
4584
4839
  async function logFileDryRunDiff(root, srcFile, destFile, itemId, say, transformed) {
4585
4840
  const printable = displayPath(root, destFile);
4586
- const nextBuf = transformed ?? await fs25.readFile(srcFile);
4587
- if (!await fs25.pathExists(destFile)) {
4841
+ const nextBuf = transformed ?? await fs26.readFile(srcFile);
4842
+ if (!await fs26.pathExists(destFile)) {
4588
4843
  say(`${printable}: would create (${itemId})`);
4589
4844
  const nextText2 = decodeIfText(nextBuf);
4590
4845
  if (nextText2 !== null) say(createUnifiedDiff(printable, "", nextText2));
4591
4846
  return;
4592
4847
  }
4593
- const prevBuf = await fs25.readFile(destFile);
4848
+ const prevBuf = await fs26.readFile(destFile);
4594
4849
  if (prevBuf.equals(nextBuf)) {
4595
4850
  say(`${printable}: unchanged (${itemId})`);
4596
4851
  return;
@@ -4619,15 +4874,15 @@ async function logSkillDryRunDiff(root, sourcePath, destination, itemId, say, in
4619
4874
  }
4620
4875
  async function removeLegacyManagedStub(root, relPathSegments, stub, dryRun, say) {
4621
4876
  const target = claudePath(root, ...relPathSegments);
4622
- if (!await fs25.pathExists(target)) return;
4623
- const content2 = await fs25.readFile(target, "utf8");
4877
+ if (!await fs26.pathExists(target)) return;
4878
+ const content2 = await fs26.readFile(target, "utf8");
4624
4879
  if (content2 !== stub && content2 !== `${stub}
4625
4880
  ` && content2 !== `${stub}\r
4626
4881
  `) return;
4627
4882
  if (dryRun) {
4628
4883
  say(`[dry-run] would remove stale ${displayPath(root, target)}`);
4629
4884
  } else {
4630
- await fs25.remove(target);
4885
+ await fs26.remove(target);
4631
4886
  }
4632
4887
  }
4633
4888
  async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
@@ -4646,17 +4901,30 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4646
4901
  };
4647
4902
  const pkgRoot = packageRoot();
4648
4903
  const hausVersion2 = (await readJson(path27.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
4649
- const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
4904
+ const { items: manifestItems, contentRoot, ...catalogMeta } = await loadCatalogContext(root);
4905
+ const profile = opts.profile ?? await readProjectProfile(root);
4906
+ const profileSpec = PROFILES[profile];
4907
+ if (parseProfileRules(catalogMeta.profiles, profile) === null) {
4908
+ throw new ProfileUnsupportedError(profile, describeCatalogTier(catalogMeta));
4909
+ }
4650
4910
  const coreFiles = [claudePath(root, "settings.json"), claudePath(root, "rules", "haus.md")];
4651
- const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun, opts.quiet);
4652
- const decisionsSeedPath = await writeDecisionsSeed(root, dryRun, opts.quiet);
4653
- const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force, opts.quiet);
4911
+ const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun, profile, opts.quiet);
4912
+ const decisionsSeedPath = profileSpec.writesDecisionsSeed ? await writeDecisionsSeed(root, dryRun, opts.quiet) : null;
4913
+ const workflowPath = await writeWorkflow(
4914
+ root,
4915
+ hausVersion2,
4916
+ dryRun,
4917
+ opts.force,
4918
+ opts.quiet,
4919
+ profileSpec.workflowTemplateRel
4920
+ );
4654
4921
  const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
4655
4922
  refill: opts.refillConfig,
4656
- quiet: opts.quiet
4923
+ quiet: opts.quiet,
4924
+ profile
4657
4925
  });
4658
4926
  const prettierIgnorePath = await writePrettierIgnore(root, dryRun, opts.quiet);
4659
- const ciGatePath = await writeCiGateWorkflow(root, dryRun, opts.quiet);
4927
+ const ciGatePath = profileSpec.writesCiGate ? await writeCiGateWorkflow(root, dryRun, opts.quiet) : null;
4660
4928
  const p6Files = [
4661
4929
  rootClaudeMdPath,
4662
4930
  ...decisionsSeedPath ? [decisionsSeedPath] : [],
@@ -4667,7 +4935,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4667
4935
  ];
4668
4936
  const files = dryRun ? [...coreFiles, ...p6Files] : [...coreFiles, ...p6Files, hausPath(root, "haus.lock.json")];
4669
4937
  if (dryRun) {
4670
- const mergedSettings = await mergeProjectSettings(root);
4938
+ const mergedSettings = await mergeProjectSettings(root, profile);
4671
4939
  await writeManagedJson(
4672
4940
  root,
4673
4941
  claudePath(root, "settings.json"),
@@ -4676,8 +4944,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4676
4944
  opts.quiet
4677
4945
  );
4678
4946
  } else {
4679
- await applyProjectSettingsMerge(root);
4680
- await assertPostApplySettingsHausContract(root);
4947
+ await applyProjectSettingsMerge(root, profile);
4948
+ await assertPostApplySettingsHausContract(root, profile);
4681
4949
  }
4682
4950
  await removeLegacyManagedStub(
4683
4951
  root,
@@ -4735,11 +5003,11 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4735
5003
  ];
4736
5004
  for (const rel of LEGACY_PRUNED_ARTIFACTS) {
4737
5005
  const legacyPath = hausPath(root, rel);
4738
- if (await fs25.pathExists(legacyPath)) {
5006
+ if (await fs26.pathExists(legacyPath)) {
4739
5007
  if (dryRun) {
4740
5008
  say(`[dry-run] would remove stale ${displayPath(root, legacyPath)}`);
4741
5009
  } else {
4742
- await fs25.remove(legacyPath);
5010
+ await fs26.remove(legacyPath);
4743
5011
  }
4744
5012
  }
4745
5013
  }
@@ -4857,7 +5125,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4857
5125
  }
4858
5126
  const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
4859
5127
  const destination = placement.destination;
4860
- if (await fs25.pathExists(sourcePath)) {
5128
+ if (await fs26.pathExists(sourcePath)) {
4861
5129
  if (dryRun) {
4862
5130
  if (item.type === "skill") {
4863
5131
  await logSkillDryRunDiff(root, sourcePath, destination, item.id, say, {
@@ -4888,13 +5156,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4888
5156
  )) {
4889
5157
  continue;
4890
5158
  } else if (item.type === "skill") {
4891
- const skillFiles = (await fs25.readdir(sourcePath, { recursive: true })).filter(
5159
+ const skillFiles = (await fs26.readdir(sourcePath, { recursive: true })).filter(
4892
5160
  (f) => typeof f === "string" && f.endsWith(".md")
4893
5161
  );
4894
5162
  let skillValid = true;
4895
5163
  for (const relFile of skillFiles) {
4896
5164
  const mdPath = path27.join(sourcePath, relFile);
4897
- const mdContent = await fs25.readFile(mdPath, "utf8");
5165
+ const mdContent = await fs26.readFile(mdPath, "utf8");
4898
5166
  const mdValidation = validateCatalogItem(
4899
5167
  {
4900
5168
  id: manifestItem.id,
@@ -4928,7 +5196,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4928
5196
  say
4929
5197
  );
4930
5198
  } else {
4931
- const fileContent = await fs25.readFile(sourcePath, "utf8");
5199
+ const fileContent = await fs26.readFile(sourcePath, "utf8");
4932
5200
  const validation = validateCatalogItem(
4933
5201
  {
4934
5202
  id: manifestItem.id,
@@ -4942,8 +5210,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
4942
5210
  warn(`Skipping ${item.id}: pre-copy validation failed \u2014 ${validation.reason}`);
4943
5211
  continue;
4944
5212
  }
4945
- await fs25.ensureDir(path27.dirname(destination));
4946
- await fs25.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
5213
+ await fs26.ensureDir(path27.dirname(destination));
5214
+ await fs26.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
4947
5215
  }
4948
5216
  files.push(destination);
4949
5217
  const relPaths = [path27.relative(root, destination)];
@@ -5069,7 +5337,7 @@ async function hasProtectedUnsafePath(root, relPaths, unsafeByRel) {
5069
5337
  for (const rel of relPaths) {
5070
5338
  const unsafe = unsafeByRel.get(rel) ?? null;
5071
5339
  if (unsafe === "symlink") return true;
5072
- if (unsafe === "escapes-root" && await fs25.pathExists(path27.resolve(root, rel))) return true;
5340
+ if (unsafe === "escapes-root" && await fs26.pathExists(path27.resolve(root, rel))) return true;
5073
5341
  }
5074
5342
  return false;
5075
5343
  }
@@ -5134,7 +5402,7 @@ async function cleanupMigratedCatalogItems(root, prevLock, migrations, dryRun, q
5134
5402
  void await backupTrackedPaths(root, allAbsPaths, say, "migrate");
5135
5403
  for (const { existing } of toRemove) {
5136
5404
  for (const { abs } of existing) {
5137
- await fs25.remove(abs);
5405
+ await fs26.remove(abs);
5138
5406
  await pruneEmptyDir(path27.dirname(abs));
5139
5407
  }
5140
5408
  }
@@ -5215,7 +5483,7 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun, quiet, activ
5215
5483
  say(`[dry-run] would remove ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
5216
5484
  continue;
5217
5485
  }
5218
- await fs25.remove(abs);
5486
+ await fs26.remove(abs);
5219
5487
  await pruneEmptyDir(path27.dirname(abs));
5220
5488
  say(`Removed ${pruneReason} ${displayPath(root, abs)} (${entry.id})`);
5221
5489
  }
@@ -5271,7 +5539,7 @@ async function pruneOrphanedCatalogItems(root, prevLock, notOrphanedIds, dryRun,
5271
5539
  // src/claude/write-gitignore.ts
5272
5540
  init_exec();
5273
5541
  import path28 from "node:path";
5274
- import fs26 from "fs-extra";
5542
+ import fs27 from "fs-extra";
5275
5543
  var GITIGNORE_BEGIN = "# HAUS:BEGIN haus-managed v=1";
5276
5544
  var GITIGNORE_END = "# HAUS:END haus-managed";
5277
5545
  var GITIGNORED_ARTIFACT_PATHS = [
@@ -5400,14 +5668,14 @@ function injectLinkContextGitignoreBlock(existing) {
5400
5668
  }
5401
5669
  async function writeGitignore(root, dryRun, quiet = false) {
5402
5670
  const filePath = path28.join(root, ".gitignore");
5403
- const prev = await fs26.pathExists(filePath) ? await fs26.readFile(filePath, "utf8") : "";
5671
+ const prev = await fs27.pathExists(filePath) ? await fs27.readFile(filePath, "utf8") : "";
5404
5672
  const next = injectGitignoreBlock(prev);
5405
5673
  await writeManagedText(root, filePath, next, dryRun, quiet);
5406
5674
  return filePath;
5407
5675
  }
5408
5676
  async function writeLinkContextGitignore(root, dryRun, quiet = false) {
5409
5677
  const filePath = path28.join(root, ".gitignore");
5410
- const prev = await fs26.pathExists(filePath) ? await fs26.readFile(filePath, "utf8") : "";
5678
+ const prev = await fs27.pathExists(filePath) ? await fs27.readFile(filePath, "utf8") : "";
5411
5679
  const next = injectLinkContextGitignoreBlock(prev);
5412
5680
  await writeManagedText(root, filePath, next, dryRun, quiet);
5413
5681
  return filePath;
@@ -5976,25 +6244,25 @@ async function cleanupStaleGlobalPlugins(desired, deps = {}) {
5976
6244
 
5977
6245
  // src/refs/fetch-refs.ts
5978
6246
  import path31 from "node:path";
5979
- import fs28 from "fs-extra";
6247
+ import fs29 from "fs-extra";
5980
6248
 
5981
6249
  // src/refs/cache-meta.ts
5982
6250
  import path30 from "node:path";
5983
- import fs27 from "fs-extra";
6251
+ import fs28 from "fs-extra";
5984
6252
  var META_FILENAME = "cache-meta.json";
5985
6253
  async function readCacheMeta(cacheDir) {
5986
6254
  const metaPath = path30.join(cacheDir, META_FILENAME);
5987
6255
  try {
5988
- const raw = await fs27.readFile(metaPath, "utf8");
6256
+ const raw = await fs28.readFile(metaPath, "utf8");
5989
6257
  return JSON.parse(raw);
5990
6258
  } catch {
5991
6259
  return {};
5992
6260
  }
5993
6261
  }
5994
6262
  async function writeCacheMeta(cacheDir, meta) {
5995
- await fs27.ensureDir(cacheDir);
6263
+ await fs28.ensureDir(cacheDir);
5996
6264
  const metaPath = path30.join(cacheDir, META_FILENAME);
5997
- await fs27.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
6265
+ await fs28.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf8");
5998
6266
  }
5999
6267
 
6000
6268
  // src/refs/fetch-refs.ts
@@ -6015,7 +6283,7 @@ async function fetchSingleRef(url, cacheDir, meta) {
6015
6283
  const existing = meta[url];
6016
6284
  const headers = {};
6017
6285
  const safeFile = existing?.file && existing.file === path31.basename(existing.file);
6018
- const cachedFileExists = safeFile && await fs28.pathExists(path31.join(cacheDir, existing.file));
6286
+ const cachedFileExists = safeFile && await fs29.pathExists(path31.join(cacheDir, existing.file));
6019
6287
  if (cachedFileExists) {
6020
6288
  if (existing?.etag) headers["If-None-Match"] = existing.etag;
6021
6289
  if (existing?.lastModified) headers["If-Modified-Since"] = existing.lastModified;
@@ -6026,8 +6294,8 @@ async function fetchSingleRef(url, cacheDir, meta) {
6026
6294
  if (!res.ok) return { result: "failed" };
6027
6295
  const text = await res.text();
6028
6296
  const file = `${urlToSlug(url)}.md`;
6029
- await fs28.ensureDir(cacheDir);
6030
- await fs28.writeFile(path31.join(cacheDir, file), text, "utf8");
6297
+ await fs29.ensureDir(cacheDir);
6298
+ await fs29.writeFile(path31.join(cacheDir, file), text, "utf8");
6031
6299
  meta[url] = {
6032
6300
  url,
6033
6301
  etag: res.headers.get("etag") ?? void 0,
@@ -6051,7 +6319,7 @@ async function pruneOrphanedRefs(cacheDir, keepUrls) {
6051
6319
  delete meta[url];
6052
6320
  removed++;
6053
6321
  const safeFile = typeof entry.file === "string" && entry.file === path31.basename(entry.file);
6054
- if (safeFile) await fs28.remove(path31.join(cacheDir, entry.file)).catch(() => {
6322
+ if (safeFile) await fs29.remove(path31.join(cacheDir, entry.file)).catch(() => {
6055
6323
  });
6056
6324
  }
6057
6325
  if (removed > 0) await writeCacheMeta(cacheDir, meta);
@@ -6199,6 +6467,12 @@ async function runApply(options) {
6199
6467
  process.exitCode = 1;
6200
6468
  return;
6201
6469
  }
6470
+ const profileRefused = await profileRefusal(root);
6471
+ if (profileRefused) {
6472
+ error(`${profileRefused} Nothing was written.`);
6473
+ process.exitCode = 1;
6474
+ return;
6475
+ }
6202
6476
  const rec = await readJson(hausPath(root, "recommendation.json"));
6203
6477
  const installableRecommended = installableRecommendedItems(rec?.recommended ?? []);
6204
6478
  const wantedIds = selectedIds ?? installableRecommended.map((item) => item.id);
@@ -6326,7 +6600,7 @@ async function runApply(options) {
6326
6600
 
6327
6601
  // src/commands/backups.ts
6328
6602
  import path32 from "node:path";
6329
- import fs29 from "fs-extra";
6603
+ import fs30 from "fs-extra";
6330
6604
  init_logger();
6331
6605
  init_paths();
6332
6606
 
@@ -6393,14 +6667,14 @@ Enter a number (1-${choices.length})`, io);
6393
6667
  // src/commands/backups.ts
6394
6668
  async function listBackups(root) {
6395
6669
  const dir = hausPath(root, "backups");
6396
- if (!await fs29.pathExists(dir)) return [];
6397
- const names = await fs29.readdir(dir);
6670
+ if (!await fs30.pathExists(dir)) return [];
6671
+ const names = await fs30.readdir(dir);
6398
6672
  const entries = [];
6399
6673
  for (const name of names) {
6400
6674
  const kind = classifyBackupName(name);
6401
6675
  if (!kind) continue;
6402
6676
  const absPath = path32.join(dir, name);
6403
- const stat = await fs29.lstat(absPath);
6677
+ const stat = await fs30.lstat(absPath);
6404
6678
  if (stat.isSymbolicLink()) continue;
6405
6679
  const expectDirectory = kind !== "lock";
6406
6680
  if (stat.isDirectory() !== expectDirectory) continue;
@@ -6445,7 +6719,7 @@ async function isUnsafeWriteTarget(root, target) {
6445
6719
  current = path32.join(current, segments[i]);
6446
6720
  const isLastSegment = i === segments.length - 1;
6447
6721
  try {
6448
- const stat = await fs29.lstat(current);
6722
+ const stat = await fs30.lstat(current);
6449
6723
  if (isLastSegment) {
6450
6724
  if (!stat.isFile()) return true;
6451
6725
  } else if (!stat.isDirectory()) {
@@ -6460,7 +6734,7 @@ async function isUnsafeWriteTarget(root, target) {
6460
6734
  }
6461
6735
  async function lstatIfRegularFile(abs) {
6462
6736
  try {
6463
- const stat = await fs29.lstat(abs);
6737
+ const stat = await fs30.lstat(abs);
6464
6738
  return stat.isFile() ? stat : null;
6465
6739
  } catch (err) {
6466
6740
  if (err.code === "ENOENT") return null;
@@ -6475,8 +6749,8 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
6475
6749
  );
6476
6750
  return { ok: false, restored: 0 };
6477
6751
  }
6478
- if (await fs29.pathExists(target)) {
6479
- const targetStat = await fs29.stat(target);
6752
+ if (await fs30.pathExists(target)) {
6753
+ const targetStat = await fs30.stat(target);
6480
6754
  if (targetStat.mtimeMs > entry.mtimeMs) {
6481
6755
  warn(
6482
6756
  `Current haus.lock.json (${new Date(targetStat.mtimeMs).toISOString()}) is newer than backup ${entry.id} (${new Date(entry.mtimeMs).toISOString()}) \u2014 it will be overwritten.`
@@ -6510,7 +6784,7 @@ async function restoreLockBackup(root, entry, yes, json, dryRun) {
6510
6784
  error(`Refusing to restore: ${entry.id} or ${target} changed since being checked.`);
6511
6785
  return { ok: false, restored: 0 };
6512
6786
  }
6513
- await fs29.copy(entry.absPath, target, { overwrite: true });
6787
+ await fs30.copy(entry.absPath, target, { overwrite: true });
6514
6788
  if (!json) log(`Restored .haus-workflow/haus.lock.json from ${entry.id}.`);
6515
6789
  return { ok: true, restored: 1 };
6516
6790
  }
@@ -6518,10 +6792,10 @@ async function collectFilesRecursive(dir) {
6518
6792
  const files = [];
6519
6793
  const skippedLinks = [];
6520
6794
  const skippedOther = [];
6521
- const names = await fs29.readdir(dir);
6795
+ const names = await fs30.readdir(dir);
6522
6796
  for (const name of names) {
6523
6797
  const abs = path32.join(dir, name);
6524
- const stat = await fs29.lstat(abs);
6798
+ const stat = await fs30.lstat(abs);
6525
6799
  if (stat.isSymbolicLink()) {
6526
6800
  skippedLinks.push(abs);
6527
6801
  continue;
@@ -6584,8 +6858,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
6584
6858
  }
6585
6859
  toRestore.push(abs);
6586
6860
  const target = path32.join(root, rel);
6587
- if (await fs29.pathExists(target)) {
6588
- const targetStat = await fs29.stat(target);
6861
+ if (await fs30.pathExists(target)) {
6862
+ const targetStat = await fs30.stat(target);
6589
6863
  if (targetStat.mtimeMs > backupFileStat.mtimeMs) {
6590
6864
  stale.push({
6591
6865
  rel,
@@ -6645,8 +6919,8 @@ async function restoreDirBackup(root, entry, yes, json, dryRun) {
6645
6919
  skippedAtWrite.push(rel);
6646
6920
  continue;
6647
6921
  }
6648
- await fs29.ensureDir(path32.dirname(target));
6649
- await fs29.copy(abs, target, { overwrite: true });
6922
+ await fs30.ensureDir(path32.dirname(target));
6923
+ await fs30.copy(abs, target, { overwrite: true });
6650
6924
  restoredCount++;
6651
6925
  }
6652
6926
  if (skippedAtWrite.length > 0) {
@@ -6727,7 +7001,7 @@ Keeping ${keepCount}.`
6727
7001
  const failed = [];
6728
7002
  for (const entry of removeList) {
6729
7003
  try {
6730
- await fs29.remove(entry.absPath);
7004
+ await fs30.remove(entry.absPath);
6731
7005
  removed.push(entry.id);
6732
7006
  } catch (err) {
6733
7007
  failed.push({ id: entry.id, message: err instanceof Error ? err.message : String(err) });
@@ -6841,7 +7115,7 @@ init_manifest_format();
6841
7115
  init_path_safety();
6842
7116
  import os2 from "node:os";
6843
7117
  import path33 from "node:path";
6844
- import fs30 from "fs-extra";
7118
+ import fs31 from "fs-extra";
6845
7119
  import * as tar from "tar";
6846
7120
  init_fs();
6847
7121
  init_logger();
@@ -6879,21 +7153,21 @@ async function runBundleExport(options) {
6879
7153
  }
6880
7154
  async function writeBundleAsTar(cacheDir, meta, dest) {
6881
7155
  const bundleMetaPath = path33.join(cacheDir, BUNDLE_META_FILE);
6882
- const previousBundleMeta = await fs30.pathExists(bundleMetaPath) ? await fs30.readFile(bundleMetaPath, "utf8") : null;
7156
+ const previousBundleMeta = await fs31.pathExists(bundleMetaPath) ? await fs31.readFile(bundleMetaPath, "utf8") : null;
6883
7157
  await writeJson(bundleMetaPath, meta);
6884
7158
  try {
6885
- await fs30.ensureDir(path33.dirname(dest));
7159
+ await fs31.ensureDir(path33.dirname(dest));
6886
7160
  await tar.create({ gzip: true, cwd: cacheDir, file: dest }, ["."]);
6887
7161
  } finally {
6888
- if (previousBundleMeta === null) await fs30.remove(bundleMetaPath);
6889
- else await fs30.writeFile(bundleMetaPath, previousBundleMeta, "utf8");
7162
+ if (previousBundleMeta === null) await fs31.remove(bundleMetaPath);
7163
+ else await fs31.writeFile(bundleMetaPath, previousBundleMeta, "utf8");
6890
7164
  }
6891
7165
  }
6892
7166
  function isMarkdownFile(relOrAbsPath) {
6893
7167
  return relOrAbsPath.toLowerCase().endsWith(".md");
6894
7168
  }
6895
7169
  async function listAllFilesUnfiltered(root, current = root) {
6896
- const entries = await fs30.readdir(current, { withFileTypes: true });
7170
+ const entries = await fs31.readdir(current, { withFileTypes: true });
6897
7171
  const out = [];
6898
7172
  for (const entry of entries) {
6899
7173
  if (entry.isSymbolicLink()) continue;
@@ -6907,16 +7181,16 @@ async function listAllFilesUnfiltered(root, current = root) {
6907
7181
  return out;
6908
7182
  }
6909
7183
  async function validateStagedItem(item, absPath) {
6910
- const stat = await fs30.stat(absPath).catch(() => null);
7184
+ const stat = await fs31.stat(absPath).catch(() => null);
6911
7185
  if (!stat) return { ok: true };
6912
7186
  if (stat.isFile()) {
6913
- const content2 = await fs30.readFile(absPath, "utf8");
7187
+ const content2 = await fs31.readFile(absPath, "utf8");
6914
7188
  return validateCatalogItem(item, content2);
6915
7189
  }
6916
7190
  const files = await listAllFilesUnfiltered(absPath);
6917
7191
  for (const rel of files) {
6918
7192
  if (!isMarkdownFile(rel)) continue;
6919
- const content2 = await fs30.readFile(path33.join(absPath, rel), "utf8");
7193
+ const content2 = await fs31.readFile(path33.join(absPath, rel), "utf8");
6920
7194
  const verdict = validateCatalogItem(item, content2);
6921
7195
  if (!verdict.ok) return verdict;
6922
7196
  }
@@ -6938,7 +7212,7 @@ async function validateStagedItems(staging, items) {
6938
7212
  continue;
6939
7213
  }
6940
7214
  coveredPaths.push(item.path);
6941
- if (!await fs30.pathExists(contentPath)) {
7215
+ if (!await fs31.pathExists(contentPath)) {
6942
7216
  if (!isSynthetic) {
6943
7217
  accepted.push(item.id);
6944
7218
  contentlessIds.add(item.id);
@@ -6948,7 +7222,7 @@ async function validateStagedItems(staging, items) {
6948
7222
  const verdict = await validateStagedItem(item, contentPath);
6949
7223
  if (!verdict.ok) {
6950
7224
  warn(`Rejected ${item.id}: ${verdict.reason}`);
6951
- await fs30.remove(contentPath);
7225
+ await fs31.remove(contentPath);
6952
7226
  if (!isSynthetic) rejected += 1;
6953
7227
  continue;
6954
7228
  }
@@ -6960,30 +7234,30 @@ async function validateStagedItems(staging, items) {
6960
7234
  const covered = coveredPaths.some((p) => rel === p || rel.startsWith(`${p}/`));
6961
7235
  if (!covered) {
6962
7236
  warn(`Refusing staged file outside any catalog item's path: ${rel}`);
6963
- await fs30.remove(path33.join(staging, rel));
7237
+ await fs31.remove(path33.join(staging, rel));
6964
7238
  }
6965
7239
  }
6966
7240
  await pruneEmptyDirs(staging);
6967
7241
  return { accepted, rejected, contentlessIds };
6968
7242
  }
6969
7243
  async function pruneEmptyDirs(root, dir = root) {
6970
- const entries = await fs30.readdir(dir, { withFileTypes: true });
7244
+ const entries = await fs31.readdir(dir, { withFileTypes: true });
6971
7245
  for (const entry of entries) {
6972
7246
  if (entry.isDirectory()) {
6973
7247
  await pruneEmptyDirs(root, path33.join(dir, entry.name));
6974
7248
  }
6975
7249
  }
6976
- if (dir !== root && (await fs30.readdir(dir)).length === 0) {
6977
- await fs30.remove(dir);
7250
+ if (dir !== root && (await fs31.readdir(dir)).length === 0) {
7251
+ await fs31.remove(dir);
6978
7252
  }
6979
7253
  }
6980
7254
  async function stripSymlinks(dir) {
6981
7255
  const removed = [];
6982
- const entries = await fs30.readdir(dir, { withFileTypes: true });
7256
+ const entries = await fs31.readdir(dir, { withFileTypes: true });
6983
7257
  for (const entry of entries) {
6984
7258
  const abs = path33.join(dir, entry.name);
6985
7259
  if (entry.isSymbolicLink()) {
6986
- await fs30.remove(abs);
7260
+ await fs31.remove(abs);
6987
7261
  removed.push(abs);
6988
7262
  continue;
6989
7263
  }
@@ -6996,12 +7270,12 @@ async function stripSymlinks(dir) {
6996
7270
  async function runBundleImport(file, options = {}) {
6997
7271
  const dryRun = Boolean(options.dryRun);
6998
7272
  const archive = path33.resolve(file);
6999
- if (!await fs30.pathExists(archive)) {
7273
+ if (!await fs31.pathExists(archive)) {
7000
7274
  error(`No such bundle: ${file}`);
7001
7275
  process.exitCode = 1;
7002
7276
  return;
7003
7277
  }
7004
- const staging = await fs30.mkdtemp(path33.join(os2.tmpdir(), "haus-catalog-bundle-import-"));
7278
+ const staging = await fs31.mkdtemp(path33.join(os2.tmpdir(), "haus-catalog-bundle-import-"));
7005
7279
  try {
7006
7280
  await tar.extract({
7007
7281
  file: archive,
@@ -7057,9 +7331,9 @@ async function runBundleImport(file, options = {}) {
7057
7331
  }
7058
7332
  return;
7059
7333
  }
7060
- await fs30.ensureDir(getCacheDir());
7061
- for (const entry of await fs30.readdir(staging)) {
7062
- await fs30.copy(path33.join(staging, entry), path33.join(getCacheDir(), entry), {
7334
+ await fs31.ensureDir(getCacheDir());
7335
+ for (const entry of await fs31.readdir(staging)) {
7336
+ await fs31.copy(path33.join(staging, entry), path33.join(getCacheDir(), entry), {
7063
7337
  overwrite: true,
7064
7338
  errorOnExist: false
7065
7339
  });
@@ -7076,7 +7350,7 @@ async function runBundleImport(file, options = {}) {
7076
7350
  }
7077
7351
  throw err;
7078
7352
  } finally {
7079
- await fs30.remove(staging);
7353
+ await fs31.remove(staging);
7080
7354
  }
7081
7355
  }
7082
7356
 
@@ -7086,7 +7360,7 @@ import fs41 from "fs-extra";
7086
7360
  init_manifest_format();
7087
7361
 
7088
7362
  // src/claude/former-install-orphans.ts
7089
- import fs31 from "fs-extra";
7363
+ import fs32 from "fs-extra";
7090
7364
  init_path_safety();
7091
7365
  init_paths();
7092
7366
  var toPosix = (value) => value.replace(/\\/g, "/");
@@ -7110,7 +7384,7 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
7110
7384
  const projectRel = `.claude/${relPath}`;
7111
7385
  if (trackedPaths.has(projectRel) || reported.has(projectRel)) continue;
7112
7386
  const abs = safeJoin(claudePath(root), relPath);
7113
- if (abs === null || !await fs31.pathExists(abs)) continue;
7387
+ if (abs === null || !await fs32.pathExists(abs)) continue;
7114
7388
  reported.add(projectRel);
7115
7389
  orphans.push({ formerId: entry.formerId, currentId: entry.currentId, relPath: projectRel });
7116
7390
  }
@@ -7118,13 +7392,13 @@ async function findOrphanedFormerInstalls(root, items, lockRows) {
7118
7392
  }
7119
7393
 
7120
7394
  // src/claude/plugin-duplicate-skills.ts
7121
- import fs32 from "node:fs";
7395
+ import fs33 from "node:fs";
7122
7396
  import path34 from "node:path";
7123
7397
  init_paths();
7124
7398
  function subdirectoryNames(dir) {
7125
7399
  let entries;
7126
7400
  try {
7127
- entries = fs32.readdirSync(dir, { withFileTypes: true });
7401
+ entries = fs33.readdirSync(dir, { withFileTypes: true });
7128
7402
  } catch {
7129
7403
  return [];
7130
7404
  }
@@ -7133,7 +7407,7 @@ function subdirectoryNames(dir) {
7133
7407
  function isDirectorySymlink(dir, entry) {
7134
7408
  if (!entry.isSymbolicLink()) return false;
7135
7409
  try {
7136
- return fs32.statSync(path34.join(dir, entry.name)).isDirectory();
7410
+ return fs33.statSync(path34.join(dir, entry.name)).isDirectory();
7137
7411
  } catch {
7138
7412
  return false;
7139
7413
  }
@@ -7172,82 +7446,6 @@ init_project_pin();
7172
7446
  init_fs();
7173
7447
  init_logger();
7174
7448
  init_paths();
7175
-
7176
- // src/claude/project-intent.ts
7177
- init_project_pin();
7178
- init_fs();
7179
- init_logger();
7180
- init_paths();
7181
- import fs33 from "fs-extra";
7182
- function normalizeIdList(value) {
7183
- if (!Array.isArray(value)) return [];
7184
- return [
7185
- ...new Set(
7186
- value.filter((entry) => typeof entry === "string" && entry.length > 0)
7187
- )
7188
- ];
7189
- }
7190
- function normalizeSignalList(value) {
7191
- return [...new Set(normalizeIdList(value).map((entry) => entry.toLowerCase()))];
7192
- }
7193
- async function readProjectConfigObject(root) {
7194
- try {
7195
- const data = await readJson(hausPath(root, PROJECT_CONFIG_FILE));
7196
- if (data == null || typeof data !== "object" || Array.isArray(data)) return null;
7197
- return data;
7198
- } catch (err) {
7199
- if (err.code === "ENOENT") return null;
7200
- warn(
7201
- `${PROJECT_CONFIG_REL} could not be read (${err instanceof Error ? err.message : String(err)}) \u2014 treating project intent as unset`
7202
- );
7203
- return null;
7204
- }
7205
- }
7206
- async function updateProjectConfig(root, mutate) {
7207
- const file = hausPath(root, PROJECT_CONFIG_FILE);
7208
- let existing = {};
7209
- if (await fs33.pathExists(file)) {
7210
- try {
7211
- const parsed = JSON.parse(await fs33.readFile(file, "utf8"));
7212
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
7213
- existing = { ...parsed };
7214
- } else {
7215
- warn(`${PROJECT_CONFIG_REL} is not a JSON object \u2014 rewriting with haus-managed keys only`);
7216
- }
7217
- } catch (err) {
7218
- warn(
7219
- `${PROJECT_CONFIG_REL} could not be parsed (${err instanceof Error ? err.message : String(err)}) \u2014 rewriting with haus-managed keys only`
7220
- );
7221
- }
7222
- }
7223
- await writeJson(file, mutate(existing));
7224
- }
7225
- async function readProjectIncludedIds(root) {
7226
- const data = await readProjectConfigObject(root);
7227
- return data == null ? [] : normalizeIdList(data.includedIds);
7228
- }
7229
- async function addProjectIncludedIds(root, ids) {
7230
- const toAdd = normalizeIdList(ids);
7231
- if (toAdd.length === 0) return;
7232
- await updateProjectConfig(root, (existing) => ({
7233
- ...existing,
7234
- includedIds: [.../* @__PURE__ */ new Set([...normalizeIdList(existing.includedIds), ...toAdd])].sort()
7235
- }));
7236
- }
7237
- function withdrawIncludedIds(existing, ids) {
7238
- const drop = new Set(ids);
7239
- return normalizeIdList(existing.includedIds).filter((id) => !drop.has(id));
7240
- }
7241
- async function readDeclaredContext(root) {
7242
- const data = await readProjectConfigObject(root);
7243
- if (data == null) return { roles: [], stacks: [] };
7244
- return {
7245
- roles: normalizeSignalList(data.declaredRoles),
7246
- stacks: normalizeSignalList(data.declaredStacks)
7247
- };
7248
- }
7249
-
7250
- // src/claude/project-exclusions.ts
7251
7449
  function normalizeExcludedIds(value) {
7252
7450
  if (!Array.isArray(value)) return [];
7253
7451
  return [
@@ -7568,12 +7766,12 @@ init_fs();
7568
7766
  import path36 from "node:path";
7569
7767
  var NUL = String.fromCharCode(0);
7570
7768
  function parseNumstat(output) {
7571
- const fields2 = output.split(NUL);
7769
+ const fields = output.split(NUL);
7572
7770
  const linesByFile = {};
7573
7771
  let linesAdded = 0;
7574
7772
  let linesRemoved = 0;
7575
- for (let i = 0; i < fields2.length; i++) {
7576
- const field = fields2[i];
7773
+ for (let i = 0; i < fields.length; i++) {
7774
+ const field = fields[i];
7577
7775
  if (!field) continue;
7578
7776
  const parts = field.split(" ");
7579
7777
  if (parts.length < 3) continue;
@@ -7582,7 +7780,7 @@ function parseNumstat(output) {
7582
7780
  if (Number.isNaN(added) || Number.isNaN(removed)) continue;
7583
7781
  let file = parts[2];
7584
7782
  if (file === "") {
7585
- file = fields2[i + 2];
7783
+ file = fields[i + 2];
7586
7784
  i += 2;
7587
7785
  }
7588
7786
  if (!file) continue;
@@ -9161,6 +9359,8 @@ async function runDoctor(options) {
9161
9359
  ok(`- WARN: ${describeUnknownDetection(context.unsupportedSignals ?? [], isLinkedWorktree)}`);
9162
9360
  }
9163
9361
  ok(`Package manager: ${context.packageManager}`);
9362
+ const profile = await readProjectProfile(root);
9363
+ ok(`Profile: ${profile}`);
9164
9364
  if (recommendation) {
9165
9365
  ok(`Recommended items: ${recommendation.recommended?.length ?? 0}`);
9166
9366
  } else {
@@ -9219,7 +9419,9 @@ async function runDoctor(options) {
9219
9419
  const block = rootClaudeMdContent.slice(beginIdx, endIdx + BLOCK_END.length);
9220
9420
  const importLinePattern = /^@(\S+)\s*$/gm;
9221
9421
  const actualTargets = [...block.matchAll(importLinePattern)].map((m) => m[1]);
9222
- const expectedTargets = [...IMPORT_CONTENT.matchAll(importLinePattern)].map((m) => m[1]);
9422
+ const expectedTargets = [...importContentFor(profile).matchAll(importLinePattern)].map(
9423
+ (m) => m[1]
9424
+ );
9223
9425
  const missingTargets = expectedTargets.filter((t) => !actualTargets.includes(t));
9224
9426
  const foreignTargets = actualTargets.filter((t) => !expectedTargets.includes(t));
9225
9427
  if (missingTargets.length > 0 || foreignTargets.length > 0) {
@@ -9271,13 +9473,13 @@ async function runDoctor(options) {
9271
9473
  ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
9272
9474
  } else {
9273
9475
  const bodyContent = workflowContent.slice(firstLine.length + 1);
9274
- const cachePath = path49.join(getCacheDir(), "templates/agentic-workflow-standard.md");
9476
+ const cachePath = path49.join(getCacheDir(), PROFILES[profile].workflowTemplateRel);
9275
9477
  const bundledPath = path49.join(
9276
9478
  packageRoot(),
9277
9479
  "library",
9278
9480
  "global",
9279
9481
  "templates",
9280
- "agentic-workflow-standard.md"
9482
+ path49.basename(PROFILES[profile].workflowTemplateRel)
9281
9483
  );
9282
9484
  const templatePath = await fs41.pathExists(cachePath) ? cachePath : bundledPath;
9283
9485
  const templateContent = await readText(templatePath);
@@ -9364,6 +9566,16 @@ async function runDoctor(options) {
9364
9566
  } else {
9365
9567
  ok("- CI GATE: OK (a workflow runs `haus ci-gate`)");
9366
9568
  }
9569
+ if (!PROFILES[profile].writesCiGate) {
9570
+ const ciGateContent = await readText(path49.join(root, CI_GATE_WORKFLOW_REL));
9571
+ if (ciGateContent !== void 0 && isHausWrittenCiGate(ciGateContent)) {
9572
+ suggest(
9573
+ `- CI GATE: ${CI_GATE_WORKFLOW_REL} is still present under the ${profile} profile, which does not write one`,
9574
+ `This repo is on the ${profile} profile (no CI gate by design), but ${CI_GATE_WORKFLOW_REL} is still here from an earlier setup`,
9575
+ `delete ${CI_GATE_WORKFLOW_REL} by hand (haus will not remove it for you)`
9576
+ );
9577
+ }
9578
+ }
9367
9579
  const trackedArtifacts = await listTrackedArtifactPaths(root);
9368
9580
  if (trackedArtifacts.length > 0) {
9369
9581
  flag(
@@ -9404,6 +9616,18 @@ async function runDoctor(options) {
9404
9616
  err.kind === "too-new" ? "npm install -g @haus-tech/haus-workflow@latest" : "fix `manifestFormat` in the catalog manifest \u2014 it must be a positive integer (upgrading haus will not help)"
9405
9617
  );
9406
9618
  }
9619
+ const profileRules = catalogCtx ? parseProfileRules(
9620
+ catalogCtx.profiles,
9621
+ profile,
9622
+ new Set(catalogCtx.items.map((item) => item.id))
9623
+ ) : null;
9624
+ if (catalogCtx && profileRules === null) {
9625
+ flag(
9626
+ `- PROFILE: the ${describeCatalogTier(catalogCtx)} catalog defines no "${profile}" profile \u2014 \`haus apply\` refuses until it does`,
9627
+ `This project is set to the ${profile} profile, but the catalog it uses defines no such profile (needs haus-workflow-catalog >= 6.5.0)`,
9628
+ 'haus update (or set "profile" back to "production" in .haus-workflow/haus.config.json)'
9629
+ );
9630
+ }
9407
9631
  const pluginPins = catalogItems === null ? [] : await loadCatalogPluginPins(root);
9408
9632
  const pluginState = await probePluginEnv();
9409
9633
  const activePins = pluginPins.filter((pin) => isPluginActive(pluginState, pin));
@@ -9571,8 +9795,21 @@ async function runDoctor(options) {
9571
9795
  const excludedIds = new Set(await readProjectExcludedIds(root));
9572
9796
  const activePinIds = activePins.map((pin) => pin.id);
9573
9797
  const lockedIds = (await readJson(hausPath(root, "haus.lock.json")) ?? []).map((entry) => entry.id).filter((id) => typeof id === "string");
9798
+ const catalogById = new Map(catalogItems.map((item) => [item.id, item]));
9799
+ const excludedByProfile = profileRules === null ? [] : lockedIds.filter((id) => {
9800
+ const item = catalogById.get(id);
9801
+ return item !== void 0 && profileVerdict(item, profileRules)?.kind === "excluded";
9802
+ });
9803
+ if (excludedByProfile.length > 0) {
9804
+ suggest(
9805
+ `- PROFILE: ${excludedByProfile.length} installed item(s) are excluded by the ${profile} profile (${excludedByProfile.join(", ")})`,
9806
+ `${excludedByProfile.length} installed item(s) are kept out by the ${profile} profile and will not be recommended again`,
9807
+ "haus apply --write --prune"
9808
+ );
9809
+ }
9810
+ const profileExcludedSet = new Set(excludedByProfile);
9574
9811
  const drifted = lockedIds.filter(
9575
- (id) => catalogIds.has(id) && !recommendedIds.has(id) && !excludedIds.has(id) && !isSupersededByPlugin(id, activePinIds)
9812
+ (id) => catalogIds.has(id) && !recommendedIds.has(id) && !excludedIds.has(id) && !profileExcludedSet.has(id) && !isSupersededByPlugin(id, activePinIds)
9576
9813
  );
9577
9814
  if (drifted.length > 0) {
9578
9815
  suggest(
@@ -9881,14 +10118,14 @@ function stampFrontmatter(content2, h) {
9881
10118
  }
9882
10119
  }
9883
10120
  if (close === -1) return content2;
9884
- const fields2 = [];
10121
+ const fields = [];
9885
10122
  for (let i = 1; i < close; i++) {
9886
10123
  if (/^haus_managed:/.test(lines[i])) continue;
9887
- fields2.push(lines[i]);
10124
+ fields.push(lines[i]);
9888
10125
  }
9889
10126
  const rebuilt = [
9890
10127
  FM_FENCE,
9891
- ...fields2,
10128
+ ...fields,
9892
10129
  `${FM_KEY}: "${buildMarkerValue(h)}"`,
9893
10130
  ...lines.slice(close)
9894
10131
  // closing fence + body
@@ -11304,6 +11541,10 @@ function normalizeRecommendation(input2) {
11304
11541
  estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
11305
11542
  };
11306
11543
  }
11544
+ var NEVER_NEAR_MISS_GATES = /* @__PURE__ */ new Set([
11545
+ "profile-excluded",
11546
+ "workflow-template-item"
11547
+ ]);
11307
11548
  function buildNearMiss(skipped) {
11308
11549
  const nearMiss = [];
11309
11550
  for (const item of skipped) {
@@ -11311,6 +11552,7 @@ function buildNearMiss(skipped) {
11311
11552
  if (!gates) continue;
11312
11553
  const failing = gates.filter((g) => !g.passed);
11313
11554
  if (failing.length !== 1) continue;
11555
+ if (NEVER_NEAR_MISS_GATES.has(failing[0].name)) continue;
11314
11556
  nearMiss.push({
11315
11557
  id: item.id,
11316
11558
  missingGate: failing[0].name,
@@ -11721,6 +11963,7 @@ async function runGuard(kind, _options) {
11721
11963
  }
11722
11964
 
11723
11965
  // src/claude/setup-core.ts
11966
+ import path59 from "node:path";
11724
11967
  init_github_rate_limit();
11725
11968
 
11726
11969
  // src/recommender/recommend.ts
@@ -11770,7 +12013,17 @@ async function readChangedFiles(root) {
11770
12013
  // src/recommender/recommend.ts
11771
12014
  async function recommend(root, context, opts = {}) {
11772
12015
  const activePluginIds = opts.activePluginIds ?? [];
11773
- const items = await loadCatalog(root);
12016
+ const catalogCtx = await loadCatalogContext(root);
12017
+ const items = catalogCtx.items;
12018
+ const profile = opts.profile ?? await readProjectProfile(root);
12019
+ const profileRules = parseProfileRules(
12020
+ catalogCtx.profiles,
12021
+ profile,
12022
+ new Set(items.map((item) => item.id))
12023
+ );
12024
+ if (profileRules === null) {
12025
+ throw new ProfileUnsupportedError(profile, describeCatalogTier(catalogCtx));
12026
+ }
11774
12027
  const excludedIds = await readProjectExcludedIds(root);
11775
12028
  const sources = buildSourcesReport(items);
11776
12029
  const deep = await readJson(hausPath(root, "deep-context.json")) ?? {};
@@ -11855,6 +12108,33 @@ async function recommend(root, context, opts = {}) {
11855
12108
  "Catalog item is deprecated",
11856
12109
  "reviewStatus:deprecated"
11857
12110
  );
12111
+ const verdict = profileVerdict(item, profileRules);
12112
+ if (verdict?.kind === "excluded") {
12113
+ add(
12114
+ "profile-excluded",
12115
+ false,
12116
+ "profile-excluded",
12117
+ `Excluded by profile ${profile} (${verdict.clause})`,
12118
+ verdict.clause
12119
+ );
12120
+ } else if (verdict?.kind === "kept") {
12121
+ add(
12122
+ "profile-excluded",
12123
+ true,
12124
+ "profile-kept",
12125
+ `Kept by profile ${profile} keepIds despite ${verdict.clause}`,
12126
+ `keep:${item.id}`
12127
+ );
12128
+ }
12129
+ if (WORKFLOW_TEMPLATE_ITEM_IDS.has(item.id)) {
12130
+ add(
12131
+ "workflow-template-item",
12132
+ false,
12133
+ "workflow-template-item",
12134
+ "Written as WORKFLOW.md by the profile, never installed as an item",
12135
+ `id:${item.id}`
12136
+ );
12137
+ }
11858
12138
  if (isItemSupersededByPlugin(item, activePluginIds)) {
11859
12139
  add(
11860
12140
  "superseded-by-global-plugin",
@@ -12157,7 +12437,11 @@ var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
12157
12437
  "curated-risk-blocked",
12158
12438
  "sensitive-policy",
12159
12439
  "source-trust",
12160
- "source-approval"
12440
+ "source-approval",
12441
+ // A `--include` must not force a second copy of the profile's workflow standard into
12442
+ // `.claude/templates/` — it is written as WORKFLOW.md by the profile itself (Task 3
12443
+ // controller ruling, ADR-0125).
12444
+ "workflow-template-item"
12161
12445
  ]);
12162
12446
  function applyManualIncludes(include, items, recommended, skipped) {
12163
12447
  const warnings = [];
@@ -12182,6 +12466,9 @@ function applyManualIncludes(include, items, recommended, skipped) {
12182
12466
  if (skipCode === "requires-any-unsatisfied") {
12183
12467
  warnings.push(`--include: "${id}" added manually though its requiresAny gate is unsatisfied`);
12184
12468
  }
12469
+ if (skipCode === "profile-excluded") {
12470
+ warnings.push(`--include: "${id}" added manually though the project profile excludes it`);
12471
+ }
12185
12472
  const previouslySkipped = skipIdx >= 0 ? skipped[skipIdx] : void 0;
12186
12473
  if (skipIdx >= 0) skipped.splice(skipIdx, 1);
12187
12474
  const isConfig = item.type === "config";
@@ -12402,6 +12689,32 @@ async function runSetupCore(root, opts) {
12402
12689
  const { json, apply, dryRun, quiet, confirm: confirm2, force } = opts;
12403
12690
  const say = quiet || json ? () => {
12404
12691
  } : log;
12692
+ const storedProfile = await readProjectProfile(root);
12693
+ if (storedProfile === "poc" && opts.profile !== "poc") {
12694
+ const lockedMessage = pocLockedMessage();
12695
+ error(lockedMessage);
12696
+ process.exitCode = 1;
12697
+ emitJsonEnvelope(json, quiet, {
12698
+ scan: null,
12699
+ recommendation: null,
12700
+ hooks: null,
12701
+ repoName: path59.basename(root),
12702
+ warnings: [lockedMessage]
12703
+ });
12704
+ return {
12705
+ root,
12706
+ repoName: path59.basename(root),
12707
+ roles: [],
12708
+ recommendedCount: 0,
12709
+ warnings: [lockedMessage],
12710
+ hooksOk: false,
12711
+ written: [],
12712
+ catalogInstallOk: true,
12713
+ setupOk: false,
12714
+ failureReason: "profile-locked"
12715
+ };
12716
+ }
12717
+ const profile = opts.profile ?? storedProfile;
12405
12718
  const scanResult = await scanProject(root);
12406
12719
  say("Haus scan complete");
12407
12720
  say(`Roles: ${scanResult.repoRoles.join(", ") || "unknown"}`);
@@ -12410,10 +12723,37 @@ async function runSetupCore(root, opts) {
12410
12723
  const context = await readContextOrScan(root);
12411
12724
  const priorManualIncludes = await readPriorManualIncludes(root);
12412
12725
  const activePluginIds = await activeGlobalPluginIds(root);
12413
- const recommendation = await recommend(root, context, {
12414
- include: priorManualIncludes,
12415
- activePluginIds
12416
- });
12726
+ let recommendation;
12727
+ try {
12728
+ recommendation = await recommend(root, context, {
12729
+ include: priorManualIncludes,
12730
+ activePluginIds,
12731
+ profile
12732
+ });
12733
+ } catch (err) {
12734
+ if (!(err instanceof ProfileUnsupportedError)) throw err;
12735
+ error(err.message);
12736
+ process.exitCode = 1;
12737
+ emitJsonEnvelope(json, quiet, {
12738
+ scan: scanResult,
12739
+ recommendation: null,
12740
+ hooks: null,
12741
+ repoName: context.repoName,
12742
+ warnings: [err.message]
12743
+ });
12744
+ return {
12745
+ root,
12746
+ repoName: context.repoName,
12747
+ roles: scanResult.repoRoles,
12748
+ recommendedCount: 0,
12749
+ warnings: [err.message],
12750
+ hooksOk: false,
12751
+ written: [],
12752
+ catalogInstallOk: true,
12753
+ setupOk: false,
12754
+ failureReason: "profile-unsupported"
12755
+ };
12756
+ }
12417
12757
  if (recommendation.recommended.length === 0 && !force) {
12418
12758
  const warningLines2 = [
12419
12759
  .../* @__PURE__ */ new Set([...context.warnings, ...recommendation.warnings ?? [], ...siblingWarning])
@@ -12565,9 +12905,13 @@ async function runSetupCore(root, opts) {
12565
12905
  );
12566
12906
  }
12567
12907
  }
12908
+ if (opts.profile !== void 0 && opts.profile !== storedProfile && !isPreview) {
12909
+ await writeProjectProfile(root, opts.profile);
12910
+ }
12568
12911
  const files = await writeClaudeFiles(root, dryRun ?? false, void 0, {
12569
12912
  quiet: opts.quiet,
12570
- activePluginIds
12913
+ activePluginIds,
12914
+ profile
12571
12915
  });
12572
12916
  say("Applied files:");
12573
12917
  files.forEach((f) => say(`- ${displayPath(root, f)}`));
@@ -12622,12 +12966,18 @@ async function runInit(options) {
12622
12966
  process.exitCode = 1;
12623
12967
  return;
12624
12968
  }
12969
+ if (options.profile !== void 0 && !isProfileId(options.profile)) {
12970
+ error(`--profile must be one of ${PROFILE_IDS.join(", ")} (got "${options.profile}").`);
12971
+ process.exitCode = 1;
12972
+ return;
12973
+ }
12625
12974
  await runSetupCore(root, {
12626
12975
  json: options.json,
12627
12976
  apply: !dryRun,
12628
12977
  dryRun: false,
12629
12978
  force: options.force,
12630
12979
  allowEmptyCache: options.allowEmptyCache,
12980
+ profile: options.profile,
12631
12981
  confirm: () => confirm("Approve and write Claude files now?")
12632
12982
  });
12633
12983
  }
@@ -12773,10 +13123,10 @@ async function runInstall(options, deps = {}) {
12773
13123
  }
12774
13124
 
12775
13125
  // src/commands/new.ts
12776
- import path60 from "node:path";
13126
+ import path61 from "node:path";
12777
13127
 
12778
13128
  // src/new-project/create-project.ts
12779
- import path59 from "node:path";
13129
+ import path60 from "node:path";
12780
13130
  import fs47 from "fs-extra";
12781
13131
  init_exec();
12782
13132
  init_logger();
@@ -12962,7 +13312,7 @@ var defaultDeps = {
12962
13312
  };
12963
13313
  var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
12964
13314
  async function recordPackageManager(projectPath, pm, deps) {
12965
- const pkgPath = path59.join(projectPath, "package.json");
13315
+ const pkgPath = path60.join(projectPath, "package.json");
12966
13316
  let pkg;
12967
13317
  try {
12968
13318
  pkg = await fs47.readJson(pkgPath);
@@ -12993,9 +13343,9 @@ var CONFIG_TOOLING = {
12993
13343
  }
12994
13344
  };
12995
13345
  async function ensureConfigTooling(projectPath, scaffolded) {
12996
- const needed = scaffolded.map((file) => CONFIG_TOOLING[path59.basename(file)]).filter((entry) => entry !== void 0);
13346
+ const needed = scaffolded.map((file) => CONFIG_TOOLING[path60.basename(file)]).filter((entry) => entry !== void 0);
12997
13347
  if (needed.length === 0) return [];
12998
- const pkgPath = path59.join(projectPath, "package.json");
13348
+ const pkgPath = path60.join(projectPath, "package.json");
12999
13349
  let pkg;
13000
13350
  try {
13001
13351
  pkg = await fs47.readJson(pkgPath);
@@ -13027,7 +13377,7 @@ async function ensureConfigTooling(projectPath, scaffolded) {
13027
13377
  }
13028
13378
  async function createProject(opts, deps = defaultDeps) {
13029
13379
  const steps = [];
13030
- const projectPath = path59.resolve(opts.parentDir, opts.name);
13380
+ const projectPath = path60.resolve(opts.parentDir, opts.name);
13031
13381
  const installHint = `cd ${opts.name} && ${opts.pm}${opts.pm === "yarn" ? "" : " install"}`;
13032
13382
  const fail = (step, detail) => {
13033
13383
  steps.push({ step, status: "failed", detail });
@@ -13035,14 +13385,14 @@ async function createProject(opts, deps = defaultDeps) {
13035
13385
  };
13036
13386
  const stack = getStack(opts.stackId);
13037
13387
  if (!stack) return fail("preflight", `Unknown stack "${opts.stackId}". Run \`haus new --list\`.`);
13038
- const parentStat = await fs47.stat(path59.dirname(projectPath)).catch(() => null);
13388
+ const parentStat = await fs47.stat(path60.dirname(projectPath)).catch(() => null);
13039
13389
  if (!parentStat?.isDirectory()) {
13040
13390
  return fail(
13041
13391
  "preflight",
13042
13392
  `Parent directory ${opts.parentDir} is not an existing directory \u2014 pass an existing one to --dir.`
13043
13393
  );
13044
13394
  }
13045
- if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path59.dirname(projectPath) !== path59.resolve(opts.parentDir)) {
13395
+ if (!NAME_RE.test(opts.name) || opts.name.includes("/") || opts.name.includes("\\") || path60.dirname(projectPath) !== path60.resolve(opts.parentDir)) {
13046
13396
  return fail(
13047
13397
  "preflight",
13048
13398
  `Invalid project name "${opts.name}" (lowercase letters, digits, . _ - only).`
@@ -13064,7 +13414,11 @@ async function createProject(opts, deps = defaultDeps) {
13064
13414
  status: opts.github ? "planned" : "skipped",
13065
13415
  detail: opts.github ? `gh repo create ${opts.org ? `${opts.org}/` : ""}${opts.name} --${opts.visibility} --source . --push` : "--no-github"
13066
13416
  });
13067
- steps.push({ step: "init", status: "planned", detail: "haus init pipeline in the new project" });
13417
+ steps.push({
13418
+ step: "init",
13419
+ status: "planned",
13420
+ detail: `haus init pipeline in the new project${opts.profile ? ` (profile ${opts.profile})` : ""}`
13421
+ });
13068
13422
  steps.push({ step: "overlay", status: "planned", detail: stack.overlayConfigIds.join(", ") });
13069
13423
  steps.push({
13070
13424
  step: "role-check",
@@ -13126,7 +13480,7 @@ async function createProject(opts, deps = defaultDeps) {
13126
13480
  steps.push({ step: "scaffold", status: "done" });
13127
13481
  phase = "git";
13128
13482
  if (!opts.json) log("\u25B6 git: init/branch/commit");
13129
- if (!await fs47.pathExists(path59.join(projectPath, ".git"))) {
13483
+ if (!await fs47.pathExists(path60.join(projectPath, ".git"))) {
13130
13484
  const init = await deps.git(["init"], { cwd: projectPath });
13131
13485
  if (init.exitCode !== 0) return fail("git", `git init failed: ${init.stderr}`);
13132
13486
  }
@@ -13167,6 +13521,7 @@ async function createProject(opts, deps = defaultDeps) {
13167
13521
  phase = "init";
13168
13522
  if (!opts.json) log("\u25B6 init: haus setup pipeline");
13169
13523
  const exitCodeBeforeSetup = process.exitCode;
13524
+ const profileFlag = opts.profile ? ` --profile ${opts.profile}` : "";
13170
13525
  let setupResult;
13171
13526
  try {
13172
13527
  setupResult = await deps.setup(projectPath, {
@@ -13175,13 +13530,14 @@ async function createProject(opts, deps = defaultDeps) {
13175
13530
  dryRun: false,
13176
13531
  quiet: opts.json,
13177
13532
  confirm: opts.yes ? async () => true : () => confirm("Approve and write Claude files now?"),
13178
- allowEmptyCache: opts.allowEmptyCache
13533
+ allowEmptyCache: opts.allowEmptyCache,
13534
+ profile: opts.profile
13179
13535
  });
13180
13536
  } catch (err) {
13181
13537
  steps.push({
13182
13538
  step: "init",
13183
13539
  status: "failed",
13184
- detail: `haus init threw (${err instanceof Error ? err.message : String(err)}). Project and repo are intact \u2014 cd ${opts.name} && haus init to retry.`
13540
+ detail: `haus init threw (${err instanceof Error ? err.message : String(err)}). Project and repo are intact \u2014 cd ${opts.name} && haus init${profileFlag} to retry.`
13185
13541
  });
13186
13542
  return { ok: false, projectPath, repoUrl, installHint, steps };
13187
13543
  }
@@ -13191,13 +13547,15 @@ async function createProject(opts, deps = defaultDeps) {
13191
13547
  const reason = setupResult?.failureReason ?? (catalogInstallRefused ? "catalog-install" : void 0);
13192
13548
  switch (reason) {
13193
13549
  case "catalog-install":
13194
- return `${enter} && haus init --allow-empty-cache`;
13550
+ return `${enter} && haus init --allow-empty-cache${profileFlag}`;
13195
13551
  case "zero-signal":
13196
- return `${enter} && haus init --force`;
13552
+ return `${enter} && haus init --force${profileFlag}`;
13197
13553
  case "manifest-format":
13198
- return `npm i -g @haus-tech/haus-workflow@latest, then ${enter} && haus init`;
13554
+ return `npm i -g @haus-tech/haus-workflow@latest, then ${enter} && haus init${profileFlag}`;
13555
+ case "profile-unsupported":
13556
+ return `${enter} && haus update, then haus init${profileFlag}`;
13199
13557
  default:
13200
- return `${enter} && haus init`;
13558
+ return `${enter} && haus init${profileFlag}`;
13201
13559
  }
13202
13560
  })();
13203
13561
  if (Array.isArray(setupResult?.written) && setupResult.written.length === 0 && setupResult.setupOk !== false) {
@@ -13208,7 +13566,7 @@ async function createProject(opts, deps = defaultDeps) {
13208
13566
  });
13209
13567
  return { ok: false, projectPath, repoUrl, installHint, steps };
13210
13568
  }
13211
- if (process.exitCode === 1 && exitCodeBeforeSetup !== 1) {
13569
+ if (process.exitCode === 1 && exitCodeBeforeSetup !== 1 || setupResult?.setupOk === false) {
13212
13570
  if (opts.allowEmptyCache && catalogInstallRefused) {
13213
13571
  steps.push({
13214
13572
  step: "init",
@@ -13352,11 +13710,14 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
13352
13710
  if (flags.dir !== void 0 && flags.dir.trim() === "") {
13353
13711
  return { ok: false, error: "--dir needs a path (omit it to scaffold into the cwd)." };
13354
13712
  }
13355
- const parentDir = flags.dir === void 0 ? cwd : path60.resolve(cwd, flags.dir);
13713
+ const parentDir = flags.dir === void 0 ? cwd : path61.resolve(cwd, flags.dir);
13356
13714
  const pm = flags.pm ?? "yarn";
13357
13715
  if (!PMS.includes(pm)) {
13358
13716
  return { ok: false, error: `Unsupported --pm "${pm}" \u2014 use yarn, npm, or pnpm.` };
13359
13717
  }
13718
+ if (flags.profile !== void 0 && !isProfileId(flags.profile)) {
13719
+ return { ok: false, error: `Unsupported --profile "${flags.profile}" \u2014 use production or poc.` };
13720
+ }
13360
13721
  if (stackArg === void 0 || nameArg === void 0) {
13361
13722
  return { ok: false, error: "missing-args" };
13362
13723
  }
@@ -13373,7 +13734,8 @@ function resolveNewOptions(stackArg, nameArg, flags, cwd) {
13373
13734
  yes: flags.yes === true,
13374
13735
  dryRun,
13375
13736
  json: flags.json === true,
13376
- allowEmptyCache: flags.allowEmptyCache === true
13737
+ allowEmptyCache: flags.allowEmptyCache === true,
13738
+ profile: flags.profile
13377
13739
  }
13378
13740
  };
13379
13741
  }
@@ -13431,7 +13793,15 @@ async function runRecommend(options) {
13431
13793
  const include = parseIdList(options.include);
13432
13794
  if (include.length > 0) await addProjectIncludedIds(root, include);
13433
13795
  const activePluginIds = await activeGlobalPluginIds(root);
13434
- const result = await recommend(root, context, { include, activePluginIds });
13796
+ let result;
13797
+ try {
13798
+ result = await recommend(root, context, { include, activePluginIds });
13799
+ } catch (err) {
13800
+ if (!(err instanceof ProfileUnsupportedError)) throw err;
13801
+ error(err.message);
13802
+ process.exitCode = 1;
13803
+ return;
13804
+ }
13435
13805
  await writeJson(hausPath(root, "recommendation.json"), result);
13436
13806
  if (options.json) {
13437
13807
  log(JSON.stringify(result, null, 2));
@@ -13450,7 +13820,7 @@ async function runRecommend(options) {
13450
13820
  }
13451
13821
 
13452
13822
  // src/commands/remove.ts
13453
- import path61 from "node:path";
13823
+ import path62 from "node:path";
13454
13824
  init_fs();
13455
13825
  init_logger();
13456
13826
  init_paths();
@@ -13557,7 +13927,7 @@ async function runRemove(ids, options = {}) {
13557
13927
  }
13558
13928
  const allAbs = cleared.flatMap(({ existing }) => existing.map(({ abs }) => abs));
13559
13929
  const backupRoot = await backupTrackedPaths(root, allAbs, say, "remove");
13560
- report.backupDir = backupRoot === null ? null : path61.basename(backupRoot);
13930
+ report.backupDir = backupRoot === null ? null : path62.basename(backupRoot);
13561
13931
  for (const { id, existing } of cleared) {
13562
13932
  await deleteTrackedPaths(root, id, existing, { verb: "Removed", reason: "" }, say);
13563
13933
  }
@@ -13606,7 +13976,15 @@ async function runRescan(options = {}) {
13606
13976
  }
13607
13977
  }
13608
13978
  const { ids: activePluginIds, probed: pluginProbeConsultedCli } = await probeActiveGlobalPluginIds(root);
13609
- const recommendation = await recommend(root, context, { activePluginIds });
13979
+ let recommendation;
13980
+ try {
13981
+ recommendation = await recommend(root, context, { activePluginIds });
13982
+ } catch (err) {
13983
+ if (!(err instanceof ProfileUnsupportedError)) throw err;
13984
+ error(err.message);
13985
+ process.exitCode = 1;
13986
+ return;
13987
+ }
13610
13988
  if (!dryRun) {
13611
13989
  await writeJson(hausPath(root, "recommendation.json"), recommendation);
13612
13990
  }
@@ -13647,7 +14025,7 @@ async function runScan(options) {
13647
14025
  }
13648
14026
 
13649
14027
  // src/commands/undo.ts
13650
- import path62 from "node:path";
14028
+ import path63 from "node:path";
13651
14029
  import fs48 from "fs-extra";
13652
14030
 
13653
14031
  // src/claude/managed-paths.ts
@@ -13681,7 +14059,7 @@ async function collectLockTrackedPaths(root) {
13681
14059
  const unsafeByRel = await unsafeReasonsFor(root, relPaths);
13682
14060
  const existing = await resolveExistingTrackedPaths(root, id, relPaths, log, unsafeByRel);
13683
14061
  for (const [rel, reason] of unsafeByRel) {
13684
- if (reason !== null) preserved.push(path62.resolve(root, rel));
14062
+ if (reason !== null) preserved.push(path63.resolve(root, rel));
13685
14063
  }
13686
14064
  if (existing.length === 0) continue;
13687
14065
  const gate = await hashGateEntry(root, { hash: row.hash }, existing);
@@ -13702,7 +14080,7 @@ async function settingsHasHausContent(root) {
13702
14080
  return settings._haus != null;
13703
14081
  }
13704
14082
  async function claudeMdHasHausBlock(root) {
13705
- const filePath = path62.join(root, "CLAUDE.md");
14083
+ const filePath = path63.join(root, "CLAUDE.md");
13706
14084
  if (!await fs48.pathExists(filePath)) return false;
13707
14085
  const text = await fs48.readFile(filePath, "utf8");
13708
14086
  return text.includes(BLOCK_BEGIN);
@@ -13715,15 +14093,15 @@ async function stripProjectSettings(root) {
13715
14093
  const hasContent = Object.keys(settings).length > 0;
13716
14094
  if (hasContent) {
13717
14095
  await writeProjectSettings(root, settings);
13718
- log(`Stripped haus rules from ${path62.relative(root, settingsPath)} (user settings preserved).`);
14096
+ log(`Stripped haus rules from ${path63.relative(root, settingsPath)} (user settings preserved).`);
13719
14097
  return true;
13720
14098
  }
13721
14099
  await fs48.remove(settingsPath);
13722
- log(`Removed ${path62.relative(root, settingsPath)} (no user-owned settings remained).`);
14100
+ log(`Removed ${path63.relative(root, settingsPath)} (no user-owned settings remained).`);
13723
14101
  return true;
13724
14102
  }
13725
14103
  async function stripRootClaudeMd(root) {
13726
- const filePath = path62.join(root, "CLAUDE.md");
14104
+ const filePath = path63.join(root, "CLAUDE.md");
13727
14105
  if (!await fs48.pathExists(filePath)) return false;
13728
14106
  const prev = await fs48.readFile(filePath, "utf8");
13729
14107
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -13751,12 +14129,12 @@ async function backupManagedFilesBeforeUndo(root, managedAbsPaths) {
13751
14129
  const backupRoot = hausPath(root, "backups", dirBackupDirName("undo", stamp));
13752
14130
  for (const abs of managedAbsPaths) {
13753
14131
  if (!await fs48.pathExists(abs)) continue;
13754
- const rel = path62.relative(root, abs);
13755
- const backupPath = path62.join(backupRoot, rel);
13756
- await fs48.ensureDir(path62.dirname(backupPath));
14132
+ const rel = path63.relative(root, abs);
14133
+ const backupPath = path63.join(backupRoot, rel);
14134
+ await fs48.ensureDir(path63.dirname(backupPath));
13757
14135
  await fs48.copy(abs, backupPath);
13758
14136
  }
13759
- log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path62.relative(root, backupRoot)}.`);
14137
+ log(`Backed up ${managedAbsPaths.length} managed file(s) to ${path63.relative(root, backupRoot)}.`);
13760
14138
  }
13761
14139
  async function runUndo(options) {
13762
14140
  const root = options.root ?? process.cwd();
@@ -13770,7 +14148,7 @@ async function runUndo(options) {
13770
14148
  log("Nothing to remove: no haus-managed files found in this directory.");
13771
14149
  return;
13772
14150
  }
13773
- const relTargets = managed.map((p) => path62.relative(root, p));
14151
+ const relTargets = managed.map((p) => path63.relative(root, p));
13774
14152
  const summaryParts = [...relTargets];
13775
14153
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
13776
14154
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -13798,7 +14176,7 @@ User-owned .claude/ files will be preserved.`
13798
14176
  for (const abs of coreManaged) {
13799
14177
  if (!await fs48.pathExists(abs)) continue;
13800
14178
  await fs48.remove(abs);
13801
- log(`Removed ${path62.relative(root, abs)}`);
14179
+ log(`Removed ${path63.relative(root, abs)}`);
13802
14180
  }
13803
14181
  for (const { id, existing } of lockRemovable) {
13804
14182
  await deleteTrackedPaths(root, id, existing, { verb: "Removed" }, log);
@@ -13815,7 +14193,7 @@ init_fs();
13815
14193
  init_logger();
13816
14194
  init_paths();
13817
14195
  import crypto4 from "node:crypto";
13818
- import path63 from "node:path";
14196
+ import path64 from "node:path";
13819
14197
  import fs49 from "fs-extra";
13820
14198
  async function runUninstall(options = {}) {
13821
14199
  const { force = false, removePlugins = removeGlobalPlugins } = options;
@@ -13872,14 +14250,14 @@ async function runUninstall(options = {}) {
13872
14250
  continue;
13873
14251
  }
13874
14252
  await fs49.remove(entry.destPath);
13875
- await pruneEmptyDir(path63.dirname(entry.destPath));
14253
+ await pruneEmptyDir(path64.dirname(entry.destPath));
13876
14254
  result.deleted.push(entry.destPath);
13877
14255
  }
13878
14256
  const settings = await readSettings();
13879
14257
  const stripped = stripHausHooks(stripHausAsk(stripHausAllow(stripHausDeny(settings))));
13880
14258
  await writeSettings(stripped);
13881
14259
  result.hooksStripped = true;
13882
- const hausDir = path63.join(globalClaudeDir(), "haus");
14260
+ const hausDir = path64.join(globalClaudeDir(), "haus");
13883
14261
  const manifestPath2 = hausManifestPath();
13884
14262
  if (fs49.pathExistsSync(manifestPath2)) {
13885
14263
  await fs49.remove(manifestPath2);
@@ -13957,7 +14335,7 @@ async function runUninstallCommand(options) {
13957
14335
  }
13958
14336
 
13959
14337
  // src/commands/validate-catalog.ts
13960
- import path65 from "node:path";
14338
+ import path66 from "node:path";
13961
14339
 
13962
14340
  // src/catalog/validate-core.ts
13963
14341
  init_forbidden_content();
@@ -13965,7 +14343,7 @@ init_manifest_item_fields();
13965
14343
  init_path_safety();
13966
14344
  init_validation_rules2();
13967
14345
  import fs50 from "node:fs";
13968
- import path64 from "node:path";
14346
+ import path65 from "node:path";
13969
14347
  var ITEM_SEMVER_RE = /^\d+\.\d+\.\d+$/;
13970
14348
  var ITEM_ID_PATTERN = /^haus\./;
13971
14349
  function auditForbiddenStacks(items) {
@@ -14132,7 +14510,7 @@ function readForAudit(absPath, label, failures) {
14132
14510
  }
14133
14511
  }
14134
14512
  function auditTemplateContent(manifestDir, absPath, itemId) {
14135
- const rel = path64.relative(manifestDir, absPath);
14513
+ const rel = path65.relative(manifestDir, absPath);
14136
14514
  const failures = [];
14137
14515
  const text = readForAudit(absPath, `${itemId}: ${rel}`, failures);
14138
14516
  if (text === null) return failures;
@@ -14154,25 +14532,25 @@ function auditShippedFiles(manifestDir, items) {
14154
14532
  const failures = [];
14155
14533
  for (const item of items) {
14156
14534
  if (!item.path || !isSafeCatalogPath(item.path)) continue;
14157
- const absPath = path64.join(manifestDir, item.path);
14535
+ const absPath = path65.join(manifestDir, item.path);
14158
14536
  if (item.type === "skill") {
14159
- const skillMd = path64.join(absPath, "SKILL.md");
14537
+ const skillMd = path65.join(absPath, "SKILL.md");
14160
14538
  if (!fs50.existsSync(skillMd)) {
14161
- failures.push(`${item.id}: missing ${path64.relative(manifestDir, skillMd)}`);
14539
+ failures.push(`${item.id}: missing ${path65.relative(manifestDir, skillMd)}`);
14162
14540
  continue;
14163
14541
  }
14164
14542
  const text = readForAudit(skillMd, `${item.id}: SKILL.md`, failures);
14165
14543
  if (text === null) continue;
14166
14544
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
14167
14545
  failures.push(
14168
- ...auditForbiddenTagsInText(text, `${item.id}: ${path64.relative(manifestDir, skillMd)}`)
14546
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path65.relative(manifestDir, skillMd)}`)
14169
14547
  );
14170
14548
  } else if (item.type === "agent") {
14171
14549
  if (!fs50.existsSync(absPath)) {
14172
14550
  failures.push(`${item.id}: missing agent file ${item.path}`);
14173
14551
  continue;
14174
14552
  }
14175
- const rel = path64.relative(manifestDir, absPath);
14553
+ const rel = path65.relative(manifestDir, absPath);
14176
14554
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
14177
14555
  if (text === null) continue;
14178
14556
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -14188,7 +14566,7 @@ function auditShippedFiles(manifestDir, items) {
14188
14566
  failures.push(`${item.id}: missing command file ${item.path}`);
14189
14567
  continue;
14190
14568
  }
14191
- const rel = path64.relative(manifestDir, absPath);
14569
+ const rel = path65.relative(manifestDir, absPath);
14192
14570
  const text = readForAudit(absPath, `${item.id}: ${rel}`, failures);
14193
14571
  if (text === null) continue;
14194
14572
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
@@ -14246,7 +14624,7 @@ function walkAllFiles(dir, fn, failures) {
14246
14624
  }
14247
14625
  for (const entry of entries) {
14248
14626
  if (entry.name === ".DS_Store") continue;
14249
- const full = path64.join(dir, entry.name);
14627
+ const full = path65.join(dir, entry.name);
14250
14628
  if (entry.isDirectory()) walkAllFiles(full, fn, failures);
14251
14629
  else fn(full);
14252
14630
  }
@@ -14270,12 +14648,12 @@ function auditDiskOrphans(manifestDir, items) {
14270
14648
  const failures = [];
14271
14649
  const claimedPaths = items.filter((item) => item.path && isSafeCatalogPath(item.path)).map((item) => item.path.replace(/\\/g, "/"));
14272
14650
  for (const dir of CONTENT_ROOTS) {
14273
- const abs = path64.join(manifestDir, dir);
14651
+ const abs = path65.join(manifestDir, dir);
14274
14652
  if (!fs50.existsSync(abs)) continue;
14275
14653
  walkAllFiles(
14276
14654
  abs,
14277
14655
  (file) => {
14278
- const relPath = path64.relative(manifestDir, file).replace(/\\/g, "/");
14656
+ const relPath = path65.relative(manifestDir, file).replace(/\\/g, "/");
14279
14657
  if (!isPathClaimed(relPath, claimedPaths)) {
14280
14658
  failures.push(`orphaned on disk, claimed by no manifest item: ${relPath}`);
14281
14659
  }
@@ -14296,7 +14674,7 @@ function walkMd(dir, fn, failures) {
14296
14674
  return;
14297
14675
  }
14298
14676
  for (const entry of entries) {
14299
- const full = path64.join(dir, entry.name);
14677
+ const full = path65.join(dir, entry.name);
14300
14678
  if (entry.isDirectory()) walkMd(full, fn, failures);
14301
14679
  else if (entry.name.endsWith(".md")) fn(full);
14302
14680
  }
@@ -14306,13 +14684,13 @@ function auditMarkdownContent(manifestDir, items) {
14306
14684
  const pathSourceMap = buildItemPathSourceMap(items);
14307
14685
  const pathTypeMap = buildItemPathTypeMap(items);
14308
14686
  for (const dir of ["skills", "agents", "templates", "commands", "sources"]) {
14309
- const abs = path64.join(manifestDir, dir);
14687
+ const abs = path65.join(manifestDir, dir);
14310
14688
  if (!fs50.existsSync(abs)) continue;
14311
14689
  const dirType = DIR_ITEM_TYPE[dir] ?? "";
14312
14690
  walkMd(
14313
14691
  abs,
14314
14692
  (file) => {
14315
- const rel = path64.relative(manifestDir, file);
14693
+ const rel = path65.relative(manifestDir, file);
14316
14694
  const text = readForAudit(file, rel, failures);
14317
14695
  if (text === null) return;
14318
14696
  const norm = rel.replace(/\\/g, "/");
@@ -14360,8 +14738,8 @@ async function runValidateCatalog(manifestPath2) {
14360
14738
  process.exitCode = 1;
14361
14739
  return;
14362
14740
  }
14363
- const abs = path65.resolve(process.cwd(), manifestPath2);
14364
- const manifestDir = path65.dirname(abs);
14741
+ const abs = path66.resolve(process.cwd(), manifestPath2);
14742
+ const manifestDir = path66.dirname(abs);
14365
14743
  const data = await readJson(abs);
14366
14744
  if (!data?.items) {
14367
14745
  error(`Could not read catalog manifest at ${abs}`);
@@ -14382,7 +14760,7 @@ init_logger();
14382
14760
 
14383
14761
  // src/workspace/worktree/add.ts
14384
14762
  init_fs();
14385
- import path72 from "node:path";
14763
+ import path73 from "node:path";
14386
14764
  import fs54 from "fs-extra";
14387
14765
 
14388
14766
  // src/workspace/worktree/git-worktree.ts
@@ -14503,12 +14881,12 @@ async function hasUnpushedWork(cwd) {
14503
14881
  }
14504
14882
 
14505
14883
  // src/workspace/worktree/hydrate.ts
14506
- import path68 from "node:path";
14884
+ import path69 from "node:path";
14507
14885
  import fs52 from "fs-extra";
14508
14886
 
14509
14887
  // src/workspace/worktree/cow-copy.ts
14510
14888
  init_exec();
14511
- import path66 from "node:path";
14889
+ import path67 from "node:path";
14512
14890
  import fs51 from "fs-extra";
14513
14891
  async function getMacFilesystemType(dir) {
14514
14892
  const df = await runCommand("df", ["-P", dir]);
@@ -14543,11 +14921,11 @@ async function cowCopyDir(src, dest) {
14543
14921
  const srcExists = await fs51.pathExists(src);
14544
14922
  if (!srcExists)
14545
14923
  return { strategy: "unsupported", attempted: false, ok: false, error: "source missing" };
14546
- const strategy = await detectCowStrategy(path66.dirname(dest));
14924
+ const strategy = await detectCowStrategy(path67.dirname(dest));
14547
14925
  if (strategy === "unsupported" || strategy === "unknown-platform") {
14548
14926
  return { strategy, attempted: false, ok: false };
14549
14927
  }
14550
- await fs51.ensureDir(path66.dirname(dest));
14928
+ await fs51.ensureDir(path67.dirname(dest));
14551
14929
  const args = strategy === "darwin-clonefile" ? ["-c", "-R", src, dest] : ["-a", "--reflink=always", src, dest];
14552
14930
  const result = await runCommand("cp", args);
14553
14931
  return {
@@ -14561,7 +14939,7 @@ async function cowCopyDir(src, dest) {
14561
14939
  // src/workspace/worktree/install.ts
14562
14940
  init_exec();
14563
14941
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
14564
- import path67 from "node:path";
14942
+ import path68 from "node:path";
14565
14943
  import fg7 from "fast-glob";
14566
14944
  function detectInstallPlan(signals) {
14567
14945
  if (signals.hasYarnLock) {
@@ -14582,10 +14960,10 @@ function detectInstallPlan(signals) {
14582
14960
  return { manager: "none" };
14583
14961
  }
14584
14962
  async function computeLockfileSignals(dir) {
14585
- const hasYarnLock = existsSync3(path67.join(dir, "yarn.lock"));
14586
- const hasPnpmLock = existsSync3(path67.join(dir, "pnpm-lock.yaml"));
14587
- const hasPackageLock = existsSync3(path67.join(dir, "package-lock.json"));
14588
- const hasComposerLock = existsSync3(path67.join(dir, "composer.lock"));
14963
+ const hasYarnLock = existsSync3(path68.join(dir, "yarn.lock"));
14964
+ const hasPnpmLock = existsSync3(path68.join(dir, "pnpm-lock.yaml"));
14965
+ const hasPackageLock = existsSync3(path68.join(dir, "package-lock.json"));
14966
+ const hasComposerLock = existsSync3(path68.join(dir, "composer.lock"));
14589
14967
  const dotnetFiles = await fg7(["**/*.csproj", "**/*.sln"], {
14590
14968
  cwd: dir,
14591
14969
  onlyFiles: true,
@@ -14599,7 +14977,7 @@ async function computeLockfileSignals(dir) {
14599
14977
  });
14600
14978
  const hasDotnetProject = dotnetFiles.length > 0;
14601
14979
  let hasPackageManagerField = false;
14602
- const pkgPath = path67.join(dir, "package.json");
14980
+ const pkgPath = path68.join(dir, "package.json");
14603
14981
  if (existsSync3(pkgPath)) {
14604
14982
  try {
14605
14983
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
@@ -14642,7 +15020,7 @@ function missingYarnrcEnvVars(content2, env) {
14642
15020
  return findRequiredYarnrcEnvVars(content2).filter((name) => env[name] === void 0);
14643
15021
  }
14644
15022
  function missingYarnrcEnvVarsInDir(dir, env) {
14645
- const yarnrcPath = path67.join(dir, ".yarnrc.yml");
15023
+ const yarnrcPath = path68.join(dir, ".yarnrc.yml");
14646
15024
  if (!existsSync3(yarnrcPath)) return [];
14647
15025
  try {
14648
15026
  return missingYarnrcEnvVars(readFileSync3(yarnrcPath, "utf8"), env);
@@ -14738,8 +15116,8 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
14738
15116
  const start = Date.now();
14739
15117
  const targets = [];
14740
15118
  for (const target of HYDRATION_TARGETS) {
14741
- const src = path68.join(mainMemberPath, target);
14742
- const dest = path68.join(worktreeMemberPath, target);
15119
+ const src = path69.join(mainMemberPath, target);
15120
+ const dest = path69.join(worktreeMemberPath, target);
14743
15121
  if (!await fs52.pathExists(src)) continue;
14744
15122
  const destExists = await fs52.pathExists(dest);
14745
15123
  if (!opts.force && destExists) continue;
@@ -14757,11 +15135,11 @@ async function hydrateMember(mainMemberPath, worktreeMemberPath, opts = {}) {
14757
15135
 
14758
15136
  // src/workspace/worktree/local-files.ts
14759
15137
  init_exec();
14760
- import path69 from "node:path";
15138
+ import path70 from "node:path";
14761
15139
  import fs53 from "fs-extra";
14762
15140
  var MACHINE_LOCAL_FILES = [
14763
15141
  "docker-compose.local.yml",
14764
- path69.join(".claude", "settings.local.json")
15142
+ path70.join(".claude", "settings.local.json")
14765
15143
  ];
14766
15144
  async function isTracked(repoRoot, relFile) {
14767
15145
  const gitPath = relFile.replace(/\\/g, "/");
@@ -14771,11 +15149,11 @@ async function isTracked(repoRoot, relFile) {
14771
15149
  async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
14772
15150
  const copied = [];
14773
15151
  for (const rel of MACHINE_LOCAL_FILES) {
14774
- const src = path69.join(mainMemberPath, rel);
15152
+ const src = path70.join(mainMemberPath, rel);
14775
15153
  if (!await fs53.pathExists(src)) continue;
14776
15154
  if (await isTracked(mainMemberPath, rel)) continue;
14777
- const dest = path69.join(worktreeMemberPath, rel);
14778
- await fs53.ensureDir(path69.dirname(dest));
15155
+ const dest = path70.join(worktreeMemberPath, rel);
15156
+ await fs53.ensureDir(path70.dirname(dest));
14779
15157
  await fs53.copy(src, dest, { overwrite: true });
14780
15158
  copied.push(rel);
14781
15159
  }
@@ -14784,15 +15162,15 @@ async function copyMachineLocalFiles(mainMemberPath, worktreeMemberPath) {
14784
15162
 
14785
15163
  // src/workspace/worktree/root.ts
14786
15164
  import { existsSync as existsSync4 } from "node:fs";
14787
- import path70 from "node:path";
15165
+ import path71 from "node:path";
14788
15166
  async function resolveWorkspaceForWorktree(start) {
14789
15167
  const rootInfo = await resolveRoots(start);
14790
15168
  if (!rootInfo.isGitRepo) {
14791
15169
  return { ok: false, reason: "Not inside a git repository." };
14792
15170
  }
14793
15171
  const workspaceRoot = rootInfo.mainRoot;
14794
- const hasYaml = existsSync4(path70.join(workspaceRoot, WORKSPACE_FILE));
14795
- const hasManifest = existsSync4(path70.join(workspaceRoot, REPOS_MANIFEST_FILE));
15172
+ const hasYaml = existsSync4(path71.join(workspaceRoot, WORKSPACE_FILE));
15173
+ const hasManifest = existsSync4(path71.join(workspaceRoot, REPOS_MANIFEST_FILE));
14796
15174
  if (!hasYaml && !hasManifest) {
14797
15175
  return {
14798
15176
  ok: false,
@@ -14818,10 +15196,10 @@ function selectMembers(members, only) {
14818
15196
 
14819
15197
  // src/workspace/worktree/state.ts
14820
15198
  init_fs();
14821
- import path71 from "node:path";
15199
+ import path72 from "node:path";
14822
15200
  var WORKTREE_STATE_FILE = ".haus-worktree.json";
14823
15201
  function worktreesDir(mainRoot) {
14824
- return path71.join(mainRoot, ".claude", "worktrees");
15202
+ return path72.join(mainRoot, ".claude", "worktrees");
14825
15203
  }
14826
15204
  function isValidWorktreeSlug(slug) {
14827
15205
  if (slug === "" || slug === "." || slug === "..") return false;
@@ -14834,22 +15212,22 @@ function worktreePath(mainRoot, slug) {
14834
15212
  `Invalid worktree slug ${JSON.stringify(slug)} \u2014 slugs may only contain letters, digits, ".", "_", "-" (no path separators, and not "." or "..").`
14835
15213
  );
14836
15214
  }
14837
- return path71.join(worktreesDir(mainRoot), slug);
15215
+ return path72.join(worktreesDir(mainRoot), slug);
14838
15216
  }
14839
15217
  function slugFromWorktreePath(mainRoot, somePath) {
14840
15218
  const dir = worktreesDir(mainRoot);
14841
- const prefix = `${dir}${path71.sep}`;
15219
+ const prefix = `${dir}${path72.sep}`;
14842
15220
  if (!somePath.startsWith(prefix)) return void 0;
14843
- const segment = somePath.slice(prefix.length).split(path71.sep)[0];
15221
+ const segment = somePath.slice(prefix.length).split(path72.sep)[0];
14844
15222
  return segment !== void 0 && segment !== "" ? segment : void 0;
14845
15223
  }
14846
15224
  async function readWorktreeState(worktreeRoot) {
14847
- const state = await readJson(path71.join(worktreeRoot, WORKTREE_STATE_FILE));
15225
+ const state = await readJson(path72.join(worktreeRoot, WORKTREE_STATE_FILE));
14848
15226
  if (state == null || typeof state !== "object" || !Array.isArray(state.members)) return void 0;
14849
15227
  return state;
14850
15228
  }
14851
15229
  async function writeWorktreeState(worktreeRoot, state) {
14852
- await writeJson(path71.join(worktreeRoot, WORKTREE_STATE_FILE), state);
15230
+ await writeJson(path72.join(worktreeRoot, WORKTREE_STATE_FILE), state);
14853
15231
  }
14854
15232
 
14855
15233
  // src/workspace/worktree/add.ts
@@ -14883,7 +15261,7 @@ async function runAdd(opts) {
14883
15261
  const memberResults = [];
14884
15262
  for (const member of selected) {
14885
15263
  const start = Date.now();
14886
- const memberWorktreePath = path72.join(wsWorktreePath, member.folder);
15264
+ const memberWorktreePath = path73.join(wsWorktreePath, member.folder);
14887
15265
  const outcome = await addWorktree(member.absPath, memberWorktreePath, branch, {
14888
15266
  preferDefaultBranchFrom: true
14889
15267
  });
@@ -14913,7 +15291,7 @@ async function runAdd(opts) {
14913
15291
  await mapWithConcurrency(okResults, async (result) => {
14914
15292
  const member = selected.find((m) => m.id === result.member);
14915
15293
  if (!member) return;
14916
- const memberWorktreePath = path72.join(wsWorktreePath, member.folder);
15294
+ const memberWorktreePath = path73.join(wsWorktreePath, member.folder);
14917
15295
  const hydrationStart = Date.now();
14918
15296
  if (opts.hydrate !== false) {
14919
15297
  result.hydration = await hydrateMember(member.absPath, memberWorktreePath);
@@ -14981,16 +15359,16 @@ async function runDryRun(args) {
14981
15359
  // src/workspace/worktree/autohydrate.ts
14982
15360
  import { spawn } from "node:child_process";
14983
15361
  import fsSync from "node:fs";
14984
- import path77 from "node:path";
15362
+ import path78 from "node:path";
14985
15363
  import fs56 from "fs-extra";
14986
15364
 
14987
15365
  // src/workspace/worktree/doctor.ts
14988
15366
  import { existsSync as existsSync6 } from "node:fs";
14989
- import path75 from "node:path";
15367
+ import path76 from "node:path";
14990
15368
 
14991
15369
  // src/workspace/worktree/hydrate-lock.ts
14992
15370
  init_paths();
14993
- import path73 from "node:path";
15371
+ import path74 from "node:path";
14994
15372
  import fs55 from "fs-extra";
14995
15373
  var STALE_LOCK_MS = 30 * 60 * 1e3;
14996
15374
  function locksDir(mainRoot) {
@@ -15008,13 +15386,13 @@ function assertSlug(slug) {
15008
15386
  return slug;
15009
15387
  }
15010
15388
  function hydrateLockPath(mainRoot, slug) {
15011
- return path73.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
15389
+ return path74.join(locksDir(mainRoot), `hydrate-${assertSlug(slug)}.lock`);
15012
15390
  }
15013
15391
  function hydrateLogPath(mainRoot, slug) {
15014
- return path73.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
15392
+ return path74.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.log`);
15015
15393
  }
15016
15394
  function hydrateLastRunPath(mainRoot, slug) {
15017
- return path73.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
15395
+ return path74.join(logsDir(mainRoot), `hydrate-${assertSlug(slug)}.last.json`);
15018
15396
  }
15019
15397
  function defaultIsAlive(pid) {
15020
15398
  try {
@@ -15026,7 +15404,7 @@ function defaultIsAlive(pid) {
15026
15404
  }
15027
15405
  async function readOwnerPid(lockPath) {
15028
15406
  try {
15029
- const raw = (await fs55.readFile(path73.join(lockPath, "pid"), "utf8")).trim();
15407
+ const raw = (await fs55.readFile(path74.join(lockPath, "pid"), "utf8")).trim();
15030
15408
  if (raw === "") return void 0;
15031
15409
  const pid = Number.parseInt(raw, 10);
15032
15410
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
@@ -15035,13 +15413,13 @@ async function readOwnerPid(lockPath) {
15035
15413
  }
15036
15414
  }
15037
15415
  async function claim(lockPath, pid) {
15038
- await fs55.ensureDir(path73.dirname(lockPath));
15416
+ await fs55.ensureDir(path74.dirname(lockPath));
15039
15417
  try {
15040
15418
  await fs55.mkdir(lockPath);
15041
15419
  } catch {
15042
15420
  return false;
15043
15421
  }
15044
- await fs55.writeFile(path73.join(lockPath, "pid"), String(pid), "utf8");
15422
+ await fs55.writeFile(path74.join(lockPath, "pid"), String(pid), "utf8");
15045
15423
  return true;
15046
15424
  }
15047
15425
  async function acquireHydrateLock(mainRoot, slug, deps = {}) {
@@ -15072,7 +15450,7 @@ async function acquireHydrateLock(mainRoot, slug, deps = {}) {
15072
15450
  return await claim(lockPath, pid) ? { acquired: true, lockPath } : { acquired: false, reason: "live", ownerPid, lockPath };
15073
15451
  }
15074
15452
  async function recordHydrateLockOwner(mainRoot, slug, pid) {
15075
- await fs55.writeFile(path73.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
15453
+ await fs55.writeFile(path74.join(hydrateLockPath(mainRoot, slug), "pid"), String(pid), "utf8");
15076
15454
  }
15077
15455
  async function releaseHydrateLock(mainRoot, slug) {
15078
15456
  await fs55.remove(hydrateLockPath(mainRoot, slug));
@@ -15092,21 +15470,21 @@ async function readHydrateLock(mainRoot, slug) {
15092
15470
 
15093
15471
  // src/workspace/worktree/orphans.ts
15094
15472
  import { existsSync as existsSync5 } from "node:fs";
15095
- import path74 from "node:path";
15473
+ import path75 from "node:path";
15096
15474
  var WORKSPACE_REPO_ID = "(workspace)";
15097
15475
  async function findOrphanedRegistrations(repos, wsWorktreesDir) {
15098
15476
  const orphans = [];
15099
15477
  for (const repo of repos) {
15100
15478
  const entries = await listWorktrees(repo.absPath);
15101
15479
  for (const entry of entries) {
15102
- const entryPath = path74.normalize(entry.path);
15103
- const marker = `${path74.sep}.claude${path74.sep}worktrees${path74.sep}`;
15480
+ const entryPath = path75.normalize(entry.path);
15481
+ const marker = `${path75.sep}.claude${path75.sep}worktrees${path75.sep}`;
15104
15482
  const idx = entryPath.indexOf(marker);
15105
15483
  if (idx === -1) continue;
15106
15484
  const rest = entryPath.slice(idx + marker.length);
15107
- const slug = rest.split(path74.sep)[0];
15485
+ const slug = rest.split(path75.sep)[0];
15108
15486
  if (!slug) continue;
15109
- if (existsSync5(path74.join(wsWorktreesDir, slug))) continue;
15487
+ if (existsSync5(path75.join(wsWorktreesDir, slug))) continue;
15110
15488
  orphans.push({
15111
15489
  repoId: repo.id,
15112
15490
  ownerRepo: repo.absPath,
@@ -15121,7 +15499,7 @@ async function findOrphanedRegistrations(repos, wsWorktreesDir) {
15121
15499
  // src/workspace/worktree/doctor.ts
15122
15500
  var HYDRATION_MARKERS = ["node_modules", "vendor", "obj", "bin"];
15123
15501
  function looksHydrated(memberPath) {
15124
- return HYDRATION_MARKERS.some((marker) => existsSync6(path75.join(memberPath, marker)));
15502
+ return HYDRATION_MARKERS.some((marker) => existsSync6(path76.join(memberPath, marker)));
15125
15503
  }
15126
15504
  async function runDoctor2() {
15127
15505
  const rootInfo = await resolveRoots();
@@ -15138,7 +15516,7 @@ async function runDoctor2() {
15138
15516
  };
15139
15517
  }
15140
15518
  const workspaceRoot = rootInfo.mainRoot;
15141
- const isWorkspace = existsSync6(path75.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path75.join(workspaceRoot, REPOS_MANIFEST_FILE));
15519
+ const isWorkspace = existsSync6(path76.join(workspaceRoot, WORKSPACE_FILE)) || existsSync6(path76.join(workspaceRoot, REPOS_MANIFEST_FILE));
15142
15520
  if (!isWorkspace) {
15143
15521
  return {
15144
15522
  isWorkspace: false,
@@ -15154,7 +15532,7 @@ async function runDoctor2() {
15154
15532
  };
15155
15533
  }
15156
15534
  const wsWorktreesDir = worktreesDir(workspaceRoot);
15157
- const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path75.sep}`);
15535
+ const underWorktreesDir = rootInfo.repoRoot.startsWith(`${wsWorktreesDir}${path76.sep}`);
15158
15536
  const inWorkspaceWorktree = rootInfo.isLinkedWorktree && underWorktreesDir;
15159
15537
  const workspaceConfig = await readWorkspaceConfig(workspaceRoot);
15160
15538
  const autoHydrate = workspaceConfig?.worktree?.autoHydrate === true;
@@ -15167,10 +15545,10 @@ async function runDoctor2() {
15167
15545
  let hydrationOwnerPid;
15168
15546
  if (inWorkspaceWorktree) {
15169
15547
  const wtPath = rootInfo.repoRoot;
15170
- slug = rootInfo.worktreeName ?? path75.basename(wtPath);
15548
+ slug = rootInfo.worktreeName ?? path76.basename(wtPath);
15171
15549
  const state = await readWorktreeState(wtPath);
15172
15550
  for (const member of members) {
15173
- const memberPath = path75.join(wtPath, member.folder);
15551
+ const memberPath = path76.join(wtPath, member.folder);
15174
15552
  const materialized = existsSync6(memberPath);
15175
15553
  const expectedBranch = state?.members.find((m) => m.folder === member.folder)?.branch ?? state?.branch;
15176
15554
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
@@ -15215,7 +15593,7 @@ async function runDoctor2() {
15215
15593
  wsWorktreesDir
15216
15594
  );
15217
15595
  const orphans = orphanScan.map(
15218
- (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path75.join(wsWorktreesDir, o.slug)} no longer exists.`
15596
+ (o) => `${o.repoId}: worktree registered at ${o.registeredPath} but ${path76.join(wsWorktreesDir, o.slug)} no longer exists.`
15219
15597
  );
15220
15598
  if (orphans.length > 0) {
15221
15599
  problems.push(...orphans);
@@ -15238,7 +15616,7 @@ async function runDoctor2() {
15238
15616
  // src/workspace/worktree/hydrate-command.ts
15239
15617
  init_fs();
15240
15618
  import { existsSync as existsSync7 } from "node:fs";
15241
- import path76 from "node:path";
15619
+ import path77 from "node:path";
15242
15620
  async function runHydrateWorktree(opts) {
15243
15621
  const rootInfo = await resolveRoots();
15244
15622
  if (!rootInfo.isGitRepo || !rootInfo.isLinkedWorktree) {
@@ -15249,7 +15627,7 @@ async function runHydrateWorktree(opts) {
15249
15627
  }
15250
15628
  const workspaceRoot = rootInfo.mainRoot;
15251
15629
  const wtPath = rootInfo.repoRoot;
15252
- if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path76.sep}`)) {
15630
+ if (!wtPath.startsWith(`${worktreesDir(workspaceRoot)}${path77.sep}`)) {
15253
15631
  return {
15254
15632
  ok: false,
15255
15633
  error: "Current worktree is not one of this workspace's .claude/worktrees/<slug> checkouts."
@@ -15261,13 +15639,13 @@ async function runHydrateWorktree(opts) {
15261
15639
  return { ok: false, error: `Unknown --only repo(s): ${unknown.join(", ")}` };
15262
15640
  }
15263
15641
  const state = await readWorktreeState(wtPath);
15264
- const slug = state?.slug ?? path76.basename(wtPath);
15642
+ const slug = state?.slug ?? path77.basename(wtPath);
15265
15643
  const detectedBranch = await currentBranch(wtPath);
15266
15644
  const targetBranch = state?.branch ?? (detectedBranch !== "HEAD" ? detectedBranch : void 0) ?? slug;
15267
15645
  if (opts.dryRun) {
15268
15646
  const results2 = await Promise.all(
15269
15647
  selected.map(async (member) => {
15270
- const memberPath = path76.join(wtPath, member.folder);
15648
+ const memberPath = path77.join(wtPath, member.folder);
15271
15649
  if (existsSync7(memberPath)) {
15272
15650
  return { member: member.id, folder: member.folder, status: "planned" };
15273
15651
  }
@@ -15288,7 +15666,7 @@ async function runHydrateWorktree(opts) {
15288
15666
  const results = await mapWithConcurrency(
15289
15667
  selected,
15290
15668
  async (member) => {
15291
- const memberPath = path76.join(wtPath, member.folder);
15669
+ const memberPath = path77.join(wtPath, member.folder);
15292
15670
  let branchAction;
15293
15671
  if (!existsSync7(memberPath)) {
15294
15672
  if (!opts.materialize) {
@@ -15409,7 +15787,7 @@ function defaultSpawnDetached(spec) {
15409
15787
  }
15410
15788
  function appendLog(logPath, line2) {
15411
15789
  try {
15412
- fsSync.mkdirSync(path77.dirname(logPath), { recursive: true });
15790
+ fsSync.mkdirSync(path78.dirname(logPath), { recursive: true });
15413
15791
  const size = fsSync.statSync(logPath, { throwIfNoEntry: false })?.size ?? 0;
15414
15792
  if (size > LOG_SIZE_CAP_BYTES) {
15415
15793
  fsSync.writeFileSync(logPath, `--- log rotated (exceeded ${LOG_SIZE_CAP_BYTES} bytes) ---
@@ -15573,16 +15951,16 @@ async function writeLastRun(mainRoot, slug, ok, failures) {
15573
15951
  return;
15574
15952
  }
15575
15953
  const record = { slug, finishedAt: (/* @__PURE__ */ new Date()).toISOString(), ok, failures };
15576
- await fs56.ensureDir(path77.dirname(file));
15954
+ await fs56.ensureDir(path78.dirname(file));
15577
15955
  await fs56.writeJson(file, record, { spaces: 2 }).catch(() => void 0);
15578
15956
  }
15579
15957
 
15580
15958
  // src/workspace/worktree/dematerialize.ts
15581
- import path79 from "node:path";
15959
+ import path80 from "node:path";
15582
15960
  import fs58 from "fs-extra";
15583
15961
 
15584
15962
  // src/workspace/worktree/member-targets.ts
15585
- import path78 from "node:path";
15963
+ import path79 from "node:path";
15586
15964
  import fs57 from "fs-extra";
15587
15965
  function normalizeStateMembers(state, allMembers) {
15588
15966
  return state?.members.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath })) ?? allMembers.map((m) => ({ id: m.id, folder: m.folder, absPath: m.absPath }));
@@ -15600,7 +15978,7 @@ function resolveMemberTargets(wsWorktreePath, stateMembers, allMembers) {
15600
15978
  targets.push({
15601
15979
  repo: configMember?.id ?? stateMember.id,
15602
15980
  ownerRepo,
15603
- path: path78.join(wsWorktreePath, stateMember.folder)
15981
+ path: path79.join(wsWorktreePath, stateMember.folder)
15604
15982
  });
15605
15983
  }
15606
15984
  return { targets, unresolvedDroppedMembers };
@@ -15686,7 +16064,7 @@ async function runDematerialize(opts) {
15686
16064
  const removed = [];
15687
16065
  const failed = [];
15688
16066
  for (const folder of unresolvedDroppedMembers) {
15689
- const droppedPath = path79.join(wsWorktreePath, folder);
16067
+ const droppedPath = path80.join(wsWorktreePath, folder);
15690
16068
  if (await fs58.pathExists(droppedPath)) {
15691
16069
  await fs58.remove(droppedPath);
15692
16070
  failed.push({ repo: folder, error: unresolvedDroppedFailureReason() });
@@ -15702,7 +16080,7 @@ async function runDematerialize(opts) {
15702
16080
  }
15703
16081
 
15704
16082
  // src/workspace/worktree/list.ts
15705
- import path80 from "node:path";
16083
+ import path81 from "node:path";
15706
16084
  import fs59 from "fs-extra";
15707
16085
  async function runList2() {
15708
16086
  const resolved = await resolveWorkspaceForWorktree();
@@ -15715,12 +16093,12 @@ async function runList2() {
15715
16093
  const configuredMembers = await readMembers(rootInfo);
15716
16094
  const worktrees = [];
15717
16095
  for (const slug of slugs) {
15718
- const wtPath = path80.join(dir, slug);
16096
+ const wtPath = path81.join(dir, slug);
15719
16097
  const state = await readWorktreeState(wtPath);
15720
16098
  const members = [];
15721
16099
  const source = state?.members ?? configuredMembers.map((m) => ({ id: m.id, folder: m.folder }));
15722
16100
  for (const m of source) {
15723
- const memberPath = path80.join(wtPath, m.folder);
16101
+ const memberPath = path81.join(wtPath, m.folder);
15724
16102
  const materialized = await fs59.pathExists(memberPath);
15725
16103
  const actualBranch = materialized ? await currentBranch(memberPath) : void 0;
15726
16104
  members.push({
@@ -15743,17 +16121,17 @@ async function runList2() {
15743
16121
 
15744
16122
  // src/workspace/worktree/reclaim.ts
15745
16123
  import { existsSync as existsSync8 } from "node:fs";
15746
- import path81 from "node:path";
16124
+ import path82 from "node:path";
15747
16125
  import fs60 from "fs-extra";
15748
16126
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
15749
16127
  async function lastTouchedMs(worktreeRoot) {
15750
- const candidates = [worktreeRoot, path81.join(worktreeRoot, ".haus-worktree.json")];
16128
+ const candidates = [worktreeRoot, path82.join(worktreeRoot, ".haus-worktree.json")];
15751
16129
  try {
15752
- const dotGit = await fs60.readFile(path81.join(worktreeRoot, ".git"), "utf8");
16130
+ const dotGit = await fs60.readFile(path82.join(worktreeRoot, ".git"), "utf8");
15753
16131
  const match = /^gitdir:\s*(.+)$/m.exec(dotGit);
15754
16132
  if (match?.[1]) {
15755
16133
  const gitdir = match[1].trim();
15756
- candidates.push(path81.join(gitdir, "HEAD"), path81.join(gitdir, "index"));
16134
+ candidates.push(path82.join(gitdir, "HEAD"), path82.join(gitdir, "index"));
15757
16135
  }
15758
16136
  } catch {
15759
16137
  }
@@ -15796,17 +16174,17 @@ async function runReclaim(opts = {}) {
15796
16174
  for (const name of unaddressable) {
15797
16175
  candidates.push({
15798
16176
  slug: name,
15799
- ageDays: (Date.now() - await lastTouchedMs(path81.join(dir, name))) / MS_PER_DAY,
16177
+ ageDays: (Date.now() - await lastTouchedMs(path82.join(dir, name))) / MS_PER_DAY,
15800
16178
  materializedMembers: 0,
15801
16179
  action: "skip",
15802
16180
  reason: "directory name is not a valid worktree slug \u2014 no haus worktree command can address it; remove it by hand if it is debris"
15803
16181
  });
15804
16182
  }
15805
16183
  for (const slug of entries) {
15806
- const wtPath = path81.join(dir, slug);
16184
+ const wtPath = path82.join(dir, slug);
15807
16185
  const state = await readWorktreeState(wtPath);
15808
16186
  const stateMembers = normalizeStateMembers(state, members);
15809
- const materialized = stateMembers.filter((m) => existsSync8(path81.join(wtPath, m.folder)));
16187
+ const materialized = stateMembers.filter((m) => existsSync8(path82.join(wtPath, m.folder)));
15810
16188
  const ageDays = (Date.now() - await lastTouchedMs(wtPath)) / MS_PER_DAY;
15811
16189
  const base = { slug, ageDays, materializedMembers: materialized.length };
15812
16190
  if (slug === currentSlug) {
@@ -16403,7 +16781,7 @@ async function runWorktree(action, opts = {}) {
16403
16781
 
16404
16782
  // src/commands/workspace.ts
16405
16783
  import { existsSync as existsSync12, statSync as statSync3 } from "node:fs";
16406
- import path90 from "node:path";
16784
+ import path91 from "node:path";
16407
16785
  init_fs();
16408
16786
  init_logger();
16409
16787
 
@@ -16462,7 +16840,7 @@ ${summaries.map(
16462
16840
 
16463
16841
  // src/commands/workspace/doctor.ts
16464
16842
  import { existsSync as existsSync9 } from "node:fs";
16465
- import path83 from "node:path";
16843
+ import path84 from "node:path";
16466
16844
  init_logger();
16467
16845
  init_paths();
16468
16846
 
@@ -16470,14 +16848,14 @@ init_paths();
16470
16848
  init_fs();
16471
16849
  init_paths();
16472
16850
  import { readFileSync as readFileSync4 } from "node:fs";
16473
- import path82 from "node:path";
16851
+ import path83 from "node:path";
16474
16852
  var MANIFEST_FILE2 = "workspace.manifest.json";
16475
16853
  function manifestPath(workspaceRoot) {
16476
16854
  return hausPath(workspaceRoot, MANIFEST_FILE2);
16477
16855
  }
16478
16856
  function hausVersion() {
16479
16857
  try {
16480
- const pkg = JSON.parse(readFileSync4(path82.join(packageRoot(), "package.json"), "utf8"));
16858
+ const pkg = JSON.parse(readFileSync4(path83.join(packageRoot(), "package.json"), "utf8"));
16481
16859
  return pkg.version ?? "0.0.0";
16482
16860
  } catch {
16483
16861
  return "0.0.0";
@@ -16569,7 +16947,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
16569
16947
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
16570
16948
  const catalogRefByRepo = [];
16571
16949
  for (const repo of config.repos) {
16572
- const repoRoot = path83.resolve(workspaceRoot, repo.path);
16950
+ const repoRoot = path84.resolve(workspaceRoot, repo.path);
16573
16951
  const entry = manifestByName.get(repo.name);
16574
16952
  if (!entry) {
16575
16953
  flag({
@@ -16642,7 +17020,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
16642
17020
  });
16643
17021
  continue;
16644
17022
  }
16645
- if (!existsSync9(path83.join(workspaceRoot, entry.path))) {
17023
+ if (!existsSync9(path84.join(workspaceRoot, entry.path))) {
16646
17024
  flag({
16647
17025
  repo: entry.repo,
16648
17026
  kind: "missing-linked-context-copy",
@@ -16688,17 +17066,17 @@ function emit(args) {
16688
17066
  init_logger();
16689
17067
 
16690
17068
  // src/workspace/link-context/link.ts
16691
- import path86 from "node:path";
17069
+ import path87 from "node:path";
16692
17070
  import fs64 from "fs-extra";
16693
17071
  init_fs();
16694
17072
  init_logger();
16695
17073
 
16696
17074
  // src/workspace/link-context/plan.ts
16697
- import path85 from "node:path";
17075
+ import path86 from "node:path";
16698
17076
  import fs63 from "fs-extra";
16699
17077
 
16700
17078
  // src/workspace/link-context/scan-source.ts
16701
- import path84 from "node:path";
17079
+ import path85 from "node:path";
16702
17080
  import fs62 from "fs-extra";
16703
17081
  var ASSET_SUBDIR = {
16704
17082
  skill: "skills",
@@ -16707,36 +17085,36 @@ var ASSET_SUBDIR = {
16707
17085
  };
16708
17086
  async function hasLinkableContext(member) {
16709
17087
  for (const subdir of Object.values(ASSET_SUBDIR)) {
16710
- if (await fs62.pathExists(path84.join(member.absPath, ".claude", subdir))) return true;
17088
+ if (await fs62.pathExists(path85.join(member.absPath, ".claude", subdir))) return true;
16711
17089
  }
16712
17090
  return false;
16713
17091
  }
16714
17092
  async function collectSkills(member) {
16715
- const dir = path84.join(member.absPath, ".claude", "skills");
17093
+ const dir = path85.join(member.absPath, ".claude", "skills");
16716
17094
  if (!await fs62.pathExists(dir)) return [];
16717
17095
  const names = await fs62.readdir(dir);
16718
17096
  const out = [];
16719
17097
  for (const name of [...names].sort()) {
16720
- const abs = path84.join(dir, name);
17098
+ const abs = path85.join(dir, name);
16721
17099
  const stat = await fs62.lstat(abs);
16722
17100
  if (!stat.isDirectory()) continue;
16723
- if (!await fs62.pathExists(path84.join(abs, "SKILL.md"))) continue;
16724
- out.push({ type: "skill", name, sourceRelPath: path84.posix.join(".claude", "skills", name) });
17101
+ if (!await fs62.pathExists(path85.join(abs, "SKILL.md"))) continue;
17102
+ out.push({ type: "skill", name, sourceRelPath: path85.posix.join(".claude", "skills", name) });
16725
17103
  }
16726
17104
  return out;
16727
17105
  }
16728
17106
  async function collectFlatMarkdown(member, type) {
16729
17107
  const subdir = ASSET_SUBDIR[type];
16730
- const dir = path84.join(member.absPath, ".claude", subdir);
17108
+ const dir = path85.join(member.absPath, ".claude", subdir);
16731
17109
  if (!await fs62.pathExists(dir)) return [];
16732
17110
  const names = await fs62.readdir(dir);
16733
17111
  const out = [];
16734
17112
  for (const fileName of [...names].sort()) {
16735
17113
  if (!fileName.endsWith(".md")) continue;
16736
- const abs = path84.join(dir, fileName);
17114
+ const abs = path85.join(dir, fileName);
16737
17115
  if (!(await fs62.lstat(abs)).isFile()) continue;
16738
17116
  const name = fileName.slice(0, -".md".length);
16739
- out.push({ type, name, sourceRelPath: path84.posix.join(".claude", subdir, fileName) });
17117
+ out.push({ type, name, sourceRelPath: path85.posix.join(".claude", subdir, fileName) });
16740
17118
  }
16741
17119
  return out;
16742
17120
  }
@@ -16751,9 +17129,9 @@ async function collectSourceAssets(member) {
16751
17129
 
16752
17130
  // src/workspace/link-context/plan.ts
16753
17131
  function repoFolderPrefix(member) {
16754
- const base = path85.posix.basename(member.folder);
17132
+ const base = path86.posix.basename(member.folder);
16755
17133
  if (base && base !== "." && base !== "/") return base;
16756
- const fromAbs = path85.basename(member.absPath);
17134
+ const fromAbs = path86.basename(member.absPath);
16757
17135
  return fromAbs || member.id;
16758
17136
  }
16759
17137
  function assetKey(type, destKey) {
@@ -16810,7 +17188,7 @@ async function buildLinkPlan(members) {
16810
17188
  name: asset.name,
16811
17189
  destKey,
16812
17190
  sourceRelPath: asset.sourceRelPath,
16813
- sourceAbsPath: path85.join(member.absPath, asset.sourceRelPath),
17191
+ sourceAbsPath: path86.join(member.absPath, asset.sourceRelPath),
16814
17192
  sourceHash
16815
17193
  });
16816
17194
  }
@@ -16821,7 +17199,7 @@ async function buildLinkPlan(members) {
16821
17199
  function destRelPathFor(type, destKey) {
16822
17200
  const subdir = `${type}s`;
16823
17201
  const leaf = type === "skill" ? destKey : `${destKey}.md`;
16824
- return path86.posix.join(".claude", subdir, leaf);
17202
+ return path87.posix.join(".claude", subdir, leaf);
16825
17203
  }
16826
17204
  async function runLinkContext(workspaceRoot, opts = {}) {
16827
17205
  const write = resolveWriteMode(opts, "preview-by-default").kind === "write";
@@ -16847,9 +17225,9 @@ async function runLinkContext(workspaceRoot, opts = {}) {
16847
17225
  const linkedAt = (/* @__PURE__ */ new Date()).toISOString();
16848
17226
  for (const entry of plan.entries) {
16849
17227
  const destRel = destRelPathFor(entry.type, entry.destKey);
16850
- const destAbs = path86.join(workspaceRoot, destRel);
17228
+ const destAbs = path87.join(workspaceRoot, destRel);
16851
17229
  if (write) {
16852
- await fs64.ensureDir(path86.dirname(destAbs));
17230
+ await fs64.ensureDir(path87.dirname(destAbs));
16853
17231
  if (await fs64.pathExists(destAbs)) await fs64.remove(destAbs);
16854
17232
  await fs64.copy(entry.sourceAbsPath, destAbs, {
16855
17233
  overwrite: true,
@@ -16938,10 +17316,10 @@ async function runWorkspaceLinkContext(workspaceRoot, opts = {}) {
16938
17316
 
16939
17317
  // src/commands/workspace/setup.ts
16940
17318
  import { existsSync as existsSync10, statSync } from "node:fs";
16941
- import path88 from "node:path";
17319
+ import path89 from "node:path";
16942
17320
 
16943
17321
  // src/claude/write-workspace-claude-md.ts
16944
- import path87 from "node:path";
17322
+ import path88 from "node:path";
16945
17323
  import fs65 from "fs-extra";
16946
17324
  init_fs();
16947
17325
  init_logger();
@@ -16967,7 +17345,7 @@ ${WORKSPACE_BLOCK_END}`;
16967
17345
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
16968
17346
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
16969
17347
  const dryRun = opts.dryRun ?? false;
16970
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path87.join(workspaceRoot, "CLAUDE.md");
17348
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path88.join(workspaceRoot, "CLAUDE.md");
16971
17349
  const say = opts.quiet ? () => {
16972
17350
  } : log;
16973
17351
  const existed = await fs65.pathExists(filePath);
@@ -17000,20 +17378,20 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
17000
17378
  init_fs();
17001
17379
  init_logger();
17002
17380
  function resolveWorkspaceRoot(start = process.cwd()) {
17003
- let dir = path88.resolve(start);
17381
+ let dir = path89.resolve(start);
17004
17382
  for (; ; ) {
17005
- if (existsSync10(path88.join(dir, WORKSPACE_FILE))) return dir;
17006
- const parent = path88.dirname(dir);
17007
- if (parent === dir) return path88.resolve(start);
17383
+ if (existsSync10(path89.join(dir, WORKSPACE_FILE))) return dir;
17384
+ const parent = path89.dirname(dir);
17385
+ if (parent === dir) return path89.resolve(start);
17008
17386
  dir = parent;
17009
17387
  }
17010
17388
  }
17011
17389
  function isRootRepo(workspaceRoot, repoPath) {
17012
- return path88.resolve(workspaceRoot, repoPath) === path88.resolve(workspaceRoot);
17390
+ return path89.resolve(workspaceRoot, repoPath) === path89.resolve(workspaceRoot);
17013
17391
  }
17014
17392
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
17015
17393
  const apply = options.write ?? false;
17016
- const configText = await readText(path88.join(workspaceRoot, WORKSPACE_FILE));
17394
+ const configText = await readText(path89.join(workspaceRoot, WORKSPACE_FILE));
17017
17395
  if (!configText) {
17018
17396
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
17019
17397
  process.exitCode = 1;
@@ -17040,7 +17418,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
17040
17418
  const statuses = [];
17041
17419
  const aggregateInputs = [];
17042
17420
  for (const repo of repos) {
17043
- const repoRoot = path88.resolve(workspaceRoot, repo.path);
17421
+ const repoRoot = path89.resolve(workspaceRoot, repo.path);
17044
17422
  say(`
17045
17423
  \u2192 ${repo.name} (${repo.path})`);
17046
17424
  let res;
@@ -17054,7 +17432,12 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
17054
17432
  // Under --json, mute the inner pipeline entirely; we serialize the
17055
17433
  // aggregated result below. Otherwise let it print its normal human output.
17056
17434
  quiet: options.json,
17057
- allowEmptyCache: options.allowEmptyCache
17435
+ allowEmptyCache: options.allowEmptyCache,
17436
+ // A workspace run sets a member up under whatever profile the member already
17437
+ // declares (ADR-0125). Passing it explicitly is what keeps the PoC rail in
17438
+ // runSetupCore from firing here: the rail exists for a deliberate `haus init`,
17439
+ // not for a bulk pass that never meant to switch anyone's profile.
17440
+ profile: await readProjectProfile(repoRoot)
17058
17441
  });
17059
17442
  } catch (err) {
17060
17443
  const message2 = err instanceof Error ? err.message : String(err);
@@ -17156,7 +17539,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
17156
17539
  const status = statusByName.get(repo.name);
17157
17540
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
17158
17541
  if (status?.status === "ok") {
17159
- const lock = await checkLock(path88.resolve(workspaceRoot, repo.path));
17542
+ const lock = await checkLock(path89.resolve(workspaceRoot, repo.path));
17160
17543
  manifestRepos.push({
17161
17544
  name: repo.name,
17162
17545
  path: repo.path,
@@ -17212,7 +17595,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
17212
17595
  try {
17213
17596
  const linkResult = await runLinkContext(workspaceRoot, { write: true, json: options.json });
17214
17597
  if (linkResult.ok) {
17215
- written.push(...linkResult.added.map((rel) => path88.join(workspaceRoot, rel)));
17598
+ written.push(...linkResult.added.map((rel) => path89.join(workspaceRoot, rel)));
17216
17599
  if (!options.json) {
17217
17600
  say(
17218
17601
  `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` : ""}.`
@@ -17245,7 +17628,7 @@ Workspace setup complete: ${ok} ok, ${failed} failed. Wrote ${written.length} fi
17245
17628
 
17246
17629
  // src/commands/workspace/undo.ts
17247
17630
  import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
17248
- import path89 from "node:path";
17631
+ import path90 from "node:path";
17249
17632
  import fs66 from "fs-extra";
17250
17633
  init_fs();
17251
17634
  init_logger();
@@ -17276,7 +17659,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
17276
17659
  }
17277
17660
  if (options.dryRun) {
17278
17661
  for (const repo of config.repos) {
17279
- const repoRoot = path89.resolve(workspaceRoot, repo.path);
17662
+ const repoRoot = path90.resolve(workspaceRoot, repo.path);
17280
17663
  log(`
17281
17664
  \u2192 ${repo.name} (${repo.path})`);
17282
17665
  try {
@@ -17308,11 +17691,11 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
17308
17691
  ];
17309
17692
  for (const abs of workspaceArtifacts) {
17310
17693
  if (await fs66.pathExists(abs)) {
17311
- log(`[dry-run] Would remove ${path89.relative(workspaceRoot, abs)}`);
17694
+ log(`[dry-run] Would remove ${path90.relative(workspaceRoot, abs)}`);
17312
17695
  }
17313
17696
  }
17314
17697
  if (!await fs66.pathExists(hausPath(workspaceRoot, "WORKSPACE.md"))) {
17315
- const claudeMd = await readText(path89.join(workspaceRoot, "CLAUDE.md"));
17698
+ const claudeMd = await readText(path90.join(workspaceRoot, "CLAUDE.md"));
17316
17699
  if (claudeMd?.includes(WORKSPACE_BLOCK_BEGIN) || claudeMd && hasLegacyWorkspaceBlock(claudeMd)) {
17317
17700
  log("[dry-run] Would remove the workspace haus import block from CLAUDE.md");
17318
17701
  }
@@ -17331,7 +17714,7 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
17331
17714
  }
17332
17715
  let anyRepoFailed = false;
17333
17716
  for (const repo of config.repos) {
17334
- const repoRoot = path89.resolve(workspaceRoot, repo.path);
17717
+ const repoRoot = path90.resolve(workspaceRoot, repo.path);
17335
17718
  log(`
17336
17719
  \u2192 ${repo.name} (${repo.path})`);
17337
17720
  try {
@@ -17360,20 +17743,20 @@ async function runWorkspaceUndo(workspaceRoot, options = {}) {
17360
17743
  const abs = hausPath(workspaceRoot, rel);
17361
17744
  if (await fs66.pathExists(abs)) {
17362
17745
  await fs66.remove(abs);
17363
- log(`Removed ${path89.relative(workspaceRoot, abs)}`);
17746
+ log(`Removed ${path90.relative(workspaceRoot, abs)}`);
17364
17747
  }
17365
17748
  }
17366
17749
  const manifestFile = manifestPath(workspaceRoot);
17367
17750
  if (await fs66.pathExists(manifestFile)) {
17368
17751
  await fs66.remove(manifestFile);
17369
- log(`Removed ${path89.relative(workspaceRoot, manifestFile)}`);
17752
+ log(`Removed ${path90.relative(workspaceRoot, manifestFile)}`);
17370
17753
  }
17371
17754
  const workspaceMdPath = hausPath(workspaceRoot, "WORKSPACE.md");
17372
17755
  if (await fs66.pathExists(workspaceMdPath)) {
17373
17756
  await fs66.remove(workspaceMdPath);
17374
- log(`Removed ${path89.relative(workspaceRoot, workspaceMdPath)}`);
17757
+ log(`Removed ${path90.relative(workspaceRoot, workspaceMdPath)}`);
17375
17758
  } else {
17376
- const claudeMdPath = path89.join(workspaceRoot, "CLAUDE.md");
17759
+ const claudeMdPath = path90.join(workspaceRoot, "CLAUDE.md");
17377
17760
  if (existsSync11(claudeMdPath)) {
17378
17761
  const prev = await readText(claudeMdPath);
17379
17762
  if (prev?.includes(WORKSPACE_BLOCK_BEGIN)) {
@@ -17442,7 +17825,7 @@ relationships: []
17442
17825
  log("Workspace initialized.");
17443
17826
  }
17444
17827
  async function scanWorkspace(workspaceRoot, opts) {
17445
- const configText = await readText(path90.join(workspaceRoot, WORKSPACE_FILE));
17828
+ const configText = await readText(path91.join(workspaceRoot, WORKSPACE_FILE));
17446
17829
  if (!configText) {
17447
17830
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
17448
17831
  process.exitCode = 1;
@@ -17464,7 +17847,7 @@ async function scanWorkspace(workspaceRoot, opts) {
17464
17847
  const inputs = [];
17465
17848
  const failures = [];
17466
17849
  for (const repo of config.repos) {
17467
- const repoRoot = path90.resolve(workspaceRoot, repo.path);
17850
+ const repoRoot = path91.resolve(workspaceRoot, repo.path);
17468
17851
  try {
17469
17852
  if (!existsSync12(repoRoot) || !statSync3(repoRoot).isDirectory()) {
17470
17853
  throw new Error("path is not a directory");
@@ -17533,7 +17916,7 @@ init_logger();
17533
17916
  init_paths();
17534
17917
  function cliVersion() {
17535
17918
  try {
17536
- const pkgPath = path91.join(packageRoot(), "package.json");
17919
+ const pkgPath = path92.join(packageRoot(), "package.json");
17537
17920
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
17538
17921
  return pkg.version ?? "0.0.0";
17539
17922
  } catch {
@@ -17543,7 +17926,7 @@ function cliVersion() {
17543
17926
  var program = new Command();
17544
17927
  function validateRuntimeNodeVersion() {
17545
17928
  try {
17546
- const pkgPath = path91.join(packageRoot(), "package.json");
17929
+ const pkgPath = path92.join(packageRoot(), "package.json");
17547
17930
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
17548
17931
  const requiredRange = pkg.engines?.node;
17549
17932
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -17616,10 +17999,16 @@ program.command("init").description("Full setup: scan, recommend, and optionally
17616
17999
  ).option(
17617
18000
  "--allow-empty-cache",
17618
18001
  "Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
18002
+ ).option(
18003
+ "--profile <profile>",
18004
+ "Project profile to record when writing: production (default) | poc. Stored in .haus-workflow/haus.config.json (ADR-0125)"
17619
18005
  ).action(runInit);
17620
18006
  program.command("new").description("Create a new project from an allowed Haus stack, push it to GitHub, and run init").argument("[stack]", "Stack id (omit to pick interactively; see --list)").argument("[name]", "Project directory and repo name").option("--list", "Print the allowed stacks and exit").option("--org <org>", "GitHub org to create the repo in (default: personal account)").option("--public", "Create the GitHub repo public").option("--private", "Create the GitHub repo private (default)").option("--no-github", "Skip GitHub repo creation and push").option("--pm <pm>", "Package manager: yarn | npm | pnpm", "yarn").option("--dir <path>", "Directory to scaffold the project into (default: the cwd)").option("-y, --yes", "Skip confirmations (non-interactive)").option("--dry-run", "Print the full plan without executing anything").option("--json", "Emit a machine-readable result document").option(
17621
18007
  "--allow-empty-cache",
17622
18008
  "Proceed when no catalog item has installable content: write core files and skip the catalog items. Still exits 1 if none installed (ADR-0060)"
18009
+ ).option(
18010
+ "--profile <profile>",
18011
+ "Project profile to record when writing: production (default) | poc. Stored in .haus-workflow/haus.config.json (ADR-0125)"
17623
18012
  ).action(runNew);
17624
18013
  program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
17625
18014
  (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })