@harness-forge/cli 1.2.1 → 1.2.3
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/.agents/skills/hforge-analyze/SKILL.md +35 -0
- package/.agents/skills/hforge-decide/SKILL.md +29 -0
- package/.agents/skills/hforge-init/SKILL.md +34 -0
- package/.agents/skills/hforge-refresh/SKILL.md +28 -0
- package/.agents/skills/hforge-review/SKILL.md +29 -0
- package/AGENTS.md +8 -1
- package/README.md +19 -0
- package/commands/hforge-analyze.md +55 -0
- package/commands/hforge-cartograph.md +35 -0
- package/commands/hforge-commands.md +34 -0
- package/commands/hforge-decide.md +35 -0
- package/commands/hforge-init.md +37 -0
- package/commands/hforge-recommend.md +34 -0
- package/commands/hforge-recursive.md +34 -0
- package/commands/hforge-refresh.md +35 -0
- package/commands/hforge-review.md +36 -0
- package/commands/hforge-status.md +34 -0
- package/commands/hforge-task.md +35 -0
- package/commands/hforge-update.md +34 -0
- package/dist/application/install/agent-manifest.d.ts +8 -0
- package/dist/application/install/agent-manifest.d.ts.map +1 -1
- package/dist/application/install/agent-manifest.js +7 -0
- package/dist/application/install/agent-manifest.js.map +1 -1
- package/dist/application/recommendations/recommend-bundles.d.ts.map +1 -1
- package/dist/application/recommendations/recommend-bundles.js +41 -2
- package/dist/application/recommendations/recommend-bundles.js.map +1 -1
- package/dist/application/runtime/command-catalog.d.ts +7 -0
- package/dist/application/runtime/command-catalog.d.ts.map +1 -1
- package/dist/application/runtime/command-catalog.js +101 -0
- package/dist/application/runtime/command-catalog.js.map +1 -1
- package/dist/cli/commands/commands.d.ts.map +1 -1
- package/dist/cli/commands/commands.js +5 -1
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/domain/intelligence/repo-intelligence.js +1 -1
- package/dist/domain/intelligence/repo-intelligence.js.map +1 -1
- package/docs/agent-usage-playbook.md +206 -0
- package/docs/agents.md +2 -0
- package/docs/commands.md +27 -0
- package/manifests/bundles/core.json +13 -1
- package/manifests/catalog/compatibility-matrix.json +171 -1
- package/manifests/catalog/package-surface.json +48 -0
- package/package.json +1 -1
- package/scripts/intelligence/shared.mjs +243 -58
- package/skills/hforge-analyze/SKILL.md +40 -0
- package/skills/hforge-analyze/references/analysis-order.md +15 -0
- package/skills/hforge-analyze/references/decision-promotion.md +9 -0
- package/skills/hforge-analyze/references/output-contract.md +7 -0
- package/skills/hforge-decide/SKILL.md +58 -0
- package/skills/hforge-decide/references/decision-rubric.md +18 -0
- package/skills/hforge-decide/references/output-contract.md +7 -0
- package/skills/hforge-init/SKILL.md +58 -0
- package/skills/hforge-init/references/bootstrap-order.md +7 -0
- package/skills/hforge-init/references/output-contract.md +7 -0
- package/skills/hforge-refresh/SKILL.md +52 -0
- package/skills/hforge-refresh/references/output-contract.md +7 -0
- package/skills/hforge-refresh/references/refresh-order.md +5 -0
- package/skills/hforge-review/SKILL.md +57 -0
- package/skills/hforge-review/references/output-contract.md +7 -0
- package/skills/hforge-review/references/review-order.md +7 -0
|
@@ -4,6 +4,9 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
|
|
5
5
|
const IGNORED_DIRS = new Set([
|
|
6
6
|
".git",
|
|
7
|
+
".claude",
|
|
8
|
+
".codex",
|
|
9
|
+
".cursor",
|
|
7
10
|
".hforge",
|
|
8
11
|
".next",
|
|
9
12
|
".nuxt",
|
|
@@ -17,11 +20,78 @@ const IGNORED_DIRS = new Set([
|
|
|
17
20
|
"vendor"
|
|
18
21
|
]);
|
|
19
22
|
|
|
23
|
+
const LOW_SIGNAL_SEGMENTS = new Set([
|
|
24
|
+
".tmp",
|
|
25
|
+
"tmp",
|
|
26
|
+
"temp",
|
|
27
|
+
"fixtures",
|
|
28
|
+
"__fixtures__",
|
|
29
|
+
"__snapshots__",
|
|
30
|
+
"snapshots",
|
|
31
|
+
"archive",
|
|
32
|
+
"archives"
|
|
33
|
+
]);
|
|
34
|
+
|
|
20
35
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
21
36
|
|
|
22
|
-
function
|
|
37
|
+
function hasLowSignalSegment(file) {
|
|
38
|
+
return file.split("/").some((segment) => LOW_SIGNAL_SEGMENTS.has(segment));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSupportSurface(file) {
|
|
42
|
+
return (
|
|
43
|
+
file.startsWith(".claude/") ||
|
|
44
|
+
file.startsWith(".codex/") ||
|
|
45
|
+
file.startsWith(".cursor/") ||
|
|
46
|
+
file.startsWith(".agents/") ||
|
|
47
|
+
file.startsWith(".github/") ||
|
|
48
|
+
file.startsWith(".hforge/")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isDocPath(file) {
|
|
53
|
+
return /\.(md|mdx|txt|rst)$/i.test(file);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isRelevantForRisk(file) {
|
|
57
|
+
return !hasLowSignalSegment(file) && !isDocPath(file) && !isSupportSurface(file);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function languageWeightForPath(file) {
|
|
61
|
+
const normalized = file.replaceAll("\\", "/");
|
|
62
|
+
|
|
63
|
+
if (isSupportSurface(normalized) || hasLowSignalSegment(normalized)) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
normalized.startsWith("src/") ||
|
|
69
|
+
normalized.startsWith("app/") ||
|
|
70
|
+
normalized.startsWith("lib/") ||
|
|
71
|
+
normalized.startsWith("seed/") ||
|
|
72
|
+
normalized.startsWith("data/")
|
|
73
|
+
) {
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
normalized.startsWith("scripts/") ||
|
|
79
|
+
normalized.startsWith("tools/") ||
|
|
80
|
+
normalized.startsWith("bin/")
|
|
81
|
+
) {
|
|
82
|
+
return 0.35;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (normalized.startsWith("tests/") || normalized.includes("/tests/")) {
|
|
86
|
+
return 0.5;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function bump(map, key, evidence, amount = 1) {
|
|
23
93
|
const current = map.get(key) ?? { count: 0, evidence: [] };
|
|
24
|
-
current.count +=
|
|
94
|
+
current.count += amount;
|
|
25
95
|
if (evidence && current.evidence.length < 5 && !current.evidence.includes(evidence)) {
|
|
26
96
|
current.evidence.push(evidence);
|
|
27
97
|
}
|
|
@@ -103,43 +173,61 @@ async function walk(root, visit, base = root) {
|
|
|
103
173
|
|
|
104
174
|
function detectLanguages(files) {
|
|
105
175
|
const languageCounts = new Map();
|
|
176
|
+
const hasCppSource = files.some((file) => {
|
|
177
|
+
const weight = languageWeightForPath(file);
|
|
178
|
+
return weight > 0 && /\.(cpp|cxx|cc|hpp|hh)$/i.test(file);
|
|
179
|
+
});
|
|
106
180
|
|
|
107
181
|
for (const file of files) {
|
|
108
|
-
|
|
109
|
-
|
|
182
|
+
const weight = languageWeightForPath(file);
|
|
183
|
+
if (weight <= 0) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (/\.(ts|tsx)$/i.test(file)) {
|
|
188
|
+
bump(languageCounts, "typescript", file, weight);
|
|
189
|
+
}
|
|
190
|
+
if (/\.(js|jsx)$/i.test(file)) {
|
|
191
|
+
bump(languageCounts, "javascript", file, weight);
|
|
110
192
|
}
|
|
111
193
|
if (/\.py$/i.test(file)) {
|
|
112
|
-
bump(languageCounts, "python", file);
|
|
194
|
+
bump(languageCounts, "python", file, weight);
|
|
113
195
|
}
|
|
114
196
|
if (/\.go$/i.test(file)) {
|
|
115
|
-
bump(languageCounts, "go", file);
|
|
197
|
+
bump(languageCounts, "go", file, weight);
|
|
116
198
|
}
|
|
117
199
|
if (/\.java$/i.test(file)) {
|
|
118
|
-
bump(languageCounts, "java", file);
|
|
200
|
+
bump(languageCounts, "java", file, weight);
|
|
119
201
|
}
|
|
120
202
|
if (/\.(kt|kts)$/i.test(file)) {
|
|
121
|
-
bump(languageCounts, "kotlin", file);
|
|
203
|
+
bump(languageCounts, "kotlin", file, weight);
|
|
122
204
|
}
|
|
123
205
|
if (/\.rs$/i.test(file)) {
|
|
124
|
-
bump(languageCounts, "rust", file);
|
|
206
|
+
bump(languageCounts, "rust", file, weight);
|
|
125
207
|
}
|
|
126
|
-
if (/\.(cpp|cxx|cc|hpp|hh
|
|
127
|
-
bump(languageCounts, "cpp", file);
|
|
208
|
+
if (/\.(cpp|cxx|cc|hpp|hh)$/i.test(file)) {
|
|
209
|
+
bump(languageCounts, "cpp", file, weight);
|
|
210
|
+
}
|
|
211
|
+
if (/\.h$/i.test(file) && hasCppSource) {
|
|
212
|
+
bump(languageCounts, "cpp", file, weight);
|
|
128
213
|
}
|
|
129
214
|
if (/\.php$/i.test(file)) {
|
|
130
|
-
bump(languageCounts, "php", file);
|
|
215
|
+
bump(languageCounts, "php", file, weight);
|
|
131
216
|
}
|
|
132
217
|
if (/\.(pl|pm)$/i.test(file)) {
|
|
133
|
-
bump(languageCounts, "perl", file);
|
|
218
|
+
bump(languageCounts, "perl", file, weight);
|
|
134
219
|
}
|
|
135
220
|
if (/\.swift$/i.test(file)) {
|
|
136
|
-
bump(languageCounts, "swift", file);
|
|
221
|
+
bump(languageCounts, "swift", file, weight);
|
|
222
|
+
}
|
|
223
|
+
if (/\.lua$/i.test(file)) {
|
|
224
|
+
bump(languageCounts, "lua", file, weight);
|
|
137
225
|
}
|
|
138
226
|
if (/\.(sh|bash|zsh)$/i.test(file)) {
|
|
139
|
-
bump(languageCounts, "shell", file);
|
|
227
|
+
bump(languageCounts, "shell", file, weight);
|
|
140
228
|
}
|
|
141
229
|
if (/\.(cs|csproj|sln)$/i.test(file)) {
|
|
142
|
-
bump(languageCounts, "dotnet", file);
|
|
230
|
+
bump(languageCounts, "dotnet", file, weight);
|
|
143
231
|
}
|
|
144
232
|
}
|
|
145
233
|
|
|
@@ -157,7 +245,7 @@ function detectLanguages(files) {
|
|
|
157
245
|
|
|
158
246
|
function detectBuildSignals(files, packageJson) {
|
|
159
247
|
const signals = [];
|
|
160
|
-
const fileSet = new Set(files);
|
|
248
|
+
const fileSet = new Set(files.filter((file) => !hasLowSignalSegment(file)));
|
|
161
249
|
|
|
162
250
|
if (packageJson) {
|
|
163
251
|
const packageManager = typeof packageJson.packageManager === "string" ? packageJson.packageManager.split("@")[0] : "npm";
|
|
@@ -185,7 +273,7 @@ function detectBuildSignals(files, packageJson) {
|
|
|
185
273
|
];
|
|
186
274
|
|
|
187
275
|
for (const [signalId, pattern] of directBuildMarkers) {
|
|
188
|
-
const evidence =
|
|
276
|
+
const evidence = [...fileSet].filter((file) => pattern.test(path.posix.basename(file)));
|
|
189
277
|
if (evidence.length > 0) {
|
|
190
278
|
signals.push(createSignal(signalId, evidence, 0.8));
|
|
191
279
|
}
|
|
@@ -204,8 +292,11 @@ function detectTestSignals(files, packageJson, textSources) {
|
|
|
204
292
|
...Object.keys(packageJson?.dependencies ?? {}),
|
|
205
293
|
...Object.keys(packageJson?.devDependencies ?? {})
|
|
206
294
|
]);
|
|
207
|
-
const
|
|
208
|
-
|
|
295
|
+
const testFiles = files.filter((file) => !hasLowSignalSegment(file));
|
|
296
|
+
const javascriptTestFiles = testFiles.filter(
|
|
297
|
+
(file) =>
|
|
298
|
+
((/(^|\/)(__tests__|tests?)\//i.test(file) && /\.(m?[jt]sx?|cjs|mjs)$/i.test(file)) ||
|
|
299
|
+
/\.(test|spec)\.(m?[jt]sx?)$/i.test(file))
|
|
209
300
|
);
|
|
210
301
|
|
|
211
302
|
if (dependencyNames.has("vitest")) {
|
|
@@ -217,17 +308,17 @@ function detectTestSignals(files, packageJson, textSources) {
|
|
|
217
308
|
if (javascriptTestFiles.length > 0) {
|
|
218
309
|
signals.push(createSignal("test:javascript-files", javascriptTestFiles, 0.8));
|
|
219
310
|
}
|
|
220
|
-
if (
|
|
221
|
-
signals.push(createSignal("test:pytest",
|
|
311
|
+
if (testFiles.some((file) => /^tests\/.+\.py$/i.test(file) || /test_.+\.py$/i.test(file))) {
|
|
312
|
+
signals.push(createSignal("test:pytest", testFiles.filter((file) => /^tests\/.+\.py$/i.test(file) || /test_.+\.py$/i.test(file)), 0.8));
|
|
222
313
|
}
|
|
223
|
-
if (
|
|
224
|
-
signals.push(createSignal("test:go",
|
|
314
|
+
if (testFiles.some((file) => /_test\.go$/i.test(file))) {
|
|
315
|
+
signals.push(createSignal("test:go", testFiles.filter((file) => /_test\.go$/i.test(file)), 0.8));
|
|
225
316
|
}
|
|
226
|
-
if (
|
|
227
|
-
signals.push(createSignal("test:junit",
|
|
317
|
+
if (testFiles.some((file) => /src\/test\/java\//i.test(file))) {
|
|
318
|
+
signals.push(createSignal("test:junit", testFiles.filter((file) => /src\/test\/java\//i.test(file)), 0.8));
|
|
228
319
|
}
|
|
229
|
-
if (
|
|
230
|
-
signals.push(createSignal("test:xunit",
|
|
320
|
+
if (testFiles.some((file) => /\.Tests?\.csproj$/i.test(file) || (/tests\//i.test(file) && /\.cs$/i.test(file)))) {
|
|
321
|
+
signals.push(createSignal("test:xunit", testFiles.filter((file) => /\.Tests?\.csproj$/i.test(file) || (/tests\//i.test(file) && /\.cs$/i.test(file))), 0.8));
|
|
231
322
|
}
|
|
232
323
|
|
|
233
324
|
const pyproject = textSources.get("pyproject.toml") ?? "";
|
|
@@ -240,6 +331,7 @@ function detectTestSignals(files, packageJson, textSources) {
|
|
|
240
331
|
|
|
241
332
|
function detectDeploymentSignals(files) {
|
|
242
333
|
const signals = [];
|
|
334
|
+
const deploymentFiles = files.filter((file) => !hasLowSignalSegment(file));
|
|
243
335
|
|
|
244
336
|
const markers = [
|
|
245
337
|
["deploy:docker", /^Dockerfile$/i],
|
|
@@ -253,7 +345,7 @@ function detectDeploymentSignals(files) {
|
|
|
253
345
|
];
|
|
254
346
|
|
|
255
347
|
for (const [signalId, pattern] of markers) {
|
|
256
|
-
const evidence =
|
|
348
|
+
const evidence = deploymentFiles.filter((file) => pattern.test(file));
|
|
257
349
|
if (evidence.length > 0) {
|
|
258
350
|
signals.push(createSignal(signalId, evidence, 0.8));
|
|
259
351
|
}
|
|
@@ -264,18 +356,21 @@ function detectDeploymentSignals(files) {
|
|
|
264
356
|
|
|
265
357
|
function detectRiskSignals(files, testSignals) {
|
|
266
358
|
const signals = [];
|
|
359
|
+
const riskFiles = files.filter(isRelevantForRisk);
|
|
360
|
+
const securityEvidence = files.filter(
|
|
361
|
+
(file) => !hasLowSignalSegment(file) && !isSupportSurface(file) && /(security|auth|threat|iam|permissions?)/i.test(file)
|
|
362
|
+
);
|
|
267
363
|
|
|
268
|
-
const securityEvidence = files.filter((file) => /(security|auth|threat|iam|permissions?)/i.test(file));
|
|
269
364
|
if (securityEvidence.length > 0) {
|
|
270
365
|
signals.push(createSignal("security", securityEvidence, 0.86));
|
|
271
366
|
}
|
|
272
367
|
|
|
273
|
-
const legacyEvidence =
|
|
368
|
+
const legacyEvidence = riskFiles.filter((file) => /(legacy|deprecated|old)/i.test(file));
|
|
274
369
|
if (legacyEvidence.length > 0) {
|
|
275
370
|
signals.push(createSignal("legacy", legacyEvidence, 0.88));
|
|
276
371
|
}
|
|
277
372
|
|
|
278
|
-
const migrationEvidence =
|
|
373
|
+
const migrationEvidence = riskFiles.filter((file) => /(migration|migrate|flyway|liquibase)/i.test(file));
|
|
279
374
|
if (migrationEvidence.length > 0) {
|
|
280
375
|
signals.push(createSignal("migration", migrationEvidence, 0.82));
|
|
281
376
|
}
|
|
@@ -289,7 +384,7 @@ function detectRiskSignals(files, testSignals) {
|
|
|
289
384
|
|
|
290
385
|
function detectMissingValidationSurfaces(files, testSignals, buildSignals) {
|
|
291
386
|
const missing = [];
|
|
292
|
-
const fileSet = new Set(files);
|
|
387
|
+
const fileSet = new Set(files.filter((file) => !hasLowSignalSegment(file)));
|
|
293
388
|
|
|
294
389
|
if (testSignals.length === 0) {
|
|
295
390
|
missing.push(
|
|
@@ -297,12 +392,15 @@ function detectMissingValidationSurfaces(files, testSignals, buildSignals) {
|
|
|
297
392
|
);
|
|
298
393
|
}
|
|
299
394
|
|
|
300
|
-
const hasCi =
|
|
395
|
+
const hasCi = [...fileSet].some((file) => /^\.github\/workflows\//i.test(file));
|
|
301
396
|
if (!hasCi) {
|
|
302
397
|
missing.push(createSignal("validation:missing-ci", ["No GitHub Actions workflows detected."], 0.75));
|
|
303
398
|
}
|
|
304
399
|
|
|
305
|
-
const hasLint =
|
|
400
|
+
const hasLint =
|
|
401
|
+
buildSignals.some((signal) => signal.id.startsWith("build:typescript")) ||
|
|
402
|
+
fileSet.has(".eslintrc") ||
|
|
403
|
+
fileSet.has("eslint.config.js");
|
|
306
404
|
if (!hasLint) {
|
|
307
405
|
missing.push(createSignal("validation:missing-lint", ["No lint configuration detected."], 0.65));
|
|
308
406
|
}
|
|
@@ -318,6 +416,7 @@ function detectFrameworksFromText(facts) {
|
|
|
318
416
|
...Object.keys(manifest.devDependencies ?? {})
|
|
319
417
|
])
|
|
320
418
|
);
|
|
419
|
+
const frameworkFiles = files.filter((file) => !hasLowSignalSegment(file));
|
|
321
420
|
const frameworks = [];
|
|
322
421
|
const pushMatch = (id, confidence, evidence) => {
|
|
323
422
|
frameworks.push({
|
|
@@ -327,26 +426,56 @@ function detectFrameworksFromText(facts) {
|
|
|
327
426
|
});
|
|
328
427
|
};
|
|
329
428
|
|
|
330
|
-
if (packageDeps.has("react") ||
|
|
331
|
-
pushMatch(
|
|
429
|
+
if (packageDeps.has("react") || frameworkFiles.some((file) => /\.tsx$/i.test(file))) {
|
|
430
|
+
pushMatch(
|
|
431
|
+
"react",
|
|
432
|
+
packageDeps.has("react") ? 0.96 : 0.7,
|
|
433
|
+
packageDeps.has("react") ? ["package.json dependency react"] : frameworkFiles.filter((file) => /\.tsx$/i.test(file))
|
|
434
|
+
);
|
|
332
435
|
}
|
|
333
|
-
if (packageDeps.has("vite") ||
|
|
334
|
-
pushMatch(
|
|
436
|
+
if (packageDeps.has("vite") || frameworkFiles.some((file) => /^vite\.config\./i.test(path.posix.basename(file)))) {
|
|
437
|
+
pushMatch(
|
|
438
|
+
"vite",
|
|
439
|
+
packageDeps.has("vite") ? 0.95 : 0.8,
|
|
440
|
+
packageDeps.has("vite")
|
|
441
|
+
? ["package.json dependency vite"]
|
|
442
|
+
: frameworkFiles.filter((file) => /^vite\.config\./i.test(path.posix.basename(file)))
|
|
443
|
+
);
|
|
335
444
|
}
|
|
336
445
|
if (packageDeps.has("express")) {
|
|
337
446
|
pushMatch("express", 0.95, ["package.json dependency express"]);
|
|
338
447
|
}
|
|
339
|
-
if (packageDeps.has("next") ||
|
|
340
|
-
pushMatch(
|
|
448
|
+
if (packageDeps.has("next") || frameworkFiles.some((file) => /^next\.config\./i.test(path.posix.basename(file)))) {
|
|
449
|
+
pushMatch(
|
|
450
|
+
"nextjs",
|
|
451
|
+
packageDeps.has("next") ? 0.97 : 0.84,
|
|
452
|
+
packageDeps.has("next")
|
|
453
|
+
? ["package.json dependency next"]
|
|
454
|
+
: frameworkFiles.filter((file) => /^next\.config\./i.test(path.posix.basename(file)))
|
|
455
|
+
);
|
|
341
456
|
}
|
|
342
457
|
|
|
343
458
|
const pyproject = textSources.get("pyproject.toml") ?? "";
|
|
344
459
|
const requirements = textSources.get("requirements.txt") ?? "";
|
|
345
460
|
if (pyproject.includes("fastapi") || requirements.includes("fastapi")) {
|
|
346
|
-
pushMatch(
|
|
461
|
+
pushMatch(
|
|
462
|
+
"fastapi",
|
|
463
|
+
0.95,
|
|
464
|
+
[pyproject.includes("fastapi") ? "pyproject.toml dependency fastapi" : "requirements.txt dependency fastapi"]
|
|
465
|
+
);
|
|
347
466
|
}
|
|
348
|
-
if (pyproject.includes("django") || requirements.includes("django") ||
|
|
349
|
-
pushMatch(
|
|
467
|
+
if (pyproject.includes("django") || requirements.includes("django") || frameworkFiles.includes("manage.py")) {
|
|
468
|
+
pushMatch(
|
|
469
|
+
"django",
|
|
470
|
+
0.94,
|
|
471
|
+
[
|
|
472
|
+
frameworkFiles.includes("manage.py")
|
|
473
|
+
? "manage.py"
|
|
474
|
+
: pyproject.includes("django")
|
|
475
|
+
? "pyproject.toml dependency django"
|
|
476
|
+
: "requirements.txt dependency django"
|
|
477
|
+
]
|
|
478
|
+
);
|
|
350
479
|
}
|
|
351
480
|
|
|
352
481
|
const pom = textSources.get("pom.xml") ?? "";
|
|
@@ -361,7 +490,11 @@ function detectFrameworksFromText(facts) {
|
|
|
361
490
|
const csproj = [...textSources.entries()].find(([filePath]) => /\.csproj$/i.test(filePath))?.[1] ?? "";
|
|
362
491
|
const programCs = textSources.get("Program.cs") ?? "";
|
|
363
492
|
if (csproj.includes("Microsoft.NET.Sdk.Web") || programCs.includes("WebApplication.CreateBuilder")) {
|
|
364
|
-
pushMatch(
|
|
493
|
+
pushMatch(
|
|
494
|
+
"aspnet-core",
|
|
495
|
+
csproj.includes("Microsoft.NET.Sdk.Web") ? 0.96 : 0.86,
|
|
496
|
+
[csproj.includes("Microsoft.NET.Sdk.Web") ? "Api.csproj Microsoft.NET.Sdk.Web" : "Program.cs WebApplication.CreateBuilder"]
|
|
497
|
+
);
|
|
365
498
|
}
|
|
366
499
|
|
|
367
500
|
const goMod = textSources.get("go.mod") ?? "";
|
|
@@ -370,15 +503,27 @@ function detectFrameworksFromText(facts) {
|
|
|
370
503
|
.map(([, content]) => content)
|
|
371
504
|
.join("\n");
|
|
372
505
|
if (goMod.includes("gin-gonic/gin") || goSources.includes("gin.Default(")) {
|
|
373
|
-
pushMatch(
|
|
506
|
+
pushMatch(
|
|
507
|
+
"gin",
|
|
508
|
+
goMod.includes("gin-gonic/gin") ? 0.93 : 0.82,
|
|
509
|
+
[goMod.includes("gin-gonic/gin") ? "go.mod gin-gonic/gin" : "gin.Default()"]
|
|
510
|
+
);
|
|
374
511
|
}
|
|
375
512
|
|
|
376
513
|
const composer = textSources.get("composer.json") ?? "";
|
|
377
|
-
if (composer.includes("symfony/") ||
|
|
378
|
-
pushMatch(
|
|
514
|
+
if (composer.includes("symfony/") || frameworkFiles.includes("bin/console")) {
|
|
515
|
+
pushMatch(
|
|
516
|
+
"symfony",
|
|
517
|
+
composer.includes("symfony/") ? 0.92 : 0.82,
|
|
518
|
+
[composer.includes("symfony/") ? "composer.json symfony/*" : "bin/console"]
|
|
519
|
+
);
|
|
379
520
|
}
|
|
380
|
-
if (composer.includes("laravel/framework") ||
|
|
381
|
-
pushMatch(
|
|
521
|
+
if (composer.includes("laravel/framework") || frameworkFiles.includes("artisan")) {
|
|
522
|
+
pushMatch(
|
|
523
|
+
"laravel",
|
|
524
|
+
composer.includes("laravel/framework") ? 0.96 : 0.88,
|
|
525
|
+
[composer.includes("laravel/framework") ? "composer.json laravel/framework" : "artisan"]
|
|
526
|
+
);
|
|
382
527
|
}
|
|
383
528
|
|
|
384
529
|
return sortSignals(frameworks);
|
|
@@ -386,7 +531,7 @@ function detectFrameworksFromText(facts) {
|
|
|
386
531
|
|
|
387
532
|
function detectRepoType(facts) {
|
|
388
533
|
const { files, buildSignals, frameworkMatches, riskSignals, packageJson } = facts;
|
|
389
|
-
const fileSet = new Set(files);
|
|
534
|
+
const fileSet = new Set(files.filter((file) => !hasLowSignalSegment(file)));
|
|
390
535
|
|
|
391
536
|
if (buildSignals.some((signal) => signal.id.startsWith("workspace:"))) {
|
|
392
537
|
return "monorepo";
|
|
@@ -394,7 +539,11 @@ function detectRepoType(facts) {
|
|
|
394
539
|
if (frameworkMatches.some((match) => ["react", "vite", "nextjs"].includes(match.id))) {
|
|
395
540
|
return "app";
|
|
396
541
|
}
|
|
397
|
-
if (
|
|
542
|
+
if (
|
|
543
|
+
frameworkMatches.some((match) =>
|
|
544
|
+
["express", "fastapi", "django", "spring-boot", "aspnet-core", "gin", "ktor", "symfony", "laravel"].includes(match.id)
|
|
545
|
+
)
|
|
546
|
+
) {
|
|
398
547
|
return "service";
|
|
399
548
|
}
|
|
400
549
|
if (fileSet.has("cmd/hforge/main.go") || packageJson?.bin) {
|
|
@@ -403,20 +552,25 @@ function detectRepoType(facts) {
|
|
|
403
552
|
if (riskSignals.some((signal) => signal.id === "legacy")) {
|
|
404
553
|
return "legacy";
|
|
405
554
|
}
|
|
406
|
-
if (
|
|
555
|
+
if ([...fileSet].some((file) => file.startsWith("scripts/")) && [...fileSet].every((file) => !file.startsWith("src/"))) {
|
|
407
556
|
return "automation";
|
|
408
557
|
}
|
|
409
558
|
|
|
410
559
|
return "library";
|
|
411
560
|
}
|
|
412
561
|
|
|
413
|
-
function normalizeRecommendations(result, frameworkCatalog) {
|
|
562
|
+
function normalizeRecommendations(result, frameworkCatalog, languageCatalog) {
|
|
414
563
|
const bundleRecommendations = [];
|
|
415
564
|
const profileRecommendations = [];
|
|
416
565
|
const skillRecommendations = [];
|
|
417
566
|
const validationRecommendations = [];
|
|
567
|
+
const knownLanguageIds = new Set(Object.keys(languageCatalog.languages ?? {}));
|
|
418
568
|
|
|
419
569
|
for (const language of result.dominantLanguages.slice(0, 3)) {
|
|
570
|
+
if (!knownLanguageIds.has(language.id)) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
420
574
|
bundleRecommendations.push({
|
|
421
575
|
id: `lang:${language.id}`,
|
|
422
576
|
kind: "bundle",
|
|
@@ -460,6 +614,7 @@ function normalizeRecommendations(result, frameworkCatalog) {
|
|
|
460
614
|
});
|
|
461
615
|
|
|
462
616
|
const riskIds = new Set(result.riskSignals.map((signal) => signal.id));
|
|
617
|
+
const languageIds = new Set(result.dominantLanguages.map((language) => language.id));
|
|
463
618
|
if (result.repoType === "monorepo" || result.repoType === "legacy" || result.dominantLanguages.length > 1) {
|
|
464
619
|
skillRecommendations.push({
|
|
465
620
|
id: "skill:repo-onboarding",
|
|
@@ -477,12 +632,29 @@ function normalizeRecommendations(result, frameworkCatalog) {
|
|
|
477
632
|
});
|
|
478
633
|
}
|
|
479
634
|
|
|
480
|
-
if (
|
|
635
|
+
if (languageIds.has("javascript")) {
|
|
636
|
+
skillRecommendations.push({
|
|
637
|
+
id: "skill:javascript-engineering",
|
|
638
|
+
kind: "skill",
|
|
639
|
+
confidence: 0.84,
|
|
640
|
+
evidence: result.dominantLanguages.find((language) => language.id === "javascript")?.evidence ?? [],
|
|
641
|
+
why: "JavaScript-heavy repositories benefit from the dedicated JavaScript engineering skill without forcing the TypeScript bundle."
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (
|
|
646
|
+
riskIds.has("security") ||
|
|
647
|
+
result.frameworkMatches.some((framework) =>
|
|
648
|
+
["express", "fastapi", "django", "spring-boot", "aspnet-core", "gin", "ktor", "symfony", "laravel"].includes(framework.id)
|
|
649
|
+
)
|
|
650
|
+
) {
|
|
481
651
|
skillRecommendations.push({
|
|
482
652
|
id: "skill:security-scan",
|
|
483
653
|
kind: "skill",
|
|
484
654
|
confidence: riskIds.has("security") ? 0.95 : 0.76,
|
|
485
|
-
evidence: riskIds.has("security")
|
|
655
|
+
evidence: riskIds.has("security")
|
|
656
|
+
? result.riskSignals.find((signal) => signal.id === "security")?.evidence ?? []
|
|
657
|
+
: result.frameworkMatches.map((framework) => `framework:${framework.id}`),
|
|
486
658
|
why: "Service and security-sensitive repositories need an explicit security boundary review."
|
|
487
659
|
});
|
|
488
660
|
}
|
|
@@ -552,6 +724,10 @@ export async function collectRepoFacts(root) {
|
|
|
552
724
|
|
|
553
725
|
const interestingFiles = new Set(
|
|
554
726
|
files.filter((file) => {
|
|
727
|
+
if (hasLowSignalSegment(file)) {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
555
731
|
const baseName = path.posix.basename(file);
|
|
556
732
|
return (
|
|
557
733
|
baseName === "package.json" ||
|
|
@@ -579,7 +755,9 @@ export async function collectRepoFacts(root) {
|
|
|
579
755
|
})
|
|
580
756
|
);
|
|
581
757
|
|
|
582
|
-
const packageManifestPaths = files.filter(
|
|
758
|
+
const packageManifestPaths = files.filter(
|
|
759
|
+
(file) => path.posix.basename(file) === "package.json" && !hasLowSignalSegment(file)
|
|
760
|
+
);
|
|
583
761
|
const packageManifestEntries = await Promise.all(
|
|
584
762
|
packageManifestPaths.map(async (relativePath) => ({
|
|
585
763
|
relativePath,
|
|
@@ -628,13 +806,20 @@ export async function loadFrameworkCatalog() {
|
|
|
628
806
|
return JSON.parse(content);
|
|
629
807
|
}
|
|
630
808
|
|
|
809
|
+
export async function loadLanguageCatalog() {
|
|
810
|
+
const languageCatalogPath = path.join(PACKAGE_ROOT, "manifests", "catalog", "language-assets.json");
|
|
811
|
+
const content = await fs.readFile(languageCatalogPath, "utf8");
|
|
812
|
+
return JSON.parse(content);
|
|
813
|
+
}
|
|
814
|
+
|
|
631
815
|
export async function scoreRecommendations(root) {
|
|
632
816
|
const facts = await collectRepoFacts(root);
|
|
633
817
|
const frameworkCatalog = await loadFrameworkCatalog();
|
|
818
|
+
const languageCatalog = await loadLanguageCatalog();
|
|
634
819
|
|
|
635
820
|
return {
|
|
636
821
|
...facts,
|
|
637
|
-
recommendations: normalizeRecommendations(facts, frameworkCatalog)
|
|
822
|
+
recommendations: normalizeRecommendations(facts, frameworkCatalog, languageCatalog)
|
|
638
823
|
};
|
|
639
824
|
}
|
|
640
825
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hforge-analyze
|
|
3
|
+
description: repository analysis, runtime fact refresh, evidence-backed notes, and decision capture using harness forge surfaces. use when the user asks to analyze the repo, understand what matters next, gather findings, inspect risks, or turn architecture-significant changes into durable decision records.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HForge Analyze
|
|
7
|
+
|
|
8
|
+
## Trigger Signals
|
|
9
|
+
|
|
10
|
+
- the user asks for repo analysis, understanding, onboarding, or a summary of what matters next
|
|
11
|
+
- the repo already has Harness Forge runtime artifacts that should be reused instead of rediscovered
|
|
12
|
+
- there are risks, validation gaps, stale findings, or architecture-significant changes to capture
|
|
13
|
+
- the next safe step depends on understanding repo shape, runtime findings, or decision coverage
|
|
14
|
+
|
|
15
|
+
## Inspect First
|
|
16
|
+
|
|
17
|
+
- `AGENTS.md`
|
|
18
|
+
- `.hforge/agent-manifest.json`
|
|
19
|
+
- `.hforge/runtime/index.json`
|
|
20
|
+
- `.hforge/runtime/repo/repo-map.json`
|
|
21
|
+
- `.hforge/runtime/repo/recommendations.json`
|
|
22
|
+
- `.hforge/runtime/repo/instruction-plan.json`
|
|
23
|
+
- `.hforge/runtime/findings/risk-signals.json`
|
|
24
|
+
- `.hforge/runtime/findings/validation-gaps.json`
|
|
25
|
+
- `.hforge/runtime/decisions/index.json`
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
1. confirm the workspace is initialized with `hforge status --root . --json`
|
|
30
|
+
2. if runtime artifacts are missing, initialize first with `hforge bootstrap --root . --yes`
|
|
31
|
+
3. if runtime artifacts are stale, run `hforge refresh --root . --json`
|
|
32
|
+
4. run `hforge review --root . --json` when runtime health, decision coverage, or stale task artifacts matter
|
|
33
|
+
5. summarize repo shape, dominant implementation surfaces, risks, validation gaps, and next actions using the existing runtime as the primary source of truth
|
|
34
|
+
6. when the analysis reveals architecture-significant work, create or update a durable ASR or ADR under `.hforge/runtime/decisions/`
|
|
35
|
+
|
|
36
|
+
## References
|
|
37
|
+
|
|
38
|
+
- `skills/hforge-analyze/references/analysis-order.md`
|
|
39
|
+
- `skills/hforge-analyze/references/output-contract.md`
|
|
40
|
+
- `skills/hforge-analyze/references/decision-promotion.md`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Analysis order
|
|
2
|
+
|
|
3
|
+
Use this order to keep analysis deterministic and low-noise.
|
|
4
|
+
|
|
5
|
+
1. `hforge status --root . --json`
|
|
6
|
+
2. `.hforge/agent-manifest.json`
|
|
7
|
+
3. `.hforge/runtime/index.json`
|
|
8
|
+
4. `.hforge/runtime/repo/repo-map.json`
|
|
9
|
+
5. `.hforge/runtime/repo/recommendations.json`
|
|
10
|
+
6. `.hforge/runtime/repo/instruction-plan.json`
|
|
11
|
+
7. `.hforge/runtime/findings/risk-signals.json`
|
|
12
|
+
8. `.hforge/runtime/findings/validation-gaps.json`
|
|
13
|
+
9. `.hforge/runtime/decisions/index.json`
|
|
14
|
+
10. `hforge review --root . --json` when runtime health or stale tasks matter
|
|
15
|
+
11. `hforge refresh --root . --json` only when the existing runtime is missing or stale
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Decision promotion
|
|
2
|
+
|
|
3
|
+
Promote a finding into `.hforge/runtime/decisions/` when one or more of these are true:
|
|
4
|
+
- the change affects runtime boundaries, package layout, support posture, or multi-target behavior
|
|
5
|
+
- the choice has meaningful trade-offs and should stay reviewable later
|
|
6
|
+
- the work changes upgrade, migration, validation, or release expectations
|
|
7
|
+
|
|
8
|
+
Use an ASR when the direction is still being evaluated.
|
|
9
|
+
Use an ADR when the decision is accepted enough to guide future work.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Output contract
|
|
2
|
+
|
|
3
|
+
A good `/hforge-analyze` result should include:
|
|
4
|
+
- a short repo-shape summary
|
|
5
|
+
- 3 to 7 high-signal findings, not a raw dump
|
|
6
|
+
- the concrete runtime artifact or command that supports each next step
|
|
7
|
+
- explicit decision candidates when the work is architecture-significant
|