bmad-module-skill-forge 3.0.0 → 4.0.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/docs/_data/pinned.yaml +1 -1
- package/docs/examples.md +1 -1
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +7 -3
- package/docs/workflows.md +3 -3
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +2 -2
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +593 -47
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-extract-public-api.py +1309 -157
- package/src/shared/scripts/skf-extraction-inventory.py +580 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-verify-provenance-completeness.py +977 -49
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -9
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +6 -6
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +22 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +8 -9
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +16 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-test-skill/references/coherence-check.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +39 -57
- package/src/skf-test-skill/references/init.md +3 -5
- package/src/skf-test-skill/references/report.md +10 -8
- package/src/skf-test-skill/references/scoring-rules.md +6 -5
- package/src/skf-test-skill/references/source-access-protocol.md +14 -17
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +315 -30
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +534 -96
- package/src/skf-test-skill/scripts/reconcile-coverage.py +276 -2
- package/src/skf-test-skill/scripts/score-signatures.py +36 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +3 -3
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +2 -2
- package/src/skf-update-skill/references/re-extract.md +2 -2
- package/src/skf-update-skill/references/report.md +3 -1
- package/src/skf-update-skill/references/write.md +43 -23
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
|
@@ -12,13 +12,25 @@ follows from records the run already wrote, so this script builds it rather
|
|
|
12
12
|
than the model retyping them from memory:
|
|
13
13
|
|
|
14
14
|
- the report frontmatter: skillName, runId, testMode, hardGate,
|
|
15
|
-
testResult, score, threshold, thresholdFallback,
|
|
16
|
-
evidenceReportPath (the steps write the verdict
|
|
17
|
-
compacted context cannot change them);
|
|
15
|
+
testResult, workspaceDrift, score, threshold, thresholdFallback,
|
|
16
|
+
originalThreshold and evidenceReportPath (the steps write the verdict
|
|
17
|
+
inputs there, so a compacted context cannot change them);
|
|
18
18
|
- the gap ledger: the gap counts by severity (gap-ledger.py's summary);
|
|
19
|
-
- the scoring script's output, which score.md saves:
|
|
20
|
-
|
|
21
|
-
has none.
|
|
19
|
+
- the scoring script's output, which score.md saves: the verdict, score
|
|
20
|
+
and threshold it settled, activeCategories and inconclusiveReasons. A
|
|
21
|
+
run the hard gate blocked was never scored and has none.
|
|
22
|
+
|
|
23
|
+
A scored run's verdict fields are checked against the scoring output
|
|
24
|
+
(`scored_fields`): testResult must be the verdict the output settled,
|
|
25
|
+
`effectiveResult` when the output has one, else `result`, in lower case,
|
|
26
|
+
with `pass` read as `pass-with-drift` when the frontmatter workspaceDrift
|
|
27
|
+
is `overridden`; score its `totalScore`; threshold the one the verdict was
|
|
28
|
+
settled against (compute-score.py's FALLBACK_FLOOR after a threshold
|
|
29
|
+
fallback, else its `threshold`); and thresholdFallback and
|
|
30
|
+
originalThreshold its override group's. A field copied from anything else
|
|
31
|
+
(`result` over a cap's FAIL, a bare pass under drift, the target threshold
|
|
32
|
+
of a run the fallback accepted) exits 1 and writes no payload, so
|
|
33
|
+
report.md halts before it publishes the report or writes a result file.
|
|
22
34
|
|
|
23
35
|
The derived fields live here, in one place:
|
|
24
36
|
|
|
@@ -49,9 +61,14 @@ Payload (written to --output, UTF-8 JSON):
|
|
|
49
61
|
"runId": <runId>, "healthCheckDispatched": <bool>}}
|
|
50
62
|
|
|
51
63
|
CLI:
|
|
52
|
-
uv run build-result-context.py --report <report.md> --
|
|
53
|
-
[--score <scoring output>] [--no-health-check]
|
|
54
|
-
--output <result-context.json>
|
|
64
|
+
uv run build-result-context.py --report <report.md> [--report-path <path>]
|
|
65
|
+
--ledger <ledger.json> [--score <scoring output>] [--no-health-check]
|
|
66
|
+
[--warning <text>]... --output <result-context.json>
|
|
67
|
+
|
|
68
|
+
--score is required unless the frontmatter says the hard gate blocked the
|
|
69
|
+
run. --report-path is the path the payload names the report by (default:
|
|
70
|
+
--report): report.md builds the payload from the report before it gives
|
|
71
|
+
the report its public name, and passes that name here.
|
|
55
72
|
|
|
56
73
|
--warning hands the emitter a warning the run kept for its result (such as
|
|
57
74
|
`customization_resolver_unavailable: <reason>`); the emitter adds the ones
|
|
@@ -65,8 +82,10 @@ takes: "stderr" for a blocked run, else "stdout".
|
|
|
65
82
|
Exit codes:
|
|
66
83
|
0 the payload was written
|
|
67
84
|
1 an input is missing or unreadable, the report's frontmatter holds no
|
|
68
|
-
settled verdict
|
|
69
|
-
"error"} on stdout)
|
|
85
|
+
settled verdict or a verdict field the scoring output does not settle,
|
|
86
|
+
or --output cannot be written ({"status": "error", "error"} on stdout)
|
|
87
|
+
2 a usage error: no --score for a run the hard gate did not block (the
|
|
88
|
+
same JSON on stdout), or a flag argparse refuses (its usage on stderr)
|
|
70
89
|
"""
|
|
71
90
|
|
|
72
91
|
from __future__ import annotations
|
|
@@ -93,6 +112,10 @@ class ContextError(Exception):
|
|
|
93
112
|
"""An input the payload cannot be built from: exit 1."""
|
|
94
113
|
|
|
95
114
|
|
|
115
|
+
class UsageError(ContextError):
|
|
116
|
+
"""A command the caller can correct and run again: exit 2."""
|
|
117
|
+
|
|
118
|
+
|
|
96
119
|
def _sibling(name: str, module: str):
|
|
97
120
|
"""Import a script that sits beside this one (gap-ledger.py, hard-gate.py)."""
|
|
98
121
|
path = Path(__file__).resolve().parent / name
|
|
@@ -152,9 +175,60 @@ def _read_json(path: str, label: str):
|
|
|
152
175
|
raise ContextError(f"{label} {path} cannot be read: {exc}") from exc
|
|
153
176
|
|
|
154
177
|
|
|
178
|
+
def _fallback_floor() -> int:
|
|
179
|
+
"""The threshold a fallback accepts a run at: compute-score.py's, which applies the fallback."""
|
|
180
|
+
return _sibling("compute-score.py", "skf_compute_score_floor").FALLBACK_FLOOR
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def settled_test_result(scored: dict, workspace_drift: str | None) -> str | None:
|
|
184
|
+
"""The testResult the scoring output settles, or None when it holds no verdict.
|
|
185
|
+
|
|
186
|
+
>>> settled_test_result({"result": "PASS"}, "ok")
|
|
187
|
+
'pass'
|
|
188
|
+
>>> settled_test_result({"result": "PASS", "effectiveResult": "FAIL", "capReason": "Cap 1"}, "ok")
|
|
189
|
+
'fail'
|
|
190
|
+
>>> settled_test_result({"result": "FAIL", "effectiveResult": "PASS"}, "overridden")
|
|
191
|
+
'pass-with-drift'
|
|
192
|
+
>>> settled_test_result({"result": "INCONCLUSIVE"}, "overridden")
|
|
193
|
+
'inconclusive'
|
|
194
|
+
>>> settled_test_result({"error": "bad flag", "code": "INVALID_INPUT"}, "ok") is None
|
|
195
|
+
True
|
|
196
|
+
"""
|
|
197
|
+
verdict = scored.get("effectiveResult") or scored.get("result")
|
|
198
|
+
settled = verdict.lower() if isinstance(verdict, str) else None
|
|
199
|
+
if settled not in ("pass", "fail", "inconclusive"):
|
|
200
|
+
return None
|
|
201
|
+
return "pass-with-drift" if settled == "pass" and workspace_drift == "overridden" else settled
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def scored_fields(scored: dict, workspace_drift: str | None) -> dict | None:
|
|
205
|
+
"""The frontmatter verdict fields the scoring output settles, or None when it holds no verdict.
|
|
206
|
+
|
|
207
|
+
>>> scored_fields({"result": "PASS", "totalScore": 91.5, "threshold": 80}, "ok")
|
|
208
|
+
{'testResult': 'pass', 'score': 91.5, 'threshold': 80, 'thresholdFallback': False, 'originalThreshold': None}
|
|
209
|
+
>>> fell_back = {"result": "FAIL", "effectiveResult": "PASS", "capReason": None, "thresholdFallback": True,
|
|
210
|
+
... "originalThreshold": 90, "totalScore": 85, "threshold": 90}
|
|
211
|
+
>>> scored_fields(fell_back, "ok")
|
|
212
|
+
{'testResult': 'pass', 'score': 85, 'threshold': 80, 'thresholdFallback': True, 'originalThreshold': 90}
|
|
213
|
+
"""
|
|
214
|
+
test_result = settled_test_result(scored, workspace_drift)
|
|
215
|
+
if test_result is None:
|
|
216
|
+
return None
|
|
217
|
+
fallback = scored.get("thresholdFallback") is True
|
|
218
|
+
return {
|
|
219
|
+
"testResult": test_result,
|
|
220
|
+
"score": scored.get("totalScore"),
|
|
221
|
+
"threshold": _fallback_floor() if fallback else scored.get("threshold"),
|
|
222
|
+
"thresholdFallback": fallback,
|
|
223
|
+
"originalThreshold": scored.get("originalThreshold") if fallback else None,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
155
227
|
def build(report: str, ledger: str, score: str | None = None, health_check: bool = True,
|
|
156
|
-
warnings: list[str] | None = None) -> dict:
|
|
157
|
-
"""The emitter payload for the run whose report and ledger are given
|
|
228
|
+
warnings: list[str] | None = None, report_path: str | None = None) -> dict:
|
|
229
|
+
"""The emitter payload for the run whose report and ledger are given; `report_path`
|
|
230
|
+
(default `report`) is the path the payload names the report by."""
|
|
231
|
+
report_path = report_path or report
|
|
158
232
|
try:
|
|
159
233
|
front = read_frontmatter(Path(report).read_bytes().decode("utf-8-sig"))
|
|
160
234
|
except (OSError, UnicodeDecodeError) as exc:
|
|
@@ -170,7 +244,7 @@ def build(report: str, ledger: str, score: str | None = None, health_check: bool
|
|
|
170
244
|
skill = front.get("skillName") or None
|
|
171
245
|
blocked = front.get("hardGate") == "blocked"
|
|
172
246
|
if blocked:
|
|
173
|
-
envelope = dict(_sibling("hard-gate.py", "skf_hard_gate").blocked_envelope(skill,
|
|
247
|
+
envelope = dict(_sibling("hard-gate.py", "skf_hard_gate").blocked_envelope(skill, report_path))
|
|
174
248
|
result, active, inconclusive = "FAIL", [], None
|
|
175
249
|
else:
|
|
176
250
|
test_result = front.get("testResult", "")
|
|
@@ -183,14 +257,29 @@ def build(report: str, ledger: str, score: str | None = None, health_check: bool
|
|
|
183
257
|
"verdict": verdict,
|
|
184
258
|
"score": percent(front.get("score")),
|
|
185
259
|
"threshold": percent(front.get("threshold")),
|
|
186
|
-
"report_path":
|
|
260
|
+
"report_path": report_path,
|
|
187
261
|
"next_workflow": next_workflow,
|
|
188
262
|
"exit_code": exit_code,
|
|
189
263
|
"halt_reason": None,
|
|
190
264
|
}
|
|
191
|
-
|
|
265
|
+
if not score:
|
|
266
|
+
raise UsageError("--score is required for a run the hard gate did not block: its verdict "
|
|
267
|
+
"fields are checked against the scoring output")
|
|
268
|
+
scored = _read_json(score, "--score")
|
|
192
269
|
if not isinstance(scored, dict):
|
|
193
270
|
raise ContextError(f"--score {score} is not a JSON object")
|
|
271
|
+
settled = scored_fields(scored, front.get("workspaceDrift"))
|
|
272
|
+
if settled is None:
|
|
273
|
+
raise ContextError(f"--score {score} holds no verdict (no effectiveResult or result)")
|
|
274
|
+
fell_back = front.get("thresholdFallback", "").lower() == "true"
|
|
275
|
+
written = {"testResult": test_result, "score": envelope["score"], "threshold": envelope["threshold"],
|
|
276
|
+
"thresholdFallback": fell_back,
|
|
277
|
+
"originalThreshold": percent(front.get("originalThreshold")) if fell_back else None}
|
|
278
|
+
wrong = [f"{key} {written[key]!r} where it settles {settled[key]!r}"
|
|
279
|
+
for key in settled if written[key] != settled[key]]
|
|
280
|
+
if wrong:
|
|
281
|
+
raise ContextError(f"--report {report} disagrees with the scoring output {score}: "
|
|
282
|
+
+ "; ".join(wrong))
|
|
194
283
|
active = scored.get("activeCategories", [])
|
|
195
284
|
inconclusive = scored.get("inconclusiveReasons") if verdict == "INCONCLUSIVE" else None
|
|
196
285
|
|
|
@@ -216,7 +305,7 @@ def build(report: str, ledger: str, score: str | None = None, health_check: bool
|
|
|
216
305
|
summary["hardGate"] = front.get("hardGate") or None
|
|
217
306
|
|
|
218
307
|
contract = {"skill": WORKFLOW, **envelope,
|
|
219
|
-
"outputs": [{"type": "report", "path":
|
|
308
|
+
"outputs": [{"type": "report", "path": report_path}],
|
|
220
309
|
"summary": summary,
|
|
221
310
|
"runId": front.get("runId") or None,
|
|
222
311
|
"healthCheckDispatched": health_check}
|
|
@@ -233,9 +322,12 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
233
322
|
description=("Build the shared emitter's payload for a finished test-skill run from its "
|
|
234
323
|
"report frontmatter, its gap ledger and the scoring output."),
|
|
235
324
|
)
|
|
236
|
-
parser.add_argument("--report", required=True, help="the test report
|
|
325
|
+
parser.add_argument("--report", required=True, help="the test report to read")
|
|
326
|
+
parser.add_argument("--report-path",
|
|
327
|
+
help="the path the payload names the report by: its public name (default: --report)")
|
|
237
328
|
parser.add_argument("--ledger", required=True, help="the run's gap ledger")
|
|
238
|
-
parser.add_argument("--score", help="the scoring script's output (
|
|
329
|
+
parser.add_argument("--score", help="the scoring script's output (required unless the hard gate blocked "
|
|
330
|
+
"the run)")
|
|
239
331
|
parser.add_argument("--no-health-check", action="store_true",
|
|
240
332
|
help="the run was given --no-health-check (healthCheckDispatched false)")
|
|
241
333
|
parser.add_argument("--warning", action="append", default=[],
|
|
@@ -247,13 +339,14 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
247
339
|
def main(argv: list[str] | None = None) -> int:
|
|
248
340
|
args = _build_parser().parse_args(argv)
|
|
249
341
|
try:
|
|
250
|
-
payload = build(args.report, args.ledger, args.score, not args.no_health_check, args.warning
|
|
342
|
+
payload = build(args.report, args.ledger, args.score, not args.no_health_check, args.warning,
|
|
343
|
+
args.report_path)
|
|
251
344
|
out = Path(args.output)
|
|
252
345
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
253
346
|
out.write_bytes((json.dumps(payload, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
254
347
|
except (ContextError, OSError) as exc:
|
|
255
348
|
print(json.dumps({"status": "error", "error": str(exc)}))
|
|
256
|
-
return 1
|
|
349
|
+
return 2 if isinstance(exc, UsageError) else 1
|
|
257
350
|
blocked = payload["halt_reason"] is not None
|
|
258
351
|
print(json.dumps({
|
|
259
352
|
"status": payload["status"],
|