@harness-forge/cli 1.2.2 → 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 +2 -2
- package/README.md +5 -2
- 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 +12 -4
- package/docs/commands.md +12 -1
- package/manifests/bundles/core.json +11 -0
- package/manifests/catalog/compatibility-matrix.json +171 -1
- package/manifests/catalog/package-surface.json +42 -0
- package/package.json +1 -1
- package/scripts/intelligence/shared.mjs +187 -51
- 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
|
@@ -20,18 +20,47 @@ const IGNORED_DIRS = new Set([
|
|
|
20
20
|
"vendor"
|
|
21
21
|
]);
|
|
22
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
|
+
|
|
23
35
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
24
36
|
|
|
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
|
+
|
|
25
60
|
function languageWeightForPath(file) {
|
|
26
61
|
const normalized = file.replaceAll("\\", "/");
|
|
27
62
|
|
|
28
|
-
if (
|
|
29
|
-
normalized.startsWith(".claude/") ||
|
|
30
|
-
normalized.startsWith(".codex/") ||
|
|
31
|
-
normalized.startsWith(".cursor/") ||
|
|
32
|
-
normalized.startsWith(".agents/") ||
|
|
33
|
-
normalized.startsWith(".github/")
|
|
34
|
-
) {
|
|
63
|
+
if (isSupportSurface(normalized) || hasLowSignalSegment(normalized)) {
|
|
35
64
|
return 0;
|
|
36
65
|
}
|
|
37
66
|
|
|
@@ -144,6 +173,10 @@ async function walk(root, visit, base = root) {
|
|
|
144
173
|
|
|
145
174
|
function detectLanguages(files) {
|
|
146
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
|
+
});
|
|
147
180
|
|
|
148
181
|
for (const file of files) {
|
|
149
182
|
const weight = languageWeightForPath(file);
|
|
@@ -151,9 +184,12 @@ function detectLanguages(files) {
|
|
|
151
184
|
continue;
|
|
152
185
|
}
|
|
153
186
|
|
|
154
|
-
if (/\.(ts|tsx
|
|
187
|
+
if (/\.(ts|tsx)$/i.test(file)) {
|
|
155
188
|
bump(languageCounts, "typescript", file, weight);
|
|
156
189
|
}
|
|
190
|
+
if (/\.(js|jsx)$/i.test(file)) {
|
|
191
|
+
bump(languageCounts, "javascript", file, weight);
|
|
192
|
+
}
|
|
157
193
|
if (/\.py$/i.test(file)) {
|
|
158
194
|
bump(languageCounts, "python", file, weight);
|
|
159
195
|
}
|
|
@@ -169,7 +205,10 @@ function detectLanguages(files) {
|
|
|
169
205
|
if (/\.rs$/i.test(file)) {
|
|
170
206
|
bump(languageCounts, "rust", file, weight);
|
|
171
207
|
}
|
|
172
|
-
if (/\.(cpp|cxx|cc|hpp|hh
|
|
208
|
+
if (/\.(cpp|cxx|cc|hpp|hh)$/i.test(file)) {
|
|
209
|
+
bump(languageCounts, "cpp", file, weight);
|
|
210
|
+
}
|
|
211
|
+
if (/\.h$/i.test(file) && hasCppSource) {
|
|
173
212
|
bump(languageCounts, "cpp", file, weight);
|
|
174
213
|
}
|
|
175
214
|
if (/\.php$/i.test(file)) {
|
|
@@ -206,7 +245,7 @@ function detectLanguages(files) {
|
|
|
206
245
|
|
|
207
246
|
function detectBuildSignals(files, packageJson) {
|
|
208
247
|
const signals = [];
|
|
209
|
-
const fileSet = new Set(files);
|
|
248
|
+
const fileSet = new Set(files.filter((file) => !hasLowSignalSegment(file)));
|
|
210
249
|
|
|
211
250
|
if (packageJson) {
|
|
212
251
|
const packageManager = typeof packageJson.packageManager === "string" ? packageJson.packageManager.split("@")[0] : "npm";
|
|
@@ -234,7 +273,7 @@ function detectBuildSignals(files, packageJson) {
|
|
|
234
273
|
];
|
|
235
274
|
|
|
236
275
|
for (const [signalId, pattern] of directBuildMarkers) {
|
|
237
|
-
const evidence =
|
|
276
|
+
const evidence = [...fileSet].filter((file) => pattern.test(path.posix.basename(file)));
|
|
238
277
|
if (evidence.length > 0) {
|
|
239
278
|
signals.push(createSignal(signalId, evidence, 0.8));
|
|
240
279
|
}
|
|
@@ -253,8 +292,11 @@ function detectTestSignals(files, packageJson, textSources) {
|
|
|
253
292
|
...Object.keys(packageJson?.dependencies ?? {}),
|
|
254
293
|
...Object.keys(packageJson?.devDependencies ?? {})
|
|
255
294
|
]);
|
|
256
|
-
const
|
|
257
|
-
|
|
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))
|
|
258
300
|
);
|
|
259
301
|
|
|
260
302
|
if (dependencyNames.has("vitest")) {
|
|
@@ -266,17 +308,17 @@ function detectTestSignals(files, packageJson, textSources) {
|
|
|
266
308
|
if (javascriptTestFiles.length > 0) {
|
|
267
309
|
signals.push(createSignal("test:javascript-files", javascriptTestFiles, 0.8));
|
|
268
310
|
}
|
|
269
|
-
if (
|
|
270
|
-
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));
|
|
271
313
|
}
|
|
272
|
-
if (
|
|
273
|
-
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));
|
|
274
316
|
}
|
|
275
|
-
if (
|
|
276
|
-
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));
|
|
277
319
|
}
|
|
278
|
-
if (
|
|
279
|
-
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));
|
|
280
322
|
}
|
|
281
323
|
|
|
282
324
|
const pyproject = textSources.get("pyproject.toml") ?? "";
|
|
@@ -289,6 +331,7 @@ function detectTestSignals(files, packageJson, textSources) {
|
|
|
289
331
|
|
|
290
332
|
function detectDeploymentSignals(files) {
|
|
291
333
|
const signals = [];
|
|
334
|
+
const deploymentFiles = files.filter((file) => !hasLowSignalSegment(file));
|
|
292
335
|
|
|
293
336
|
const markers = [
|
|
294
337
|
["deploy:docker", /^Dockerfile$/i],
|
|
@@ -302,7 +345,7 @@ function detectDeploymentSignals(files) {
|
|
|
302
345
|
];
|
|
303
346
|
|
|
304
347
|
for (const [signalId, pattern] of markers) {
|
|
305
|
-
const evidence =
|
|
348
|
+
const evidence = deploymentFiles.filter((file) => pattern.test(file));
|
|
306
349
|
if (evidence.length > 0) {
|
|
307
350
|
signals.push(createSignal(signalId, evidence, 0.8));
|
|
308
351
|
}
|
|
@@ -313,18 +356,21 @@ function detectDeploymentSignals(files) {
|
|
|
313
356
|
|
|
314
357
|
function detectRiskSignals(files, testSignals) {
|
|
315
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
|
+
);
|
|
316
363
|
|
|
317
|
-
const securityEvidence = files.filter((file) => /(security|auth|threat|iam|permissions?)/i.test(file));
|
|
318
364
|
if (securityEvidence.length > 0) {
|
|
319
365
|
signals.push(createSignal("security", securityEvidence, 0.86));
|
|
320
366
|
}
|
|
321
367
|
|
|
322
|
-
const legacyEvidence =
|
|
368
|
+
const legacyEvidence = riskFiles.filter((file) => /(legacy|deprecated|old)/i.test(file));
|
|
323
369
|
if (legacyEvidence.length > 0) {
|
|
324
370
|
signals.push(createSignal("legacy", legacyEvidence, 0.88));
|
|
325
371
|
}
|
|
326
372
|
|
|
327
|
-
const migrationEvidence =
|
|
373
|
+
const migrationEvidence = riskFiles.filter((file) => /(migration|migrate|flyway|liquibase)/i.test(file));
|
|
328
374
|
if (migrationEvidence.length > 0) {
|
|
329
375
|
signals.push(createSignal("migration", migrationEvidence, 0.82));
|
|
330
376
|
}
|
|
@@ -338,7 +384,7 @@ function detectRiskSignals(files, testSignals) {
|
|
|
338
384
|
|
|
339
385
|
function detectMissingValidationSurfaces(files, testSignals, buildSignals) {
|
|
340
386
|
const missing = [];
|
|
341
|
-
const fileSet = new Set(files);
|
|
387
|
+
const fileSet = new Set(files.filter((file) => !hasLowSignalSegment(file)));
|
|
342
388
|
|
|
343
389
|
if (testSignals.length === 0) {
|
|
344
390
|
missing.push(
|
|
@@ -346,12 +392,15 @@ function detectMissingValidationSurfaces(files, testSignals, buildSignals) {
|
|
|
346
392
|
);
|
|
347
393
|
}
|
|
348
394
|
|
|
349
|
-
const hasCi =
|
|
395
|
+
const hasCi = [...fileSet].some((file) => /^\.github\/workflows\//i.test(file));
|
|
350
396
|
if (!hasCi) {
|
|
351
397
|
missing.push(createSignal("validation:missing-ci", ["No GitHub Actions workflows detected."], 0.75));
|
|
352
398
|
}
|
|
353
399
|
|
|
354
|
-
const hasLint =
|
|
400
|
+
const hasLint =
|
|
401
|
+
buildSignals.some((signal) => signal.id.startsWith("build:typescript")) ||
|
|
402
|
+
fileSet.has(".eslintrc") ||
|
|
403
|
+
fileSet.has("eslint.config.js");
|
|
355
404
|
if (!hasLint) {
|
|
356
405
|
missing.push(createSignal("validation:missing-lint", ["No lint configuration detected."], 0.65));
|
|
357
406
|
}
|
|
@@ -367,6 +416,7 @@ function detectFrameworksFromText(facts) {
|
|
|
367
416
|
...Object.keys(manifest.devDependencies ?? {})
|
|
368
417
|
])
|
|
369
418
|
);
|
|
419
|
+
const frameworkFiles = files.filter((file) => !hasLowSignalSegment(file));
|
|
370
420
|
const frameworks = [];
|
|
371
421
|
const pushMatch = (id, confidence, evidence) => {
|
|
372
422
|
frameworks.push({
|
|
@@ -376,26 +426,56 @@ function detectFrameworksFromText(facts) {
|
|
|
376
426
|
});
|
|
377
427
|
};
|
|
378
428
|
|
|
379
|
-
if (packageDeps.has("react") ||
|
|
380
|
-
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
|
+
);
|
|
381
435
|
}
|
|
382
|
-
if (packageDeps.has("vite") ||
|
|
383
|
-
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
|
+
);
|
|
384
444
|
}
|
|
385
445
|
if (packageDeps.has("express")) {
|
|
386
446
|
pushMatch("express", 0.95, ["package.json dependency express"]);
|
|
387
447
|
}
|
|
388
|
-
if (packageDeps.has("next") ||
|
|
389
|
-
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
|
+
);
|
|
390
456
|
}
|
|
391
457
|
|
|
392
458
|
const pyproject = textSources.get("pyproject.toml") ?? "";
|
|
393
459
|
const requirements = textSources.get("requirements.txt") ?? "";
|
|
394
460
|
if (pyproject.includes("fastapi") || requirements.includes("fastapi")) {
|
|
395
|
-
pushMatch(
|
|
461
|
+
pushMatch(
|
|
462
|
+
"fastapi",
|
|
463
|
+
0.95,
|
|
464
|
+
[pyproject.includes("fastapi") ? "pyproject.toml dependency fastapi" : "requirements.txt dependency fastapi"]
|
|
465
|
+
);
|
|
396
466
|
}
|
|
397
|
-
if (pyproject.includes("django") || requirements.includes("django") ||
|
|
398
|
-
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
|
+
);
|
|
399
479
|
}
|
|
400
480
|
|
|
401
481
|
const pom = textSources.get("pom.xml") ?? "";
|
|
@@ -410,7 +490,11 @@ function detectFrameworksFromText(facts) {
|
|
|
410
490
|
const csproj = [...textSources.entries()].find(([filePath]) => /\.csproj$/i.test(filePath))?.[1] ?? "";
|
|
411
491
|
const programCs = textSources.get("Program.cs") ?? "";
|
|
412
492
|
if (csproj.includes("Microsoft.NET.Sdk.Web") || programCs.includes("WebApplication.CreateBuilder")) {
|
|
413
|
-
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
|
+
);
|
|
414
498
|
}
|
|
415
499
|
|
|
416
500
|
const goMod = textSources.get("go.mod") ?? "";
|
|
@@ -419,15 +503,27 @@ function detectFrameworksFromText(facts) {
|
|
|
419
503
|
.map(([, content]) => content)
|
|
420
504
|
.join("\n");
|
|
421
505
|
if (goMod.includes("gin-gonic/gin") || goSources.includes("gin.Default(")) {
|
|
422
|
-
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
|
+
);
|
|
423
511
|
}
|
|
424
512
|
|
|
425
513
|
const composer = textSources.get("composer.json") ?? "";
|
|
426
|
-
if (composer.includes("symfony/") ||
|
|
427
|
-
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
|
+
);
|
|
428
520
|
}
|
|
429
|
-
if (composer.includes("laravel/framework") ||
|
|
430
|
-
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
|
+
);
|
|
431
527
|
}
|
|
432
528
|
|
|
433
529
|
return sortSignals(frameworks);
|
|
@@ -435,7 +531,7 @@ function detectFrameworksFromText(facts) {
|
|
|
435
531
|
|
|
436
532
|
function detectRepoType(facts) {
|
|
437
533
|
const { files, buildSignals, frameworkMatches, riskSignals, packageJson } = facts;
|
|
438
|
-
const fileSet = new Set(files);
|
|
534
|
+
const fileSet = new Set(files.filter((file) => !hasLowSignalSegment(file)));
|
|
439
535
|
|
|
440
536
|
if (buildSignals.some((signal) => signal.id.startsWith("workspace:"))) {
|
|
441
537
|
return "monorepo";
|
|
@@ -443,7 +539,11 @@ function detectRepoType(facts) {
|
|
|
443
539
|
if (frameworkMatches.some((match) => ["react", "vite", "nextjs"].includes(match.id))) {
|
|
444
540
|
return "app";
|
|
445
541
|
}
|
|
446
|
-
if (
|
|
542
|
+
if (
|
|
543
|
+
frameworkMatches.some((match) =>
|
|
544
|
+
["express", "fastapi", "django", "spring-boot", "aspnet-core", "gin", "ktor", "symfony", "laravel"].includes(match.id)
|
|
545
|
+
)
|
|
546
|
+
) {
|
|
447
547
|
return "service";
|
|
448
548
|
}
|
|
449
549
|
if (fileSet.has("cmd/hforge/main.go") || packageJson?.bin) {
|
|
@@ -452,20 +552,25 @@ function detectRepoType(facts) {
|
|
|
452
552
|
if (riskSignals.some((signal) => signal.id === "legacy")) {
|
|
453
553
|
return "legacy";
|
|
454
554
|
}
|
|
455
|
-
if (
|
|
555
|
+
if ([...fileSet].some((file) => file.startsWith("scripts/")) && [...fileSet].every((file) => !file.startsWith("src/"))) {
|
|
456
556
|
return "automation";
|
|
457
557
|
}
|
|
458
558
|
|
|
459
559
|
return "library";
|
|
460
560
|
}
|
|
461
561
|
|
|
462
|
-
function normalizeRecommendations(result, frameworkCatalog) {
|
|
562
|
+
function normalizeRecommendations(result, frameworkCatalog, languageCatalog) {
|
|
463
563
|
const bundleRecommendations = [];
|
|
464
564
|
const profileRecommendations = [];
|
|
465
565
|
const skillRecommendations = [];
|
|
466
566
|
const validationRecommendations = [];
|
|
567
|
+
const knownLanguageIds = new Set(Object.keys(languageCatalog.languages ?? {}));
|
|
467
568
|
|
|
468
569
|
for (const language of result.dominantLanguages.slice(0, 3)) {
|
|
570
|
+
if (!knownLanguageIds.has(language.id)) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
469
574
|
bundleRecommendations.push({
|
|
470
575
|
id: `lang:${language.id}`,
|
|
471
576
|
kind: "bundle",
|
|
@@ -509,6 +614,7 @@ function normalizeRecommendations(result, frameworkCatalog) {
|
|
|
509
614
|
});
|
|
510
615
|
|
|
511
616
|
const riskIds = new Set(result.riskSignals.map((signal) => signal.id));
|
|
617
|
+
const languageIds = new Set(result.dominantLanguages.map((language) => language.id));
|
|
512
618
|
if (result.repoType === "monorepo" || result.repoType === "legacy" || result.dominantLanguages.length > 1) {
|
|
513
619
|
skillRecommendations.push({
|
|
514
620
|
id: "skill:repo-onboarding",
|
|
@@ -526,12 +632,29 @@ function normalizeRecommendations(result, frameworkCatalog) {
|
|
|
526
632
|
});
|
|
527
633
|
}
|
|
528
634
|
|
|
529
|
-
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
|
+
) {
|
|
530
651
|
skillRecommendations.push({
|
|
531
652
|
id: "skill:security-scan",
|
|
532
653
|
kind: "skill",
|
|
533
654
|
confidence: riskIds.has("security") ? 0.95 : 0.76,
|
|
534
|
-
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}`),
|
|
535
658
|
why: "Service and security-sensitive repositories need an explicit security boundary review."
|
|
536
659
|
});
|
|
537
660
|
}
|
|
@@ -601,6 +724,10 @@ export async function collectRepoFacts(root) {
|
|
|
601
724
|
|
|
602
725
|
const interestingFiles = new Set(
|
|
603
726
|
files.filter((file) => {
|
|
727
|
+
if (hasLowSignalSegment(file)) {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
604
731
|
const baseName = path.posix.basename(file);
|
|
605
732
|
return (
|
|
606
733
|
baseName === "package.json" ||
|
|
@@ -628,7 +755,9 @@ export async function collectRepoFacts(root) {
|
|
|
628
755
|
})
|
|
629
756
|
);
|
|
630
757
|
|
|
631
|
-
const packageManifestPaths = files.filter(
|
|
758
|
+
const packageManifestPaths = files.filter(
|
|
759
|
+
(file) => path.posix.basename(file) === "package.json" && !hasLowSignalSegment(file)
|
|
760
|
+
);
|
|
632
761
|
const packageManifestEntries = await Promise.all(
|
|
633
762
|
packageManifestPaths.map(async (relativePath) => ({
|
|
634
763
|
relativePath,
|
|
@@ -677,13 +806,20 @@ export async function loadFrameworkCatalog() {
|
|
|
677
806
|
return JSON.parse(content);
|
|
678
807
|
}
|
|
679
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
|
+
|
|
680
815
|
export async function scoreRecommendations(root) {
|
|
681
816
|
const facts = await collectRepoFacts(root);
|
|
682
817
|
const frameworkCatalog = await loadFrameworkCatalog();
|
|
818
|
+
const languageCatalog = await loadLanguageCatalog();
|
|
683
819
|
|
|
684
820
|
return {
|
|
685
821
|
...facts,
|
|
686
|
-
recommendations: normalizeRecommendations(facts, frameworkCatalog)
|
|
822
|
+
recommendations: normalizeRecommendations(facts, frameworkCatalog, languageCatalog)
|
|
687
823
|
};
|
|
688
824
|
}
|
|
689
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
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hforge-decide
|
|
3
|
+
description: create or update durable asr or adr records for harness forge workspaces. use when an analysis or implementation choice changes runtime behavior, package boundaries, support posture, migration strategy, or any other architecture-significant concern that should remain reviewable after the current session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HForge Decide
|
|
7
|
+
|
|
8
|
+
## Trigger Signals
|
|
9
|
+
|
|
10
|
+
- an analysis or implementation choice has architecture-significant consequences
|
|
11
|
+
- the repo already has decision records and the new work should link to them
|
|
12
|
+
- multiple credible options exist and the rationale should stay durable
|
|
13
|
+
- a task pack, impact analysis, or review surfaced a decision candidate that should not stay implicit
|
|
14
|
+
|
|
15
|
+
## Inspect First
|
|
16
|
+
|
|
17
|
+
- `.hforge/runtime/decisions/index.json`
|
|
18
|
+
- `.hforge/runtime/tasks/`
|
|
19
|
+
- `.hforge/runtime/findings/`
|
|
20
|
+
- `.hforge/runtime/repo/recommendations.json`
|
|
21
|
+
- the most relevant existing ASR or ADR if one already exists
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
1. frame the decision clearly before writing anything
|
|
26
|
+
2. inspect related task packs, impact analysis, risk signals, and existing decision records
|
|
27
|
+
3. create an ASR when the direction is still being evaluated and an ADR when the decision is accepted enough to guide future work
|
|
28
|
+
4. write the record under `.hforge/runtime/decisions/` with context, decision, consequences, constraints, risks, and follow-up actions
|
|
29
|
+
5. update supersession or related-record links when the new decision changes an earlier one
|
|
30
|
+
|
|
31
|
+
## Output Contract
|
|
32
|
+
|
|
33
|
+
- a concise ASR or ADR with explicit status and rationale
|
|
34
|
+
- links to related tasks, impacts, and superseded records
|
|
35
|
+
- follow-up actions or open questions when the work is not fully settled
|
|
36
|
+
|
|
37
|
+
## Validation Path
|
|
38
|
+
|
|
39
|
+
- `.hforge/runtime/decisions/index.json`
|
|
40
|
+
- `schemas/runtime/decision-record.schema.json`
|
|
41
|
+
- `schemas/runtime/architecture-significance.schema.json`
|
|
42
|
+
|
|
43
|
+
## Failure Modes
|
|
44
|
+
|
|
45
|
+
- the real decision is still too vague to record
|
|
46
|
+
- there is not enough evidence to justify a durable record yet
|
|
47
|
+
- the new record conflicts with an existing one and the supersession path is unclear
|
|
48
|
+
|
|
49
|
+
## Escalation
|
|
50
|
+
|
|
51
|
+
- escalate when the decision changes public support or target guarantees
|
|
52
|
+
- escalate when migration or rollback cost is materially different across options
|
|
53
|
+
- escalate when a new record would contradict an existing accepted ADR
|
|
54
|
+
|
|
55
|
+
## References
|
|
56
|
+
|
|
57
|
+
- `skills/hforge-decide/references/decision-rubric.md`
|
|
58
|
+
- `skills/hforge-decide/references/output-contract.md`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Decision rubric
|
|
2
|
+
|
|
3
|
+
Create or update a record when the choice affects one or more of these:
|
|
4
|
+
- runtime behavior
|
|
5
|
+
- package or bundle boundaries
|
|
6
|
+
- target support posture
|
|
7
|
+
- migration or rollback strategy
|
|
8
|
+
- validation, release, or maintenance guarantees
|
|
9
|
+
|
|
10
|
+
Prefer ASR when:
|
|
11
|
+
- exploration is still active
|
|
12
|
+
- the trade-offs are being narrowed
|
|
13
|
+
- the work needs a reviewable candidate before acceptance
|
|
14
|
+
|
|
15
|
+
Prefer ADR when:
|
|
16
|
+
- the direction is stable enough to guide future work
|
|
17
|
+
- the team needs a durable accepted rationale
|
|
18
|
+
- the decision supersedes an earlier record
|