bmad-module-skill-forge 7.0.0 → 8.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/README.md +1 -1
- package/docs/_data/pinned.yaml +9 -10
- package/docs/campaign.md +2 -2
- package/docs/skill-model.md +3 -3
- package/docs/verifying-a-skill.md +17 -18
- package/docs/workflows.md +7 -7
- package/package.json +1 -1
- package/src/shared/references/description-guard-protocol.md +12 -10
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +38 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +5 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +8 -3
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +6 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +7 -2
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +1 -1
- package/src/shared/scripts/skf-parse-gaps.py +4 -1
- package/src/shared/scripts/skf-quick-batch.py +8 -4
- package/src/shared/scripts/skf-verify-provenance-completeness.py +727 -179
- package/src/skf-audit-skill/customize.toml +4 -3
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
- package/src/skf-campaign/references/campaign-contracts.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +2 -2
- package/src/skf-campaign/references/step-07-capstone.md +1 -1
- package/src/skf-campaign/references/step-08-verify.md +1 -1
- package/src/skf-campaign/references/step-09-refine.md +1 -1
- package/src/skf-campaign/references/step-11-maintenance.md +1 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
- package/src/skf-campaign/scripts/campaign-report.py +60 -4
- package/src/skf-campaign/scripts/campaign-state.py +67 -19
- package/src/skf-campaign/templates/campaign-report-template.md +4 -0
- package/src/skf-create-skill/assets/skill-sections.md +2 -2
- package/src/skf-create-skill/references/report.md +2 -2
- package/src/skf-create-skill/references/validate.md +49 -29
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/report.md +3 -3
- package/src/skf-create-stack-skill/references/validate.md +9 -4
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-quick-skill/references/batch-mode.md +28 -2
- package/src/skf-quick-skill/references/compile.md +2 -0
- package/src/skf-quick-skill/references/finalize.md +3 -2
- package/src/skf-quick-skill/references/halt-contract.md +3 -3
- package/src/skf-quick-skill/references/quick-extract.md +2 -1
- package/src/skf-quick-skill/references/write-and-validate.md +11 -5
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +2 -2
- package/src/skf-rename-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +2 -2
- package/src/skf-test-skill/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/scoring-rules.md +1 -1
- package/src/skf-test-skill/references/signature-comparison-rules.md +4 -1
- package/src/skf-test-skill/scripts/gap-ledger.py +196 -46
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +12 -6
- package/src/skf-update-skill/references/gap-driven.md +1 -1
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +1 -1
- package/src/skf-update-skill/references/report.md +18 -7
- package/src/skf-update-skill/references/write.md +24 -12
- package/src/skf-verify-stack/references/report.md +1 -1
|
@@ -246,18 +246,19 @@ When `{run_dir}/warnings.jsonl` holds a `workspace-clone-not-updated:` line, add
|
|
|
246
246
|
|
|
247
247
|
### 5b. Result Contract
|
|
248
248
|
|
|
249
|
-
The shared emitter writes the result contract and prints the line; never type either. Stage the payload in the run folder, from the files this run wrote rather than from memory (`{run_dir}/change-manifest.json` for the counts, the artifacts write.md wrote for `files_written`, the Validation Summary §3 read for `validation_status`):
|
|
249
|
+
The shared emitter writes the result contract and prints the line; never type either. Stage the payload in the run folder, from the files this run wrote rather than from memory (`{run_dir}/change-manifest.json` for the counts, the artifacts write.md wrote for `files_written`, the Validation Summary §3 read for `validation_status` and for `security_result`, the first word of its `Security` row with `PASS` as `pass`, `WARN` as `warn` and `SKIP` as `skipped`, left out of the payload and its `summary` when that row is missing or starts with another word; §1, §1a and §1b run no scan and leave `security_result` out):
|
|
250
250
|
|
|
251
251
|
```bash
|
|
252
252
|
cat > "{run_dir}/result-context.json" <<'SKF_JSON'
|
|
253
|
-
{"status": "success", "skill_name": "{skill_name}", "version": "{new_version}", "previous_version": "{baseline_version}", "update_mode": "{update_mode}", "files_written": [<each artifact write.md wrote and verified: SKILL.md, references (when this run wrote, added or removed a references/*.md file, as the files on disk show), metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, active-symlink>], "error": null,
|
|
254
|
-
"result_contract": {"skill": "skf-update-skill", "status": "success", "outputs": [{"type": "skill", "path": "<each modified file's path>"}], "summary": {"update_status": "success", "exports_affected": <total_export_changes, or the gap count in gap-driven mode>, "files_modified": <the files written>, "validation_status": "<passed, warnings or failures>"}}}
|
|
253
|
+
{"status": "success", "skill_name": "{skill_name}", "version": "{new_version}", "previous_version": "{baseline_version}", "update_mode": "{update_mode}", "security_result": "<pass, warn or skipped>", "files_written": [<each artifact write.md wrote and verified: SKILL.md, references (when this run wrote, added or removed a references/*.md file, as the files on disk show), metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, active-symlink>], "error": null,
|
|
254
|
+
"result_contract": {"skill": "skf-update-skill", "status": "success", "outputs": [{"type": "skill", "path": "<each modified file's path>"}], "summary": {"update_status": "success", "exports_affected": <total_export_changes, or the gap count in gap-driven mode>, "files_modified": <the files written>, "validation_status": "<passed, warnings or failures>", "security_result": "<pass, warn or skipped>"}}}
|
|
255
255
|
SKF_JSON
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
Then, from `{project-root}`, run the emitter with the run folder and
|
|
258
|
+
Then, from `{project-root}`, create the version's forge folder and run the emitter with the run folder and that folder. The emitter writes result files only into a folder that exists, and §1's no-change exit may find none when the skill's provenance map is still flat; a folder that cannot be created leaves a `result_file_write_failed` warning, so the hook does not run:
|
|
259
259
|
|
|
260
260
|
```bash
|
|
261
|
+
mkdir -p "{forge_version}" || uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning 'result_file_write_failed: cannot create the version forge folder'
|
|
261
262
|
uv run {emitEnvelopeHelper} emit --workflow skf-update-skill --run-dir "{run_dir}" --result-dir "{forge_version}" < "{run_dir}/result-context.json"
|
|
262
263
|
```
|
|
263
264
|
|
|
@@ -267,12 +268,22 @@ It writes the per-run record `{forge_version}/update-skill-result-{YYYYMMDD-HHmm
|
|
|
267
268
|
SKF_UPDATE_RESULT_JSON: {"skf_update": {"status": "success", "skill_name": ..., "version": ..., "previous_version": ..., "update_mode": ..., "files_written": [...], "headless_decisions": [...], "warnings": [...], "error": null}}
|
|
268
269
|
```
|
|
269
270
|
|
|
270
|
-
In `{headless_mode}`, bind `{result_envelope_line}` to that line and do not display it here: the shared health check displays it verbatim as the run's last line. An interactive run runs the command for its result files and need not show the line. When the emitter exits non-zero and its `message` names the payload, fix the payload once and run it again; a write it could not make adds `result_file_write_failed` to the line's `warnings[]`. When the emitter exits non-zero and no retry applies, or fails again, or no path resolved for `{emitEnvelopeHelper}`, this run has no result contract: display its `message` (or that the emitter is missing), bind no `{result_envelope_line}
|
|
271
|
+
In `{headless_mode}`, bind `{result_envelope_line}` to that line and do not display it here: the shared health check displays it verbatim as the run's last line. An interactive run runs the command for its result files and need not show the line. When the emitter exits non-zero and its `message` names the payload, fix the payload once and run it again; a write it could not make adds `result_file_write_failed` to the line's `warnings[]`. When the emitter exits non-zero and no retry applies, or fails again, or no path resolved for `{emitEnvelopeHelper}`, this run has no result contract: display its `message` (or that the emitter is missing), bind no `{result_envelope_line}` when the run is interactive, when no emitter resolved or when the failure line below fails too, and skip the hook as below.
|
|
271
272
|
|
|
272
|
-
|
|
273
|
+
A headless run whose emitter resolved then prints a failure line in place of the result line, so a pipeline learns why the run has none. From `{project-root}`, run `emit-halt` with the values staged in `{run_dir}/result-context.json` and with no `--result-dir` (with one, `emit-halt` would overwrite `update-skill-result-latest.json`):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
uv run {emitEnvelopeHelper} emit-halt --workflow skf-update-skill --run-dir "{run_dir}" <<'SKF_JSON'
|
|
277
|
+
{"status": "halted-for-write-failure", "phase": "report:result-contract", "path": "{forge_version}", "reason": "result-contract-not-written: <the emitter's message>", "skill_name": "{skill_name}", "version": "<the staged version>", "previous_version": "<the staged previous_version>", "update_mode": "<the staged update_mode>", "files_written": [<the staged files_written>]}
|
|
278
|
+
SKF_JSON
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Leave `files_written` out when the emitter's message names it (the line then holds `[]`), and write each value as a JSON string, as init.md's halt procedure does: escape `"` and `\`, and write every path with `/`. Bind `{result_envelope_line}` to the line it prints and do not display it here: the shared health check displays it as the run's last line. When it exits non-zero or prints no line, bind nothing. Either way the run has no result contract (`emit-halt` writes none, even when it exits 0): skip the hook with the no-result-contract line below, then go on to §6. The failure line undoes nothing: what this run wrote stays in place.
|
|
282
|
+
|
|
283
|
+
- `status`: `"success"` here; §1 stages `"no-changes"`, §1a `"detect-only"` and §1b `"dry-run"`; the failure line above prints `"halted-for-write-failure"` at `error.phase` `report:result-contract`; a halt prints its own `halted-for-*` or `blocked` line through its step's halt procedure and never reaches this step. The full enum lives in the schema.
|
|
273
284
|
- `files_written[]`: `references` stands for every `references/*.md` file of the package this run wrote, added or removed, one value however many: merge.md §6b's merge, at any priority, write.md §6's provenance fixes and write.md §7's body split. Read it from the files on disk, never from memory: a `references/` path in the `files_written[]` write.md §6's provenance `fix` printed; a file the `Body split` row lists in this run's `## Update Operation` section of `{forge_version}/evidence-report.md`, the one write.md §4 appended, never an earlier update's; outside gap-driven mode, a `references/*.md` file that `{skill_package}` and the previous version's package (`{skill_group}/{baseline_version}/{skill_name}/references/`) do not hold alike, one lacking it or the two differing; in gap-driven mode, which repairs the version in place, a file in `{run_dir}/references/`, where merge.md §6b staged each one it wrote, or a path `{run_dir}/references-removed.txt` lists.
|
|
274
285
|
- `headless_decisions[]`: every gate's record in the run's decision log (init.md §4 degraded-rebuild and §8 confirmation, detect-changes.md §1b / §1c / §2.2, gap-driven.md §1 rule R1, merge.md §8). Each entry `{gate, default_action, taken_action, reason, evidence?}`. Empty when no gate auto-resolved.
|
|
275
|
-
- `error`: null on every exit this step prints
|
|
286
|
+
- `error`: null on every exit this step prints except the failure line above, which carries `{phase: "report:result-contract", path, reason}`. A halt's line carries `{phase, path?, reason}`, and pipelines branch on `error !== null` for non-zero exit semantics.
|
|
276
287
|
- `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `new-files-not-checked` (Category D: a skill with no brief, so no new script or asset was looked for), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category}): {reason}` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished`, `result_file_write_failed` (a result file the emitter could not write; the hook then does not run), `public-api-counted-by-hand:` (write.md §2: the recipe runner exited 2, so the public names were found without it; it quotes the runner's error line) and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
|
|
277
288
|
|
|
278
289
|
**Post-finalization hook.** A finished run, this one or §1's no-change exit, fires the hook; `--detect-only`, `--dry-run` and a halt never do (customize.toml says so). If `{onCompleteCommand}` (resolved in SKILL.md On Activation §4 from `workflow.on_complete`) is non-empty, the emitter exited 0 and the line's `warnings[]` hold no entry that starts with `result_file_write_failed` (the emitter wrote both result files), invoke it:
|
|
@@ -37,7 +37,8 @@ atomicWriteProbeOrder:
|
|
|
37
37
|
# candidate exists. §1 and §7 use its `manual-verify` subcommand to verify
|
|
38
38
|
# SKILL.md against the byte-exact [MANUAL] inventory step 4 §4 amended; a
|
|
39
39
|
# marker-count comparison would pass a block whose interior was truncated
|
|
40
|
-
# without changing the marker count.
|
|
40
|
+
# without changing the marker count. §7 also runs its `hash` subcommand
|
|
41
|
+
# around `skill-check --fix` to tell whether the call changed SKILL.md.
|
|
41
42
|
hashContentProbeOrder:
|
|
42
43
|
- '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
|
|
43
44
|
- '{project-root}/src/shared/scripts/skf-hash-content.py'
|
|
@@ -147,9 +148,9 @@ A `Write` or `Edit` call in §2 to §5 that errors (permission denied, disk full
|
|
|
147
148
|
|
|
148
149
|
The deterministic phases are executed via `{descriptionGuardHelper}`: §7 invokes the helper at the capture and verify-restore points around every `skill-check` call.
|
|
149
150
|
|
|
150
|
-
**Guard outputs.**
|
|
151
|
+
**Guard outputs.** Each `capture`, run while the in-context SKILL.md copy matches the file on disk, writes the description it reads from `{skill_package}/SKILL.md` to `{run_dir}/description-guard.json`, and each `verify-restore` reads it there with `--captured-file`, so the description never passes through a shell string. Bind `{guarded_description}` ← that file's `description`. Bind `{guard_restored}` ← `restored` and `{guard_diff_kind}` ← `diff_kind` from each `verify-restore`. When `{guard_restored}` is true, set the in-context `description` to `{guarded_description}` so later sections do not work from the tool-mutated value, and record `description_guard_restored: true` with the tool name and `description_guard_diff_kind: {guard_diff_kind}` in workflow context for the evidence report (§4). A later `verify-restore` that exits 0 with `{guard_restored}` false leaves those records in place.
|
|
151
152
|
|
|
152
|
-
**Empty-snapshot rule.** `verify-restore` refuses an empty or whitespace-only
|
|
153
|
+
**Empty-snapshot rule.** `verify-restore` refuses an empty or whitespace-only description in `{run_dir}/description-guard.json`, and a file it cannot read (exit 1, file untouched). Never re-run it with the empty value: writing it back would blank the field the guard protects. If the merged description is still in context (the in-context SKILL.md copy), re-run `verify-restore` with that value: write `{"description": <that value as a JSON string>}` to `{run_dir}/description-guard.json` with a file write, never `echo` (the value can hold quotes, `$` or backticks), and run the same `verify-restore` line again, once. Otherwise, or when that re-run exits 1 too, record `description_guard_restored: false` and `description_guard_refused: empty-capture` with the tool name; the evidence report (§4) renders that as a fired guard, not as a clean run.
|
|
153
154
|
|
|
154
155
|
### 1. Verify SKILL.md Write
|
|
155
156
|
|
|
@@ -254,12 +255,18 @@ It sets the `content_hash` and `recorded_at` of each `doc_sources[]` entry whose
|
|
|
254
255
|
|
|
255
256
|
### 3. Write Updated provenance-map.json
|
|
256
257
|
|
|
257
|
-
Write `{forge_version}/provenance-map.json` with `{buildChangeManifestHelper}` `apply` (resolve it ← first existing path in `{buildChangeManifestProbeOrder}`), never by hand: it reads the map the update started from and this run's records in `{run_dir}`, and writes the whole updated map through a temporary file and one rename. When step 4 §6b created a new version, `{forge_version}` is its forge folder, which holds a copy of the previous map when there was one: write the whole updated map there, and leave the previous version's map as it was. From `{project-root}`, run:
|
|
258
|
+
Write `{forge_version}/provenance-map.json` with `{buildChangeManifestHelper}` `apply` (resolve it ← first existing path in `{buildChangeManifestProbeOrder}`), never by hand: it reads `{provenance_map_path}`, the map the update started from (init.md §4: the current version's map, else the flat one an older skill keeps in `{forge_data_folder}/{skill_name}/`), and this run's records in `{run_dir}`, and writes the whole updated map through a temporary file and one rename. When step 4 §6b created a new version, `{forge_version}` is its forge folder, which holds a copy of the previous map when there was one, the same bytes `apply` reads: write the whole updated map there, and leave the previous version's map as it was. In a repair `{provenance_map_path}` is the only map there is: `apply` writes the updated map to `{forge_version}`, and a flat map stays as it was. A repair of a skill whose forge data is flat (init.md §4 bound the flat map) may find no forge folder for its version, and `apply` writes only into a folder that exists. In gap-driven mode, first create it, as report.md §5b does. From `{project-root}`, run:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
mkdir -p "{forge_version}"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
On a non-zero exit, HALT with status `halted-for-write-failure` before `apply` runs: display "**The forge folder of this version of {skill_name} could not be created** at `{forge_version}`: {its stderr}. This repair already wrote SKILL.md in place; the halt restored the package and the skill brief from the snapshot step 4 took, so the skill is as it was. Make `{forge_data_folder}/{skill_name}/` writable, or move what is at `{forge_version}` out of the way, then re-run update-skill." The halt procedure takes `phase: "write:provenance-map"`, `path: "{forge_version}"`, `reason: "forge-folder-not-created: <its stderr>"`. Then, in a repair whose `{provenance_map_path}` is the flat map, copy into `{forge_version}` each of `evidence-report.md` and `extraction-rules.yaml` from `{forge_data_folder}/{skill_name}/` when it is there and `{forge_version}` does not hold it yet. Copy, never move: the flat files stay as they are, and a file `{forge_version}` already holds is never overwritten. §4 appends this update to the evidence report there; the extraction rules record how the skill was extracted. When a copy fails, HALT with the same status and phase, `path: "{forge_version}/<the file>"` and `reason: "forge-file-not-copied: <its error>"`. A later halt removes the map and evidence report this run writes or copies there, which `begin` found absent, and leaves the folder empty but for a copied `extraction-rules.yaml`, a copy of the flat file and harmless; every reader looks for a file in it and falls back to the flat one, so an empty folder reads as none. Then, from `{project-root}`, run:
|
|
258
265
|
|
|
259
266
|
```bash
|
|
260
267
|
uv run {buildChangeManifestHelper} apply \
|
|
261
268
|
--update-type "{update_type}" \
|
|
262
|
-
[--provenance-map "{
|
|
269
|
+
[--provenance-map "{provenance_map_path}"] \
|
|
263
270
|
[--manifest "{run_dir}/change-manifest.json"] \
|
|
264
271
|
[--category-c "{run_dir}/category-c.json"] \
|
|
265
272
|
[--ccc-pairs "{run_dir}/ccc-pairs.json"] \
|
|
@@ -338,7 +345,7 @@ Append update operation section to `{forge_version}/evidence-report.md` (create
|
|
|
338
345
|
- Confidence tiers: {PASS/WARN}
|
|
339
346
|
- Provenance: {PASS/WARN}
|
|
340
347
|
- Diff: {new} new, {fixed} fixed issues, or SKIP
|
|
341
|
-
- Security: {PASS/WARN/SKIP}
|
|
348
|
+
- Security: {PASS/WARN/SKIP} ({note})
|
|
342
349
|
|
|
343
350
|
### Description Guard
|
|
344
351
|
- Restored: {true/false}
|
|
@@ -447,22 +454,27 @@ skill-check runs once per update, here, against the written package, and once mo
|
|
|
447
454
|
|
|
448
455
|
```bash
|
|
449
456
|
# Phase 1: capture before any frontmatter-touching tool call
|
|
450
|
-
uv run {descriptionGuardHelper} capture "{skill_package}/SKILL.md"
|
|
451
|
-
#
|
|
457
|
+
uv run {descriptionGuardHelper} capture "{skill_package}/SKILL.md" > "{run_dir}/description-guard.json"
|
|
458
|
+
# the snapshot lands in the run folder; read its `description` as `guarded_description`
|
|
452
459
|
|
|
453
460
|
# Phase 2: run the tool (skill-check --fix, the body split, split-body --write)
|
|
454
461
|
|
|
455
462
|
# Phases 3+4: verify and restore after the tool call
|
|
456
463
|
uv run {descriptionGuardHelper} verify-restore "{skill_package}/SKILL.md" \
|
|
457
|
-
--captured-
|
|
464
|
+
--captured-file "{run_dir}/description-guard.json"
|
|
458
465
|
```
|
|
459
466
|
|
|
460
467
|
Do not rely on per-call ad-hoc preservation logic: use the helper.
|
|
461
468
|
|
|
462
469
|
**If skill-check is available:**
|
|
463
470
|
|
|
464
|
-
-
|
|
465
|
-
-
|
|
471
|
+
- **Hash before --fix:** before the §0 guard's `capture` for the `--fix` call below, run `uv run {hashContentHelper} hash "{skill_package}/SKILL.md"` from `{project-root}` and keep the `content_hash` it prints.
|
|
472
|
+
- Run `npx skill-check check "{skill_package}" --fix --format json` **inside the §0 guard**, with its output captured apart (`> "{run_dir}/skill-check.json" 2> "{run_dir}/skill-check.stderr"; echo "skill-check exited $?"` after it). One call validates the package against the spec, scores it, fixes what has one answer and runs the security scan (on unless `--no-security-scan` is passed). Read the JSON, not the exit code: the quality score (`scores[].score`, the entry matching the skill, or a top-level `qualityScore` on an older skill-check), the remaining `diagnostics[]` and the security findings, from the JSON object at the start of `{run_dir}/skill-check.json` (stdout can go on after it, a `Wrote <path>` line when the project config sets a report path). Give the call the longest timeout your shell tool takes. When no "skill-check exited" line came back, the shell tool stopped the call before skill-check finished (a cold `uvx mcp-scan` install can outlast its timeout): still run the guard's `verify-restore` and the second `hash` call below before reading `{run_dir}/skill-check.json`.
|
|
473
|
+
- **Security scan:** skill-check prints its JSON before the security scan runs, then exits 1 when `summary.errorCount` is above 0, when `--strict` or `--fail-on-warning` is passed and `summary.warningCount` is above 0 (this call passes neither), or when the scan exits non-zero, and 2 when no scan runner starts, so a non-zero exit is not a security finding. skill-check 1.2.0 puts no security key in the JSON. The scan exited non-zero, which without `--ci` means it did not finish, when skill-check exits 1 although `summary.errorCount` is 0: stderr is then the scan's last lines (its tail), even when blank. On exit 0, stderr is never a scan tail (npm and Node warnings can be there); when exit 1 has another cause, stderr is a scan tail only when it holds the scanner's own lines, such as its token message or a finding code. A finding code is `[`, a capital letter, three digits and `]` (such as `[E001]`), the codes skill-check's own scan summary counts. Record the Validation Summary's `Security` row from `{run_dir}/skill-check.stderr` and the exit code, the first that applies:
|
|
474
|
+
- **SKIP:** skill-check exits 2, the call stopped before it exited, or the scan exited non-zero and its tail carries no finding code. Record `Security: SKIP (SNYK_TOKEN not configured)` when the tail says to set the `SNYK_TOKEN` environment variable (the scanner prints "To use Agent Scan, set the SNYK_TOKEN environment variable."), otherwise `Security: SKIP (scanner could not run ({the stderr line that names the cause, or its last line}))`, or `Security: SKIP (scanner exited non-zero with no output)` when stderr is blank.
|
|
475
|
+
- **WARN:** `security[]` holds findings, or the tail carries finding codes. Record `Security: WARN ({each finding})`, each finding an advisory warning.
|
|
476
|
+
- **PASS:** otherwise. The scan exited 0 and skill-check reported no finding. Record `Security: PASS (scan exited 0; skill-check 1.2.0 does not pass the scan's findings on)`. Without `--ci`, agent-scan also exits 0 when its analysis fails (a 401 from a bad token, a 429, a network error), so this does not show that the scan found nothing (#892).
|
|
477
|
+
- **Context sync after --fix:** after that call's `verify-restore`, run `uv run {hashContentHelper} hash "{skill_package}/SKILL.md"` again. When the `content_hash` it prints differs from the one kept before the call, or either `hash` call printed no `content_hash`, re-read SKILL.md into the in-context copy, so the report does not work from a copy that differs from the file on disk. The §0 guard has already restored `description` if it diverged.
|
|
466
478
|
- If `body.max_lines` or `body.max_tokens` is reported, split the body selectively with `{shardBodyHelper}` (resolve it ← first existing path in `{shardBodyProbeOrder}`), **inside the §0 guard**: from `{project-root}`, run `uv run {shardBodyHelper} "{skill_package}/SKILL.md" --budget 400 --word-budget 4500 --skip-manual`, with `--budget 500` in place of `--budget 400` when only `body.max_tokens` is reported. It moves the largest Tier 2 (`## Full`) sections to `references/`, each replaced by a `> See` pointer to a file it never overwrites, until the body is within that many lines and 4,500 words, the same headroom under skill-check's 5,000 that 400 lines leave under its 500, and keeps Tier 1 inline (inline passive context achieves 100% task accuracy vs 79% for on-demand retrieval). **Never move a section that holds a `[MANUAL]` block:** init.md §5 inventories SKILL.md only, so a block moved to `references/` would leave the next update's inventory; `--skip-manual` keeps the helper off every section a block reaches into. No Tier 1 content is trimmed, and `split-body` never runs after the helper. Add each WARN below to `warnings[]` as well. Act on its report:
|
|
467
479
|
- **Exit 1 or 2, or no JSON:** HALT with status `blocked` (halt procedure: `phase: "write:body-split"`, `path: "{skill_package}/SKILL.md"`, `reason: "body split failed: {its stderr line}"`). On exit 2 that line ends `already written: {each references/*.md file it wrote before the failure, or none}`: name each of those files in the halt message.
|
|
468
480
|
- **`tier1_preserved` false (it then wrote nothing) or `xref_ok` false:** HALT with status `blocked` (halt procedure: `phase: "write:body-split"`, `path: "{skill_package}/SKILL.md"`, `reason: "body split: tier1_missing {tier1_missing}; xref_ok {xref_ok}"`).
|
|
@@ -484,7 +496,7 @@ On `ok` false, HALT with status `halted-for-manual-mismatch`, as §1 does. The h
|
|
|
484
496
|
|
|
485
497
|
**If skill-check is unavailable:** record `Diff: SKIP` and `Security: SKIP (skill-check unavailable)`, and check the structure by script instead: resolve `{validateOutputHelper}` ← first existing path in `{validateOutputProbeOrder}` and, from `{project-root}`, run `uv run {validateOutputHelper} "{skill_package}"`. Record `Spec compliance: PASS (skf-validate-output.py)` when it reports no issue, and `Spec compliance: WARN (skf-validate-output.py)` with each issue it reports otherwise; when no candidate resolves, record `Spec compliance: SKIP (skill-check unavailable, no validator)`. Nothing here changes SKILL.md, so the [MANUAL] re-check above does not run.
|
|
486
498
|
|
|
487
|
-
Record the results in the evidence report's Validation Summary (§4): `Spec compliance` from the score and the remaining `diagnostics[]`, `Diff
|
|
499
|
+
Record the results in the evidence report's Validation Summary (§4): `Spec compliance` from the score and the remaining `diagnostics[]`, `Diff`, and `Security` as the security scan rule above reads it (its first word `PASS`, `WARN` or `SKIP`, its note in parentheses after it), with any `description_guard_restored` event the §0 protocol recorded. They are advisory: do not block on warnings.
|
|
488
500
|
|
|
489
501
|
### 8. Update the Active Symlink
|
|
490
502
|
|
|
@@ -143,7 +143,7 @@ mkdir -p "{forge_data_folder}" || uv run {emitEnvelopeHelper} record --run-dir "
|
|
|
143
143
|
uv run {emitEnvelopeHelper} emit --workflow skf-verify-stack --run-dir "{run_dir}" --result-dir "{forge_data_folder}" < "{run_dir}/result-context.json"
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
It writes `{forge_data_folder}/verify-stack-result-{timestamp}.json`, named after the run's own `{timestamp}` (the one the timestamped report carries), and its `verify-stack-result-latest.json` copy, and prints the `SKF_VERIFY_STACK_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim before chaining to step 7. A
|
|
146
|
+
It writes `{forge_data_folder}/verify-stack-result-{timestamp}.json`, named after the run's own `{timestamp}` (the one the timestamped report carries), and its `verify-stack-result-latest.json` copy, and prints the `SKF_VERIFY_STACK_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim before chaining to step 7. A per-run record that could not be written leaves `result_path` null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, correct `result-context.json` from the message on its stderr and run it once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`, adding `"report_latest_path": "{outputFileLatest}"` to the halt payload: the run's result contract could not be written.
|
|
147
147
|
|
|
148
148
|
### 5. Finish
|
|
149
149
|
|