@haus-tech/haus-workflow 0.20.0 → 0.22.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 +21 -0
- package/README.md +9 -6
- package/dist/cli.js +654 -448
- package/library/global/commands/haus-cloneandsetup.md +51 -0
- package/library/global/skills/haus-workflow/SKILL.md +20 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,66 @@ import fs12 from "fs-extra";
|
|
|
12
12
|
|
|
13
13
|
// src/catalog/remote-catalog.ts
|
|
14
14
|
import os from "os";
|
|
15
|
+
import path2 from "path";
|
|
16
|
+
import fs2 from "fs-extra";
|
|
17
|
+
|
|
18
|
+
// src/utils/fs.ts
|
|
19
|
+
import crypto from "crypto";
|
|
15
20
|
import path from "path";
|
|
21
|
+
import fg from "fast-glob";
|
|
16
22
|
import fs from "fs-extra";
|
|
23
|
+
async function readJson(file) {
|
|
24
|
+
try {
|
|
25
|
+
return await fs.readJson(file);
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function writeJson(file, value) {
|
|
31
|
+
await fs.ensureDir(path.dirname(file));
|
|
32
|
+
await fs.writeFile(file, `${JSON.stringify(value, null, 2)}
|
|
33
|
+
`, "utf8");
|
|
34
|
+
}
|
|
35
|
+
async function pruneEmptyDir(dir) {
|
|
36
|
+
try {
|
|
37
|
+
const entries = await fs.readdir(dir);
|
|
38
|
+
if (entries.length === 0) await fs.remove(dir);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function readText(file) {
|
|
43
|
+
try {
|
|
44
|
+
return await fs.readFile(file, "utf8");
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function writeText(file, value) {
|
|
50
|
+
await fs.ensureDir(path.dirname(file));
|
|
51
|
+
await fs.writeFile(file, value, "utf8");
|
|
52
|
+
}
|
|
53
|
+
async function listFiles(root, patterns) {
|
|
54
|
+
const files = await fg(patterns, {
|
|
55
|
+
cwd: root,
|
|
56
|
+
dot: true,
|
|
57
|
+
onlyFiles: true,
|
|
58
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
59
|
+
});
|
|
60
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
}
|
|
62
|
+
function hashText(value) {
|
|
63
|
+
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
64
|
+
}
|
|
65
|
+
async function mapWithConcurrency(items, fn, concurrency = 24) {
|
|
66
|
+
const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
|
|
67
|
+
const results = new Array(items.length);
|
|
68
|
+
for (let i = 0; i < items.length; i += size) {
|
|
69
|
+
const batch = items.slice(i, i + size);
|
|
70
|
+
const settled = await Promise.all(batch.map((item, j) => fn(item, i + j)));
|
|
71
|
+
for (let j = 0; j < settled.length; j += 1) results[i + j] = settled[j];
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
17
75
|
|
|
18
76
|
// src/utils/logger.ts
|
|
19
77
|
var log = (msg, ...args) => {
|
|
@@ -28,15 +86,362 @@ var error = (msg, ...args) => {
|
|
|
28
86
|
|
|
29
87
|
// src/catalog/constants.ts
|
|
30
88
|
var CATALOG_REPO_URL = "https://raw.githubusercontent.com/wearehaustech/haus-workflow-catalog";
|
|
31
|
-
var CATALOG_REF = process.env.HAUS_CATALOG_REF
|
|
89
|
+
var CATALOG_REF = process.env.HAUS_CATALOG_REF;
|
|
32
90
|
var CATALOG_CACHE_SUBDIR = ".claude/haus/catalog-cache";
|
|
33
91
|
|
|
92
|
+
// library/catalog/validation-rules.json
|
|
93
|
+
var validation_rules_default = {
|
|
94
|
+
forbiddenTags: [
|
|
95
|
+
"python",
|
|
96
|
+
"django",
|
|
97
|
+
"go",
|
|
98
|
+
"rust",
|
|
99
|
+
"java",
|
|
100
|
+
"spring",
|
|
101
|
+
"kotlin",
|
|
102
|
+
"swift",
|
|
103
|
+
"android",
|
|
104
|
+
"flutter",
|
|
105
|
+
"dart",
|
|
106
|
+
"c++",
|
|
107
|
+
"perl",
|
|
108
|
+
"defi",
|
|
109
|
+
"trading"
|
|
110
|
+
],
|
|
111
|
+
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
112
|
+
requiredSkillFrontmatter: ["description"],
|
|
113
|
+
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
114
|
+
riskyInstallPatterns: [
|
|
115
|
+
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
116
|
+
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
117
|
+
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
118
|
+
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
119
|
+
],
|
|
120
|
+
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
121
|
+
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
122
|
+
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
123
|
+
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
124
|
+
allowedStacks: [
|
|
125
|
+
"haus",
|
|
126
|
+
"security",
|
|
127
|
+
"quality",
|
|
128
|
+
"frontend",
|
|
129
|
+
"backend",
|
|
130
|
+
"testing",
|
|
131
|
+
"review",
|
|
132
|
+
"workflow",
|
|
133
|
+
"reference-pack",
|
|
134
|
+
"core-skill",
|
|
135
|
+
"workflow-skill",
|
|
136
|
+
"stack-skill",
|
|
137
|
+
"review-skill",
|
|
138
|
+
"agent",
|
|
139
|
+
"hook",
|
|
140
|
+
"rule",
|
|
141
|
+
"react",
|
|
142
|
+
"typescript",
|
|
143
|
+
"php",
|
|
144
|
+
"csharp",
|
|
145
|
+
"vendure",
|
|
146
|
+
"vendure3",
|
|
147
|
+
"nestjs",
|
|
148
|
+
"graphql",
|
|
149
|
+
"nx21",
|
|
150
|
+
"turbo",
|
|
151
|
+
"nextjs",
|
|
152
|
+
"react19",
|
|
153
|
+
"typescript5",
|
|
154
|
+
"vite8",
|
|
155
|
+
"tanstack-query",
|
|
156
|
+
"tanstack-router",
|
|
157
|
+
"radix",
|
|
158
|
+
"radix-ui",
|
|
159
|
+
"shadcn",
|
|
160
|
+
"shadcn-ui",
|
|
161
|
+
"tailwind",
|
|
162
|
+
"tailwindcss",
|
|
163
|
+
"scss",
|
|
164
|
+
"scss-modules",
|
|
165
|
+
"vue",
|
|
166
|
+
"expressjs",
|
|
167
|
+
"soup-base",
|
|
168
|
+
"laravel",
|
|
169
|
+
"laravel-nova",
|
|
170
|
+
"wordpress",
|
|
171
|
+
"bedrock",
|
|
172
|
+
"elementor-pro",
|
|
173
|
+
"acf-pro",
|
|
174
|
+
"jetengine",
|
|
175
|
+
"dotnet",
|
|
176
|
+
"oidc",
|
|
177
|
+
"azure-ad",
|
|
178
|
+
"bankid",
|
|
179
|
+
"myid",
|
|
180
|
+
"cgi",
|
|
181
|
+
"crypto",
|
|
182
|
+
"collection2",
|
|
183
|
+
"postgresql",
|
|
184
|
+
"mariadb",
|
|
185
|
+
"mssql",
|
|
186
|
+
"elasticsearch",
|
|
187
|
+
"yarn4",
|
|
188
|
+
"pnpm89",
|
|
189
|
+
"playwright",
|
|
190
|
+
"testing-library",
|
|
191
|
+
"phpunit",
|
|
192
|
+
"storybook",
|
|
193
|
+
"wisest",
|
|
194
|
+
"vitest",
|
|
195
|
+
"jest",
|
|
196
|
+
"redis",
|
|
197
|
+
"sanity",
|
|
198
|
+
"strapi",
|
|
199
|
+
"prisma",
|
|
200
|
+
"cms",
|
|
201
|
+
"database",
|
|
202
|
+
"mysql",
|
|
203
|
+
"saml2",
|
|
204
|
+
"next-auth",
|
|
205
|
+
"auth",
|
|
206
|
+
"expo",
|
|
207
|
+
"react-native",
|
|
208
|
+
"mobile",
|
|
209
|
+
"i18next",
|
|
210
|
+
"i18n",
|
|
211
|
+
"bullmq",
|
|
212
|
+
"queue",
|
|
213
|
+
"sentry",
|
|
214
|
+
"observability",
|
|
215
|
+
"tooling",
|
|
216
|
+
"prettier",
|
|
217
|
+
"eslint",
|
|
218
|
+
"missing-prettier",
|
|
219
|
+
"missing-eslint",
|
|
220
|
+
"docker",
|
|
221
|
+
"pm2",
|
|
222
|
+
"deployer-php",
|
|
223
|
+
"stripe",
|
|
224
|
+
"qliro",
|
|
225
|
+
"supabase",
|
|
226
|
+
"payments"
|
|
227
|
+
],
|
|
228
|
+
alwaysAllowedTags: [
|
|
229
|
+
"haus",
|
|
230
|
+
"security",
|
|
231
|
+
"quality",
|
|
232
|
+
"review",
|
|
233
|
+
"workflow",
|
|
234
|
+
"baseline",
|
|
235
|
+
"project-instructions"
|
|
236
|
+
],
|
|
237
|
+
patternTagSuffixes: ["-patterns"]
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/catalog/validation-rules.ts
|
|
241
|
+
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
242
|
+
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
243
|
+
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
244
|
+
var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
|
|
245
|
+
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
246
|
+
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
247
|
+
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
248
|
+
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
249
|
+
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
250
|
+
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
251
|
+
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
252
|
+
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
253
|
+
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
254
|
+
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
255
|
+
function isTagAllowed(tag) {
|
|
256
|
+
const lower = tag.toLowerCase();
|
|
257
|
+
if (ALLOWED_SET.has(lower)) return true;
|
|
258
|
+
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
259
|
+
}
|
|
260
|
+
function auditDisallowedTags(items) {
|
|
261
|
+
const failures = [];
|
|
262
|
+
for (const item of items) {
|
|
263
|
+
if (!item.id) continue;
|
|
264
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
265
|
+
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return failures;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/catalog/forbidden-content.ts
|
|
272
|
+
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
273
|
+
function escapeRegExp(value) {
|
|
274
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
275
|
+
}
|
|
276
|
+
function extractUseWhenSection(text) {
|
|
277
|
+
const marker = "## Use when";
|
|
278
|
+
const idx = text.toLowerCase().indexOf(marker.toLowerCase());
|
|
279
|
+
if (idx < 0) return "";
|
|
280
|
+
const tail = text.slice(idx + marker.length);
|
|
281
|
+
const next = tail.search(/\n##\s+/);
|
|
282
|
+
return next < 0 ? tail : tail.slice(0, next);
|
|
283
|
+
}
|
|
284
|
+
function isYamlBlockScalarHeader(rest) {
|
|
285
|
+
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
286
|
+
}
|
|
287
|
+
function extractFrontmatterValue(text, key) {
|
|
288
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
289
|
+
if (!m) return "";
|
|
290
|
+
const lines = m[1].split(/\r?\n/);
|
|
291
|
+
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
292
|
+
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
293
|
+
if (idx < 0) return "";
|
|
294
|
+
const rest = lines[idx].replace(keyRe, "").trim();
|
|
295
|
+
if (!rest) return "";
|
|
296
|
+
if (!isYamlBlockScalarHeader(rest)) {
|
|
297
|
+
return rest.replace(/^["']|["']$/g, "").trim();
|
|
298
|
+
}
|
|
299
|
+
const body = [];
|
|
300
|
+
for (let j = idx + 1; j < lines.length; j++) {
|
|
301
|
+
const line2 = lines[j];
|
|
302
|
+
if (/^[a-zA-Z_][\w.-]*:[ \t]/.test(line2)) break;
|
|
303
|
+
if (line2.trim() === "") continue;
|
|
304
|
+
if (/^\s+/.test(line2)) body.push(line2.trimStart());
|
|
305
|
+
else break;
|
|
306
|
+
}
|
|
307
|
+
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
308
|
+
}
|
|
309
|
+
function extractFrontmatterDescription(text) {
|
|
310
|
+
return extractFrontmatterValue(text, "description");
|
|
311
|
+
}
|
|
312
|
+
function auditForbiddenTagsInText(text, label) {
|
|
313
|
+
const body = `${extractFrontmatterDescription(text)}
|
|
314
|
+
${extractUseWhenSection(text)}`;
|
|
315
|
+
if (!body.trim()) return [];
|
|
316
|
+
const failures = [];
|
|
317
|
+
for (const word of PROSE_FORBIDDEN_TAGS) {
|
|
318
|
+
const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "i");
|
|
319
|
+
if (re.test(body)) failures.push(`${label}: forbidden stack/tag "${word}" in content`);
|
|
320
|
+
}
|
|
321
|
+
return failures;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/catalog/ingest-catalog.ts
|
|
325
|
+
function validateCatalogItem(item, content2) {
|
|
326
|
+
const label = item.id;
|
|
327
|
+
const lines = content2.split(/\r?\n/);
|
|
328
|
+
for (let i = 0; i < lines.length; i++) {
|
|
329
|
+
const line2 = lines[i] ?? "";
|
|
330
|
+
if (RISKY_INSTALL_PATTERNS.some((re) => re.test(line2))) {
|
|
331
|
+
return { ok: false, reason: `${label}: risky install pattern at line ${i + 1}` };
|
|
332
|
+
}
|
|
333
|
+
if (ANY_NPX_PATTERN.test(line2) && !ALLOWED_NPX_PATTERN.test(line2)) {
|
|
334
|
+
return { ok: false, reason: `${label}: disallowed npx at line ${i + 1}` };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const lower = content2.toLowerCase();
|
|
338
|
+
for (const phrase of BANNED_AGENT_PHRASES) {
|
|
339
|
+
if (lower.includes(phrase)) {
|
|
340
|
+
return { ok: false, reason: `${label}: banned phrase "${phrase}"` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const tagFailures = auditForbiddenTagsInText(content2, label);
|
|
344
|
+
if (tagFailures.length > 0) {
|
|
345
|
+
return { ok: false, reason: tagFailures[0] };
|
|
346
|
+
}
|
|
347
|
+
return { ok: true };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/catalog/manifest-schema.ts
|
|
351
|
+
var POLLUTION_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
352
|
+
function isNonEmptyString(value) {
|
|
353
|
+
return typeof value === "string" && value.length > 0;
|
|
354
|
+
}
|
|
355
|
+
function safeParse(json) {
|
|
356
|
+
return JSON.parse(json, (key, value) => {
|
|
357
|
+
if (POLLUTION_KEYS.has(key)) return void 0;
|
|
358
|
+
return value;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function parseManifest(json) {
|
|
362
|
+
let data;
|
|
363
|
+
try {
|
|
364
|
+
data = safeParse(json);
|
|
365
|
+
} catch {
|
|
366
|
+
return { ok: false, error: "invalid JSON" };
|
|
367
|
+
}
|
|
368
|
+
if (!data || typeof data !== "object") {
|
|
369
|
+
return { ok: false, error: "manifest must be an object" };
|
|
370
|
+
}
|
|
371
|
+
const root = data;
|
|
372
|
+
if (!isNonEmptyString(root.version)) {
|
|
373
|
+
return { ok: false, error: "missing version" };
|
|
374
|
+
}
|
|
375
|
+
if (!Array.isArray(root.items)) {
|
|
376
|
+
return { ok: false, error: "missing items array" };
|
|
377
|
+
}
|
|
378
|
+
const items = [];
|
|
379
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
380
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
381
|
+
for (let i = 0; i < root.items.length; i++) {
|
|
382
|
+
const raw = root.items[i];
|
|
383
|
+
if (!raw || typeof raw !== "object") {
|
|
384
|
+
return { ok: false, error: `item[${i}]: invalid entry` };
|
|
385
|
+
}
|
|
386
|
+
const item = raw;
|
|
387
|
+
if (!isNonEmptyString(item.id)) {
|
|
388
|
+
return { ok: false, error: `item[${i}]: missing id` };
|
|
389
|
+
}
|
|
390
|
+
if (!isNonEmptyString(item.type)) {
|
|
391
|
+
return { ok: false, error: `${item.id}: missing type` };
|
|
392
|
+
}
|
|
393
|
+
if (!isNonEmptyString(item.path)) {
|
|
394
|
+
return { ok: false, error: `${item.id}: missing path` };
|
|
395
|
+
}
|
|
396
|
+
if (seenIds.has(item.id)) {
|
|
397
|
+
return { ok: false, error: `${item.id}: duplicate id` };
|
|
398
|
+
}
|
|
399
|
+
seenIds.add(item.id);
|
|
400
|
+
const normPath = item.path.replace(/\\/g, "/");
|
|
401
|
+
if (seenPaths.has(normPath)) {
|
|
402
|
+
return { ok: false, error: `${item.id}: duplicate path "${normPath}"` };
|
|
403
|
+
}
|
|
404
|
+
seenPaths.add(normPath);
|
|
405
|
+
if (item.source === "curated") {
|
|
406
|
+
if (!isNonEmptyString(item.reviewStatus)) {
|
|
407
|
+
return { ok: false, error: `${item.id}: curated item missing reviewStatus` };
|
|
408
|
+
}
|
|
409
|
+
if (!isNonEmptyString(item.riskLevel)) {
|
|
410
|
+
return { ok: false, error: `${item.id}: curated item missing riskLevel` };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
items.push(raw);
|
|
414
|
+
}
|
|
415
|
+
return { ok: true, manifest: { version: root.version, items } };
|
|
416
|
+
}
|
|
417
|
+
|
|
34
418
|
// src/catalog/remote-catalog.ts
|
|
35
419
|
function getCacheDir() {
|
|
36
|
-
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ??
|
|
420
|
+
return process.env["HAUS_CATALOG_CACHE_DIR_OVERRIDE"] ?? path2.join(os.homedir(), CATALOG_CACHE_SUBDIR);
|
|
421
|
+
}
|
|
422
|
+
var cachedCatalogRef;
|
|
423
|
+
function getResolvedCatalogRef() {
|
|
424
|
+
return cachedCatalogRef ?? process.env["HAUS_CATALOG_REF"] ?? "main";
|
|
425
|
+
}
|
|
426
|
+
function isCatalogRefResolved() {
|
|
427
|
+
return cachedCatalogRef !== void 0 || process.env["HAUS_CATALOG_REF"] !== void 0;
|
|
428
|
+
}
|
|
429
|
+
async function resolveCatalogRef(opts) {
|
|
430
|
+
const env = opts?.env ?? process.env;
|
|
431
|
+
if (env["HAUS_CATALOG_REF"]) return env["HAUS_CATALOG_REF"];
|
|
432
|
+
const fetchLatest = opts?.fetchLatestTag ?? fetchLatestCatalogTag;
|
|
433
|
+
const tag = await fetchLatest();
|
|
434
|
+
return tag ?? "main";
|
|
435
|
+
}
|
|
436
|
+
async function remoteBase() {
|
|
437
|
+
if (process.env["HAUS_CATALOG_REMOTE_BASE"]) {
|
|
438
|
+
return process.env["HAUS_CATALOG_REMOTE_BASE"];
|
|
439
|
+
}
|
|
440
|
+
if (cachedCatalogRef === void 0) {
|
|
441
|
+
cachedCatalogRef = await resolveCatalogRef();
|
|
442
|
+
}
|
|
443
|
+
return `${CATALOG_REPO_URL}/${cachedCatalogRef}`;
|
|
37
444
|
}
|
|
38
|
-
var REMOTE_BASE = process.env["HAUS_CATALOG_REMOTE_BASE"] ?? `${CATALOG_REPO_URL}/${CATALOG_REF}`;
|
|
39
|
-
var REMOTE_MANIFEST_URL = `${REMOTE_BASE}/manifest.json`;
|
|
40
445
|
async function fetchText(url) {
|
|
41
446
|
try {
|
|
42
447
|
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
@@ -47,55 +452,59 @@ async function fetchText(url) {
|
|
|
47
452
|
}
|
|
48
453
|
}
|
|
49
454
|
async function fetchRemoteManifest() {
|
|
50
|
-
const
|
|
455
|
+
const base = await remoteBase();
|
|
456
|
+
const text = await fetchText(`${base}/manifest.json`);
|
|
51
457
|
if (!text) return null;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} catch {
|
|
458
|
+
const parsed = parseManifest(text);
|
|
459
|
+
if (!parsed.ok) {
|
|
460
|
+
warn(`Remote manifest failed schema validation: ${parsed.error}`);
|
|
56
461
|
return null;
|
|
57
462
|
}
|
|
463
|
+
if (!parsed.manifest.items.length) return null;
|
|
464
|
+
return parsed.manifest;
|
|
58
465
|
}
|
|
59
466
|
async function writeTextIfChanged(dest, text) {
|
|
60
|
-
if (await
|
|
61
|
-
const local = await
|
|
467
|
+
if (await fs2.pathExists(dest)) {
|
|
468
|
+
const local = await fs2.readFile(dest, "utf8");
|
|
62
469
|
if (local === text) return "unchanged";
|
|
63
|
-
await
|
|
470
|
+
await fs2.writeFile(dest, text, "utf8");
|
|
64
471
|
return "updated";
|
|
65
472
|
}
|
|
66
|
-
await
|
|
67
|
-
await
|
|
473
|
+
await fs2.ensureDir(path2.dirname(dest));
|
|
474
|
+
await fs2.writeFile(dest, text, "utf8");
|
|
68
475
|
return "created";
|
|
69
476
|
}
|
|
70
477
|
var WORKFLOW_TEMPLATE_REL = "templates/agentic-workflow-standard.md";
|
|
71
478
|
async function readWorkflowTemplate(opts = {}) {
|
|
72
|
-
const dest =
|
|
73
|
-
const
|
|
479
|
+
const dest = path2.join(getCacheDir(), WORKFLOW_TEMPLATE_REL);
|
|
480
|
+
const base = await remoteBase();
|
|
481
|
+
const text = await fetchText(`${base}/${WORKFLOW_TEMPLATE_REL}`);
|
|
74
482
|
if (text === null) {
|
|
75
|
-
if (await
|
|
483
|
+
if (await fs2.pathExists(dest)) return fs2.readFile(dest, "utf8");
|
|
76
484
|
return null;
|
|
77
485
|
}
|
|
78
486
|
if (!opts.dryRun) {
|
|
79
487
|
await writeTextIfChanged(dest, text);
|
|
80
|
-
} else if (await
|
|
81
|
-
return
|
|
488
|
+
} else if (await fs2.pathExists(dest)) {
|
|
489
|
+
return fs2.readFile(dest, "utf8");
|
|
82
490
|
}
|
|
83
491
|
return text;
|
|
84
492
|
}
|
|
85
493
|
function isSafeCatalogPath(itemPath) {
|
|
86
|
-
if (!itemPath ||
|
|
87
|
-
const normalized =
|
|
494
|
+
if (!itemPath || path2.isAbsolute(itemPath) || itemPath.includes("\\")) return false;
|
|
495
|
+
const normalized = path2.normalize(itemPath);
|
|
88
496
|
return !normalized.startsWith("..") && !normalized.includes("/..");
|
|
89
497
|
}
|
|
90
498
|
function safeJoin(base, itemPath) {
|
|
91
499
|
if (!isSafeCatalogPath(itemPath)) return null;
|
|
92
|
-
const resolved =
|
|
93
|
-
return resolved.startsWith(base +
|
|
500
|
+
const resolved = path2.resolve(base, itemPath);
|
|
501
|
+
return resolved.startsWith(base + path2.sep) || resolved === base ? resolved : null;
|
|
94
502
|
}
|
|
503
|
+
var KNOWN_ITEM_TYPES = /* @__PURE__ */ new Set(["skill", "agent", "template", "command"]);
|
|
95
504
|
function isExternalReference(ref) {
|
|
96
505
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(ref);
|
|
97
506
|
}
|
|
98
|
-
async function downloadSkillReferences(item, destDir) {
|
|
507
|
+
async function downloadSkillReferences(item, destDir, base) {
|
|
99
508
|
for (const ref of item.references ?? []) {
|
|
100
509
|
if (isExternalReference(ref)) continue;
|
|
101
510
|
const refDest = safeJoin(destDir, ref);
|
|
@@ -103,7 +512,7 @@ async function downloadSkillReferences(item, destDir) {
|
|
|
103
512
|
warn(`Skipping reference "${ref}" for ${item.id}: path traversal detected`);
|
|
104
513
|
continue;
|
|
105
514
|
}
|
|
106
|
-
const text = await fetchText(`${
|
|
515
|
+
const text = await fetchText(`${base}/${item.path}/${ref}`);
|
|
107
516
|
if (text === null) {
|
|
108
517
|
warn(`Failed to fetch reference "${ref}" for ${item.id}`);
|
|
109
518
|
continue;
|
|
@@ -111,18 +520,79 @@ async function downloadSkillReferences(item, destDir) {
|
|
|
111
520
|
await writeTextIfChanged(refDest, text);
|
|
112
521
|
}
|
|
113
522
|
}
|
|
523
|
+
async function syncOneItem(item, base) {
|
|
524
|
+
if (!KNOWN_ITEM_TYPES.has(item.type)) {
|
|
525
|
+
warn(
|
|
526
|
+
`Skipping ${item.id}: type "${item.type}" is unknown to this haus version \u2014 upgrade to use it`
|
|
527
|
+
);
|
|
528
|
+
return "failed";
|
|
529
|
+
}
|
|
530
|
+
if (!item.path) return "failed";
|
|
531
|
+
if (!isSafeCatalogPath(item.path)) {
|
|
532
|
+
warn(`Skipping ${item.id}: invalid path "${item.path}"`);
|
|
533
|
+
return "failed";
|
|
534
|
+
}
|
|
535
|
+
if (item.type === "skill") {
|
|
536
|
+
const destDir = safeJoin(getCacheDir(), item.path);
|
|
537
|
+
if (!destDir) {
|
|
538
|
+
warn(`Skipping ${item.id}: path traversal detected`);
|
|
539
|
+
return "failed";
|
|
540
|
+
}
|
|
541
|
+
const dest2 = path2.join(destDir, "SKILL.md");
|
|
542
|
+
const text2 = await fetchText(`${base}/${item.path}/SKILL.md`);
|
|
543
|
+
if (!text2) {
|
|
544
|
+
warn(`Failed to fetch content for ${item.id}`);
|
|
545
|
+
return "failed";
|
|
546
|
+
}
|
|
547
|
+
const verdict2 = validateCatalogItem(item, text2);
|
|
548
|
+
if (!verdict2.ok) {
|
|
549
|
+
warn(`Rejected ${item.id} at ingest: ${verdict2.reason}`);
|
|
550
|
+
return "failed";
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const outcome = await writeTextIfChanged(dest2, text2);
|
|
554
|
+
await downloadSkillReferences(item, destDir, base);
|
|
555
|
+
return outcome;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
558
|
+
return "failed";
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const dest = safeJoin(getCacheDir(), item.path);
|
|
562
|
+
if (!dest) {
|
|
563
|
+
warn(`Skipping ${item.id}: path traversal detected`);
|
|
564
|
+
return "failed";
|
|
565
|
+
}
|
|
566
|
+
const text = await fetchText(`${base}/${item.path}`);
|
|
567
|
+
if (!text) {
|
|
568
|
+
warn(`Failed to fetch content for ${item.id}`);
|
|
569
|
+
return "failed";
|
|
570
|
+
}
|
|
571
|
+
const verdict = validateCatalogItem(item, text);
|
|
572
|
+
if (!verdict.ok) {
|
|
573
|
+
warn(`Rejected ${item.id} at ingest: ${verdict.reason}`);
|
|
574
|
+
return "failed";
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
return await writeTextIfChanged(dest, text);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
580
|
+
return "failed";
|
|
581
|
+
}
|
|
582
|
+
}
|
|
114
583
|
async function syncRemoteCatalog() {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
584
|
+
const manifest = await fetchRemoteManifest();
|
|
585
|
+
if (!manifest) {
|
|
117
586
|
warn("Remote catalog fetch failed \u2014 using bundled catalog");
|
|
118
587
|
return { newItems: [], refreshed: [], unchanged: 0, failed: [] };
|
|
119
588
|
}
|
|
589
|
+
const { version, items } = manifest;
|
|
120
590
|
const cacheDir = getCacheDir();
|
|
121
591
|
try {
|
|
122
|
-
await
|
|
123
|
-
await
|
|
124
|
-
|
|
125
|
-
`${JSON.stringify({ items }, null, 2)}
|
|
592
|
+
await fs2.ensureDir(cacheDir);
|
|
593
|
+
await fs2.writeFile(
|
|
594
|
+
path2.join(cacheDir, "manifest.json"),
|
|
595
|
+
`${JSON.stringify({ version, items }, null, 2)}
|
|
126
596
|
`,
|
|
127
597
|
"utf8"
|
|
128
598
|
);
|
|
@@ -136,63 +606,15 @@ async function syncRemoteCatalog() {
|
|
|
136
606
|
const refreshed = [];
|
|
137
607
|
let unchanged = 0;
|
|
138
608
|
const failed = [];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
const destDir = safeJoin(getCacheDir(), item.path);
|
|
149
|
-
if (!destDir) {
|
|
150
|
-
warn(`Skipping ${item.id}: path traversal detected`);
|
|
151
|
-
failed.push(item.id);
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const dest = path.join(destDir, "SKILL.md");
|
|
155
|
-
const url = `${REMOTE_BASE}/${item.path}/SKILL.md`;
|
|
156
|
-
const text = await fetchText(url);
|
|
157
|
-
if (!text) {
|
|
158
|
-
warn(`Failed to fetch content for ${item.id}`);
|
|
159
|
-
failed.push(item.id);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
const outcome = await writeTextIfChanged(dest, text);
|
|
164
|
-
await downloadSkillReferences(item, destDir);
|
|
165
|
-
if (outcome === "created") newItems.push(item.id);
|
|
166
|
-
else if (outcome === "updated") refreshed.push(item.id);
|
|
167
|
-
else unchanged++;
|
|
168
|
-
} catch (err) {
|
|
169
|
-
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
-
failed.push(item.id);
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
const dest = safeJoin(getCacheDir(), item.path);
|
|
174
|
-
if (!dest) {
|
|
175
|
-
warn(`Skipping ${item.id}: path traversal detected`);
|
|
176
|
-
failed.push(item.id);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const url = `${REMOTE_BASE}/${item.path}`;
|
|
180
|
-
const text = await fetchText(url);
|
|
181
|
-
if (!text) {
|
|
182
|
-
warn(`Failed to fetch content for ${item.id}`);
|
|
183
|
-
failed.push(item.id);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
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++;
|
|
191
|
-
} catch (err) {
|
|
192
|
-
warn(`Failed to cache ${item.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
-
failed.push(item.id);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
609
|
+
const base = await remoteBase();
|
|
610
|
+
const outcomes = await mapWithConcurrency(items, (item) => syncOneItem(item, base), 8);
|
|
611
|
+
for (let i = 0; i < items.length; i++) {
|
|
612
|
+
const item = items[i];
|
|
613
|
+
const outcome = outcomes[i];
|
|
614
|
+
if (outcome === "created") newItems.push(item.id);
|
|
615
|
+
else if (outcome === "updated") refreshed.push(item.id);
|
|
616
|
+
else if (outcome === "unchanged") unchanged++;
|
|
617
|
+
else if (outcome === "failed") failed.push(item.id);
|
|
196
618
|
}
|
|
197
619
|
return { newItems, refreshed, unchanged, failed };
|
|
198
620
|
}
|
|
@@ -213,7 +635,7 @@ async function fetchLatestCatalogTag() {
|
|
|
213
635
|
}
|
|
214
636
|
async function getCacheManifestAge() {
|
|
215
637
|
try {
|
|
216
|
-
const stat = await
|
|
638
|
+
const stat = await fs2.stat(path2.join(getCacheDir(), "manifest.json"));
|
|
217
639
|
return Date.now() - stat.mtimeMs;
|
|
218
640
|
} catch {
|
|
219
641
|
return null;
|
|
@@ -237,64 +659,6 @@ function buildAllowRules() {
|
|
|
237
659
|
import path4 from "path";
|
|
238
660
|
import fs3 from "fs-extra";
|
|
239
661
|
|
|
240
|
-
// src/utils/fs.ts
|
|
241
|
-
import crypto from "crypto";
|
|
242
|
-
import path2 from "path";
|
|
243
|
-
import fg from "fast-glob";
|
|
244
|
-
import fs2 from "fs-extra";
|
|
245
|
-
async function readJson(file) {
|
|
246
|
-
try {
|
|
247
|
-
return await fs2.readJson(file);
|
|
248
|
-
} catch {
|
|
249
|
-
return void 0;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async function writeJson(file, value) {
|
|
253
|
-
await fs2.ensureDir(path2.dirname(file));
|
|
254
|
-
await fs2.writeFile(file, `${JSON.stringify(value, null, 2)}
|
|
255
|
-
`, "utf8");
|
|
256
|
-
}
|
|
257
|
-
async function pruneEmptyDir(dir) {
|
|
258
|
-
try {
|
|
259
|
-
const entries = await fs2.readdir(dir);
|
|
260
|
-
if (entries.length === 0) await fs2.remove(dir);
|
|
261
|
-
} catch {
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
async function readText(file) {
|
|
265
|
-
try {
|
|
266
|
-
return await fs2.readFile(file, "utf8");
|
|
267
|
-
} catch {
|
|
268
|
-
return void 0;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function writeText(file, value) {
|
|
272
|
-
await fs2.ensureDir(path2.dirname(file));
|
|
273
|
-
await fs2.writeFile(file, value, "utf8");
|
|
274
|
-
}
|
|
275
|
-
async function listFiles(root, patterns) {
|
|
276
|
-
const files = await fg(patterns, {
|
|
277
|
-
cwd: root,
|
|
278
|
-
dot: true,
|
|
279
|
-
onlyFiles: true,
|
|
280
|
-
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
|
|
281
|
-
});
|
|
282
|
-
return files.sort((a, b) => a.localeCompare(b));
|
|
283
|
-
}
|
|
284
|
-
function hashText(value) {
|
|
285
|
-
return `sha256-${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
286
|
-
}
|
|
287
|
-
async function mapWithConcurrency(items, fn, concurrency = 24) {
|
|
288
|
-
const size = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 24;
|
|
289
|
-
const results = new Array(items.length);
|
|
290
|
-
for (let i = 0; i < items.length; i += size) {
|
|
291
|
-
const batch = items.slice(i, i + size);
|
|
292
|
-
const settled = await Promise.all(batch.map((item, j) => fn(item, i + j)));
|
|
293
|
-
for (let j = 0; j < settled.length; j += 1) results[i + j] = settled[j];
|
|
294
|
-
}
|
|
295
|
-
return results;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
662
|
// src/install/manifest.ts
|
|
299
663
|
import os2 from "os";
|
|
300
664
|
import path3 from "path";
|
|
@@ -332,26 +696,39 @@ async function readSettings() {
|
|
|
332
696
|
async function writeSettings(settings) {
|
|
333
697
|
await writeJson(settingsJsonPath(), settings);
|
|
334
698
|
}
|
|
699
|
+
function collectEventHookCommands(entries) {
|
|
700
|
+
const cmds = /* @__PURE__ */ new Set();
|
|
701
|
+
for (const entry of entries) {
|
|
702
|
+
for (const h of entry.hooks ?? []) {
|
|
703
|
+
if (h.command) cmds.add(h.command);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return cmds;
|
|
707
|
+
}
|
|
335
708
|
function mergeHooks(settings, fragments) {
|
|
336
709
|
const existing = settings._haus?.hooks ?? [];
|
|
337
710
|
const existingCommands = settings._haus?.hookCommands ?? [];
|
|
338
|
-
const existingSet = new Set(existing);
|
|
339
711
|
const updated = { ...settings };
|
|
340
712
|
updated.hooks = { ...settings.hooks ?? {} };
|
|
341
713
|
const addedIds = [];
|
|
342
714
|
const addedCommands = [];
|
|
343
715
|
for (const fragment of fragments) {
|
|
344
716
|
if (fragment.gate !== "keep") continue;
|
|
345
|
-
if (existingSet.has(fragment.id)) continue;
|
|
346
717
|
const event = fragment.event;
|
|
718
|
+
const eventEntries = updated.hooks[event] ?? [];
|
|
719
|
+
const presentCommands = collectEventHookCommands(eventEntries);
|
|
720
|
+
if (presentCommands.has(fragment.command)) {
|
|
721
|
+
if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
347
724
|
if (!updated.hooks[event]) updated.hooks[event] = [];
|
|
348
725
|
const entry = {
|
|
349
726
|
hooks: [{ type: "command", command: fragment.command }]
|
|
350
727
|
};
|
|
351
728
|
if (fragment.matcher) entry.matcher = fragment.matcher;
|
|
352
729
|
updated.hooks[event] = [...updated.hooks[event] ?? [], entry];
|
|
353
|
-
addedIds.push(fragment.id);
|
|
354
|
-
addedCommands.push(fragment.command);
|
|
730
|
+
if (!existing.includes(fragment.id)) addedIds.push(fragment.id);
|
|
731
|
+
if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
|
|
355
732
|
}
|
|
356
733
|
updated._haus = {
|
|
357
734
|
hooks: [...existing, ...addedIds],
|
|
@@ -703,6 +1080,27 @@ function catalogItemContentPath(contentRoot, item) {
|
|
|
703
1080
|
import path7 from "path";
|
|
704
1081
|
import fg2 from "fast-glob";
|
|
705
1082
|
import fs4 from "fs-extra";
|
|
1083
|
+
|
|
1084
|
+
// src/claude/managed-template.ts
|
|
1085
|
+
function normaliseLF(content2) {
|
|
1086
|
+
return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1087
|
+
}
|
|
1088
|
+
var SCHEMA_VERSION = 1;
|
|
1089
|
+
function parseHausManagedHeader(line2) {
|
|
1090
|
+
const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
|
|
1091
|
+
if (!match) return null;
|
|
1092
|
+
const vMatch = line2.match(/\bv=(\d+)/);
|
|
1093
|
+
const sourceMatch = line2.match(/\bsource=([^\s]+)/);
|
|
1094
|
+
const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1095
|
+
return {
|
|
1096
|
+
id: match[1],
|
|
1097
|
+
v: vMatch ? Number(vMatch[1]) : void 0,
|
|
1098
|
+
source: sourceMatch?.[1],
|
|
1099
|
+
hash: hashMatch?.[1]
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/update/hash-installed.ts
|
|
706
1104
|
var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
|
|
707
1105
|
async function hashInstalledPaths(root, relPaths) {
|
|
708
1106
|
if (relPaths.length === 0) {
|
|
@@ -716,7 +1114,7 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
716
1114
|
const stat = await fs4.stat(abs);
|
|
717
1115
|
if (stat.isFile()) {
|
|
718
1116
|
const body = await fs4.readFile(abs, "utf8");
|
|
719
|
-
fileDigests.push({ rel, digest: hashText(body) });
|
|
1117
|
+
fileDigests.push({ rel, digest: hashText(normaliseLF(body)) });
|
|
720
1118
|
continue;
|
|
721
1119
|
}
|
|
722
1120
|
if (!stat.isDirectory()) continue;
|
|
@@ -725,7 +1123,7 @@ async function hashInstalledPaths(root, relPaths) {
|
|
|
725
1123
|
const relFile = path7.join(rel, sub).replace(/\\/g, "/");
|
|
726
1124
|
const absFile = path7.join(abs, sub);
|
|
727
1125
|
const body = await fs4.readFile(absFile, "utf8");
|
|
728
|
-
fileDigests.push({ rel: relFile, digest: hashText(body) });
|
|
1126
|
+
fileDigests.push({ rel: relFile, digest: hashText(normaliseLF(body)) });
|
|
729
1127
|
}
|
|
730
1128
|
}
|
|
731
1129
|
if (fileDigests.length === 0) {
|
|
@@ -1100,25 +1498,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
|
|
|
1100
1498
|
|
|
1101
1499
|
// src/claude/write-workflow.ts
|
|
1102
1500
|
import fs10 from "fs-extra";
|
|
1103
|
-
|
|
1104
|
-
// src/claude/managed-template.ts
|
|
1105
|
-
function normaliseLF(content2) {
|
|
1106
|
-
return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1107
|
-
}
|
|
1108
|
-
function parseHausManagedHeader(line2) {
|
|
1109
|
-
const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
|
|
1110
|
-
if (!match) return null;
|
|
1111
|
-
const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
|
|
1112
|
-
return { id: match[1], hash: hashMatch?.[1] };
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// src/claude/write-workflow.ts
|
|
1116
1501
|
var STABLE_ID = "template.workflow";
|
|
1117
|
-
var SCHEMA_VERSION = "1";
|
|
1118
1502
|
function makeWorkflowHeader(pkgVersion, contentHash) {
|
|
1119
1503
|
return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
|
|
1120
1504
|
}
|
|
1121
|
-
async function writeWorkflow(root, pkgVersion, dryRun) {
|
|
1505
|
+
async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
|
|
1122
1506
|
const templateContent = await readWorkflowTemplate({ dryRun });
|
|
1123
1507
|
if (templateContent === null) {
|
|
1124
1508
|
warn(
|
|
@@ -1144,8 +1528,14 @@ ${templateContent}`;
|
|
|
1144
1528
|
warn(`${printable}: HAUS-MANAGED id mismatch (expected ${STABLE_ID}) \u2014 skipping`);
|
|
1145
1529
|
return null;
|
|
1146
1530
|
}
|
|
1531
|
+
if (parsed.v !== void 0 && parsed.v > SCHEMA_VERSION) {
|
|
1532
|
+
warn(
|
|
1533
|
+
`${printable}: written by a newer haus (template v${parsed.v}) \u2014 upgrade the CLI to manage it`
|
|
1534
|
+
);
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1147
1537
|
const existingContent = existing.slice(firstLine.length + 1);
|
|
1148
|
-
if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash) {
|
|
1538
|
+
if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash && !force) {
|
|
1149
1539
|
warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
|
|
1150
1540
|
return null;
|
|
1151
1541
|
}
|
|
@@ -1170,6 +1560,13 @@ ${templateContent}`;
|
|
|
1170
1560
|
}
|
|
1171
1561
|
|
|
1172
1562
|
// src/claude/write-claude-files.ts
|
|
1563
|
+
function targetDirForType(type) {
|
|
1564
|
+
if (type === "agent") return "agents";
|
|
1565
|
+
if (type === "template") return "templates";
|
|
1566
|
+
if (type === "command") return "commands";
|
|
1567
|
+
if (type === "skill") return "skills";
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1173
1570
|
async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
1174
1571
|
const rec = await readJson(hausPath(root, "recommendation.json")) ?? {
|
|
1175
1572
|
mode: "fast",
|
|
@@ -1191,7 +1588,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1191
1588
|
claudePath(root, "commands", "haus-review.md")
|
|
1192
1589
|
];
|
|
1193
1590
|
const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
|
|
1194
|
-
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun);
|
|
1591
|
+
const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force);
|
|
1195
1592
|
const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
|
|
1196
1593
|
refill: opts.refillConfig
|
|
1197
1594
|
});
|
|
@@ -1246,14 +1643,18 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1246
1643
|
const installedPathsByItem = /* @__PURE__ */ new Map();
|
|
1247
1644
|
const installedIds = /* @__PURE__ */ new Set();
|
|
1248
1645
|
const catalogItems = selectedIds !== void 0 ? rec.recommended.filter((r) => selectedIds.includes(r.id)) : rec.recommended;
|
|
1646
|
+
let curatedReviewStatusSkips = 0;
|
|
1249
1647
|
for (const item of catalogItems) {
|
|
1250
1648
|
const manifestItem = manifestById.get(item.id);
|
|
1251
1649
|
if (!manifestItem?.path) continue;
|
|
1252
1650
|
if (manifestItem.source === "curated") {
|
|
1253
1651
|
if (manifestItem.reviewStatus !== "approved") {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1652
|
+
curatedReviewStatusSkips++;
|
|
1653
|
+
if (curatedReviewStatusSkips === 1) {
|
|
1654
|
+
warn(
|
|
1655
|
+
`Skipping curated item ${item.id}: reviewStatus is not approved (${manifestItem.reviewStatus ?? "unset"})`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1257
1658
|
continue;
|
|
1258
1659
|
}
|
|
1259
1660
|
if (manifestItem.riskLevel === "blocked") {
|
|
@@ -1262,7 +1663,13 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1262
1663
|
}
|
|
1263
1664
|
}
|
|
1264
1665
|
const sourcePath = catalogItemContentPath(contentRoot, manifestItem);
|
|
1265
|
-
const target = item.type
|
|
1666
|
+
const target = targetDirForType(item.type);
|
|
1667
|
+
if (!target) {
|
|
1668
|
+
warn(
|
|
1669
|
+
`Skipping ${item.id}: type "${item.type}" is unknown to this haus version \u2014 upgrade the CLI to use it`
|
|
1670
|
+
);
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1266
1673
|
const destination = claudePath(root, target, path12.basename(sourcePath));
|
|
1267
1674
|
if (await fs11.pathExists(sourcePath)) {
|
|
1268
1675
|
if (dryRun) {
|
|
@@ -1284,9 +1691,19 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1284
1691
|
);
|
|
1285
1692
|
}
|
|
1286
1693
|
}
|
|
1694
|
+
if (curatedReviewStatusSkips > 1) {
|
|
1695
|
+
warn(
|
|
1696
|
+
`${curatedReviewStatusSkips} curated items skipped: reviewStatus is not approved \u2014 possible catalog field rename upstream`
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1287
1699
|
await cleanupStaleCatalogItems(root, new Set(manifestById.keys()), dryRun);
|
|
1288
1700
|
if (dryRun) return [...new Set(files)];
|
|
1289
1701
|
const installedItems = catalogItems.filter((r) => installedIds.has(r.id));
|
|
1702
|
+
const prevLock = await readJson(hausPath(root, "haus.lock.json"));
|
|
1703
|
+
const prevRefById = new Map(
|
|
1704
|
+
(prevLock ?? []).filter((e) => e.id && e.catalogRef).map((e) => [e.id, e.catalogRef])
|
|
1705
|
+
);
|
|
1706
|
+
const lockCatalogRef = (itemId) => isCatalogRefResolved() ? getResolvedCatalogRef() : prevRefById.get(itemId) ?? getResolvedCatalogRef();
|
|
1290
1707
|
await writeManagedJson(
|
|
1291
1708
|
root,
|
|
1292
1709
|
hausPath(root, "selected-context.json"),
|
|
@@ -1308,7 +1725,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
|
|
|
1308
1725
|
type: r.type,
|
|
1309
1726
|
source: isCurated ? "curated" : "haus",
|
|
1310
1727
|
version: hausVersion2,
|
|
1311
|
-
catalogRef:
|
|
1728
|
+
catalogRef: lockCatalogRef(r.id),
|
|
1312
1729
|
hash: await hashInstalledPaths(root, relPaths),
|
|
1313
1730
|
installMode: "copied",
|
|
1314
1731
|
paths: relPaths
|
|
@@ -1411,13 +1828,19 @@ async function runApply(options) {
|
|
|
1411
1828
|
const rec = await readJson(hausPath(root, "recommendation.json"));
|
|
1412
1829
|
const catalogItemCount = selectedIds !== void 0 ? selectedIds.length : rec?.recommended.length ?? 0;
|
|
1413
1830
|
if (catalogItemCount > 0 && !await cacheHasItems()) {
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1831
|
+
const message = "No catalog content found. Run `haus update` first.";
|
|
1832
|
+
if (isDryRun) {
|
|
1833
|
+
warn(`Catalog cache is empty \u2014 dry-run will skip catalog items. ${message}`);
|
|
1834
|
+
} else {
|
|
1835
|
+
error(message);
|
|
1836
|
+
process.exitCode = 1;
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1417
1839
|
}
|
|
1418
1840
|
}
|
|
1419
1841
|
const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
|
|
1420
|
-
refillConfig: options.refillConfig
|
|
1842
|
+
refillConfig: options.refillConfig,
|
|
1843
|
+
force: options.force
|
|
1421
1844
|
});
|
|
1422
1845
|
if (isDryRun) {
|
|
1423
1846
|
log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
|
|
@@ -1439,185 +1862,6 @@ async function refreshProjectApply(root) {
|
|
|
1439
1862
|
return writeClaudeFiles(root, false, void 0, { refillConfig: false });
|
|
1440
1863
|
}
|
|
1441
1864
|
|
|
1442
|
-
// library/catalog/validation-rules.json
|
|
1443
|
-
var validation_rules_default = {
|
|
1444
|
-
forbiddenTags: [
|
|
1445
|
-
"python",
|
|
1446
|
-
"django",
|
|
1447
|
-
"go",
|
|
1448
|
-
"rust",
|
|
1449
|
-
"java",
|
|
1450
|
-
"spring",
|
|
1451
|
-
"kotlin",
|
|
1452
|
-
"swift",
|
|
1453
|
-
"android",
|
|
1454
|
-
"flutter",
|
|
1455
|
-
"dart",
|
|
1456
|
-
"c++",
|
|
1457
|
-
"perl",
|
|
1458
|
-
"defi",
|
|
1459
|
-
"trading"
|
|
1460
|
-
],
|
|
1461
|
-
bannedAgentPhrases: ["autonomous", "swarm", "delegate", "orchestrat", "marketplace"],
|
|
1462
|
-
requiredSkillFrontmatter: ["description"],
|
|
1463
|
-
requiredAgentSections: ["## Use when", "## Do not use when", "## Verification"],
|
|
1464
|
-
riskyInstallPatterns: [
|
|
1465
|
-
{ source: "\\bnpx\\s+-y\\b", flags: "i" },
|
|
1466
|
-
{ source: "\\bnpx\\s+--yes\\b", flags: "i" },
|
|
1467
|
-
{ source: "\\byarn\\s+dlx\\b", flags: "i" },
|
|
1468
|
-
{ source: "\\bpnpm\\s+dlx\\b", flags: "i" }
|
|
1469
|
-
],
|
|
1470
|
-
allowedNpxPattern: { source: "\\bnpx\\s+tsx\\b", flags: "i" },
|
|
1471
|
-
anyNpxPattern: { source: "\\bnpx\\s+\\S+", flags: "i" },
|
|
1472
|
-
httpUrlPattern: { source: "^http:\\/\\/", flags: "i" },
|
|
1473
|
-
placeholderPattern: { source: "\\bTODO\\b|\\bPLACEHOLDER\\b", flags: "i" },
|
|
1474
|
-
allowedStacks: [
|
|
1475
|
-
"haus",
|
|
1476
|
-
"security",
|
|
1477
|
-
"quality",
|
|
1478
|
-
"frontend",
|
|
1479
|
-
"backend",
|
|
1480
|
-
"testing",
|
|
1481
|
-
"review",
|
|
1482
|
-
"workflow",
|
|
1483
|
-
"reference-pack",
|
|
1484
|
-
"core-skill",
|
|
1485
|
-
"workflow-skill",
|
|
1486
|
-
"stack-skill",
|
|
1487
|
-
"review-skill",
|
|
1488
|
-
"agent",
|
|
1489
|
-
"hook",
|
|
1490
|
-
"rule",
|
|
1491
|
-
"react",
|
|
1492
|
-
"typescript",
|
|
1493
|
-
"php",
|
|
1494
|
-
"csharp",
|
|
1495
|
-
"vendure",
|
|
1496
|
-
"vendure3",
|
|
1497
|
-
"nestjs",
|
|
1498
|
-
"graphql",
|
|
1499
|
-
"nx21",
|
|
1500
|
-
"turbo",
|
|
1501
|
-
"nextjs",
|
|
1502
|
-
"react19",
|
|
1503
|
-
"typescript5",
|
|
1504
|
-
"vite8",
|
|
1505
|
-
"tanstack-query",
|
|
1506
|
-
"tanstack-router",
|
|
1507
|
-
"radix",
|
|
1508
|
-
"radix-ui",
|
|
1509
|
-
"shadcn",
|
|
1510
|
-
"shadcn-ui",
|
|
1511
|
-
"tailwind",
|
|
1512
|
-
"tailwindcss",
|
|
1513
|
-
"scss",
|
|
1514
|
-
"scss-modules",
|
|
1515
|
-
"vue",
|
|
1516
|
-
"expressjs",
|
|
1517
|
-
"soup-base",
|
|
1518
|
-
"laravel",
|
|
1519
|
-
"laravel-nova",
|
|
1520
|
-
"wordpress",
|
|
1521
|
-
"bedrock",
|
|
1522
|
-
"elementor-pro",
|
|
1523
|
-
"acf-pro",
|
|
1524
|
-
"jetengine",
|
|
1525
|
-
"dotnet",
|
|
1526
|
-
"oidc",
|
|
1527
|
-
"azure-ad",
|
|
1528
|
-
"bankid",
|
|
1529
|
-
"myid",
|
|
1530
|
-
"cgi",
|
|
1531
|
-
"crypto",
|
|
1532
|
-
"collection2",
|
|
1533
|
-
"postgresql",
|
|
1534
|
-
"mariadb",
|
|
1535
|
-
"mssql",
|
|
1536
|
-
"elasticsearch",
|
|
1537
|
-
"yarn4",
|
|
1538
|
-
"pnpm89",
|
|
1539
|
-
"playwright",
|
|
1540
|
-
"testing-library",
|
|
1541
|
-
"phpunit",
|
|
1542
|
-
"storybook",
|
|
1543
|
-
"wisest",
|
|
1544
|
-
"vitest",
|
|
1545
|
-
"jest",
|
|
1546
|
-
"redis",
|
|
1547
|
-
"sanity",
|
|
1548
|
-
"strapi",
|
|
1549
|
-
"prisma",
|
|
1550
|
-
"cms",
|
|
1551
|
-
"database",
|
|
1552
|
-
"mysql",
|
|
1553
|
-
"saml2",
|
|
1554
|
-
"next-auth",
|
|
1555
|
-
"auth",
|
|
1556
|
-
"expo",
|
|
1557
|
-
"react-native",
|
|
1558
|
-
"mobile",
|
|
1559
|
-
"i18next",
|
|
1560
|
-
"i18n",
|
|
1561
|
-
"bullmq",
|
|
1562
|
-
"queue",
|
|
1563
|
-
"sentry",
|
|
1564
|
-
"observability",
|
|
1565
|
-
"tooling",
|
|
1566
|
-
"prettier",
|
|
1567
|
-
"eslint",
|
|
1568
|
-
"missing-prettier",
|
|
1569
|
-
"missing-eslint",
|
|
1570
|
-
"docker",
|
|
1571
|
-
"pm2",
|
|
1572
|
-
"deployer-php",
|
|
1573
|
-
"stripe",
|
|
1574
|
-
"qliro",
|
|
1575
|
-
"supabase",
|
|
1576
|
-
"payments"
|
|
1577
|
-
],
|
|
1578
|
-
alwaysAllowedTags: [
|
|
1579
|
-
"haus",
|
|
1580
|
-
"security",
|
|
1581
|
-
"quality",
|
|
1582
|
-
"review",
|
|
1583
|
-
"workflow",
|
|
1584
|
-
"baseline",
|
|
1585
|
-
"project-instructions"
|
|
1586
|
-
],
|
|
1587
|
-
patternTagSuffixes: ["-patterns"]
|
|
1588
|
-
};
|
|
1589
|
-
|
|
1590
|
-
// src/catalog/validation-rules.ts
|
|
1591
|
-
var toRegExp = (r) => new RegExp(r.source, r.flags);
|
|
1592
|
-
var FORBIDDEN_TAGS = validation_rules_default.forbiddenTags;
|
|
1593
|
-
var BANNED_AGENT_PHRASES = validation_rules_default.bannedAgentPhrases;
|
|
1594
|
-
var REQUIRED_SKILL_FRONTMATTER = validation_rules_default.requiredSkillFrontmatter;
|
|
1595
|
-
var REQUIRED_AGENT_SECTIONS = validation_rules_default.requiredAgentSections;
|
|
1596
|
-
var RISKY_INSTALL_PATTERNS = validation_rules_default.riskyInstallPatterns.map(toRegExp);
|
|
1597
|
-
var ALLOWED_NPX_PATTERN = toRegExp(validation_rules_default.allowedNpxPattern);
|
|
1598
|
-
var ANY_NPX_PATTERN = toRegExp(validation_rules_default.anyNpxPattern);
|
|
1599
|
-
var HTTP_URL_PATTERN = toRegExp(validation_rules_default.httpUrlPattern);
|
|
1600
|
-
var PLACEHOLDER_PATTERN = toRegExp(validation_rules_default.placeholderPattern);
|
|
1601
|
-
var ALLOWED_STACKS = validation_rules_default.allowedStacks;
|
|
1602
|
-
var ALWAYS_ALLOWED_TAGS = validation_rules_default.alwaysAllowedTags;
|
|
1603
|
-
var PATTERN_TAG_SUFFIXES = validation_rules_default.patternTagSuffixes;
|
|
1604
|
-
var ALLOWED_SET = new Set([...ALLOWED_STACKS, ...ALWAYS_ALLOWED_TAGS].map((t) => t.toLowerCase()));
|
|
1605
|
-
function isTagAllowed(tag) {
|
|
1606
|
-
const lower = tag.toLowerCase();
|
|
1607
|
-
if (ALLOWED_SET.has(lower)) return true;
|
|
1608
|
-
return PATTERN_TAG_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
1609
|
-
}
|
|
1610
|
-
function auditDisallowedTags(items) {
|
|
1611
|
-
const failures = [];
|
|
1612
|
-
for (const item of items) {
|
|
1613
|
-
if (!item.id) continue;
|
|
1614
|
-
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
1615
|
-
if (!isTagAllowed(tag)) failures.push(`${item.id}: tag not in allowlist: "${tag}"`);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return failures;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
1865
|
// src/commands/catalog-audit.ts
|
|
1622
1866
|
function auditForbiddenTags(items) {
|
|
1623
1867
|
const failures = [];
|
|
@@ -1731,6 +1975,17 @@ async function runConfig(key, action) {
|
|
|
1731
1975
|
log(`${key} ${action}d`);
|
|
1732
1976
|
}
|
|
1733
1977
|
|
|
1978
|
+
// src/recommender/token-estimate.ts
|
|
1979
|
+
var TOKENS_PER_ITEM = 320;
|
|
1980
|
+
function estimateContextTokens(selectedCount) {
|
|
1981
|
+
return selectedCount * TOKENS_PER_ITEM;
|
|
1982
|
+
}
|
|
1983
|
+
function tokenReductionPct(selected, skipped) {
|
|
1984
|
+
const total = selected + skipped;
|
|
1985
|
+
if (total === 0) return 0;
|
|
1986
|
+
return Math.max(0, Math.round(skipped / total * 100));
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1734
1989
|
// src/recommender/explain-recommendation.ts
|
|
1735
1990
|
function normalizeRecommendation(input2) {
|
|
1736
1991
|
const recommended = (input2.recommended ?? []).map((item) => {
|
|
@@ -1765,13 +2020,10 @@ function normalizeRecommendation(input2) {
|
|
|
1765
2020
|
recommended,
|
|
1766
2021
|
skipped,
|
|
1767
2022
|
warnings: input2.warnings ?? [],
|
|
1768
|
-
estimatedContextTokens: input2.estimatedContextTokens ?? recommended.length
|
|
2023
|
+
estimatedContextTokens: input2.estimatedContextTokens ?? estimateContextTokens(recommended.length),
|
|
1769
2024
|
selectedRules: input2.selectedRules ?? recommended.length,
|
|
1770
2025
|
skippedRules: input2.skippedRules ?? skipped.length,
|
|
1771
|
-
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ??
|
|
1772
|
-
0,
|
|
1773
|
-
Math.round(skipped.length / Math.max(recommended.length + skipped.length, 1) * 100)
|
|
1774
|
-
)
|
|
2026
|
+
estimatedTokenReductionPct: input2.estimatedTokenReductionPct ?? tokenReductionPct(recommended.length, skipped.length)
|
|
1775
2027
|
};
|
|
1776
2028
|
}
|
|
1777
2029
|
function buildRecommendationExplanation(recommendation) {
|
|
@@ -2796,35 +3048,45 @@ async function runDoctor(options) {
|
|
|
2796
3048
|
"haus apply --write"
|
|
2797
3049
|
);
|
|
2798
3050
|
} else {
|
|
2799
|
-
const workflowContent = await readText(workflowPath);
|
|
2800
|
-
const firstLine = workflowContent
|
|
3051
|
+
const workflowContent = await readText(workflowPath) ?? "";
|
|
3052
|
+
const firstLine = workflowContent.split("\n")[0] ?? "";
|
|
2801
3053
|
if (!firstLine.includes("HAUS-MANAGED")) {
|
|
2802
3054
|
ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
|
|
2803
3055
|
} else {
|
|
2804
3056
|
const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
|
|
2805
|
-
const
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
3057
|
+
const bodyContent = workflowContent.slice(firstLine.length + 1);
|
|
3058
|
+
const onDiskBodyHash = hashText(normaliseLF(bodyContent));
|
|
3059
|
+
if (storedHashMatch && onDiskBodyHash !== storedHashMatch[1]) {
|
|
3060
|
+
flag(
|
|
3061
|
+
"- .haus-workflow/WORKFLOW.md: modified locally (run `haus apply --write --force` to restore)",
|
|
3062
|
+
"The workflow standard file was edited after haus wrote it",
|
|
3063
|
+
"haus apply --write --force"
|
|
3064
|
+
);
|
|
3065
|
+
} else {
|
|
3066
|
+
const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
|
|
3067
|
+
const bundledPath = path20.join(
|
|
3068
|
+
packageRoot(),
|
|
3069
|
+
"library",
|
|
3070
|
+
"global",
|
|
3071
|
+
"templates",
|
|
3072
|
+
"agentic-workflow-standard.md"
|
|
3073
|
+
);
|
|
3074
|
+
const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
|
|
3075
|
+
const templateContent = await readText(templatePath);
|
|
3076
|
+
if (storedHashMatch && templateContent) {
|
|
3077
|
+
const currentHash = hashText(normaliseLF(templateContent));
|
|
3078
|
+
if (storedHashMatch[1] !== currentHash) {
|
|
3079
|
+
flag(
|
|
3080
|
+
"- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
|
|
3081
|
+
"The workflow standard is out of date",
|
|
3082
|
+
"haus apply --write"
|
|
3083
|
+
);
|
|
3084
|
+
} else {
|
|
3085
|
+
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
3086
|
+
}
|
|
2823
3087
|
} else {
|
|
2824
3088
|
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
2825
3089
|
}
|
|
2826
|
-
} else {
|
|
2827
|
-
ok("- .haus-workflow/WORKFLOW.md: OK");
|
|
2828
3090
|
}
|
|
2829
3091
|
}
|
|
2830
3092
|
}
|
|
@@ -3036,7 +3298,7 @@ async function confirm(question) {
|
|
|
3036
3298
|
async function readChangedFiles(root) {
|
|
3037
3299
|
if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
|
|
3038
3300
|
try {
|
|
3039
|
-
const result = await runGit(["diff", "--name-only"], { cwd: root });
|
|
3301
|
+
const result = await runGit(["diff", "--name-only"], { cwd: root, timeout: 3e3 });
|
|
3040
3302
|
if (result.exitCode !== 0) {
|
|
3041
3303
|
return [];
|
|
3042
3304
|
}
|
|
@@ -3199,7 +3461,9 @@ async function recommend(root, context) {
|
|
|
3199
3461
|
(t) => goals.includes(t) || goals.includes(t.replace(/-/g, " "))
|
|
3200
3462
|
);
|
|
3201
3463
|
if (goalMatch) push("goal-match", "guided goal match", `goal:${goalMatch}`);
|
|
3202
|
-
|
|
3464
|
+
const pm = context.packageManager;
|
|
3465
|
+
const pmVersionedMatch = pm === "yarn" || pm === "pnpm" ? item.tags.includes(pm) || item.tags.includes(`${pm}4`) || item.tags.includes(`${pm}89`) : item.tags.includes(pm);
|
|
3466
|
+
if (pmVersionedMatch) {
|
|
3203
3467
|
push(
|
|
3204
3468
|
"package-manager-match",
|
|
3205
3469
|
"package manager match",
|
|
@@ -3250,13 +3514,10 @@ async function recommend(root, context) {
|
|
|
3250
3514
|
}
|
|
3251
3515
|
recommended.sort((a, b) => a.id.localeCompare(b.id));
|
|
3252
3516
|
skipped.sort((a, b) => a.id.localeCompare(b.id));
|
|
3253
|
-
const estimatedContextTokens = recommended.length * 320;
|
|
3254
3517
|
const selectedRules = recommended.length;
|
|
3255
3518
|
const skippedRules = skipped.length;
|
|
3256
|
-
const
|
|
3257
|
-
|
|
3258
|
-
Math.round(skippedRules / Math.max(selectedRules + skippedRules, 1) * 100)
|
|
3259
|
-
);
|
|
3519
|
+
const estimatedContextTokens = estimateContextTokens(selectedRules);
|
|
3520
|
+
const estimatedTokenReductionPct = tokenReductionPct(selectedRules, skippedRules);
|
|
3260
3521
|
return {
|
|
3261
3522
|
mode: context.mode,
|
|
3262
3523
|
recommended,
|
|
@@ -4130,61 +4391,6 @@ async function detectGlobalInstallDrift() {
|
|
|
4130
4391
|
// src/commands/validate-catalog.ts
|
|
4131
4392
|
import fs19 from "fs";
|
|
4132
4393
|
import path27 from "path";
|
|
4133
|
-
|
|
4134
|
-
// src/catalog/forbidden-content.ts
|
|
4135
|
-
var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
|
|
4136
|
-
function escapeRegExp(value) {
|
|
4137
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4138
|
-
}
|
|
4139
|
-
function extractUseWhenSection(text) {
|
|
4140
|
-
const marker = "## Use when";
|
|
4141
|
-
const idx = text.toLowerCase().indexOf(marker.toLowerCase());
|
|
4142
|
-
if (idx < 0) return "";
|
|
4143
|
-
const tail = text.slice(idx + marker.length);
|
|
4144
|
-
const next = tail.search(/\n##\s+/);
|
|
4145
|
-
return next < 0 ? tail : tail.slice(0, next);
|
|
4146
|
-
}
|
|
4147
|
-
function isYamlBlockScalarHeader(rest) {
|
|
4148
|
-
return /^[>|][-+]?(\d+)?(?:\s+#.*)?$/.test(rest);
|
|
4149
|
-
}
|
|
4150
|
-
function extractFrontmatterValue(text, key) {
|
|
4151
|
-
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
4152
|
-
if (!m) return "";
|
|
4153
|
-
const lines = m[1].split(/\r?\n/);
|
|
4154
|
-
const keyRe = new RegExp(`^${escapeRegExp(key)}:[ \\t]*`);
|
|
4155
|
-
const idx = lines.findIndex((l) => keyRe.test(l));
|
|
4156
|
-
if (idx < 0) return "";
|
|
4157
|
-
const rest = lines[idx].replace(keyRe, "").trim();
|
|
4158
|
-
if (!rest) return "";
|
|
4159
|
-
if (!isYamlBlockScalarHeader(rest)) {
|
|
4160
|
-
return rest.replace(/^["']|["']$/g, "").trim();
|
|
4161
|
-
}
|
|
4162
|
-
const body = [];
|
|
4163
|
-
for (let j = idx + 1; j < lines.length; j++) {
|
|
4164
|
-
const line2 = lines[j];
|
|
4165
|
-
if (/^[a-zA-Z_][\w.-]*:[ \t]/.test(line2)) break;
|
|
4166
|
-
if (line2.trim() === "") continue;
|
|
4167
|
-
if (/^\s+/.test(line2)) body.push(line2.trimStart());
|
|
4168
|
-
else break;
|
|
4169
|
-
}
|
|
4170
|
-
return body.join(" ").replace(/\s+/g, " ").trim();
|
|
4171
|
-
}
|
|
4172
|
-
function extractFrontmatterDescription(text) {
|
|
4173
|
-
return extractFrontmatterValue(text, "description");
|
|
4174
|
-
}
|
|
4175
|
-
function auditForbiddenTagsInText(text, label) {
|
|
4176
|
-
const body = `${extractFrontmatterDescription(text)}
|
|
4177
|
-
${extractUseWhenSection(text)}`;
|
|
4178
|
-
if (!body.trim()) return [];
|
|
4179
|
-
const failures = [];
|
|
4180
|
-
for (const word of PROSE_FORBIDDEN_TAGS) {
|
|
4181
|
-
const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "i");
|
|
4182
|
-
if (re.test(body)) failures.push(`${label}: forbidden stack/tag "${word}" in content`);
|
|
4183
|
-
}
|
|
4184
|
-
return failures;
|
|
4185
|
-
}
|
|
4186
|
-
|
|
4187
|
-
// src/commands/validate-catalog.ts
|
|
4188
4394
|
function auditForbiddenStacks(items) {
|
|
4189
4395
|
const failures = [];
|
|
4190
4396
|
for (const item of items) {
|
|
@@ -5090,7 +5296,7 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
|
|
|
5090
5296
|
).option(
|
|
5091
5297
|
"--refill-config",
|
|
5092
5298
|
"Fill still-blank fields in an existing workflow-config.md without touching edited ones"
|
|
5093
|
-
).action(runApply);
|
|
5299
|
+
).option("--force", "Overwrite user-modified managed workflow files").action(runApply);
|
|
5094
5300
|
program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
|
|
5095
5301
|
program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
|
|
5096
5302
|
program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
|