@haus-tech/haus-workflow 0.13.0 → 0.13.2

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.13.2](https://github.com/WeAreHausTech/haus-workflow/compare/v0.13.1...v0.13.2) (2026-06-04)
4
+
5
+ ### Bug Fixes
6
+
7
+ - catalog-audit drift + scanner EMFILE (TDD) ([#65](https://github.com/WeAreHausTech/haus-workflow/issues/65)) ([815ad79](https://github.com/WeAreHausTech/haus-workflow/commit/815ad798b5c28b98f53a8b8fc62c4e8decf9350e))
8
+
9
+ ## [0.13.1](https://github.com/WeAreHausTech/haus-workflow/compare/v0.13.0...v0.13.1) (2026-06-03)
10
+
11
+ ### Bug Fixes
12
+
13
+ - **catalog:** download skill reference files into cache ([#63](https://github.com/WeAreHausTech/haus-workflow/issues/63)) ([db67180](https://github.com/WeAreHausTech/haus-workflow/commit/db671807273a45e97d5f5cd9505b43284d7b7389))
14
+
3
15
  ## [0.13.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.12.1...v0.13.0) (2026-06-03)
4
16
 
5
17
  ### Features
package/dist/cli.js CHANGED
@@ -75,6 +75,27 @@ function safeJoin(base, itemPath) {
75
75
  const resolved = path.resolve(base, itemPath);
76
76
  return resolved.startsWith(base + path.sep) || resolved === base ? resolved : null;
77
77
  }
78
+ function isExternalReference(ref) {
79
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
80
+ }
81
+ async function downloadSkillReferences(item, destDir) {
82
+ for (const ref of item.references ?? []) {
83
+ if (isExternalReference(ref)) continue;
84
+ const refDest = safeJoin(destDir, ref);
85
+ if (!refDest) {
86
+ warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
87
+ continue;
88
+ }
89
+ if (await fs.pathExists(refDest)) continue;
90
+ const text = await fetchText(`${REMOTE_BASE}/${item.path}/${ref}`);
91
+ if (text === null) {
92
+ warn(`Failed to fetch reference "${ref}" for ${item.id}`);
93
+ continue;
94
+ }
95
+ await fs.ensureDir(path.dirname(refDest));
96
+ await fs.writeFile(refDest, text, "utf8");
97
+ }
98
+ }
78
99
  async function syncRemoteCatalog() {
79
100
  const items = await fetchRemoteManifest();
80
101
  if (!items) {
@@ -108,6 +129,7 @@ async function syncRemoteCatalog() {
108
129
  }
109
130
  const dest = path.join(destDir, "SKILL.md");
110
131
  if (await fs.pathExists(dest)) {
132
+ await downloadSkillReferences(item, destDir);
111
133
  unchanged++;
112
134
  continue;
113
135
  }
@@ -120,6 +142,7 @@ async function syncRemoteCatalog() {
120
142
  }
121
143
  await fs.ensureDir(path.dirname(dest));
122
144
  await fs.writeFile(dest, text, "utf8");
145
+ await downloadSkillReferences(item, destDir);
123
146
  newItems.push(item.id);
124
147
  } else {
125
148
  const dest = safeJoin(CACHE_DIR, item.path);
@@ -219,6 +242,16 @@ async function listFiles(root, patterns) {
219
242
  function hashText(value) {
220
243
  return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
221
244
  }
245
+ async function mapWithConcurrency(items, fn, concurrency = 24) {
246
+ const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
247
+ const results = new Array(items.length);
248
+ for (let i = 0; i < items.length; i += size) {
249
+ const batch = items.slice(i, i + size);
250
+ const settled = await Promise.all(batch.map((item, j) => fn(item, i + j)));
251
+ for (let j = 0; j < settled.length; j += 1) results[i + j] = settled[j];
252
+ }
253
+ return results;
254
+ }
222
255
 
223
256
  // src/update/hash-installed.ts
224
257
  var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
@@ -1046,32 +1079,202 @@ async function loadCatalog(root) {
1046
1079
  return data?.items ?? [];
1047
1080
  }
1048
1081
 
1082
+ // library/catalog/validation-rules.json
1083
+ var validation_rules_default = {
1084
+ forbiddenTags: [
1085
+ "python",
1086
+ "django",
1087
+ "go",
1088
+ "rust",
1089
+ "java",
1090
+ "spring",
1091
+ "kotlin",
1092
+ "swift",
1093
+ "android",
1094
+ "flutter",
1095
+ "dart",
1096
+ "c++",
1097
+ "perl",
1098
+ "defi",
1099
+ "trading"
1100
+ ],
1101
+ bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
1102
+ requiredSkillSections: ["## Use when", "## Do not use when"],
1103
+ requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
1104
+ riskyInstallPatterns: [
1105
+ { source: "\\bnpx\\s+-y\\b", flags: "i" },
1106
+ { source: "\\bnpx\\s+--yes\\b", flags: "i" },
1107
+ { source: "\\byarn\\s+dlx\\b", flags: "i" },
1108
+ { source: "\\bpnpm\\s+dlx\\b", flags: "i" }
1109
+ ],
1110
+ allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
1111
+ anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
1112
+ httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
1113
+ placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
1114
+ allowedStacks: [
1115
+ "haus",
1116
+ "security",
1117
+ "quality",
1118
+ "frontend",
1119
+ "backend",
1120
+ "testing",
1121
+ "review",
1122
+ "workflow",
1123
+ "reference-pack",
1124
+ "core-skill",
1125
+ "workflow-skill",
1126
+ "stack-skill",
1127
+ "review-skill",
1128
+ "agent",
1129
+ "hook",
1130
+ "rule",
1131
+ "react",
1132
+ "typescript",
1133
+ "php",
1134
+ "csharp",
1135
+ "vendure",
1136
+ "vendure3",
1137
+ "nestjs",
1138
+ "graphql",
1139
+ "nx21",
1140
+ "turbo",
1141
+ "nextjs",
1142
+ "react19",
1143
+ "typescript5",
1144
+ "vite8",
1145
+ "tanstack-query",
1146
+ "tanstack-router",
1147
+ "radix",
1148
+ "radix-ui",
1149
+ "shadcn",
1150
+ "shadcn-ui",
1151
+ "tailwind",
1152
+ "tailwindcss",
1153
+ "scss",
1154
+ "scss-modules",
1155
+ "vue",
1156
+ "expressjs",
1157
+ "soup-base",
1158
+ "laravel",
1159
+ "laravel-nova",
1160
+ "wordpress",
1161
+ "bedrock",
1162
+ "elementor-pro",
1163
+ "acf-pro",
1164
+ "jetengine",
1165
+ "dotnet",
1166
+ "oidc",
1167
+ "azure-ad",
1168
+ "bankid",
1169
+ "myid",
1170
+ "cgi",
1171
+ "crypto",
1172
+ "collection2",
1173
+ "postgresql",
1174
+ "mariadb",
1175
+ "mssql",
1176
+ "elasticsearch",
1177
+ "yarn4",
1178
+ "pnpm89",
1179
+ "playwright",
1180
+ "testing-library",
1181
+ "phpunit",
1182
+ "storybook",
1183
+ "wisest",
1184
+ "vitest",
1185
+ "jest",
1186
+ "redis",
1187
+ "sanity",
1188
+ "strapi",
1189
+ "prisma",
1190
+ "cms",
1191
+ "database",
1192
+ "mysql",
1193
+ "saml2",
1194
+ "next-auth",
1195
+ "auth",
1196
+ "expo",
1197
+ "react-native",
1198
+ "mobile",
1199
+ "i18next",
1200
+ "i18n",
1201
+ "bullmq",
1202
+ "queue",
1203
+ "sentry",
1204
+ "observability",
1205
+ "tooling",
1206
+ "prettier",
1207
+ "eslint",
1208
+ "missing-prettier",
1209
+ "missing-eslint",
1210
+ "docker",
1211
+ "pm2",
1212
+ "deployer-php",
1213
+ "stripe",
1214
+ "qliro",
1215
+ "supabase",
1216
+ "payments"
1217
+ ],
1218
+ alwaysAllowedTags: [
1219
+ "haus",
1220
+ "security",
1221
+ "quality",
1222
+ "review",
1223
+ "workflow",
1224
+ "baseline",
1225
+ "project-instructions"
1226
+ ],
1227
+ patternTagSuffixes: ["-patterns"]
1228
+ };
1229
+
1230
+ // src/catalog/validation-rules.ts
1231
+ var toRegExp = (r) => new RegExp(r.source, r.flags);
1232
+ var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
1233
+ var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
1234
+ var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
1235
+ var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
1236
+ var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
1237
+ var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
1238
+ var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
1239
+ var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
1240
+ var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
1241
+ var ALLOWED_STACKS = validation_rules_default.allowedStacks;
1242
+ var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
1243
+ var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
1244
+ var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
1245
+ function isTagAllowed(tag) {
1246
+ const lower = tag.toLowerCase();
1247
+ if (ALLOWED_SET.has(lower)) return true;
1248
+ return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
1249
+ }
1250
+ function auditDisallowedTags(items) {
1251
+ const failures = [];
1252
+ for (const item of items) {
1253
+ if (!item.id) continue;
1254
+ for (const tag of Array.isArray(item.tags) ? item.tags : []) {
1255
+ if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
1256
+ }
1257
+ }
1258
+ return failures;
1259
+ }
1260
+
1049
1261
  // src/commands/catalog-audit.ts
1050
- var FORBIDDEN = [
1051
- "python",
1052
- "django",
1053
- "go",
1054
- "rust",
1055
- "java",
1056
- "spring",
1057
- "kotlin",
1058
- "swift",
1059
- "android",
1060
- "flutter",
1061
- "dart",
1062
- "c++",
1063
- "perl",
1064
- "defi",
1065
- "trading"
1066
- ];
1067
- async function runCatalogAudit() {
1068
- const items = await loadCatalog(process.cwd());
1262
+ function auditForbiddenTags(items) {
1069
1263
  const failures = [];
1264
+ const forbidden = new Set(FORBIDDEN_TAGS.map((w) => w.toLowerCase()));
1070
1265
  for (const item of items) {
1071
- const text = `${item.id} ${item.tags.join(" ")}`.toLowerCase();
1072
- for (const word of FORBIDDEN)
1073
- if (text.includes(word)) failures.push(`${item.id} has unsupported tag ${word}`);
1266
+ for (const tag of item.tags) {
1267
+ if (forbidden.has(tag.toLowerCase())) failures.push(`${item.id} has unsupported tag ${tag}`);
1268
+ }
1269
+ for (const token of item.id.toLowerCase().split(/[^a-z0-9+]+/)) {
1270
+ if (forbidden.has(token)) failures.push(`${item.id} has unsupported tag ${token}`);
1271
+ }
1074
1272
  }
1273
+ return failures;
1274
+ }
1275
+ async function runCatalogAudit() {
1276
+ const items = await loadCatalog(process.cwd());
1277
+ const failures = auditForbiddenTags(items);
1075
1278
  if (failures.length) {
1076
1279
  failures.forEach((f) => error(f));
1077
1280
  process.exitCode = 1;
@@ -1808,20 +2011,13 @@ async function buildContentBlob(root, files) {
1808
2011
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".php") || f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")
1809
2012
  );
1810
2013
  const slice = candidates.slice(0, 300);
1811
- const CHUNK = 24;
1812
- const parts = [];
1813
- for (let i = 0; i < slice.length; i += CHUNK) {
1814
- const batch = await Promise.all(
1815
- slice.slice(i, i + CHUNK).map(async (rel) => {
1816
- try {
1817
- return await readFile(path15.join(root, rel), "utf8");
1818
- } catch {
1819
- return "";
1820
- }
1821
- })
1822
- );
1823
- parts.push(...batch);
1824
- }
2014
+ const parts = await mapWithConcurrency(slice, async (rel) => {
2015
+ try {
2016
+ return await readFile(path15.join(root, rel), "utf8");
2017
+ } catch {
2018
+ return "";
2019
+ }
2020
+ });
1825
2021
  return parts.join("\n");
1826
2022
  }
1827
2023
  function renderSummary(context) {
@@ -1934,10 +2130,9 @@ async function scanProject(root, mode = "fast") {
1934
2130
  composer: isRecord(composer?.require) ? Object.keys(composer.require) : []
1935
2131
  };
1936
2132
  const scanHashes = Object.fromEntries(
1937
- await Promise.all(
1938
- safeFiles.map(
1939
- async (f) => [f, hashText(await readFile2(path16.join(root, f), "utf8"))]
1940
- )
2133
+ await mapWithConcurrency(
2134
+ safeFiles,
2135
+ async (f) => [f, hashText(await readFile2(path16.join(root, f), "utf8"))]
1941
2136
  )
1942
2137
  );
1943
2138
  const repoSummary = renderSummary(context);
@@ -3502,187 +3697,6 @@ async function runUpdate(options) {
3502
3697
  // src/commands/validate-catalog.ts
3503
3698
  import fs17 from "fs";
3504
3699
  import path26 from "path";
3505
-
3506
- // library/catalog/validation-rules.json
3507
- var validation_rules_default = {
3508
- forbiddenTags: [
3509
- "python",
3510
- "django",
3511
- "go",
3512
- "rust",
3513
- "java",
3514
- "spring",
3515
- "kotlin",
3516
- "swift",
3517
- "android",
3518
- "flutter",
3519
- "dart",
3520
- "c++",
3521
- "perl",
3522
- "defi",
3523
- "trading"
3524
- ],
3525
- bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
3526
- requiredSkillSections: ["## Use when", "## Do not use when"],
3527
- requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
3528
- riskyInstallPatterns: [
3529
- { source: "\\bnpx\\s+-y\\b", flags: "i" },
3530
- { source: "\\bnpx\\s+--yes\\b", flags: "i" },
3531
- { source: "\\byarn\\s+dlx\\b", flags: "i" },
3532
- { source: "\\bpnpm\\s+dlx\\b", flags: "i" }
3533
- ],
3534
- allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
3535
- anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
3536
- httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
3537
- placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
3538
- allowedStacks: [
3539
- "haus",
3540
- "security",
3541
- "quality",
3542
- "frontend",
3543
- "backend",
3544
- "testing",
3545
- "review",
3546
- "workflow",
3547
- "reference-pack",
3548
- "core-skill",
3549
- "workflow-skill",
3550
- "stack-skill",
3551
- "review-skill",
3552
- "agent",
3553
- "hook",
3554
- "rule",
3555
- "react",
3556
- "typescript",
3557
- "php",
3558
- "csharp",
3559
- "vendure",
3560
- "vendure3",
3561
- "nestjs",
3562
- "graphql",
3563
- "nx21",
3564
- "turbo",
3565
- "nextjs",
3566
- "react19",
3567
- "typescript5",
3568
- "vite8",
3569
- "tanstack-query",
3570
- "tanstack-router",
3571
- "radix",
3572
- "radix-ui",
3573
- "shadcn",
3574
- "shadcn-ui",
3575
- "tailwind",
3576
- "tailwindcss",
3577
- "scss",
3578
- "scss-modules",
3579
- "vue",
3580
- "expressjs",
3581
- "soup-base",
3582
- "laravel",
3583
- "laravel-nova",
3584
- "wordpress",
3585
- "bedrock",
3586
- "elementor-pro",
3587
- "acf-pro",
3588
- "jetengine",
3589
- "dotnet",
3590
- "oidc",
3591
- "azure-ad",
3592
- "bankid",
3593
- "myid",
3594
- "cgi",
3595
- "crypto",
3596
- "collection2",
3597
- "postgresql",
3598
- "mariadb",
3599
- "mssql",
3600
- "elasticsearch",
3601
- "yarn4",
3602
- "pnpm89",
3603
- "playwright",
3604
- "testing-library",
3605
- "phpunit",
3606
- "storybook",
3607
- "wisest",
3608
- "vitest",
3609
- "jest",
3610
- "redis",
3611
- "sanity",
3612
- "strapi",
3613
- "prisma",
3614
- "cms",
3615
- "database",
3616
- "mysql",
3617
- "saml2",
3618
- "next-auth",
3619
- "auth",
3620
- "expo",
3621
- "react-native",
3622
- "mobile",
3623
- "i18next",
3624
- "i18n",
3625
- "bullmq",
3626
- "queue",
3627
- "sentry",
3628
- "observability",
3629
- "tooling",
3630
- "prettier",
3631
- "eslint",
3632
- "missing-prettier",
3633
- "missing-eslint",
3634
- "docker",
3635
- "pm2",
3636
- "deployer-php",
3637
- "stripe",
3638
- "qliro",
3639
- "supabase",
3640
- "payments"
3641
- ],
3642
- alwaysAllowedTags: [
3643
- "haus",
3644
- "security",
3645
- "quality",
3646
- "review",
3647
- "workflow",
3648
- "baseline",
3649
- "project-instructions"
3650
- ],
3651
- patternTagSuffixes: ["-patterns"]
3652
- };
3653
-
3654
- // src/catalog/validation-rules.ts
3655
- var toRegExp = (r) => new RegExp(r.source, r.flags);
3656
- var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
3657
- var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
3658
- var REQUIRED_SKILL_SECTIONS = validation_rules_default.requiredSkillSections;
3659
- var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
3660
- var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
3661
- var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
3662
- var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
3663
- var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
3664
- var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
3665
- var ALLOWED_STACKS = validation_rules_default.allowedStacks;
3666
- var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
3667
- var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
3668
- var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
3669
- function isTagAllowed(tag) {
3670
- const lower = tag.toLowerCase();
3671
- if (ALLOWED_SET.has(lower)) return true;
3672
- return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
3673
- }
3674
- function auditDisallowedTags(items) {
3675
- const failures = [];
3676
- for (const item of items) {
3677
- if (!item.id) continue;
3678
- for (const tag of Array.isArray(item.tags) ? item.tags : []) {
3679
- if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
3680
- }
3681
- }
3682
- return failures;
3683
- }
3684
-
3685
- // src/commands/validate-catalog.ts
3686
3700
  function auditForbiddenStacks(items) {
3687
3701
  const failures = [];
3688
3702
  for (const item of items) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.4.0",
2
+ "version": "2.4.1",
3
3
  "items": [
4
4
  {
5
5
  "id": "haus.nextjs-patterns",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,8 @@
26
26
  "build": "tsup src/cli.ts --format esm --dts --clean --out-dir dist --external @inquirer/checkbox",
27
27
  "dev": "tsx src/cli.ts",
28
28
  "test": "node --import tsx --test tests/**/*.test.js",
29
+ "test:coverage": "c8 yarn test",
30
+ "coverage:check": "c8 --check-coverage yarn test",
29
31
  "lint": "eslint src scripts",
30
32
  "format:check": "prettier --check .",
31
33
  "format": "prettier --write .",
@@ -38,6 +40,7 @@
38
40
  "release:dry": "GITHUB_TOKEN=$(gh auth token) release-it --dry-run",
39
41
  "prepack": "yarn build",
40
42
  "verify": "yarn typecheck && yarn typecheck:scripts && yarn lint && yarn build && yarn test",
43
+ "verify:full": "yarn verify && yarn coverage:check",
41
44
  "postinstall": "node ./scripts/postinstall.mjs || true",
42
45
  "prepare": "lefthook install || true",
43
46
  "security:audit": "yarn npm audit -A -R --severity high --environment production"
@@ -62,6 +65,7 @@
62
65
  "@types/semver": "7.7.1",
63
66
  "@typescript-eslint/eslint-plugin": "8.59.3",
64
67
  "@typescript-eslint/parser": "8.59.3",
68
+ "c8": "11.0.0",
65
69
  "eslint": "9.39.4",
66
70
  "eslint-plugin-import": "2.32.0",
67
71
  "lefthook": "1.13.6",
@@ -0,0 +1,120 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "_note": "Policy gates test fixture. One item per gate exercised by recommend-eligibility.test.js.",
4
+ "items": [
5
+ {
6
+ "id": "test.unsupported-python",
7
+ "source": "haus",
8
+ "type": "skill",
9
+ "path": "skills/test.unsupported-python",
10
+ "title": "Test: Unsupported Python",
11
+ "purpose": "Triggers UNSUPPORTED gate (python in tags).",
12
+ "tags": ["python", "backend"],
13
+ "repoRoles": [],
14
+ "default": false,
15
+ "tokenEstimate": 100
16
+ },
17
+ {
18
+ "id": "test.curated-not-approved",
19
+ "source": "curated",
20
+ "type": "skill",
21
+ "path": "skills/test.curated-not-approved",
22
+ "title": "Test: Curated Not Approved",
23
+ "purpose": "Triggers curated-not-approved gate (reviewStatus:candidate).",
24
+ "tags": ["frontend"],
25
+ "repoRoles": [],
26
+ "reviewStatus": "candidate",
27
+ "default": false,
28
+ "tokenEstimate": 100
29
+ },
30
+ {
31
+ "id": "test.curated-risk-blocked",
32
+ "source": "curated",
33
+ "type": "skill",
34
+ "path": "skills/test.curated-risk-blocked",
35
+ "title": "Test: Curated Risk Blocked",
36
+ "purpose": "Triggers curated-risk-blocked gate (reviewStatus:approved but riskLevel:blocked).",
37
+ "tags": ["frontend"],
38
+ "repoRoles": [],
39
+ "reviewStatus": "approved",
40
+ "riskLevel": "blocked",
41
+ "default": false,
42
+ "tokenEstimate": 100
43
+ },
44
+ {
45
+ "id": "test.env-management",
46
+ "source": "haus",
47
+ "type": "skill",
48
+ "path": "skills/test.env-management",
49
+ "title": "Test: Sensitive Secrets Item",
50
+ "purpose": "Triggers sensitive-policy gate (secrets in tags, matched by SENSITIVE_ITEM_KEYWORDS).",
51
+ "tags": ["secrets", "workflow"],
52
+ "repoRoles": [],
53
+ "default": false,
54
+ "tokenEstimate": 100
55
+ },
56
+ {
57
+ "id": "test.third-party-unapproved",
58
+ "source": "third-party-plugin",
59
+ "type": "skill",
60
+ "path": "skills/test.third-party-unapproved",
61
+ "title": "Test: Third-Party Unapproved",
62
+ "purpose": "Triggers source-approval gate (non-haus, non-curated, not in sources-report.json).",
63
+ "tags": ["typescript"],
64
+ "repoRoles": [],
65
+ "default": false,
66
+ "tokenEstimate": 100
67
+ },
68
+ {
69
+ "id": "test.requires-svelte",
70
+ "source": "haus",
71
+ "type": "skill",
72
+ "path": "skills/test.requires-svelte",
73
+ "title": "Test: Requires Svelte",
74
+ "purpose": "Triggers requiresAny unsatisfied gate when svelte not in context.",
75
+ "tags": ["svelte"],
76
+ "repoRoles": [],
77
+ "requiresAny": [{ "dependency": "svelte" }],
78
+ "default": false,
79
+ "tokenEstimate": 100
80
+ },
81
+ {
82
+ "id": "test.default-baseline",
83
+ "source": "haus",
84
+ "type": "skill",
85
+ "path": "skills/test.default-baseline",
86
+ "title": "Test: Default Baseline",
87
+ "purpose": "Passes all gates because default:true. Used to verify tokenEstimate is preserved.",
88
+ "tags": ["workflow"],
89
+ "repoRoles": [],
90
+ "default": true,
91
+ "tokenEstimate": 999
92
+ },
93
+ {
94
+ "id": "test.role-matched",
95
+ "source": "haus",
96
+ "type": "skill",
97
+ "path": "skills/test.role-matched",
98
+ "title": "Test: Role Matched",
99
+ "purpose": "Recommended when context has nextjs stack. Tests positive matching path.",
100
+ "tags": ["nextjs", "frontend"],
101
+ "repoRoles": ["next-app"],
102
+ "requiresAny": [{ "stack": "nextjs" }],
103
+ "default": false,
104
+ "tokenEstimate": 100
105
+ },
106
+ {
107
+ "id": "haus.nx21-monorepo-patterns",
108
+ "source": "haus",
109
+ "type": "skill",
110
+ "path": "skills/haus.nx21-monorepo-patterns",
111
+ "title": "Haus Nx 21 Monorepo Patterns",
112
+ "purpose": "Triggers required-role-missing gate when nx-monorepo role is absent. Hardcoded check in recommend().",
113
+ "tags": ["nx", "monorepo"],
114
+ "repoRoles": ["nx-monorepo"],
115
+ "requiresAny": [{ "stack": "nx" }],
116
+ "default": false,
117
+ "tokenEstimate": 100
118
+ }
119
+ ]
120
+ }