@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 +12 -0
- package/dist/cli.js +235 -221
- package/library/catalog/manifest.json +1 -1
- package/package.json +5 -1
- package/tests/fixtures/catalog/policy-gates-manifest.json +120 -0
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
|
-
|
|
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
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
|
1938
|
-
safeFiles
|
|
1939
|
-
|
|
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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/haus-workflow",
|
|
3
|
-
"version": "0.13.
|
|
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
|
+
}
|