@haus-tech/haus-workflow 0.30.0 → 0.31.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.31.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.30.0...v0.31.0) (2026-06-23)
4
+
5
+ ### Features
6
+
7
+ - opt-in UX for tiered skills + config scaffold ([#141](https://github.com/WeAreHausTech/haus-workflow/issues/141)) ([f60207b](https://github.com/WeAreHausTech/haus-workflow/commit/f60207b78af4651656d457c6f80665bd25deaa7f)), closes [#33](https://github.com/WeAreHausTech/haus-workflow/issues/33) [#33](https://github.com/WeAreHausTech/haus-workflow/issues/33)
8
+
3
9
  ## [0.30.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.29.0...v0.30.0) (2026-06-22)
4
10
 
5
11
  ### Features
package/README.md CHANGED
@@ -87,6 +87,10 @@ On `haus apply` / `haus update`, items **removed from the catalog** or marked
87
87
  lock hash; user-edited copies are kept. Approved items deselected via `--select` are
88
88
  not pruned.
89
89
 
90
+ > **Upgrading from a pre-`0.30.0` CLI:** deprecated-skill pruning shipped in `0.30.0`.
91
+ > If an older CLI left deprecated skills on disk, upgrade first
92
+ > (`npm i -g @haus-tech/haus-workflow`) then run `haus update` to prune them.
93
+
90
94
  ## Internal docs
91
95
 
92
96
  [docs/SUMMARY.md](docs/SUMMARY.md) — full documentation index
package/dist/cli.js CHANGED
@@ -2398,6 +2398,13 @@ async function cleanupStaleCatalogItems(root, manifestById, dryRun) {
2398
2398
  }
2399
2399
  }
2400
2400
 
2401
+ // src/utils/args.ts
2402
+ function parseIdList(value) {
2403
+ if (!value) return [];
2404
+ const raw = Array.isArray(value) ? value : [value];
2405
+ return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v.length > 0);
2406
+ }
2407
+
2401
2408
  // src/commands/apply.ts
2402
2409
  async function cacheHasItems() {
2403
2410
  const data = await readJson(path13.join(getCacheDir(), "manifest.json"));
@@ -2414,6 +2421,34 @@ async function runApply(options) {
2414
2421
  const root = process.cwd();
2415
2422
  const isDryRun = Boolean(options.dryRun) && !options.write;
2416
2423
  let selectedIds;
2424
+ const explicitIds = parseIdList(options.ids);
2425
+ if (explicitIds.length > 0) {
2426
+ if (options.select) {
2427
+ error("Use either --select (interactive) or --ids (explicit), not both");
2428
+ process.exitCode = 1;
2429
+ return;
2430
+ }
2431
+ const rec = await readJson(hausPath(root, "recommendation.json"));
2432
+ const recommended = rec?.recommended ?? [];
2433
+ const installableIds = new Set(installableRecommendedItems(recommended).map((i) => i.id));
2434
+ const configIds = new Set(recommended.filter((i) => i.install === false).map((i) => i.id));
2435
+ const skippedIds = new Set((rec?.skipped ?? []).map((s) => s.id));
2436
+ for (const id of explicitIds.filter((id2) => !installableIds.has(id2))) {
2437
+ if (configIds.has(id)) {
2438
+ warn(`--ids: "${id}" is a config item \u2014 install it with \`haus scaffold ${id}\`. Ignoring.`);
2439
+ } else if (skippedIds.has(id)) {
2440
+ warn(
2441
+ `--ids: "${id}" is not currently recommended (skipped) \u2014 add it with \`haus recommend --include ${id}\` first. Ignoring.`
2442
+ );
2443
+ } else {
2444
+ warn(
2445
+ `--ids: "${id}" is not in recommendation.json \u2014 run \`haus recommend\` (or \`--include ${id}\`) first. Ignoring.`
2446
+ );
2447
+ }
2448
+ }
2449
+ selectedIds = explicitIds.filter((id) => installableIds.has(id));
2450
+ log(`Applying ${selectedIds.length} explicitly selected catalog item(s).`);
2451
+ }
2417
2452
  if (options.select) {
2418
2453
  if (!process.stdin.isTTY) {
2419
2454
  error("--select requires an interactive terminal (stdin is not a TTY)");
@@ -3546,7 +3581,7 @@ function mergeRecommendationWarnings(context) {
3546
3581
  }
3547
3582
 
3548
3583
  // src/recommender/recommend.ts
3549
- async function recommend(root, context) {
3584
+ async function recommend(root, context, opts = {}) {
3550
3585
  const items = await loadCatalog(root);
3551
3586
  const sources = await readJson(
3552
3587
  hausPath(root, "sources-report.json")
@@ -3719,6 +3754,8 @@ async function recommend(root, context) {
3719
3754
  }
3720
3755
  }
3721
3756
  applyCoInstallSuppression(recommended, skipped);
3757
+ const includeWarnings = applyManualIncludes(opts.include ?? [], items, recommended, skipped);
3758
+ const optInEligible = buildOptInEligible(items, skipped);
3722
3759
  recommended.sort((a, b) => a.id.localeCompare(b.id));
3723
3760
  skipped.sort((a, b) => a.id.localeCompare(b.id));
3724
3761
  const installableRecommended = recommended.filter((item) => item.install);
@@ -3729,7 +3766,8 @@ async function recommend(root, context) {
3729
3766
  return {
3730
3767
  recommended,
3731
3768
  skipped,
3732
- warnings: mergeRecommendationWarnings(context),
3769
+ optInEligible,
3770
+ warnings: [...mergeRecommendationWarnings(context), ...includeWarnings],
3733
3771
  estimatedContextTokens,
3734
3772
  selectedRules,
3735
3773
  skippedRules,
@@ -3815,6 +3853,80 @@ function applyCoInstallSuppression(recommended, skipped) {
3815
3853
  });
3816
3854
  }
3817
3855
  }
3856
+ var HARD_SKIP_CODES = /* @__PURE__ */ new Set([
3857
+ "unsupported-policy",
3858
+ "deprecated",
3859
+ "curated-not-approved",
3860
+ "curated-risk-blocked",
3861
+ "sensitive-policy",
3862
+ "source-trust",
3863
+ "source-approval"
3864
+ ]);
3865
+ function applyManualIncludes(include, items, recommended, skipped) {
3866
+ const warnings = [];
3867
+ if (include.length === 0) return warnings;
3868
+ const byId = new Map(items.map((i) => [i.id, i]));
3869
+ const recommendedIds = new Set(recommended.map((r) => r.id));
3870
+ for (const id of include) {
3871
+ const item = byId.get(id);
3872
+ if (!item) {
3873
+ warnings.push(`--include: unknown catalog id "${id}" (not in manifest)`);
3874
+ continue;
3875
+ }
3876
+ if (recommendedIds.has(id)) continue;
3877
+ const skipIdx = skipped.findIndex((s) => s.id === id);
3878
+ const skipCode = skipIdx >= 0 ? skipped[skipIdx].skipReasons[0]?.code : void 0;
3879
+ if (skipCode && HARD_SKIP_CODES.has(skipCode)) {
3880
+ warnings.push(`--include: "${id}" cannot be force-installed (blocked by ${skipCode})`);
3881
+ continue;
3882
+ }
3883
+ if (skipCode === "requires-any-unsatisfied") {
3884
+ warnings.push(`--include: "${id}" added manually though its requiresAny gate is unsatisfied`);
3885
+ }
3886
+ if (skipIdx >= 0) skipped.splice(skipIdx, 1);
3887
+ const isConfig = item.type === "config";
3888
+ recommended.push({
3889
+ id: item.id,
3890
+ type: item.type,
3891
+ reason: "manually included",
3892
+ reasons: [
3893
+ {
3894
+ code: "manual-include",
3895
+ message: "manually included via --include",
3896
+ signal: "selection:manual"
3897
+ }
3898
+ ],
3899
+ selectionMode: "manual",
3900
+ install: !isConfig,
3901
+ tags: item.tags,
3902
+ ecosystem: item.ecosystem,
3903
+ tokenEstimate: item.tokenEstimate
3904
+ });
3905
+ recommendedIds.add(id);
3906
+ }
3907
+ return warnings;
3908
+ }
3909
+ function buildOptInEligible(items, skipped) {
3910
+ const byId = new Map(items.map((i) => [i.id, i]));
3911
+ const eligible = [];
3912
+ for (const entry of skipped) {
3913
+ if (entry.skipReasons[0]?.code !== "requires-any-unsatisfied") continue;
3914
+ const item = byId.get(entry.id);
3915
+ if (!item?.optInTier || !item.optInGroup) continue;
3916
+ eligible.push({
3917
+ id: item.id,
3918
+ type: item.type,
3919
+ title: item.title,
3920
+ optInTier: item.optInTier,
3921
+ optInGroup: item.optInGroup,
3922
+ purpose: item.purpose,
3923
+ tokenEstimate: item.tokenEstimate,
3924
+ requires: entry.skipReasons[0]?.signal ?? describeRequiresAny(item.requiresAny ?? [])
3925
+ });
3926
+ }
3927
+ eligible.sort((a, b) => a.optInGroup.localeCompare(b.optInGroup) || a.id.localeCompare(b.id));
3928
+ return eligible;
3929
+ }
3818
3930
 
3819
3931
  // src/commands/setup-core.ts
3820
3932
  async function runSetupCore(root, opts) {
@@ -4275,7 +4387,8 @@ async function runInstall(options) {
4275
4387
  async function runRecommend(options) {
4276
4388
  const root = process.cwd();
4277
4389
  const context = await readContextOrScan(root);
4278
- const result = await recommend(root, context);
4390
+ const include = parseIdList(options.include);
4391
+ const result = await recommend(root, context, { include });
4279
4392
  await writeJson(hausPath(root, "recommendation.json"), result);
4280
4393
  if (options.json) {
4281
4394
  log(JSON.stringify(result, null, 2));
@@ -4284,6 +4397,12 @@ async function runRecommend(options) {
4284
4397
  log("Haus recommendation ready");
4285
4398
  log(`Recommended: ${result.recommended.length}`);
4286
4399
  log(`Skipped: ${result.skipped.length}`);
4400
+ if (result.optInEligible && result.optInEligible.length > 0) {
4401
+ log(
4402
+ `Opt-in available: ${result.optInEligible.length} (add with \`haus recommend --include <id>\`)`
4403
+ );
4404
+ }
4405
+ for (const w of result.warnings.filter((w2) => w2.startsWith("--include:"))) log(w);
4287
4406
  }
4288
4407
 
4289
4408
  // src/commands/refresh.ts
@@ -4345,19 +4464,20 @@ async function scaffoldConfigItems(projectRoot, catalogRoot, items, opts = {}) {
4345
4464
  if (stat.isFile()) {
4346
4465
  const filename = path22.basename(sourcePath);
4347
4466
  const dest = path22.join(projectRoot, filename);
4348
- record(filename, await scaffoldFile(sourcePath, dest, item.id, opts));
4467
+ record(filename, await scaffoldFile(sourcePath, dest, item.id, projectRoot, opts));
4349
4468
  } else if (stat.isDirectory()) {
4350
4469
  const entries = await fs17.readdir(sourcePath);
4351
4470
  for (const entry of entries) {
4352
4471
  const src = path22.join(sourcePath, entry);
4353
4472
  const dest = path22.join(projectRoot, entry);
4354
- record(entry, await scaffoldFile(src, dest, item.id, opts));
4473
+ record(entry, await scaffoldFile(src, dest, item.id, projectRoot, opts));
4355
4474
  }
4356
4475
  }
4357
4476
  }
4358
4477
  return { scaffolded, skipped, skippedExisting };
4359
4478
  }
4360
- async function scaffoldFile(src, dest, itemId, opts) {
4479
+ async function scaffoldFile(src, dest, itemId, projectRoot, opts) {
4480
+ const destDisplay = path22.relative(projectRoot, dest) || path22.basename(dest);
4361
4481
  const srcStat = await fs17.lstat(src).catch(() => null);
4362
4482
  if (!srcStat) {
4363
4483
  warn(`Skipping ${path22.basename(src)}: source not found`);
@@ -4369,7 +4489,7 @@ async function scaffoldFile(src, dest, itemId, opts) {
4369
4489
  }
4370
4490
  const exists = await fs17.pathExists(dest);
4371
4491
  if (exists && !opts.force) {
4372
- warn(`Skipping ${path22.basename(dest)}: already exists (use --force to overwrite)`);
4492
+ warn(`Skipping ${destDisplay}: already exists \u2014 re-run with --force to overwrite it`);
4373
4493
  return "skipped-exists";
4374
4494
  }
4375
4495
  if (opts.dryRun) {
@@ -4885,6 +5005,39 @@ function auditForbiddenStacks(items) {
4885
5005
  }
4886
5006
  return failures;
4887
5007
  }
5008
+ var OPT_IN_TIERS = /* @__PURE__ */ new Set(["workflow", "ops", "review", "design"]);
5009
+ function auditOptInMetadata(items) {
5010
+ const failures = [];
5011
+ for (const item of items) {
5012
+ const hasTier = item.optInTier !== void 0;
5013
+ const hasGroup = item.optInGroup !== void 0;
5014
+ if (hasTier !== hasGroup) {
5015
+ failures.push(`${item.id}: optInTier and optInGroup must be set together`);
5016
+ }
5017
+ if (hasTier) {
5018
+ if (!OPT_IN_TIERS.has(item.optInTier)) {
5019
+ failures.push(
5020
+ `${item.id}: invalid optInTier "${item.optInTier}" (expected one of ${[...OPT_IN_TIERS].join(", ")})`
5021
+ );
5022
+ }
5023
+ if (typeof item.optInGroup !== "string" || item.optInGroup.length === 0) {
5024
+ failures.push(`${item.id}: optInGroup must be a non-empty string`);
5025
+ }
5026
+ if (item.default === true) {
5027
+ failures.push(`${item.id}: opt-in items (optInTier set) must not be default:true`);
5028
+ }
5029
+ if (typeof item.purpose !== "string" || item.purpose.length === 0) {
5030
+ failures.push(`${item.id}: opt-in items (optInTier set) must have a non-empty purpose`);
5031
+ }
5032
+ }
5033
+ const requiresAny = Array.isArray(item.requiresAny) ? item.requiresAny : [];
5034
+ const roleOnly = requiresAny.length > 0 && requiresAny.every((clause) => clause && clause.role);
5035
+ if (item.default !== true && roleOnly && !hasTier) {
5036
+ failures.push(`${item.id}: role-gated opt-in item missing optInTier/optInGroup`);
5037
+ }
5038
+ }
5039
+ return failures;
5040
+ }
4888
5041
  function auditManifestStructure(manifestVersion, items) {
4889
5042
  const failures = [];
4890
5043
  const seenIds = /* @__PURE__ */ new Map();
@@ -5058,6 +5211,7 @@ function auditMarkdownContent(manifestDir, items) {
5058
5211
  function validateCatalogData(manifestDir, manifestVersion, items) {
5059
5212
  const failures = [
5060
5213
  ...auditManifestStructure(manifestVersion, items),
5214
+ ...auditOptInMetadata(items),
5061
5215
  ...auditForbiddenStacks(items),
5062
5216
  ...auditShippedFiles(manifestDir, items),
5063
5217
  ...auditMarkdownContent(manifestDir, items),
@@ -5770,10 +5924,16 @@ function validateRuntimeNodeVersion() {
5770
5924
  validateRuntimeNodeVersion();
5771
5925
  program.name("haus").description("Haus AI workflow CLI").version(cliVersion());
5772
5926
  program.command("scan").option("--json").action(runScan);
5773
- program.command("recommend").option("--json").action(runRecommend);
5927
+ program.command("recommend").option("--json").option(
5928
+ "--include <ids...>",
5929
+ "Force opt-in catalog items into the recommendation as manual selections (space- or comma-separated ids)"
5930
+ ).action(runRecommend);
5774
5931
  program.command("setup-project").option("--json").action(runSetupProject);
5775
5932
  program.command("doctor").option("--hooks", "Verify .claude/settings.json matches the hook contract").action(runDoctor);
5776
5933
  program.command("apply").option("--dry-run").option("--write").option("--select", "Interactively select catalog items before applying").option(
5934
+ "--ids <ids...>",
5935
+ "Install exactly these recommended item ids non-interactively (skill backend; space- or comma-separated)"
5936
+ ).option(
5777
5937
  "--allow-empty-cache",
5778
5938
  "Apply core files only when catalog cache is empty (skip catalog items without error)"
5779
5939
  ).option(
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.12.2",
2
+ "version": "2.13.1",
3
3
  "items": [
4
4
  {
5
5
  "id": "haus.agentic-workflow-standard",
@@ -619,6 +619,8 @@
619
619
  "originUrl": "https://github.com/affaan-m/ECC/blob/ec92b528471df708c2384ebbcc82b390b60f535a/skills/laravel-plugin-discovery/SKILL.md",
620
620
  "pinnedRef": "ec92b528471df708c2384ebbcc82b390b60f535a",
621
621
  "ecosystem": "laravel",
622
+ "optInTier": "ops",
623
+ "optInGroup": "Laravel plugin discovery",
622
624
  "requiresAny": [
623
625
  {
624
626
  "role": "laravel-plugins"
@@ -753,6 +755,8 @@
753
755
  "originUrl": "https://github.com/affaan-m/ECC/blob/5b173d2e6c11b976a0f13b2f59125e08956c1d47/agents/performance-optimizer.md",
754
756
  "pinnedRef": "5b173d2e6c11b976a0f13b2f59125e08956c1d47",
755
757
  "ecosystem": "ecc",
758
+ "optInTier": "review",
759
+ "optInGroup": "Performance review",
756
760
  "default": false,
757
761
  "requiresAny": [
758
762
  {
@@ -986,6 +990,8 @@
986
990
  "originUrl": "https://github.com/affaan-m/ECC/blob/5b173d2e6c11b976a0f13b2f59125e08956c1d47/.kiro/agents/refactor-cleaner.md",
987
991
  "pinnedRef": "5b173d2e6c11b976a0f13b2f59125e08956c1d47",
988
992
  "ecosystem": "ecc",
993
+ "optInTier": "review",
994
+ "optInGroup": "Refactor cleanup",
989
995
  "default": false,
990
996
  "requiresAny": [
991
997
  {
@@ -1015,6 +1021,8 @@
1015
1021
  "tags": ["agent", "review", "security"],
1016
1022
  "tokenEstimate": 1369,
1017
1023
  "ecosystem": "security",
1024
+ "optInTier": "review",
1025
+ "optInGroup": "Security review",
1018
1026
  "originUrl": "https://github.com/affaan-m/ECC/blob/ec92b528471df708c2384ebbcc82b390b60f535a/agents/security-reviewer.md",
1019
1027
  "requiresAny": [
1020
1028
  {
@@ -1374,6 +1382,8 @@
1374
1382
  "originUrl": "https://github.com/yeachan-heo/oh-my-claudecode/blob/50f6ff05eb5d9ebed66f05d8c4580c0b119f37af/agents/designer.md",
1375
1383
  "pinnedRef": "50f6ff05eb5d9ebed66f05d8c4580c0b119f37af",
1376
1384
  "ecosystem": "oh-my-claudecode",
1385
+ "optInTier": "design",
1386
+ "optInGroup": "UI design",
1377
1387
  "default": false,
1378
1388
  "requiresAny": [
1379
1389
  {
@@ -1437,6 +1447,8 @@
1437
1447
  "originUrl": "https://github.com/yeachan-heo/oh-my-claudecode/blob/50f6ff05eb5d9ebed66f05d8c4580c0b119f37af/agents/tracer.md",
1438
1448
  "pinnedRef": "50f6ff05eb5d9ebed66f05d8c4580c0b119f37af",
1439
1449
  "ecosystem": "oh-my-claudecode",
1450
+ "optInTier": "review",
1451
+ "optInGroup": "Incident tracing",
1440
1452
  "default": false,
1441
1453
  "requiresAny": [
1442
1454
  {
@@ -1609,6 +1621,8 @@
1609
1621
  "originUrl": "https://github.com/redis/agent-skills/blob/5ca2e1a2d82a768221e8f71a02e3ca095a37d38e/skills/redis-observability/SKILL.md",
1610
1622
  "pinnedRef": "5ca2e1a2d82a768221e8f71a02e3ca095a37d38e",
1611
1623
  "ecosystem": "redis",
1624
+ "optInTier": "ops",
1625
+ "optInGroup": "Redis security & observability",
1612
1626
  "requiresAny": [
1613
1627
  {
1614
1628
  "role": "redis-ops"
@@ -1639,6 +1653,8 @@
1639
1653
  "originUrl": "https://github.com/redis/agent-skills/blob/5ca2e1a2d82a768221e8f71a02e3ca095a37d38e/skills/redis-security/SKILL.md",
1640
1654
  "pinnedRef": "5ca2e1a2d82a768221e8f71a02e3ca095a37d38e",
1641
1655
  "ecosystem": "redis",
1656
+ "optInTier": "ops",
1657
+ "optInGroup": "Redis security & observability",
1642
1658
  "requiresAny": [
1643
1659
  {
1644
1660
  "role": "redis-ops"
@@ -1714,7 +1730,7 @@
1714
1730
  },
1715
1731
  {
1716
1732
  "id": "haus.sentry-sentry-nextjs-sdk",
1717
- "version": "1.0.0",
1733
+ "version": "1.0.1",
1718
1734
  "source": "curated",
1719
1735
  "type": "skill",
1720
1736
  "path": "skills/sentry/sentry-nextjs-sdk",
@@ -1724,7 +1740,7 @@
1724
1740
  "whenNotToUse": "Do not use when a more specific skill or command applies.",
1725
1741
  "tags": ["workflow", "sentry", "nextjs", "observability"],
1726
1742
  "repoRoles": [],
1727
- "tokenEstimate": 4542,
1743
+ "tokenEstimate": 4544,
1728
1744
  "installMode": "copy-selected",
1729
1745
  "default": false,
1730
1746
  "reviewStatus": "approved",
@@ -1733,8 +1749,8 @@
1733
1749
  "license": "MIT",
1734
1750
  "licenseConfidence": "high",
1735
1751
  "originSourceId": "getsentry-sentry-for-ai",
1736
- "originUrl": "https://github.com/getsentry/sentry-for-ai/blob/9ab3551758913a4144517d4fafb71476a48c64f8/skills/sentry-nextjs-sdk/SKILL.md",
1737
- "pinnedRef": "9ab3551758913a4144517d4fafb71476a48c64f8",
1752
+ "originUrl": "https://github.com/getsentry/sentry-for-ai/blob/bd84576db74e3e1d13361c7eda3bb303ae017b90/skills/sentry-nextjs-sdk/SKILL.md",
1753
+ "pinnedRef": "bd84576db74e3e1d13361c7eda3bb303ae017b90",
1738
1754
  "ecosystem": "sentry",
1739
1755
  "requiresAny": [
1740
1756
  {
@@ -1747,7 +1763,7 @@
1747
1763
  },
1748
1764
  {
1749
1765
  "id": "haus.sentry-sentry-node-sdk",
1750
- "version": "1.0.0",
1766
+ "version": "1.0.1",
1751
1767
  "source": "curated",
1752
1768
  "type": "skill",
1753
1769
  "path": "skills/sentry/sentry-node-sdk",
@@ -1757,7 +1773,7 @@
1757
1773
  "whenNotToUse": "Prefer sentry-nestjs-sdk for NestJS apps using @sentry/nestjs.",
1758
1774
  "tags": ["workflow", "sentry", "backend", "observability"],
1759
1775
  "repoRoles": [],
1760
- "tokenEstimate": 8085,
1776
+ "tokenEstimate": 8087,
1761
1777
  "installMode": "copy-selected",
1762
1778
  "default": false,
1763
1779
  "reviewStatus": "approved",
@@ -1766,8 +1782,8 @@
1766
1782
  "license": "MIT",
1767
1783
  "licenseConfidence": "high",
1768
1784
  "originSourceId": "getsentry-sentry-for-ai",
1769
- "originUrl": "https://github.com/getsentry/sentry-for-ai/blob/9ab3551758913a4144517d4fafb71476a48c64f8/skills/sentry-node-sdk/SKILL.md",
1770
- "pinnedRef": "9ab3551758913a4144517d4fafb71476a48c64f8",
1785
+ "originUrl": "https://github.com/getsentry/sentry-for-ai/blob/bd84576db74e3e1d13361c7eda3bb303ae017b90/skills/sentry-node-sdk/SKILL.md",
1786
+ "pinnedRef": "bd84576db74e3e1d13361c7eda3bb303ae017b90",
1771
1787
  "ecosystem": "sentry",
1772
1788
  "requiresAny": [
1773
1789
  {
@@ -2124,6 +2140,8 @@
2124
2140
  "originSourceId": "superpowers-pcvelz",
2125
2141
  "originUrl": "https://github.com/pcvelz/superpowers/tree/bd000c6ec1df53449a74397f55305b22802b9a2b/skills/finishing-a-development-branch",
2126
2142
  "ecosystem": "superpowers",
2143
+ "optInTier": "workflow",
2144
+ "optInGroup": "Git worktrees & branch finishing",
2127
2145
  "requiresAny": [
2128
2146
  {
2129
2147
  "role": "branch-completion"
@@ -2153,6 +2171,8 @@
2153
2171
  "originSourceId": "superpowers-pcvelz",
2154
2172
  "originUrl": "https://github.com/pcvelz/superpowers/tree/bd000c6ec1df53449a74397f55305b22802b9a2b/skills/receiving-code-review",
2155
2173
  "ecosystem": "superpowers",
2174
+ "optInTier": "workflow",
2175
+ "optInGroup": "Code review workflow",
2156
2176
  "requiresAny": [
2157
2177
  {
2158
2178
  "role": "code-review"
@@ -2182,6 +2202,8 @@
2182
2202
  "originSourceId": "superpowers-pcvelz",
2183
2203
  "originUrl": "https://github.com/pcvelz/superpowers/tree/bd000c6ec1df53449a74397f55305b22802b9a2b/skills/requesting-code-review",
2184
2204
  "ecosystem": "superpowers",
2205
+ "optInTier": "workflow",
2206
+ "optInGroup": "Code review workflow",
2185
2207
  "requiresAny": [
2186
2208
  {
2187
2209
  "role": "code-review"
@@ -2211,6 +2233,8 @@
2211
2233
  "originSourceId": "superpowers-pcvelz",
2212
2234
  "originUrl": "https://github.com/pcvelz/superpowers/tree/bd000c6ec1df53449a74397f55305b22802b9a2b/skills/specifying-gates",
2213
2235
  "ecosystem": "superpowers",
2236
+ "optInTier": "workflow",
2237
+ "optInGroup": "Quality gates",
2214
2238
  "requiresAny": [
2215
2239
  {
2216
2240
  "role": "user-gate"
@@ -2240,6 +2264,8 @@
2240
2264
  "originSourceId": "superpowers-pcvelz",
2241
2265
  "originUrl": "https://github.com/pcvelz/superpowers/tree/48f5123e18a1a92dfba962da8dc2509391c821ee/skills/subagent-driven-development",
2242
2266
  "ecosystem": "superpowers",
2267
+ "optInTier": "workflow",
2268
+ "optInGroup": "Subagent-driven development",
2243
2269
  "pinnedRef": "48f5123e18a1a92dfba962da8dc2509391c821ee",
2244
2270
  "requiresAny": [
2245
2271
  {
@@ -2294,6 +2320,8 @@
2294
2320
  "originSourceId": "superpowers-pcvelz",
2295
2321
  "originUrl": "https://github.com/pcvelz/superpowers/tree/bd000c6ec1df53449a74397f55305b22802b9a2b/skills/test-driven-development",
2296
2322
  "ecosystem": "superpowers",
2323
+ "optInTier": "workflow",
2324
+ "optInGroup": "TDD workflow",
2297
2325
  "requiresAny": [
2298
2326
  {
2299
2327
  "role": "tdd-workflow"
@@ -2323,6 +2351,8 @@
2323
2351
  "originSourceId": "superpowers-pcvelz",
2324
2352
  "originUrl": "https://github.com/pcvelz/superpowers/tree/20c0b1ed71cb073c847432fe01379f48c5278a5f/skills/using-git-worktrees",
2325
2353
  "ecosystem": "superpowers",
2354
+ "optInTier": "workflow",
2355
+ "optInGroup": "Git worktrees & branch finishing",
2326
2356
  "pinnedRef": "20c0b1ed71cb073c847432fe01379f48c5278a5f",
2327
2357
  "requiresAny": [
2328
2358
  {
@@ -2427,6 +2457,8 @@
2427
2457
  "originSourceId": "superpowers-pcvelz",
2428
2458
  "originUrl": "https://github.com/pcvelz/superpowers/tree/bd000c6ec1df53449a74397f55305b22802b9a2b/skills/writing-skills",
2429
2459
  "ecosystem": "superpowers",
2460
+ "optInTier": "workflow",
2461
+ "optInGroup": "Skill authoring",
2430
2462
  "requiresAny": [
2431
2463
  {
2432
2464
  "role": "skill-authoring"
@@ -31,13 +31,37 @@ Do this in order:
31
31
  deep read discovered. You MUST run this before the next apply — `haus apply`
32
32
  does not re-calculate recommendations on its own.
33
33
 
34
- 5. **Apply the rest.** Run `haus apply --write` again. It only writes what changed,
35
- so this just adds the newly-matched helpers from step 4.
34
+ 5. **Offer optional helpers.** Read `.haus-workflow/recommendation.json` yourself
35
+ (never show the JSON). Two kinds of optional extras may be listed; offer only
36
+ the ones present, all **unchecked** by default — these are opt-in:
37
+ - **Optional skills & agents** (`optInEligible[]`): workflow/ops/review/design
38
+ helpers that don't auto-install for this stack. Group them by their
39
+ `optInGroup` label and present one `AskUserQuestion` option per group
40
+ (use each item's `purpose` and `tokenEstimate` to describe it in plain
41
+ language, e.g. "Code review workflow — request/receive structured reviews").
42
+ Collect the item ids from **all** the groups the user picked into one list
43
+ and run a **single** `haus recommend --include <id> <id> …` (one command with
44
+ every chosen id). Don't run it once per group — each run rewrites
45
+ `recommendation.json`, so repeated calls would drop the earlier includes.
46
+ - **Project config** (entries in `recommended[]` with `install: false`, e.g.
47
+ Haus ESLint / Prettier baselines): offer each as a plain choice ("Add the
48
+ Haus ESLint baseline"). On confirm, run `haus scaffold <id>`. `haus scaffold`
49
+ **preserves existing config files by default** and tells you when a file
50
+ already exists — only re-run with `haus scaffold <id> --force` if the user
51
+ explicitly chooses to **replace** their current config.
36
52
 
37
- 6. **Confirm.** End with one plain-language line, for example:
53
+ If neither list has anything, skip this step silently — don't ask an empty
54
+ question.
55
+
56
+ 6. **Apply the rest.** Run `haus apply --write` again. It only writes what changed,
57
+ so this adds the newly-matched helpers from step 4 plus any opt-ins from step 5.
58
+
59
+ 7. **Confirm.** End with one plain-language line, for example:
38
60
  "✅ Your project is configured — I wrote your project docs, added N guardrails
39
- and M coding helpers (K matched after reading your code in depth). Run
40
- `/haus-doctor` any time to re-check." Fill the numbers from the apply output.
61
+ and M coding helpers (K matched after reading your code in depth), plus the J
62
+ optional helpers you chose: <name them>. Run `/haus-doctor` any time to
63
+ re-check." Fill the numbers from the apply output, and name the opt-ins the
64
+ user picked in step 5 (omit that clause if they chose none).
41
65
 
42
66
  If anything fails, explain what happened in plain language and what you'll try
43
67
  next — don't dump raw errors.
@@ -26,6 +26,7 @@ The short legacy aliases still work but the names below are canonical.
26
26
  | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
27
  | `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
28
28
  | `project:cloneandsetup [name]` (`cloneandsetup`) | _Clone & setup procedure below_ | project | Run `project:clone`, then set up each repo for local dev — deps, databases, cross-repo links, and env — via each repo's `.haus-workflow/localdev.yml` (+ the workspace's order/links/env) |
29
+ | `project:add-skills` (`add-skills`, `opt-in`) | _Add-skills procedure below_ | project | Add optional skills, agents, or config (ESLint/Prettier) to an already-set-up project without re-running full setup |
29
30
  | `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
30
31
  | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
31
32
  | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
@@ -51,6 +52,8 @@ Options:
51
52
  (no name: clone a workspace from repos.manifest.json; with a name: find & clone one repo by name from GitHub)
52
53
  5. [project] project:cloneandsetup [name] — clone repos, then set them up for local dev
53
54
  (project:clone, then per-repo deps + databases + cross-repo links + env from localdev.yml)
55
+ 6. [project] project:add-skills — add optional skills, agents, or config to this project
56
+ (offers opt-in helpers matching your stack that aren't installed yet — no full re-setup)
54
57
  ```
55
58
 
56
59
  Map the user's selection to the command from the alias table, then continue to Step 2.
@@ -65,6 +68,8 @@ Run the mapped command via Bash. Quote the exact command you are running before
65
68
 
66
69
  **Exception — `project:cloneandsetup` (`cloneandsetup`):** clone followed by a per-repo setup pass, with confirmations. Skip to **Clone & setup (`project:cloneandsetup`)** under Step 3 and follow it.
67
70
 
71
+ **Exception — `project:add-skills` (`add-skills` / `opt-in`):** a short scan → recommend → choose → apply procedure. Skip to **Add optional skills (`project:add-skills`)** under Step 3 and follow it.
72
+
68
73
  ## Step 3 — Post-run steps
69
74
 
70
75
  After the command completes, follow the relevant post-run steps below.
@@ -82,6 +87,31 @@ After the command completes, follow the relevant post-run steps below.
82
87
 
83
88
  1. Open and follow `~/.claude/commands/haus-cloneandsetup.md` — it runs the full `project:clone` flow, then sets up each cloned repo locally: selects the node version (`nvm install` from `.nvmrc`), enables corepack, installs JS/PHP dependencies, and seeds `.env`, confirming before each phase. It does not start servers.
84
89
 
90
+ ### Add optional skills (`project:add-skills`)
91
+
92
+ For a project that's already set up but where the user wants more helpers later —
93
+ without re-running the full `project:init` flow. Never show raw JSON; offer plain
94
+ choices.
95
+
96
+ 1. Refresh the picture: run `haus scan --json` then `haus recommend` (it re-reads
97
+ `.haus-workflow/deep-context.json` if present).
98
+ 2. Read `.haus-workflow/recommendation.json` and `.haus-workflow/haus.lock.json`
99
+ yourself. Build the choices from items **not already installed** (lock ids):
100
+ - **Optional skills & agents** — `optInEligible[]`, grouped by `optInGroup`
101
+ (each carries `purpose` + `tokenEstimate` for a plain-language label).
102
+ - **Project config** — `recommended[]` entries with `install: false` (Haus
103
+ ESLint / Prettier) whose files aren't already present.
104
+ 3. If nothing is eligible, say so plainly and stop — e.g. "Everything matching
105
+ your stack is already installed" or "No optional helpers for this stack."
106
+ 4. Otherwise present a single `AskUserQuestion` (multi-select, all unchecked)
107
+ with one option per group / config item. Then:
108
+ - Skills/agents → `haus recommend --include <id> <id> …`, then
109
+ `haus apply --write`.
110
+ - Config → `haus scaffold <id>` (add `--force` only if the user chose to
111
+ replace an existing file; scaffold preserves files by default).
112
+ 5. Confirm with the names added and their combined token estimate, e.g.
113
+ "Added 2 optional helpers: Code review workflow, TDD workflow (~3k tokens)."
114
+
85
115
  ### After `haus apply --write`
86
116
 
87
117
  Verify that the root `CLAUDE.md` imports all three haus files:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {