@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.
- 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/skills/token-budget-optimizer/SKILL.md +26 -0
- package/AGENTS.md +6 -3
- package/README.md +9 -5
- 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 +34 -4
- package/docs/agents.md +5 -0
- package/docs/authoring/token-budget-optimizer-port.md +78 -0
- package/docs/commands.md +12 -1
- package/manifests/bundles/core.json +11 -0
- package/manifests/catalog/compatibility-matrix.json +205 -1
- package/manifests/catalog/index.json +1 -0
- package/manifests/catalog/package-surface.json +57 -0
- package/manifests/catalog/token-budget-optimizer-import-inventory.json +146 -0
- package/package.json +1 -1
- package/scripts/intelligence/shared.mjs +187 -51
- package/skills/README.md +6 -0
- 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
- package/skills/token-budget-optimizer/SKILL.md +56 -0
- package/skills/token-budget-optimizer/references/audit-dimensions.md +37 -0
- package/skills/token-budget-optimizer/references/promotion-ladder.md +44 -0
- package/skills/token-budget-optimizer/references/report-template.md +25 -0
- package/skills/token-budget-optimizer/references/scoring-model.md +43 -0
- 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
|
@@ -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
|
|
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`.
|