bmad-module-skill-forge 2.0.2 → 2.2.0
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -1
- package/docs/404.md +4 -4
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/RELEASING.md +41 -4
- package/docs/agents.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/bmad-synergy.md +13 -13
- package/docs/campaign.md +3 -3
- package/docs/concepts.md +3 -3
- package/docs/examples.md +9 -9
- package/docs/forge-auto.md +6 -6
- package/docs/getting-started.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/index.md +7 -1
- package/docs/skill-model.md +2 -2
- package/docs/troubleshooting.md +7 -7
- package/docs/why-skf.md +8 -8
- package/docs/workflows.md +5 -5
- package/package.json +7 -3
- package/src/shared/references/description-guard-protocol.md +2 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -0
- package/src/shared/scripts/skf-description-guard.py +22 -2
- package/src/shared/scripts/skf-detect-docs.py +198 -2
- package/src/shared/scripts/skf-detect-language.py +20 -0
- package/src/shared/scripts/skf-detect-tools.py +40 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +20 -0
- package/src/shared/scripts/skf-language-corpora.py +20 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +20 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +44 -2
- package/src/shared/scripts/skf-recommend-scope-type.py +20 -0
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-shape-detect.py +20 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +2 -1
- package/src/skf-analyze-source/references/recommend.md +1 -1
- package/src/skf-audit-skill/references/re-index.md +1 -1
- package/src/skf-brief-skill/assets/description-voice-examples.md +5 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/analyze-target.md +2 -2
- package/src/skf-brief-skill/references/gather-intent.md +4 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +3 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -2
- package/src/skf-create-skill/assets/skill-sections.md +3 -2
- package/src/skf-create-skill/references/compile.md +12 -3
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +5 -3
- package/src/skf-create-skill/references/step-doc-rot.md +23 -6
- package/src/skf-create-skill/references/step-doc-sources.md +31 -3
- package/src/skf-create-skill/references/sub/fetch-docs.md +4 -2
- package/src/skf-create-skill/references/validate.md +2 -2
- package/src/skf-create-skill/scripts/scan-doc-rot.py +191 -27
- package/src/skf-create-stack-skill/references/detect-integrations.md +1 -1
- package/src/skf-setup/references/ccc-index.md +5 -4
- package/src/skf-setup/references/detect-and-tier.md +1 -0
- package/src/skf-setup/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +11 -5
- package/src/skf-test-skill/references/score.md +7 -1
- package/src/skf-test-skill/scripts/aggregate-coherence.py +13 -3
- package/src/skf-test-skill/scripts/compute-score.py +17 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +36 -9
- package/src/skf-update-skill/references/write.md +2 -2
- package/tools/cli/lib/installer.js +31 -1
- package/tools/validate-docs-links.js +665 -0
|
@@ -39,11 +39,17 @@ Verdict override (post-score caps + threshold fallback — see compute_score):
|
|
|
39
39
|
and `result` is the final verdict. `result` itself is never mutated — it is
|
|
40
40
|
always the pre-cap/pre-fallback score-vs-threshold (or evidence-floor) verdict.
|
|
41
41
|
|
|
42
|
-
Exit codes:
|
|
43
|
-
0 —
|
|
44
|
-
PASS/FAIL/INCONCLUSIVE) or a schema/validation error object
|
|
45
|
-
({"error": ..., "code": "INVALID_INPUT"}) was emitted as JSON
|
|
42
|
+
Exit codes (same convention as reconcile-coverage.py):
|
|
43
|
+
0 — a score was computed; the verdict may be PASS/FAIL/INCONCLUSIVE
|
|
46
44
|
1 — input could not be parsed at all (no input provided, or malformed JSON)
|
|
45
|
+
2 — input parsed but schema/semantics invalid
|
|
46
|
+
|
|
47
|
+
Both 1 and 2 emit an {"error": ..., "code": "INVALID_INPUT"} envelope on stdout,
|
|
48
|
+
so the presence of that envelope — not the specific code — is what tells a caller
|
|
49
|
+
the input was refused. A refused input is distinct from an unavailable script:
|
|
50
|
+
the numbers handed in are wrong and must be fixed, NOT hand-computed into a total
|
|
51
|
+
from the values this script declined to score. score.md §3c keys its
|
|
52
|
+
manual-redistribution fallback on that distinction (no envelope at all).
|
|
47
53
|
"""
|
|
48
54
|
|
|
49
55
|
from __future__ import annotations
|
|
@@ -508,6 +514,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
508
514
|
|
|
509
515
|
result = compute_score(data)
|
|
510
516
|
print(json.dumps(result, indent=2))
|
|
517
|
+
# A rejected input exits 2, matching reconcile-coverage.py. Exiting 0 here
|
|
518
|
+
# made a malformed scoring input indistinguishable from a scored run, so the
|
|
519
|
+
# error envelope could be skimmed past and the score hand-computed from the
|
|
520
|
+
# very numbers the script refused. score.md §3c keys the manual-redistribution
|
|
521
|
+
# fallback on this distinction.
|
|
522
|
+
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
523
|
+
return 2
|
|
511
524
|
return 0
|
|
512
525
|
|
|
513
526
|
|
|
@@ -22,15 +22,24 @@ It covers the three branches §2c enumerates:
|
|
|
22
22
|
* "scalar" (§4 priority-1 effective_denominator, no enumerated name set) —
|
|
23
23
|
grep each documented name across SKILL.md ∪ references/*.md;
|
|
24
24
|
Documented = count present, denominator = effective_denominator,
|
|
25
|
-
Missing = denominator − Documented, Stale not enumerable (empty),
|
|
26
|
-
Export Coverage = Documented / denominator * 100.
|
|
25
|
+
Missing = max(0, denominator − Documented), Stale not enumerable (empty),
|
|
26
|
+
Export Coverage = min(100, Documented / denominator * 100).
|
|
27
27
|
|
|
28
28
|
* "stack" (skill_type == "stack", empty source barrel) — grep each
|
|
29
29
|
composition-surface name (provenance-map cited contracts, ::-excluded, or
|
|
30
30
|
libraries + integration_pairs — resolved upstream) across
|
|
31
31
|
SKILL.md ∪ references/*.md; denominator = stack_denominator,
|
|
32
|
-
Missing = denominator − Documented, Stale not enumerable,
|
|
33
|
-
Export Coverage = Documented / stack_denominator * 100.
|
|
32
|
+
Missing = max(0, denominator − Documented), Stale not enumerable,
|
|
33
|
+
Export Coverage = min(100, Documented / stack_denominator * 100).
|
|
34
|
+
|
|
35
|
+
The two grep branches bound their outputs because the numerator and the
|
|
36
|
+
denominator are independent measures of different sets — the grep count can
|
|
37
|
+
exceed a consumer-surface denominator without either being wrong. Unbounded,
|
|
38
|
+
that produced a negative Missing and a >100% coverage that compute-score.py
|
|
39
|
+
rejects as out of range. `documented` is left as the true count (it is reported
|
|
40
|
+
verbatim as "Documented in SKILL.md"); the derived ratio is what gets bounded,
|
|
41
|
+
and `numeratorSurplus` / `coverageUncapped` / `coverageCapped` report the
|
|
42
|
+
overshoot so a deflated denominator stays visible instead of being swallowed.
|
|
34
43
|
|
|
35
44
|
CLI usage:
|
|
36
45
|
uv run reconcile-coverage.py '<JSON>' # JSON literal positional
|
|
@@ -55,11 +64,14 @@ Output (stdout, one object):
|
|
|
55
64
|
"denominator": <int>,
|
|
56
65
|
"documented": <int>,
|
|
57
66
|
"missing": [names], # enumerated: source names not documented; scalar/stack: []
|
|
58
|
-
"missingCount": <int>, # always present
|
|
67
|
+
"missingCount": <int>, # always present; never negative
|
|
59
68
|
"stale": [names], # enumerated: documented names not in source; scalar/stack: []
|
|
60
69
|
"staleCount": <int>, # always present
|
|
61
70
|
"staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
|
|
62
|
-
"exportCoverage": <float
|
|
71
|
+
"exportCoverage": <float>, # scalar/stack: capped at 100
|
|
72
|
+
"numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
|
|
73
|
+
"coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
|
|
74
|
+
"coverageCapped": <bool> # scalar/stack only: true when the cap bound the ratio
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
Exit codes:
|
|
@@ -232,18 +244,33 @@ def reconcile(inp, doc_text=None):
|
|
|
232
244
|
"upstream §2b guard should HALT before reconciliation"
|
|
233
245
|
)
|
|
234
246
|
|
|
235
|
-
|
|
247
|
+
# The grep numerator and the resolved denominator are independent measures,
|
|
248
|
+
# so `documented` can legitimately exceed `denominator` — a consumer-surface
|
|
249
|
+
# denominator counts one surface while the documented body may also name
|
|
250
|
+
# migration aliases, re-exported sibling symbols, and other extras. Left
|
|
251
|
+
# unbounded that yields a negative Missing and >100% coverage (which
|
|
252
|
+
# compute-score.py then rejects outright as out of range).
|
|
253
|
+
#
|
|
254
|
+
# `documented` stays the true grep count: it is reported as "Documented in
|
|
255
|
+
# SKILL.md", so capping it would print a number that was never measured.
|
|
256
|
+
# The derived ratio is what gets bounded, and the surplus is surfaced rather
|
|
257
|
+
# than swallowed so a deflated denominator stays visible.
|
|
258
|
+
surplus = max(0, documented - denominator)
|
|
259
|
+
coverage_uncapped = round2(documented / denominator * 100)
|
|
236
260
|
return {
|
|
237
261
|
"branch": source,
|
|
238
262
|
"denominatorSource": source,
|
|
239
263
|
"denominator": denominator,
|
|
240
264
|
"documented": documented,
|
|
241
265
|
"missing": [],
|
|
242
|
-
"missingCount":
|
|
266
|
+
"missingCount": max(0, denominator - documented),
|
|
267
|
+
"numeratorSurplus": surplus,
|
|
243
268
|
"stale": [],
|
|
244
269
|
"staleCount": 0,
|
|
245
270
|
"staleApplicable": False,
|
|
246
|
-
"exportCoverage":
|
|
271
|
+
"exportCoverage": min(100.0, coverage_uncapped),
|
|
272
|
+
"coverageUncapped": coverage_uncapped,
|
|
273
|
+
"coverageCapped": surplus > 0,
|
|
247
274
|
}
|
|
248
275
|
|
|
249
276
|
|
|
@@ -65,7 +65,7 @@ Verify the merged SKILL.md that step 4 section 6b wrote to disk, then write the
|
|
|
65
65
|
|
|
66
66
|
**Used by:** §7 (`skill-check check --fix` and `skill-check split-body --write`), and any future tool invocation that may modify SKILL.md's frontmatter on disk.
|
|
67
67
|
|
|
68
|
-
Load `{descriptionGuardProtocol}` for the full prose explanation of the four-phase guard (why it exists, what counts as divergence, why token-stream comparison is the right shape). The deterministic phases are executed via `{descriptionGuardHelper}` — §7 invokes the helper at the capture and verify-restore points around every `skill-check` call.
|
|
68
|
+
Load `{descriptionGuardProtocol}` for the full prose explanation of the four-phase guard (why it exists, what counts as divergence, why token-stream comparison is the right shape). The deterministic phases are executed via `{descriptionGuardHelper}` — §7 invokes the helper at the capture and verify-restore points around every `skill-check` call. `verify-restore` refuses an empty `--captured-description` (exit 1, file untouched); when that happens, follow the protocol's empty-snapshot rule instead of re-running with the empty value.
|
|
69
69
|
|
|
70
70
|
Update-skill does not run the optional post-restore frontmatter re-validation today — the post-write checks in §1 catch downstream issues, and a `restored: true` outcome is already surfaced through the evidence report (§4).
|
|
71
71
|
|
|
@@ -203,7 +203,7 @@ Append update operation section to `{forge_version}/evidence-report.md` (create
|
|
|
203
203
|
- Notes: {one-sentence detail or —}
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
**Description Guard population** (used by §7 Post-Write Validation when the §0 protocol fires): fill all four fields from context when `description_guard_restored == true` (triggering tool, whether restore succeeded, what changed). When `Restored: false`, the other three fields are `—` — this is the clean-run expected state. Same field semantics and populator logic as create-skill step 6 §8.
|
|
206
|
+
**Description Guard population** (used by §7 Post-Write Validation when the §0 protocol fires): fill all four fields from context when `description_guard_restored == true` (triggering tool, whether restore succeeded, what changed). When `Restored: false`, the other three fields are `—` — this is the clean-run expected state — except when `description_guard_refused == "empty-capture"` (the §0 protocol's empty-snapshot rule): then set `Triggering tool` to the recorded tool name, `Original description preserved: false`, and `Notes: guard refused — empty captured snapshot (empty-capture)`, so a refused restore is distinguishable from a run where the guard never fired. Same field semantics and populator logic as create-skill step 6 §8.
|
|
207
207
|
|
|
208
208
|
**Context Snippet population** (used by §5 after the staleness check runs): §4 writes the sub-block with placeholders; §5 updates the on-disk evidence report in place after deciding whether to regenerate. Set `Regenerated: true` and populate `Triggers fired` with any combination of `headline-exports`, `version`, `gotchas` when at least one trigger fired. Set `Regenerated: false` and `Triggers fired: —` when none fired (the gap-driven / internals-only outcome). Always fill `Notes` with a one-sentence reason (e.g., `"Gap-driven repair — no snippet surface changed"`, `"Version bumped 0.1.0 → 0.2.0; headline exports re-ranked"`).
|
|
209
209
|
|
|
@@ -205,10 +205,39 @@ class Installer {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Read the health_check_repo default from src/module.yaml (single source of truth).
|
|
210
|
+
*/
|
|
211
|
+
async readHealthCheckRepoDefault() {
|
|
212
|
+
try {
|
|
213
|
+
const moduleData = yaml.load(await fs.readFile(path.join(this.srcDir, 'module.yaml'), 'utf8'));
|
|
214
|
+
const value = moduleData && moduleData.health_check_repo && moduleData.health_check_repo.default;
|
|
215
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
216
|
+
} catch {
|
|
217
|
+
/* fall through to literal fallback */
|
|
218
|
+
}
|
|
219
|
+
return 'armelhbobdad/bmad-module-skill-forge';
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
async writeConfig(skfDir, config) {
|
|
209
223
|
// On update, restore the user's existing config
|
|
210
224
|
if (config._savedConfigYaml) {
|
|
211
|
-
|
|
225
|
+
let savedYaml = config._savedConfigYaml;
|
|
226
|
+
try {
|
|
227
|
+
// Inject health_check_repo into configs generated before it existed.
|
|
228
|
+
// Append one line instead of re-dumping so user comments/ordering survive.
|
|
229
|
+
// Only mapping-shaped (or empty/comments-only) docs are appendable —
|
|
230
|
+
// appending a key to an array or scalar doc would corrupt the YAML.
|
|
231
|
+
const savedData = yaml.load(savedYaml);
|
|
232
|
+
const isPlainObject = typeof savedData === 'object' && savedData !== null && !Array.isArray(savedData);
|
|
233
|
+
if (savedData == null || (isPlainObject && !('health_check_repo' in savedData))) {
|
|
234
|
+
const suffix = savedYaml.endsWith('\n') ? '' : '\n';
|
|
235
|
+
savedYaml = `${savedYaml}${suffix}health_check_repo: ${await this.readHealthCheckRepoDefault()}\n`;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
/* unparseable saved config — restore verbatim */
|
|
239
|
+
}
|
|
240
|
+
await fs.writeFile(path.join(skfDir, 'config.yaml'), savedYaml, 'utf8');
|
|
212
241
|
return;
|
|
213
242
|
}
|
|
214
243
|
|
|
@@ -232,6 +261,7 @@ class Installer {
|
|
|
232
261
|
forge_data_folder: config.forge_data_folder || 'forge-data',
|
|
233
262
|
sidecar_path: '_bmad/_memory/forger-sidecar',
|
|
234
263
|
skf_folder: config.skfFolder,
|
|
264
|
+
health_check_repo: await this.readHealthCheckRepoDefault(),
|
|
235
265
|
ides: config.ides || [],
|
|
236
266
|
install_learning: config.install_learning !== false,
|
|
237
267
|
};
|