@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.
Files changed (57) hide show
  1. package/.agents/skills/hforge-analyze/SKILL.md +35 -0
  2. package/.agents/skills/hforge-decide/SKILL.md +29 -0
  3. package/.agents/skills/hforge-init/SKILL.md +34 -0
  4. package/.agents/skills/hforge-refresh/SKILL.md +28 -0
  5. package/.agents/skills/hforge-review/SKILL.md +29 -0
  6. package/AGENTS.md +2 -2
  7. package/README.md +5 -2
  8. package/commands/hforge-cartograph.md +35 -0
  9. package/commands/hforge-commands.md +34 -0
  10. package/commands/hforge-decide.md +35 -0
  11. package/commands/hforge-init.md +37 -0
  12. package/commands/hforge-recommend.md +34 -0
  13. package/commands/hforge-recursive.md +34 -0
  14. package/commands/hforge-refresh.md +35 -0
  15. package/commands/hforge-review.md +36 -0
  16. package/commands/hforge-status.md +34 -0
  17. package/commands/hforge-task.md +35 -0
  18. package/commands/hforge-update.md +34 -0
  19. package/dist/application/install/agent-manifest.d.ts +8 -0
  20. package/dist/application/install/agent-manifest.d.ts.map +1 -1
  21. package/dist/application/install/agent-manifest.js +7 -0
  22. package/dist/application/install/agent-manifest.js.map +1 -1
  23. package/dist/application/recommendations/recommend-bundles.d.ts.map +1 -1
  24. package/dist/application/recommendations/recommend-bundles.js +41 -2
  25. package/dist/application/recommendations/recommend-bundles.js.map +1 -1
  26. package/dist/application/runtime/command-catalog.d.ts +7 -0
  27. package/dist/application/runtime/command-catalog.d.ts.map +1 -1
  28. package/dist/application/runtime/command-catalog.js +101 -0
  29. package/dist/application/runtime/command-catalog.js.map +1 -1
  30. package/dist/cli/commands/commands.d.ts.map +1 -1
  31. package/dist/cli/commands/commands.js +5 -1
  32. package/dist/cli/commands/commands.js.map +1 -1
  33. package/dist/domain/intelligence/repo-intelligence.js +1 -1
  34. package/dist/domain/intelligence/repo-intelligence.js.map +1 -1
  35. package/docs/agent-usage-playbook.md +12 -4
  36. package/docs/commands.md +12 -1
  37. package/manifests/bundles/core.json +11 -0
  38. package/manifests/catalog/compatibility-matrix.json +171 -1
  39. package/manifests/catalog/package-surface.json +42 -0
  40. package/package.json +1 -1
  41. package/scripts/intelligence/shared.mjs +187 -51
  42. package/skills/hforge-analyze/SKILL.md +40 -0
  43. package/skills/hforge-analyze/references/analysis-order.md +15 -0
  44. package/skills/hforge-analyze/references/decision-promotion.md +9 -0
  45. package/skills/hforge-analyze/references/output-contract.md +7 -0
  46. package/skills/hforge-decide/SKILL.md +58 -0
  47. package/skills/hforge-decide/references/decision-rubric.md +18 -0
  48. package/skills/hforge-decide/references/output-contract.md +7 -0
  49. package/skills/hforge-init/SKILL.md +58 -0
  50. package/skills/hforge-init/references/bootstrap-order.md +7 -0
  51. package/skills/hforge-init/references/output-contract.md +7 -0
  52. package/skills/hforge-refresh/SKILL.md +52 -0
  53. package/skills/hforge-refresh/references/output-contract.md +7 -0
  54. package/skills/hforge-refresh/references/refresh-order.md +5 -0
  55. package/skills/hforge-review/SKILL.md +57 -0
  56. package/skills/hforge-review/references/output-contract.md +7 -0
  57. 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|js|jsx)$/i.test(file)) {
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|h)$/i.test(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) {
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 = files.filter((file) => pattern.test(path.posix.basename(file)));
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 javascriptTestFiles = files.filter(
257
- (file) => /(^|\/)(__tests__|tests?)\//i.test(file) && /\.(m?[jt]sx?|cjs|mjs)$/i.test(file) || /\.(test|spec)\.(m?[jt]sx?)$/i.test(file)
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 (files.some((file) => /^tests\/.+\.py$/i.test(file) || /test_.+\.py$/i.test(file))) {
270
- signals.push(createSignal("test:pytest", files.filter((file) => /^tests\/.+\.py$/i.test(file) || /test_.+\.py$/i.test(file)), 0.8));
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 (files.some((file) => /_test\.go$/i.test(file))) {
273
- signals.push(createSignal("test:go", files.filter((file) => /_test\.go$/i.test(file)), 0.8));
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 (files.some((file) => /src\/test\/java\//i.test(file))) {
276
- signals.push(createSignal("test:junit", files.filter((file) => /src\/test\/java\//i.test(file)), 0.8));
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 (files.some((file) => /\.Tests?\.csproj$/i.test(file) || /tests\//i.test(file) && /\.cs$/i.test(file))) {
279
- signals.push(createSignal("test:xunit", files.filter((file) => /\.Tests?\.csproj$/i.test(file) || (/tests\//i.test(file) && /\.cs$/i.test(file))), 0.8));
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 = files.filter((file) => pattern.test(file));
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 = files.filter((file) => /(legacy|deprecated|old)/i.test(file));
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 = files.filter((file) => /(migration|migrate|flyway|liquibase)/i.test(file));
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 = files.some((file) => /^\.github\/workflows\//i.test(file));
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 = buildSignals.some((signal) => signal.id.startsWith("build:typescript")) || fileSet.has(".eslintrc") || fileSet.has("eslint.config.js");
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") || files.some((file) => /\.tsx$/i.test(file))) {
380
- pushMatch("react", packageDeps.has("react") ? 0.96 : 0.7, packageDeps.has("react") ? ["package.json dependency react"] : files.filter((file) => /\.tsx$/i.test(file)));
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") || files.some((file) => /^vite\.config\./i.test(path.posix.basename(file)))) {
383
- pushMatch("vite", packageDeps.has("vite") ? 0.95 : 0.8, packageDeps.has("vite") ? ["package.json dependency vite"] : files.filter((file) => /^vite\.config\./i.test(path.posix.basename(file))));
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") || files.some((file) => /^next\.config\./i.test(path.posix.basename(file)))) {
389
- pushMatch("nextjs", packageDeps.has("next") ? 0.97 : 0.84, packageDeps.has("next") ? ["package.json dependency next"] : files.filter((file) => /^next\.config\./i.test(path.posix.basename(file))));
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("fastapi", 0.95, [pyproject.includes("fastapi") ? "pyproject.toml dependency fastapi" : "requirements.txt dependency fastapi"]);
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") || files.includes("manage.py")) {
398
- pushMatch("django", 0.94, [files.includes("manage.py") ? "manage.py" : pyproject.includes("django") ? "pyproject.toml dependency django" : "requirements.txt dependency django"]);
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("aspnet-core", csproj.includes("Microsoft.NET.Sdk.Web") ? 0.96 : 0.86, [csproj.includes("Microsoft.NET.Sdk.Web") ? "Api.csproj Microsoft.NET.Sdk.Web" : "Program.cs WebApplication.CreateBuilder"]);
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("gin", goMod.includes("gin-gonic/gin") ? 0.93 : 0.82, [goMod.includes("gin-gonic/gin") ? "go.mod gin-gonic/gin" : "gin.Default()"]);
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/") || files.includes("bin/console")) {
427
- pushMatch("symfony", composer.includes("symfony/") ? 0.92 : 0.82, [composer.includes("symfony/") ? "composer.json symfony/*" : "bin/console"]);
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") || files.includes("artisan")) {
430
- pushMatch("laravel", composer.includes("laravel/framework") ? 0.96 : 0.88, [composer.includes("laravel/framework") ? "composer.json laravel/framework" : "artisan"]);
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 (frameworkMatches.some((match) => ["express", "fastapi", "django", "spring-boot", "aspnet-core", "gin", "ktor", "symfony", "laravel"].includes(match.id))) {
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 (files.some((file) => file.startsWith("scripts/")) && files.every((file) => !file.startsWith("src/"))) {
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 (riskIds.has("security") || result.frameworkMatches.some((framework) => ["express", "fastapi", "django", "spring-boot", "aspnet-core", "gin", "ktor", "symfony", "laravel"].includes(framework.id))) {
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") ? result.riskSignals.find((signal) => signal.id === "security")?.evidence ?? [] : result.frameworkMatches.map((framework) => `framework:${framework.id}`),
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((file) => path.posix.basename(file) === "package.json");
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
@@ -0,0 +1,7 @@
1
+ # Output contract
2
+
3
+ A good `/hforge-decide` result should include:
4
+ - the record type and why it was chosen
5
+ - the decision statement in one or two lines
6
+ - constraints, risks, and consequences
7
+ - task refs or runtime evidence that justify the record