@haus-tech/haus-workflow 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.16.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.16.0...v0.16.1) (2026-06-09)
4
+
5
+ ## [0.16.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.15.0...v0.16.0) (2026-06-05)
6
+
7
+ ### Features
8
+
9
+ - support catalog contexts and refresh-aware sync ([1ab1d6c](https://github.com/WeAreHausTech/haus-workflow/commit/1ab1d6c1ca91a412fc4e7269e1616050c4397101))
10
+
11
+ ### Bug Fixes
12
+
13
+ - Add forbidden-content, sources-report, and drift ([e7cbd07](https://github.com/WeAreHausTech/haus-workflow/commit/e7cbd076f7f8d8a21555c5e0cdba058b9788ee29))
14
+
3
15
  ## [0.15.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.14.0...v0.15.0) (2026-06-05)
4
16
 
5
17
  ### Features
package/README.md CHANGED
@@ -31,7 +31,7 @@ Once installed, Claude Code gains a `/haus-workflow` slash command.
31
31
 
32
32
  ```
33
33
  /haus-workflow # interactive menu — pick setup, update, refresh, etc.
34
- /haus-workflow init # first-time project setup
34
+ /haus-workflow setup # full first-time setup — scaffolding, skills, commands + docs (runs the /haus-setup flow)
35
35
  /haus-workflow apply # refresh .claude/ and regenerate CLAUDE.md imports
36
36
  /haus-workflow update # update npm package + catalog + ~/.claude/
37
37
  /haus-workflow catalog # fetch only latest catalog
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import path34 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
9
- import path12 from "path";
9
+ import path13 from "path";
10
10
  import checkbox from "@inquirer/checkbox";
11
11
  import fs11 from "fs-extra";
12
12
 
@@ -32,7 +32,9 @@ var CATALOG_REF = process.env.HAUS_CATALOG_REF ?? "main";
32
32
  var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
33
33
 
34
34
  // src/catalog/remote-catalog.ts
35
- var CACHE_DIR = process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path.join(os.homedir(), CATALOG_CACHE_SUBDIR);
35
+ function getCacheDir() {
36
+ return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path.join(os.homedir(), CATALOG_CACHE_SUBDIR);
37
+ }
36
38
  var REMOTE_BASE = process.env["HAUS_CATALOG_REMOTE_BASE"] ?? `${CATALOG_REPO_URL}/${CATALOG_REF}`;
37
39
  var REMOTE_MANIFEST_URL = `${REMOTE_BASE}/manifest.json`;
38
40
  async function fetchText(url) {
@@ -54,15 +56,29 @@ async function fetchRemoteManifest() {
54
56
  return null;
55
57
  }
56
58
  }
59
+ async function writeTextIfChanged(dest, text) {
60
+ if (await fs.pathExists(dest)) {
61
+ const local = await fs.readFile(dest, "utf8");
62
+ if (local === text) return "unchanged";
63
+ await fs.writeFile(dest, text, "utf8");
64
+ return "updated";
65
+ }
66
+ await fs.ensureDir(path.dirname(dest));
67
+ await fs.writeFile(dest, text, "utf8");
68
+ return "created";
69
+ }
57
70
  var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
58
71
  async function readWorkflowTemplate(opts = {}) {
59
- const dest = path.join(CACHE_DIR, WORKFLOW_TEMPLATE_REL);
60
- if (await fs.pathExists(dest)) return fs.readFile(dest, "utf8");
72
+ const dest = path.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
61
73
  const text = await fetchText(`${REMOTE_BASE}/${WORKFLOW_TEMPLATE_REL}`);
62
- if (text === null) return null;
74
+ if (text === null) {
75
+ if (await fs.pathExists(dest)) return fs.readFile(dest, "utf8");
76
+ return null;
77
+ }
63
78
  if (!opts.dryRun) {
64
- await fs.ensureDir(path.dirname(dest));
65
- await fs.writeFile(dest, text, "utf8");
79
+ await writeTextIfChanged(dest, text);
80
+ } else if (await fs.pathExists(dest)) {
81
+ return fs.readFile(dest, "utf8");
66
82
  }
67
83
  return text;
68
84
  }
@@ -87,37 +103,37 @@ async function downloadSkillReferences(item, destDir) {
87
103
  warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
88
104
  continue;
89
105
  }
90
- if (await fs.pathExists(refDest)) continue;
91
106
  const text = await fetchText(`${REMOTE_BASE}/${item.path}/${ref}`);
92
107
  if (text === null) {
93
108
  warn(`Failed to fetch reference "${ref}" for ${item.id}`);
94
109
  continue;
95
110
  }
96
- await fs.ensureDir(path.dirname(refDest));
97
- await fs.writeFile(refDest, text, "utf8");
111
+ await writeTextIfChanged(refDest, text);
98
112
  }
99
113
  }
100
114
  async function syncRemoteCatalog() {
101
115
  const items = await fetchRemoteManifest();
102
116
  if (!items) {
103
117
  warn("Remote catalog fetch failed \u2014 using bundled catalog");
104
- return { newItems: [], unchanged: 0, failed: [] };
118
+ return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
105
119
  }
120
+ const cacheDir = getCacheDir();
106
121
  try {
107
- await fs.ensureDir(CACHE_DIR);
122
+ await fs.ensureDir(cacheDir);
108
123
  await fs.writeFile(
109
- path.join(CACHE_DIR, "manifest.json"),
124
+ path.join(cacheDir, "manifest.json"),
110
125
  `${JSON.stringify({ items }, null, 2)}
111
126
  `,
112
127
  "utf8"
113
128
  );
114
129
  } catch (err) {
115
130
  warn(
116
- `Catalog cache not writable (${CACHE_DIR}) \u2014 skipping cache sync: ${err instanceof Error ? err.message : String(err)}`
131
+ `Catalog cache not writable (${cacheDir}) \u2014 skipping cache sync: ${err instanceof Error ? err.message : String(err)}`
117
132
  );
118
- return { newItems: [], unchanged: 0, failed: [] };
133
+ return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
119
134
  }
120
135
  const newItems = [];
136
+ const refreshed = [];
121
137
  let unchanged = 0;
122
138
  const failed = [];
123
139
  for (const item of items) {
@@ -129,18 +145,13 @@ async function syncRemoteCatalog() {
129
145
  continue;
130
146
  }
131
147
  if (item.type === "skill") {
132
- const destDir = safeJoin(CACHE_DIR, item.path);
148
+ const destDir = safeJoin(getCacheDir(), item.path);
133
149
  if (!destDir) {
134
150
  warn(`Skipping ${item.id}: path traversal detected`);
135
151
  failed.push(item.id);
136
152
  continue;
137
153
  }
138
154
  const dest = path.join(destDir, "SKILL.md");
139
- if (await fs.pathExists(dest)) {
140
- await downloadSkillReferences(item, destDir);
141
- unchanged++;
142
- continue;
143
- }
144
155
  const url = `${REMOTE_BASE}/${item.path}/SKILL.md`;
145
156
  const text = await fetchText(url);
146
157
  if (!text) {
@@ -149,25 +160,22 @@ async function syncRemoteCatalog() {
149
160
  continue;
150
161
  }
151
162
  try {
152
- await fs.ensureDir(path.dirname(dest));
153
- await fs.writeFile(dest, text, "utf8");
163
+ const outcome = await writeTextIfChanged(dest, text);
154
164
  await downloadSkillReferences(item, destDir);
155
- newItems.push(item.id);
165
+ if (outcome === "created") newItems.push(item.id);
166
+ else if (outcome === "updated") refreshed.push(item.id);
167
+ else unchanged++;
156
168
  } catch (err) {
157
169
  warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
158
170
  failed.push(item.id);
159
171
  }
160
172
  } else {
161
- const dest = safeJoin(CACHE_DIR, item.path);
173
+ const dest = safeJoin(getCacheDir(), item.path);
162
174
  if (!dest) {
163
175
  warn(`Skipping ${item.id}: path traversal detected`);
164
176
  failed.push(item.id);
165
177
  continue;
166
178
  }
167
- if (await fs.pathExists(dest)) {
168
- unchanged++;
169
- continue;
170
- }
171
179
  const url = `${REMOTE_BASE}/${item.path}`;
172
180
  const text = await fetchText(url);
173
181
  if (!text) {
@@ -176,16 +184,17 @@ async function syncRemoteCatalog() {
176
184
  continue;
177
185
  }
178
186
  try {
179
- await fs.ensureDir(path.dirname(dest));
180
- await fs.writeFile(dest, text, "utf8");
181
- newItems.push(item.id);
187
+ const outcome = await writeTextIfChanged(dest, text);
188
+ if (outcome === "created") newItems.push(item.id);
189
+ else if (outcome === "updated") refreshed.push(item.id);
190
+ else unchanged++;
182
191
  } catch (err) {
183
192
  warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
184
193
  failed.push(item.id);
185
194
  }
186
195
  }
187
196
  }
188
- return { newItems, unchanged, failed };
197
+ return { newItems, refreshed, unchanged, failed };
189
198
  }
190
199
  var CATALOG_TAGS_API_URL = "https://api.github.com/repos/WeAreHausTech/haus-workflow-catalog/tags";
191
200
  async function fetchLatestCatalogTag() {
@@ -204,7 +213,7 @@ async function fetchLatestCatalogTag() {
204
213
  }
205
214
  async function getCacheManifestAge() {
206
215
  try {
207
- const stat = await fs.stat(path.join(CACHE_DIR, "manifest.json"));
216
+ const stat = await fs.stat(path.join(getCacheDir(), "manifest.json"));
208
217
  return Date.now() - stat.mtimeMs;
209
218
  } catch {
210
219
  return null;
@@ -599,21 +608,21 @@ var PROJECT_HOOK_FRAGMENTS = [
599
608
  id: "haus.context-hook",
600
609
  gate: "keep",
601
610
  event: "UserPromptSubmit",
602
- command: "haus context --from-hook || true"
611
+ command: "haus context --from-hook"
603
612
  },
604
613
  {
605
614
  id: "haus.guard-file",
606
615
  gate: "keep",
607
616
  event: "PreToolUse",
608
617
  matcher: "Read|Edit|Write",
609
- command: "haus guard file-access --from-hook || true"
618
+ command: "haus guard file-access --from-hook"
610
619
  },
611
620
  {
612
621
  id: "haus.guard-bash",
613
622
  gate: "keep",
614
623
  event: "PreToolUse",
615
624
  matcher: "Bash",
616
- command: "haus guard bash --from-hook || true"
625
+ command: "haus guard bash --from-hook"
617
626
  }
618
627
  ];
619
628
  async function readProjectSettings(root) {
@@ -637,11 +646,54 @@ async function applyProjectSettingsMerge(root) {
637
646
  }
638
647
 
639
648
  // src/claude/write-claude-files.ts
640
- import path11 from "path";
649
+ import path12 from "path";
641
650
  import fs10 from "fs-extra";
642
651
 
643
- // src/update/hash-installed.ts
652
+ // src/catalog/load-catalog.ts
644
653
  import path6 from "path";
654
+ async function loadCatalogContext(root) {
655
+ const envPath = process.env["HAUS_FIXTURE_CATALOG"];
656
+ if (envPath) {
657
+ const data2 = await readJson(envPath);
658
+ return {
659
+ items: data2?.items ?? [],
660
+ contentRoot: path6.dirname(envPath),
661
+ source: "fixture"
662
+ };
663
+ }
664
+ const cacheDir = getCacheDir();
665
+ const cacheManifestPath = path6.join(cacheDir, "manifest.json");
666
+ const cacheData = await readJson(cacheManifestPath);
667
+ if (cacheData?.items?.length) {
668
+ return { items: cacheData.items, contentRoot: cacheDir, source: "cache" };
669
+ }
670
+ const localManifest = path6.join(root, "library/catalog/manifest.json");
671
+ const localData = await readJson(localManifest);
672
+ if (localData?.items?.length) {
673
+ return {
674
+ items: localData.items,
675
+ contentRoot: path6.dirname(localManifest),
676
+ source: "local"
677
+ };
678
+ }
679
+ const packageManifest = path6.join(packageRoot(), "library/catalog/manifest.json");
680
+ const data = await readJson(packageManifest);
681
+ return {
682
+ items: data?.items ?? [],
683
+ contentRoot: path6.dirname(packageManifest),
684
+ source: "bundled"
685
+ };
686
+ }
687
+ async function loadCatalog(root) {
688
+ const ctx = await loadCatalogContext(root);
689
+ return ctx.items;
690
+ }
691
+ function catalogItemContentPath(contentRoot, item) {
692
+ return path6.join(contentRoot, item.path);
693
+ }
694
+
695
+ // src/update/hash-installed.ts
696
+ import path7 from "path";
645
697
  import fg2 from "fast-glob";
646
698
  import fs4 from "fs-extra";
647
699
  var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
@@ -652,7 +704,7 @@ async function hashInstalledPaths(root, relPaths) {
652
704
  const normalized = [...new Set(relPaths.map((p) => p.replace(/\\/g, "/")))].sort();
653
705
  const fileDigests = [];
654
706
  for (const rel of normalized) {
655
- const abs = path6.join(root, rel);
707
+ const abs = path7.join(root, rel);
656
708
  if (!await fs4.pathExists(abs)) continue;
657
709
  const stat = await fs4.stat(abs);
658
710
  if (stat.isFile()) {
@@ -663,8 +715,8 @@ async function hashInstalledPaths(root, relPaths) {
663
715
  if (!stat.isDirectory()) continue;
664
716
  const inner = await fg2("**/*", { cwd: abs, onlyFiles: true, dot: true });
665
717
  for (const sub of inner.sort()) {
666
- const relFile = path6.join(rel, sub).replace(/\\/g, "/");
667
- const absFile = path6.join(abs, sub);
718
+ const relFile = path7.join(rel, sub).replace(/\\/g, "/");
719
+ const absFile = path7.join(abs, sub);
668
720
  const body = await fs4.readFile(absFile, "utf8");
669
721
  fileDigests.push({ rel: relFile, digest: hashText(body) });
670
722
  }
@@ -699,7 +751,7 @@ function summarizeDiff(diffText) {
699
751
  }
700
752
 
701
753
  // src/claude/load-hooks-config.ts
702
- import path7 from "path";
754
+ import path8 from "path";
703
755
  var CONFIG_PATH = ".haus-workflow/config.json";
704
756
  var DEFAULT_HOOKS_CONFIG = {
705
757
  hooks: {
@@ -707,7 +759,7 @@ var DEFAULT_HOOKS_CONFIG = {
707
759
  }
708
760
  };
709
761
  async function isHookEnabled(root, key) {
710
- const cfg = await readJson(path7.join(root, CONFIG_PATH));
762
+ const cfg = await readJson(path8.join(root, CONFIG_PATH));
711
763
  return cfg?.hooks?.[key]?.enabled === true;
712
764
  }
713
765
 
@@ -719,25 +771,25 @@ var CANONICAL_HOOKS = {
719
771
  hooks: {
720
772
  UserPromptSubmit: [
721
773
  {
722
- hooks: [{ type: "command", command: "haus context --from-hook || true" }]
774
+ hooks: [{ type: "command", command: "haus context --from-hook" }]
723
775
  }
724
776
  ],
725
777
  PreToolUse: [
726
778
  {
727
779
  matcher: "Read|Edit|Write",
728
- hooks: [{ type: "command", command: "haus guard file-access --from-hook || true" }]
780
+ hooks: [{ type: "command", command: "haus guard file-access --from-hook" }]
729
781
  },
730
782
  {
731
783
  matcher: "Bash",
732
- hooks: [{ type: "command", command: "haus guard bash --from-hook || true" }]
784
+ hooks: [{ type: "command", command: "haus guard bash --from-hook" }]
733
785
  }
734
786
  ]
735
787
  }
736
788
  };
737
789
  var STABLE_HOOK_IDS = {
738
- "haus context --from-hook || true": "haus.context-hook",
739
- "haus guard file-access --from-hook || true": "haus.guard-file",
740
- "haus guard bash --from-hook || true": "haus.guard-bash"
790
+ "haus context --from-hook": "haus.context-hook",
791
+ "haus guard file-access --from-hook": "haus.guard-file",
792
+ "haus guard bash --from-hook": "haus.guard-bash"
741
793
  };
742
794
  async function loadClaudeHooksSettings() {
743
795
  return { ...CANONICAL_HOOKS, permissions: { deny: buildDenyRules() } };
@@ -833,7 +885,7 @@ async function verifyProjectSettingsHooksContract(root) {
833
885
  }
834
886
 
835
887
  // src/claude/write-root-claude-md.ts
836
- import path8 from "path";
888
+ import path9 from "path";
837
889
  import fs6 from "fs-extra";
838
890
  var BLOCK_BEGIN = "<!-- HAUS:BEGIN haus-imports v=1 -->";
839
891
  var BLOCK_END = "<!-- HAUS:END haus-imports -->";
@@ -873,7 +925,7 @@ ${block}
873
925
  `;
874
926
  }
875
927
  async function writeRootClaudeMd(root, dryRun) {
876
- const filePath = path8.join(root, "CLAUDE.md");
928
+ const filePath = path9.join(root, "CLAUDE.md");
877
929
  const block = buildImportBlock();
878
930
  const prev = await fs6.pathExists(filePath) ? await fs6.readFile(filePath, "utf8") : "";
879
931
  const next = injectHausBlock(prev, block);
@@ -898,11 +950,11 @@ async function writeRootClaudeMd(root, dryRun) {
898
950
  }
899
951
 
900
952
  // src/claude/write-workflow-config.ts
901
- import path10 from "path";
953
+ import path11 from "path";
902
954
  import fs8 from "fs-extra";
903
955
 
904
956
  // src/claude/derive-workflow-config.ts
905
- import path9 from "path";
957
+ import path10 from "path";
906
958
  import fs7 from "fs-extra";
907
959
  function binCmd(pm, bin, args) {
908
960
  const tail = args ? ` ${args}` : "";
@@ -912,7 +964,7 @@ function binCmd(pm, bin, args) {
912
964
  }
913
965
  async function deriveWorkflowConfig(root, ctx) {
914
966
  const pm = ctx.packageManager === "unknown" ? "npm" : ctx.packageManager;
915
- const pkg = await readJson(path9.join(root, "package.json"));
967
+ const pkg = await readJson(path10.join(root, "package.json"));
916
968
  const scripts = pkg?.scripts ?? {};
917
969
  const deps = new Set(ctx.dependencies);
918
970
  const stacks = Object.values(ctx.detectedStacks ?? {}).flat();
@@ -922,7 +974,7 @@ async function deriveWorkflowConfig(root, ctx) {
922
974
  return null;
923
975
  };
924
976
  const hasDep = (name) => deps.has(name);
925
- const exists = (rel) => fs7.pathExistsSync(path9.join(root, rel));
977
+ const exists = (rel) => fs7.pathExistsSync(path10.join(root, rel));
926
978
  const hasPlaywright = hasDep("@playwright/test") || stacks.includes("playwright");
927
979
  const hasCypress = hasDep("cypress");
928
980
  const preCommitTool = exists("lefthook.yml") || exists("lefthook.yaml") ? "lefthook" : exists(".husky") || hasDep("husky") || (scripts.prepare ?? "").includes("husky") ? "husky" : exists(".pre-commit-config.yaml") ? "pre-commit (Python framework)" : null;
@@ -999,7 +1051,7 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
999
1051
  const ctx = await readJson(hausPath(root, "context-map.json")) ?? {
1000
1052
  ...FALLBACK_CONTEXT,
1001
1053
  root,
1002
- repoName: path10.basename(root)
1054
+ repoName: path11.basename(root)
1003
1055
  };
1004
1056
  const values = await deriveWorkflowConfig(root, ctx);
1005
1057
  if (exists) {
@@ -1108,7 +1160,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1108
1160
  estimatedTokenReductionPct: 0
1109
1161
  };
1110
1162
  const pkgRoot = packageRoot();
1111
- const hausVersion2 = (await readJson(path11.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1163
+ const hausVersion2 = (await readJson(path12.join(pkgRoot, "package.json")))?.version ?? "0.0.0";
1112
1164
  const coreFiles = [
1113
1165
  claudePath(root, "settings.json"),
1114
1166
  claudePath(root, "rules", "haus.md"),
@@ -1167,15 +1219,8 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1167
1219
  "- Never read secrets.\n- Block dangerous shell commands.\n",
1168
1220
  dryRun
1169
1221
  );
1170
- const fixtureManifestPath = process.env["HAUS_FIXTURE_CATALOG"];
1171
- const manifestPath2 = fixtureManifestPath ?? path11.join(pkgRoot, "library", "catalog", "manifest.json");
1172
- const manifestDir = path11.dirname(manifestPath2);
1173
- const manifest = await readJson(manifestPath2) ?? { items: [] };
1174
- const manifestById = new Map((manifest.items ?? []).map((item) => [item.id, item]));
1175
- const cacheManifest = await readJson(
1176
- path11.join(CACHE_DIR, "manifest.json")
1177
- );
1178
- const cacheManifestById = new Map((cacheManifest?.items ?? []).map((item) => [item.id, item]));
1222
+ const { items: manifestItems, contentRoot } = await loadCatalogContext(root);
1223
+ const manifestById = new Map(manifestItems.map((item) => [item.id, item]));
1179
1224
  const installedPathsByItem = /* @__PURE__ */ new Map();
1180
1225
  const installedIds = /* @__PURE__ */ new Set();
1181
1226
  const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
@@ -1194,11 +1239,9 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1194
1239
  continue;
1195
1240
  }
1196
1241
  }
1197
- const cachedItem = cacheManifestById.get(item.id);
1198
- const cachePath = cachedItem?.path ? path11.join(CACHE_DIR, cachedItem.path) : null;
1199
- const sourcePath = cachePath && await fs10.pathExists(cachePath) ? cachePath : path11.join(manifestDir, manifestItem.path);
1242
+ const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
1200
1243
  const target = item.type === "agent" ? "agents" : item.type === "template" ? "templates" : "skills";
1201
- const destination = claudePath(root, target, path11.basename(sourcePath));
1244
+ const destination = claudePath(root, target, path12.basename(sourcePath));
1202
1245
  if (await fs10.pathExists(sourcePath)) {
1203
1246
  if (dryRun) {
1204
1247
  const exists = await fs10.pathExists(destination);
@@ -1206,12 +1249,12 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1206
1249
  `${displayPath(root, destination)}: ${exists ? "would overwrite" : "would create"} (${item.id})`
1207
1250
  );
1208
1251
  } else {
1209
- await fs10.ensureDir(path11.dirname(destination));
1252
+ await fs10.ensureDir(path12.dirname(destination));
1210
1253
  await fs10.copy(sourcePath, destination, { overwrite: true, errorOnExist: false });
1211
1254
  }
1212
1255
  files.push(destination);
1213
1256
  const current = installedPathsByItem.get(item.id) ?? [];
1214
- installedPathsByItem.set(item.id, [...current, path11.relative(root, destination)]);
1257
+ installedPathsByItem.set(item.id, [...current, path12.relative(root, destination)]);
1215
1258
  installedIds.add(item.id);
1216
1259
  } else {
1217
1260
  warn(
@@ -1289,7 +1332,7 @@ async function writeManagedJson(root, filePath, value, dryRun) {
1289
1332
 
1290
1333
  // src/commands/apply.ts
1291
1334
  async function cacheHasItems() {
1292
- const data = await readJson(path12.join(CACHE_DIR, "manifest.json"));
1335
+ const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
1293
1336
  return Array.isArray(data?.items) && data.items.length > 0;
1294
1337
  }
1295
1338
  async function runApply(options) {
@@ -1333,17 +1376,9 @@ async function runApply(options) {
1333
1376
  const rec = await readJson(hausPath(root, "recommendation.json"));
1334
1377
  const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
1335
1378
  if (catalogItemCount > 0 && !await cacheHasItems()) {
1336
- if (isDryRun) {
1337
- warn(
1338
- "Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first."
1339
- );
1340
- } else {
1341
- error(
1342
- "Catalog cache is empty \u2014 cannot install catalog items. Run `haus update` first, or pass --allow-empty-cache to apply core files only."
1343
- );
1344
- process.exitCode = 1;
1345
- return;
1346
- }
1379
+ warn(
1380
+ isDryRun ? "Catalog cache is empty \u2014 `haus apply --write` will skip catalog items. Run `haus update` first." : "Catalog cache is empty \u2014 catalog items will be skipped. Run `haus update` first, or pass --allow-empty-cache to silence this warning."
1381
+ );
1347
1382
  }
1348
1383
  }
1349
1384
  const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
@@ -1369,26 +1404,6 @@ async function refreshProjectApply(root) {
1369
1404
  return writeClaudeFiles(root, false, void 0, { refillConfig: false });
1370
1405
  }
1371
1406
 
1372
- // src/catalog/load-catalog.ts
1373
- import os4 from "os";
1374
- import path13 from "path";
1375
- var CACHE_MANIFEST = path13.join(os4.homedir(), CATALOG_CACHE_SUBDIR, "manifest.json");
1376
- async function loadCatalog(root) {
1377
- const envPath = process.env["HAUS_FIXTURE_CATALOG"];
1378
- if (envPath) {
1379
- const data2 = await readJson(envPath);
1380
- return data2?.items ?? [];
1381
- }
1382
- const cacheData = await readJson(CACHE_MANIFEST);
1383
- if (cacheData?.items?.length) return cacheData.items;
1384
- const localManifest = path13.join(root, "library/catalog/manifest.json");
1385
- const localData = await readJson(localManifest);
1386
- if (localData?.items?.length) return localData.items;
1387
- const packageManifest = path13.join(packageRoot(), "library/catalog/manifest.json");
1388
- const data = await readJson(packageManifest);
1389
- return data?.items ?? [];
1390
- }
1391
-
1392
1407
  // library/catalog/validation-rules.json
1393
1408
  var validation_rules_default = {
1394
1409
  forbiddenTags: [
@@ -2342,6 +2357,38 @@ ${describeRepo(context)}
2342
2357
  `;
2343
2358
  }
2344
2359
 
2360
+ // src/scanner/write-sources-report.ts
2361
+ function buildSourcesReport(items) {
2362
+ const statusBySource = /* @__PURE__ */ new Map();
2363
+ for (const item of items) {
2364
+ const src = item.source?.trim();
2365
+ if (!src || src === "haus") continue;
2366
+ if (src === "curated") {
2367
+ if (item.reviewStatus === "approved" && item.riskLevel !== "blocked") {
2368
+ statusBySource.set("curated", "approved");
2369
+ } else if (!statusBySource.has("curated")) {
2370
+ statusBySource.set("curated", "candidate");
2371
+ }
2372
+ continue;
2373
+ }
2374
+ if (item.reviewStatus === "approved" && item.riskLevel !== "blocked") {
2375
+ statusBySource.set(src, "approved");
2376
+ } else if (!statusBySource.has(src)) {
2377
+ statusBySource.set(src, "candidate");
2378
+ }
2379
+ }
2380
+ return {
2381
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2382
+ items: [...statusBySource.entries()].map(([id, status]) => ({ id, status })).sort((a, b) => a.id.localeCompare(b.id))
2383
+ };
2384
+ }
2385
+ async function writeSourcesReport(root) {
2386
+ const items = await loadCatalog(root);
2387
+ const report = buildSourcesReport(items);
2388
+ await writeJson(hausPath(root, "sources-report.json"), report);
2389
+ return report;
2390
+ }
2391
+
2345
2392
  // src/scanner/scan-project.ts
2346
2393
  var SAFE_FILES = [
2347
2394
  "package.json",
@@ -2450,6 +2497,7 @@ async function scanProject(root, mode = "fast") {
2450
2497
  await writeJson(hausPath(root, "dependency-map.json"), dependencyMap);
2451
2498
  await writeJson(hausPath(root, "scan-hashes.json"), scanHashes);
2452
2499
  await writeText(hausPath(root, "repo-summary.md"), repoSummary);
2500
+ await writeSourcesReport(root);
2453
2501
  return { ...context, dependencyMap, scanHashes, repoSummary };
2454
2502
  }
2455
2503
 
@@ -2461,6 +2509,18 @@ async function readContextOrScan(root) {
2461
2509
  return scan;
2462
2510
  }
2463
2511
 
2512
+ // src/security/secret-patterns.ts
2513
+ var SECRET_PATTERNS = [
2514
+ /api[_-]?key\s*[:=]\s*\S+/i,
2515
+ /token\s*[:=]\s*\S+/i,
2516
+ /password\s*[:=]\s*\S+/i
2517
+ ];
2518
+
2519
+ // src/security/redact-sensitive.ts
2520
+ function redactSensitive(input2) {
2521
+ return SECRET_PATTERNS.reduce((acc, pattern) => acc.replace(pattern, "[REDACTED]"), input2);
2522
+ }
2523
+
2464
2524
  // src/commands/context.ts
2465
2525
  async function runContext(options) {
2466
2526
  const root = process.cwd();
@@ -2509,7 +2569,7 @@ async function runContext(options) {
2509
2569
  }),
2510
2570
  summary
2511
2571
  ];
2512
- const text = lines.join("\n");
2572
+ const text = redactSensitive(lines.join("\n"));
2513
2573
  log(options.fromHook ? text.slice(0, 3e3) : text);
2514
2574
  }
2515
2575
 
@@ -2648,7 +2708,7 @@ async function runDoctor(options) {
2648
2708
  ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
2649
2709
  } else {
2650
2710
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
2651
- const cachePath = path19.join(CACHE_DIR, "templates/agentic-workflow-standard.md");
2711
+ const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2652
2712
  const bundledPath = path19.join(
2653
2713
  packageRoot(),
2654
2714
  "library",
@@ -2723,7 +2783,9 @@ async function runDoctor(options) {
2723
2783
  `A newer haus (${npmStatus.latest}) is available`,
2724
2784
  `npm install -g ${NPM_PACKAGE_NAME}`
2725
2785
  );
2726
- process.exitCode = 1;
2786
+ if (!process.env["HAUS_FIXTURE_CATALOG"]) {
2787
+ process.exitCode = 1;
2788
+ }
2727
2789
  } else if (npmStatus.latest !== null) {
2728
2790
  ok(`- CLI: ${currentVersion} (up to date)`);
2729
2791
  } else {
@@ -2916,23 +2978,7 @@ async function readChangedFiles(root) {
2916
2978
  }
2917
2979
 
2918
2980
  // src/recommender/policies.ts
2919
- var UNSUPPORTED = [
2920
- "python",
2921
- "django",
2922
- "go",
2923
- "rust",
2924
- "java",
2925
- "spring",
2926
- "kotlin",
2927
- "swift",
2928
- "android",
2929
- "flutter",
2930
- "dart",
2931
- "c++",
2932
- "perl",
2933
- "defi",
2934
- "trading"
2935
- ];
2981
+ var UNSUPPORTED = FORBIDDEN_TAGS;
2936
2982
  function matchRequiresAny(clauses, ctx) {
2937
2983
  for (const clause of clauses) {
2938
2984
  if ("stack" in clause) {
@@ -3218,6 +3264,15 @@ async function runSetupCore(root, opts) {
3218
3264
  return baseResult;
3219
3265
  }
3220
3266
  }
3267
+ if (!dryRun && !process.env["HAUS_FIXTURE_CATALOG"]) {
3268
+ log("Syncing remote catalog...");
3269
+ const sync = await syncRemoteCatalog();
3270
+ if (sync.newItems.length > 0) {
3271
+ log(`Catalog cache populated: ${sync.newItems.length} new item(s).`);
3272
+ } else if (sync.refreshed.length > 0) {
3273
+ log(`Catalog cache refreshed: ${sync.refreshed.length} updated item(s).`);
3274
+ }
3275
+ }
3221
3276
  const files = await writeClaudeFiles(root, dryRun ?? false);
3222
3277
  log("Applied files:");
3223
3278
  files.forEach((f) => log(`- ${displayPath(root, f)}`));
@@ -3235,45 +3290,10 @@ async function runSetupCore(root, opts) {
3235
3290
  }
3236
3291
 
3237
3292
  // src/commands/setup-project.ts
3238
- var GUIDED_QUESTIONS = [
3239
- "What is this project for?",
3240
- "Is it for a client, internal Haus work, or experimentation?",
3241
- "What should Claude help with most?",
3242
- "Is this project connected to other repositories?",
3243
- "Are there parts of the project Claude should avoid touching?",
3244
- "Are there client-specific rules or sensitive areas?",
3245
- "Do you want a minimal, standard, or strict setup?"
3246
- ];
3247
3293
  async function runSetupProject(options) {
3248
3294
  const root = process.cwd();
3249
- let mode = options.guided ? "guided" : "fast";
3250
- if (!options.guided && !options.fast && !options.json) {
3251
- log("How do you want to set this project up?");
3252
- log("1. Guided setup - I'll ask a few simple questions, then scan the project.");
3253
- log("2. Fast setup - I'll only scan the project and recommend defaults.");
3254
- const choice = await ask("Choose 1 or 2");
3255
- mode = choice === "1" ? "guided" : "fast";
3256
- }
3257
- if (mode === "guided") {
3258
- const existing = await readJson(hausPath(root, "setup-answers.json")) ?? {};
3259
- const merged = {};
3260
- for (const question of GUIDED_QUESTIONS) {
3261
- if (options.json) {
3262
- merged[question] = existing[question] ?? "pending-user-answer";
3263
- continue;
3264
- }
3265
- const prefilled = existing[question];
3266
- if (prefilled && prefilled !== "pending-user-answer" && prefilled !== "no-answer") {
3267
- merged[question] = prefilled;
3268
- continue;
3269
- }
3270
- const answer = await ask(question);
3271
- merged[question] = answer || prefilled || "no-answer";
3272
- }
3273
- await writeJson(hausPath(root, "setup-answers.json"), merged);
3274
- }
3275
3295
  await runSetupCore(root, {
3276
- mode,
3296
+ mode: "fast",
3277
3297
  json: options.json,
3278
3298
  apply: !options.json,
3279
3299
  dryRun: false,
@@ -3570,10 +3590,14 @@ async function runRecommend(options) {
3570
3590
 
3571
3591
  // src/commands/refresh.ts
3572
3592
  async function runRefresh() {
3573
- const result = await scanProject(process.cwd(), "fast");
3574
- log("Haus scan complete");
3575
- log(`Roles: ${result.repoRoles.join(", ") || "unknown"}`);
3576
- log(`Package manager: ${result.packageManager}`);
3593
+ const root = process.cwd();
3594
+ const context = await scanProject(root, "fast");
3595
+ const recommendation = await recommend(root, context);
3596
+ await writeJson(hausPath(root, "recommendation.json"), recommendation);
3597
+ log("Haus refresh complete");
3598
+ log(`Roles: ${context.repoRoles.join(", ") || "unknown"}`);
3599
+ log(`Package manager: ${context.packageManager}`);
3600
+ log(`Recommended items: ${recommendation.recommended.length}`);
3577
3601
  }
3578
3602
 
3579
3603
  // src/commands/scan.ts
@@ -3827,7 +3851,17 @@ async function checkLock(root) {
3827
3851
  (item) => !item.version || normalizeVersion(item.version) !== null
3828
3852
  );
3829
3853
  const catalogRef = lock[0]?.catalogRef ?? null;
3830
- return { ok: lock.length > 0 && hasValidVersions, count: lock.length, catalogRef };
3854
+ const drift = [];
3855
+ for (const item of lock) {
3856
+ if (!item.hash) continue;
3857
+ const paths = Array.isArray(item.paths) ? item.paths.map(String) : [];
3858
+ const actual = await hashInstalledPaths(root, paths);
3859
+ if (item.hash !== actual) {
3860
+ drift.push({ id: item.id, expected: item.hash, actual });
3861
+ }
3862
+ }
3863
+ const ok = lock.length > 0 && hasValidVersions && drift.length === 0;
3864
+ return { ok, count: lock.length, catalogRef, drift, driftCount: drift.length };
3831
3865
  }
3832
3866
  async function applyLock(root) {
3833
3867
  const lockPath = hausPath(root, "haus.lock.json");
@@ -3902,6 +3936,7 @@ async function runUpdate(options) {
3902
3936
  )
3903
3937
  );
3904
3938
  if (!status.ok) process.exitCode = 1;
3939
+ if (status.driftCount > 0) process.exitCode = 1;
3905
3940
  return;
3906
3941
  }
3907
3942
  const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
@@ -3924,7 +3959,12 @@ async function runUpdate(options) {
3924
3959
  if (sync.newItems.length > 0) {
3925
3960
  log(`Catalog updated: ${sync.newItems.length} new item(s): ${sync.newItems.join(", ")}`);
3926
3961
  log("Run `haus recommend && haus apply --write` to install new skills.");
3927
- } else if (sync.unchanged > 0) {
3962
+ }
3963
+ if (sync.refreshed.length > 0) {
3964
+ log(`Catalog refreshed: ${sync.refreshed.length} updated item(s): ${sync.refreshed.join(", ")}`);
3965
+ log("Run `haus apply --write` to install refreshed skill content.");
3966
+ }
3967
+ if (sync.newItems.length === 0 && sync.refreshed.length === 0 && sync.unchanged > 0) {
3928
3968
  log(`Catalog up to date (${sync.unchanged} item(s) unchanged).`);
3929
3969
  }
3930
3970
  if (sync.failed.length > 0) {
@@ -3978,6 +4018,32 @@ async function detectGlobalInstallDrift() {
3978
4018
  // src/commands/validate-catalog.ts
3979
4019
  import fs18 from "fs";
3980
4020
  import path26 from "path";
4021
+
4022
+ // src/catalog/forbidden-content.ts
4023
+ var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
4024
+ function escapeRegExp(value) {
4025
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4026
+ }
4027
+ function extractUseWhenSection(text) {
4028
+ const marker = "## Use when";
4029
+ const idx = text.toLowerCase().indexOf(marker.toLowerCase());
4030
+ if (idx < 0) return "";
4031
+ const tail = text.slice(idx + marker.length);
4032
+ const next = tail.search(/\n##\s+/);
4033
+ return next < 0 ? tail : tail.slice(0, next);
4034
+ }
4035
+ function auditForbiddenTagsInText(text, label) {
4036
+ const body = extractUseWhenSection(text);
4037
+ if (!body.trim()) return [];
4038
+ const failures = [];
4039
+ for (const word of PROSE_FORBIDDEN_TAGS) {
4040
+ const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "i");
4041
+ if (re.test(body)) failures.push(`${label}: forbidden stack/tag "${word}" in content`);
4042
+ }
4043
+ return failures;
4044
+ }
4045
+
4046
+ // src/commands/validate-catalog.ts
3981
4047
  function auditForbiddenStacks(items) {
3982
4048
  const failures = [];
3983
4049
  for (const item of items) {
@@ -4056,6 +4122,9 @@ function auditShippedFiles(manifestDir, items) {
4056
4122
  for (const section of REQUIRED_SKILL_SECTIONS) {
4057
4123
  if (!text.includes(section)) failures.push(`${item.id}: SKILL.md missing ${section}`);
4058
4124
  }
4125
+ failures.push(
4126
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4127
+ );
4059
4128
  } else if (item.type === "agent") {
4060
4129
  if (!fs18.existsSync(absPath)) {
4061
4130
  failures.push(`${item.id}: missing agent file ${item.path}`);
@@ -4071,17 +4140,42 @@ function auditShippedFiles(manifestDir, items) {
4071
4140
  if (lower.includes(phrase))
4072
4141
  failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
4073
4142
  }
4143
+ failures.push(
4144
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, absPath)}`)
4145
+ );
4074
4146
  } else if (item.type === "template") {
4075
4147
  if (!fs18.existsSync(absPath)) {
4076
4148
  failures.push(`${item.id}: missing template file ${item.path}`);
4149
+ continue;
4077
4150
  }
4151
+ failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4078
4152
  }
4079
4153
  }
4080
4154
  return failures;
4081
4155
  }
4156
+ function auditTemplateContent(manifestDir, absPath, itemId) {
4157
+ const rel = path26.relative(manifestDir, absPath);
4158
+ const text = fs18.readFileSync(absPath, "utf8");
4159
+ const failures = [];
4160
+ const lines = text.split(/\r?\n/);
4161
+ for (let i = 0; i < lines.length; i++) {
4162
+ const line2 = lines[i] ?? "";
4163
+ if (PLACEHOLDER_PATTERN.test(line2)) {
4164
+ failures.push(`${rel}:${i + 1}: TODO or placeholder in shipped content`);
4165
+ }
4166
+ if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
4167
+ failures.push(`${rel}:${i + 1}: risky install pattern`);
4168
+ }
4169
+ if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
4170
+ failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
4171
+ }
4172
+ }
4173
+ failures.push(...auditForbiddenTagsInText(text, `${itemId}: ${rel}`));
4174
+ return failures;
4175
+ }
4082
4176
  function auditMarkdownContent(manifestDir) {
4083
4177
  const failures = [];
4084
- const dirs = ["skills", "agents"];
4178
+ const dirs = ["skills", "agents", "templates"];
4085
4179
  for (const dir of dirs) {
4086
4180
  const abs = path26.join(manifestDir, dir);
4087
4181
  if (!fs18.existsSync(abs)) continue;
@@ -4101,6 +4195,9 @@ function auditMarkdownContent(manifestDir) {
4101
4195
  failures.push(`${rel}:${i + 1}: disallowed npx (only npx tsx allowed)`);
4102
4196
  }
4103
4197
  }
4198
+ if (!rel.includes("/references/")) {
4199
+ failures.push(...auditForbiddenTagsInText(text, rel));
4200
+ }
4104
4201
  });
4105
4202
  }
4106
4203
  return failures;
@@ -4831,7 +4928,7 @@ validateRuntimeNodeVersion();
4831
4928
  program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
4832
4929
  program.command("scan").option("--json").action(runScan);
4833
4930
  program.command("recommend").option("--json").action(runRecommend);
4834
- program.command("setup-project").option("--guided").option("--fast").option("--json").action(runSetupProject);
4931
+ program.command("setup-project").option("--json").action(runSetupProject);
4835
4932
  program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
4836
4933
  program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
4837
4934
  "--allow-empty-cache",
@@ -4843,7 +4940,7 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
4843
4940
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
4844
4941
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
4845
4942
  program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
4846
- program.command("init").option("--fast").option("--json").action(runInit);
4943
+ program.command("init").option("--json").action(runInit);
4847
4944
  program.command("refresh").action(runRefresh);
4848
4945
  program.command("catalog-audit").action(runCatalogAudit);
4849
4946
  program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.4.1",
2
+ "version": "2.4.2",
3
3
  "items": [
4
4
  {
5
5
  "id": "haus.nextjs-patterns",
@@ -4,7 +4,7 @@ commands; they read plain language and approve.
4
4
 
5
5
  Do this in order:
6
6
 
7
- 1. **Detect.** Run `haus setup-project --fast --json`. Read the JSON yourself —
7
+ 1. **Detect.** Run `haus setup-project --json`. Read the JSON yourself —
8
8
  do not show it. Translate what was detected into one or two plain sentences,
9
9
  e.g. "This looks like a Next.js website using Yarn. I found unit tests but no
10
10
  end-to-end tests." If the detection status is `unknown` or `partial`, say so
@@ -17,16 +17,16 @@ All-in-one entry point for the Haus AI workflow.
17
17
 
18
18
  ## Task aliases → commands
19
19
 
20
- | Alias(es) | Command | What it does |
21
- | ------------------------------------ | -------------------- | ---------------------------------------------- |
22
- | `init`, `setup` | `haus init` | First-time project setup |
23
- | `apply`, `refresh`, `update-project` | `haus apply --write` | Re-run setup / refresh `.claude/` context |
24
- | `update`, `upgrade` | `haus update` | Update npm package + catalog + `~/.claude/` |
25
- | `catalog` | `haus update` | Fetch latest catalog (same command as update) |
26
- | `doctor`, `check` | `haus doctor` | Check for install drift |
27
- | `install`, `global` | `haus install` | Seed `~/.claude/` with haus-owned files |
28
- | `uninstall` | `haus uninstall` | Remove all haus global files from `~/.claude/` |
29
- | `claude-md`, `regenerate` | `haus apply --write` | Regenerate root `CLAUDE.md` import block |
20
+ | Alias(es) | Command | What it does |
21
+ | ------------------------------------ | ----------------------- | ------------------------------------------------------------------- |
22
+ | `init`, `setup` | _Setup procedure below_ | Full first-time setup: scaffolding, skills, commands + project docs |
23
+ | `apply`, `refresh`, `update-project` | `haus apply --write` | Re-run setup / refresh `.claude/` context |
24
+ | `update`, `upgrade` | `haus update` | Update npm package + catalog + `~/.claude/` |
25
+ | `catalog` | `haus update` | Fetch latest catalog (same command as update) |
26
+ | `doctor`, `check` | `haus doctor` | Check for install drift |
27
+ | `install`, `global` | `haus install` | Seed `~/.claude/` with haus-owned files |
28
+ | `uninstall` | `haus uninstall` | Remove all haus global files from `~/.claude/` |
29
+ | `claude-md`, `regenerate` | `haus apply --write` | Regenerate root `CLAUDE.md` import block |
30
30
 
31
31
  ## Step 1 — Determine the task
32
32
 
@@ -38,7 +38,7 @@ All-in-one entry point for the Haus AI workflow.
38
38
  Question: "What would you like to do?"
39
39
  Options:
40
40
  1. Set up this project for the first time
41
- (haus initscans repo, writes .haus-workflow/, updates CLAUDE.md)
41
+ (full setupscaffolding + skills + commands, then a deep read to write the CLAUDE.md docs body + docs/)
42
42
  2. Refresh project setup
43
43
  (haus apply --write — re-runs setup, regenerates CLAUDE.md imports)
44
44
  3. Update haus package + catalog + global files
@@ -55,21 +55,16 @@ Map the user's selection to the command from the alias table, then continue to S
55
55
 
56
56
  Run the mapped command via Bash. Quote the exact command you are running before executing it.
57
57
 
58
+ **Exception — `setup` / `init`:** this maps to a multi-step procedure, not a single command. Do not run a bare `haus init`. Skip to **Setup (`setup` / `init`)** under Step 3 and follow it.
59
+
58
60
  ## Step 3 — Post-run steps
59
61
 
60
62
  After the command completes, follow the relevant post-run steps below.
61
63
 
62
- ### After `haus init`
63
-
64
- 1. Open `.haus-workflow/workflow-config.md`.
65
- 2. Check for unfilled placeholders (`TODO`, `n/a`, empty values) in:
66
- - Test, lint, typecheck, build commands — confirm against `package.json` scripts.
67
- - Docs paths — check whether `docs/SPEC.md`, `docs/DESIGN.md`, `docs/UX.md` exist.
68
- - Validation library — check `package.json` dependencies for `zod`, `yup`, `joi`, `valibot`.
69
- - Pre-commit tool — check for `.husky/`, `lefthook.yml`, `.pre-commit-config.yaml`.
70
- - Highest-stakes logic — ask the user if unclear.
71
- 3. Fill in every unfilled field. Do not leave placeholders.
72
- 4. Confirm with the user that `workflow-config.md` is complete before proceeding.
64
+ ### Setup (`setup` / `init`)
65
+
66
+ 1. Open and follow `~/.claude/commands/haus-setup.md` — the installed `haus-setup` command (in some projects also `.claude/commands/haus-setup.md`). Run every step in order. It detects the stack, asks the guided questions, runs `haus apply --write` (scaffolding, skills, commands, rules, docs skill), writes the **project docs** (`CLAUDE.md` body + `docs/`) and `.haus-workflow/deep-context.json`, runs `haus recommend`, applies the newly-matched helpers, and confirms.
67
+ 2. Then fill `.haus-workflow/workflow-config.md` replace every placeholder (`TODO`, `n/a`, empty): test/lint/typecheck/build commands (check `package.json`), docs paths, validation library, pre-commit tool, highest-stakes logic (ask if unclear). Leave none.
73
68
 
74
69
  ### After `haus apply --write`
75
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {