@harness-forge/cli 1.2.2 → 1.2.4

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 (69) 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/skills/token-budget-optimizer/SKILL.md +26 -0
  7. package/AGENTS.md +6 -3
  8. package/README.md +9 -5
  9. package/commands/hforge-cartograph.md +35 -0
  10. package/commands/hforge-commands.md +34 -0
  11. package/commands/hforge-decide.md +35 -0
  12. package/commands/hforge-init.md +37 -0
  13. package/commands/hforge-recommend.md +34 -0
  14. package/commands/hforge-recursive.md +34 -0
  15. package/commands/hforge-refresh.md +35 -0
  16. package/commands/hforge-review.md +36 -0
  17. package/commands/hforge-status.md +34 -0
  18. package/commands/hforge-task.md +35 -0
  19. package/commands/hforge-update.md +34 -0
  20. package/dist/application/install/agent-manifest.d.ts +8 -0
  21. package/dist/application/install/agent-manifest.d.ts.map +1 -1
  22. package/dist/application/install/agent-manifest.js +7 -0
  23. package/dist/application/install/agent-manifest.js.map +1 -1
  24. package/dist/application/recommendations/recommend-bundles.d.ts.map +1 -1
  25. package/dist/application/recommendations/recommend-bundles.js +41 -2
  26. package/dist/application/recommendations/recommend-bundles.js.map +1 -1
  27. package/dist/application/runtime/command-catalog.d.ts +7 -0
  28. package/dist/application/runtime/command-catalog.d.ts.map +1 -1
  29. package/dist/application/runtime/command-catalog.js +101 -0
  30. package/dist/application/runtime/command-catalog.js.map +1 -1
  31. package/dist/cli/commands/commands.d.ts.map +1 -1
  32. package/dist/cli/commands/commands.js +5 -1
  33. package/dist/cli/commands/commands.js.map +1 -1
  34. package/dist/domain/intelligence/repo-intelligence.js +1 -1
  35. package/dist/domain/intelligence/repo-intelligence.js.map +1 -1
  36. package/docs/agent-usage-playbook.md +34 -4
  37. package/docs/agents.md +5 -0
  38. package/docs/authoring/token-budget-optimizer-port.md +78 -0
  39. package/docs/commands.md +12 -1
  40. package/manifests/bundles/core.json +11 -0
  41. package/manifests/catalog/compatibility-matrix.json +205 -1
  42. package/manifests/catalog/index.json +1 -0
  43. package/manifests/catalog/package-surface.json +57 -0
  44. package/manifests/catalog/token-budget-optimizer-import-inventory.json +146 -0
  45. package/package.json +1 -1
  46. package/scripts/intelligence/shared.mjs +187 -51
  47. package/skills/README.md +6 -0
  48. package/skills/hforge-analyze/SKILL.md +40 -0
  49. package/skills/hforge-analyze/references/analysis-order.md +15 -0
  50. package/skills/hforge-analyze/references/decision-promotion.md +9 -0
  51. package/skills/hforge-analyze/references/output-contract.md +7 -0
  52. package/skills/hforge-decide/SKILL.md +58 -0
  53. package/skills/hforge-decide/references/decision-rubric.md +18 -0
  54. package/skills/hforge-decide/references/output-contract.md +7 -0
  55. package/skills/hforge-init/SKILL.md +58 -0
  56. package/skills/hforge-init/references/bootstrap-order.md +7 -0
  57. package/skills/hforge-init/references/output-contract.md +7 -0
  58. package/skills/hforge-refresh/SKILL.md +52 -0
  59. package/skills/hforge-refresh/references/output-contract.md +7 -0
  60. package/skills/hforge-refresh/references/refresh-order.md +5 -0
  61. package/skills/hforge-review/SKILL.md +57 -0
  62. package/skills/hforge-review/references/output-contract.md +7 -0
  63. package/skills/hforge-review/references/review-order.md +7 -0
  64. package/skills/token-budget-optimizer/SKILL.md +56 -0
  65. package/skills/token-budget-optimizer/references/audit-dimensions.md +37 -0
  66. package/skills/token-budget-optimizer/references/promotion-ladder.md +44 -0
  67. package/skills/token-budget-optimizer/references/report-template.md +25 -0
  68. package/skills/token-budget-optimizer/references/scoring-model.md +43 -0
  69. package/skills/token-budget-optimizer/scripts/inspect_token_surfaces.py +68 -0
@@ -0,0 +1,146 @@
1
+ {
2
+ "packId": "token-budget-optimizer-2026-03",
3
+ "sourceName": "Harness Forge Token Budget Optimizer Skill Pack",
4
+ "sourceVersion": "2026-03-29-import",
5
+ "resourceRoots": [
6
+ "hforge-token-budget-optimizer/"
7
+ ],
8
+ "summary": "Governed port record for the imported token-budget-optimizer skill, preserving its context-compaction and reuse-first intent while translating the pack into project-owned canonical skill, wrapper, references, and helper surfaces.",
9
+ "validationScope": "The port validates canonical skill ownership, wrapper discovery coverage, package-surface registration, and front-door promotion for token-efficient agent operation. Runtime-native slash commands are intentionally not required for this skill.",
10
+ "researchScope": "The imported pack contributes context-compaction guidance, reuse-first operating rules, token-surface audit dimensions, a scoring model, a promotion ladder, and a deterministic helper script for identifying high-value low-cost runtime surfaces.",
11
+ "entries": [
12
+ {
13
+ "artifactPath": "hforge-token-budget-optimizer/SKILL.md",
14
+ "artifactType": "skill",
15
+ "skillId": "token-budget-optimizer",
16
+ "existingProjectSurface": null,
17
+ "decision": "embed",
18
+ "decisionReason": "The imported pack owns a distinct runtime responsibility around context compaction and reuse-first navigation, so it should ship as one canonical skill rather than being folded into a broader helper.",
19
+ "destinationPath": "skills/token-budget-optimizer/SKILL.md",
20
+ "reviewStatus": "accepted"
21
+ },
22
+ {
23
+ "artifactPath": "hforge-token-budget-optimizer/references/audit-dimensions.md",
24
+ "artifactType": "reference",
25
+ "skillId": "token-budget-optimizer",
26
+ "existingProjectSurface": null,
27
+ "decision": "embed",
28
+ "decisionReason": "The audit dimensions are direct supporting guidance for deciding which surfaces deserve active prompt space.",
29
+ "destinationPath": "skills/token-budget-optimizer/references/audit-dimensions.md",
30
+ "reviewStatus": "accepted"
31
+ },
32
+ {
33
+ "artifactPath": "hforge-token-budget-optimizer/references/promotion-ladder.md",
34
+ "artifactType": "reference",
35
+ "skillId": "token-budget-optimizer",
36
+ "existingProjectSurface": null,
37
+ "decision": "embed",
38
+ "decisionReason": "The promotion ladder turns the imported context-reuse idea into a reusable operating model for agents in installed workspaces.",
39
+ "destinationPath": "skills/token-budget-optimizer/references/promotion-ladder.md",
40
+ "reviewStatus": "accepted"
41
+ },
42
+ {
43
+ "artifactPath": "hforge-token-budget-optimizer/references/scoring-model.md",
44
+ "artifactType": "reference",
45
+ "skillId": "token-budget-optimizer",
46
+ "existingProjectSurface": null,
47
+ "decision": "embed",
48
+ "decisionReason": "The scoring model is a direct supporting heuristic for choosing low-cost high-authority context surfaces.",
49
+ "destinationPath": "skills/token-budget-optimizer/references/scoring-model.md",
50
+ "reviewStatus": "accepted"
51
+ },
52
+ {
53
+ "artifactPath": "hforge-token-budget-optimizer/references/report-template.md",
54
+ "artifactType": "reference",
55
+ "skillId": "token-budget-optimizer",
56
+ "existingProjectSurface": null,
57
+ "decision": "embed",
58
+ "decisionReason": "The report template preserves the imported pack's goal of making compaction decisions explicit and reviewable.",
59
+ "destinationPath": "skills/token-budget-optimizer/references/report-template.md",
60
+ "reviewStatus": "accepted"
61
+ },
62
+ {
63
+ "artifactPath": "hforge-token-budget-optimizer/scripts/inspect_token_surfaces.py",
64
+ "artifactType": "script",
65
+ "skillId": "token-budget-optimizer",
66
+ "existingProjectSurface": null,
67
+ "decision": "embed",
68
+ "decisionReason": "The helper script is deterministic, package-owned, and directly useful for ranking reusable context surfaces before an agent expands prompt history.",
69
+ "destinationPath": "skills/token-budget-optimizer/scripts/inspect_token_surfaces.py",
70
+ "reviewStatus": "accepted"
71
+ },
72
+ {
73
+ "artifactPath": "hforge-token-budget-optimizer/agents/openai.yaml",
74
+ "artifactType": "metadata",
75
+ "skillId": "token-budget-optimizer",
76
+ "existingProjectSurface": null,
77
+ "decision": "translate",
78
+ "decisionReason": "The source runtime metadata is treated as pack provenance while the project expresses cross-agent discovery through the canonical wrapper and maintainer-facing port note.",
79
+ "destinationPath": "docs/authoring/token-budget-optimizer-port.md",
80
+ "reviewStatus": "accepted"
81
+ },
82
+ {
83
+ "artifactPath": "hforge-token-budget-optimizer/.agents-wrapper",
84
+ "artifactType": "wrapper",
85
+ "skillId": "token-budget-optimizer",
86
+ "existingProjectSurface": null,
87
+ "decision": "embed",
88
+ "decisionReason": "The imported skill needs a visible discovery wrapper so Codex and Claude Code can find the canonical token-optimization workflow quickly.",
89
+ "destinationPath": ".agents/skills/token-budget-optimizer/SKILL.md",
90
+ "reviewStatus": "accepted"
91
+ }
92
+ ],
93
+ "compatibilityProfiles": [
94
+ {
95
+ "targetId": "codex",
96
+ "supportLevel": "translated",
97
+ "metadataMode": "translated",
98
+ "helperMode": "packaged-script",
99
+ "notes": "Codex consumes the canonical skill and wrapper directly, and can optionally run the helper script when a deterministic token-surface audit is useful."
100
+ },
101
+ {
102
+ "targetId": "claude-code",
103
+ "supportLevel": "translated",
104
+ "metadataMode": "translated",
105
+ "helperMode": "packaged-script",
106
+ "notes": "Claude Code consumes the same canonical skill and wrapper, with the helper script available when the operator wants explicit compaction evidence."
107
+ },
108
+ {
109
+ "targetId": "cursor",
110
+ "supportLevel": "guidance-only",
111
+ "metadataMode": "unsupported",
112
+ "helperMode": "documentation-first",
113
+ "notes": "Cursor can follow the guidance surfaces, but the primary promotion path is still the canonical skill and maintainer note."
114
+ },
115
+ {
116
+ "targetId": "opencode",
117
+ "supportLevel": "guidance-only",
118
+ "metadataMode": "unsupported",
119
+ "helperMode": "documentation-first",
120
+ "notes": "OpenCode receives the canonical guidance without overstating runtime-native metadata parity."
121
+ }
122
+ ],
123
+ "portingRules": [
124
+ {
125
+ "ruleId": "promote-reuse-first-runtime-skills",
126
+ "matchCriteria": "An imported skill owns a distinct responsibility for context compaction, reuse-first reasoning, or token-cost discipline.",
127
+ "preferredOutcome": "embed",
128
+ "requiredFollowUps": [
129
+ "create the canonical skill under skills/",
130
+ "ship supporting references and helper surfaces under the same skill directory",
131
+ "add a discovery wrapper under .agents/skills/",
132
+ "register the skill in package-surface and front-door docs"
133
+ ]
134
+ },
135
+ {
136
+ "ruleId": "translate-runtime-specific-metadata",
137
+ "matchCriteria": "The imported artifact is runtime-specific metadata rather than canonical cross-agent guidance.",
138
+ "preferredOutcome": "translate",
139
+ "requiredFollowUps": [
140
+ "summarize the metadata in maintainer-facing provenance",
141
+ "avoid claiming universal runtime-native parity",
142
+ "keep the canonical wrapper and skill as the active discovery path"
143
+ ]
144
+ }
145
+ ]
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-forge/cli",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Harness Forge: modular agentic AI workspace installer, catalog, and workflow runtime.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
 
package/skills/README.md CHANGED
@@ -57,6 +57,7 @@ and modernization guidance sourced into project-owned `references/` directories.
57
57
  - `skills/security-scan/`
58
58
  - `skills/release-readiness/`
59
59
  - `skills/architecture-decision-records/`
60
+ - `skills/token-budget-optimizer/`
60
61
 
61
62
  ## Workload-specialized skills
62
63
 
@@ -92,3 +93,8 @@ maintainer-facing provenance instead of shipping duplicate skill identities.
92
93
  Single-skill ports such as `engineering-assistant` should preserve the same
93
94
  discipline through `manifests/catalog/engineering-assistant-import-inventory.json`
94
95
  and `docs/authoring/engineering-assistant-port.md`.
96
+
97
+ Context-compaction ports such as `token-budget-optimizer` should preserve the
98
+ same discipline through
99
+ `manifests/catalog/token-budget-optimizer-import-inventory.json` and
100
+ `docs/authoring/token-budget-optimizer-port.md`.