bmad-module-skill-forge 6.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.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/docs/_data/pinned.yaml +9 -10
  4. package/docs/campaign.md +2 -2
  5. package/docs/skill-model.md +3 -3
  6. package/docs/verifying-a-skill.md +17 -18
  7. package/docs/workflows.md +10 -10
  8. package/package.json +1 -1
  9. package/src/shared/references/description-guard-protocol.md +12 -10
  10. package/src/shared/references/output-contract-schema.md +1 -1
  11. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +1 -1
  12. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +1 -1
  13. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +38 -0
  14. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +5 -0
  15. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +1 -1
  16. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +1 -1
  17. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +8 -3
  18. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +1 -1
  19. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +6 -1
  20. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
  21. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +7 -2
  22. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +1 -1
  23. package/src/shared/scripts/skf-parse-gaps.py +4 -1
  24. package/src/shared/scripts/skf-quick-batch.py +24 -7
  25. package/src/shared/scripts/skf-verify-provenance-completeness.py +809 -85
  26. package/src/skf-analyze-source/customize.toml +3 -2
  27. package/src/skf-analyze-source/references/auto-docs-only.md +2 -2
  28. package/src/skf-analyze-source/references/generate-briefs.md +5 -3
  29. package/src/skf-analyze-source/references/headless-contract.md +2 -2
  30. package/src/skf-analyze-source/references/step-auto-scope.md +2 -2
  31. package/src/skf-audit-skill/customize.toml +4 -3
  32. package/src/skf-audit-skill/references/report.md +1 -1
  33. package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
  34. package/src/skf-campaign/references/campaign-contracts.md +1 -1
  35. package/src/skf-campaign/references/step-06-batch.md +2 -2
  36. package/src/skf-campaign/references/step-07-capstone.md +3 -3
  37. package/src/skf-campaign/references/step-08-verify.md +3 -3
  38. package/src/skf-campaign/references/step-09-refine.md +3 -3
  39. package/src/skf-campaign/references/step-11-maintenance.md +1 -1
  40. package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
  41. package/src/skf-campaign/scripts/campaign-report.py +60 -4
  42. package/src/skf-campaign/scripts/campaign-state.py +82 -17
  43. package/src/skf-campaign/templates/campaign-report-template.md +4 -0
  44. package/src/skf-create-skill/SKILL.md +2 -2
  45. package/src/skf-create-skill/assets/skill-sections.md +2 -2
  46. package/src/skf-create-skill/customize.toml +4 -4
  47. package/src/skf-create-skill/references/batch-mode.md +3 -1
  48. package/src/skf-create-skill/references/report.md +9 -4
  49. package/src/skf-create-skill/references/validate.md +49 -29
  50. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  51. package/src/skf-create-stack-skill/references/report.md +3 -3
  52. package/src/skf-create-stack-skill/references/validate.md +21 -8
  53. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  54. package/src/skf-export-skill/references/summary.md +1 -1
  55. package/src/skf-quick-skill/SKILL.md +1 -1
  56. package/src/skf-quick-skill/references/batch-mode.md +30 -2
  57. package/src/skf-quick-skill/references/compile.md +2 -0
  58. package/src/skf-quick-skill/references/finalize.md +3 -2
  59. package/src/skf-quick-skill/references/halt-contract.md +3 -3
  60. package/src/skf-quick-skill/references/quick-extract.md +2 -1
  61. package/src/skf-quick-skill/references/write-and-validate.md +27 -11
  62. package/src/skf-refine-architecture/customize.toml +1 -1
  63. package/src/skf-refine-architecture/references/report.md +2 -2
  64. package/src/skf-rename-skill/references/report.md +1 -1
  65. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  66. package/src/skf-test-skill/references/coverage-check.md +4 -4
  67. package/src/skf-test-skill/references/inventory-contract.md +1 -1
  68. package/src/skf-test-skill/references/invocation-contract.md +1 -1
  69. package/src/skf-test-skill/references/report.md +1 -1
  70. package/src/skf-test-skill/references/scoring-rules.md +2 -2
  71. package/src/skf-test-skill/references/signature-comparison-rules.md +7 -2
  72. package/src/skf-test-skill/scripts/gap-ledger.py +196 -46
  73. package/src/skf-test-skill/scripts/load-coverage-inputs.py +253 -17
  74. package/src/skf-test-skill/scripts/score-signatures.py +9 -13
  75. package/src/skf-test-skill/scripts/validate-inventory.py +91 -12
  76. package/src/skf-update-skill/customize.toml +1 -1
  77. package/src/skf-update-skill/references/gap-driven.md +1 -1
  78. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  79. package/src/skf-update-skill/references/merge.md +1 -1
  80. package/src/skf-update-skill/references/report.md +27 -9
  81. package/src/skf-update-skill/references/write.md +24 -12
  82. package/src/skf-verify-stack/references/report.md +1 -1
@@ -44,7 +44,7 @@ Field rules:
44
44
  - `skill`, `drop_mode`, `versions_affected`, `files_deleted`, `manifest_updated`: the values the emit site knows; a key not resolved yet takes the default shown in the template (`null`, `[]` or `false`). `versions_affected` is a list holding the one version dropped, or the string `"all"` for the whole skill.
45
45
  - `would_delete`: in a dry run of a purge, the folders the purge would delete (the step 1 purge check's `affected_directories`), so an automator can check the blast radius before it runs the drop; `[]` otherwise, and in a dry run of a deprecate.
46
46
  - `forge_left_in_place`: the forge folder a purge leaves in place (or, in a dry run, would leave) because SKF did not generate it; `null` otherwise.
47
- - `result_path`: the per-run result file step 3 wrote, `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC), stamped by the emitter; `null` for a dry run and a HALT, which write no result file.
47
+ - `result_path`: the per-run result file step 3 wrote, `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC), stamped by the emitter; `null` for a dry run and a HALT, which write no result file. A per-run record that could not be written leaves `result_path` null with a `result_file_write_failed` warning.
48
48
  - `error`: `null` on success and dry-run; on a HALT, `{"phase", "reason"}`, plus `path` when the halt names one: the step and gate that halted, and the halt message.
49
49
  - `warnings`: present only when the run recorded at least one. A run that reports `success` can still carry the degraded outcomes step 2 records, which the report also shows: `context_rebuild_failed: <context file>: <error>` (a context file it could not rebuild), `active_link_dangling: <skill folder>/active: <manual fix>` (an `active` link a version purge left pointing at no version), `delete_failed: <path>: <error>` (a folder a partial purge could not delete) and `verification_failed: <what failed>: <manual fix>` (a final-state check that failed). Any run can carry `result_file_write_failed: <path>: <reason>` when the emitter could not write a result file, and `customization_resolver_unavailable: <reason>` when On Activation records that the customization resolver could not run, so the overrides under `{project-root}/_bmad/custom/` were not applied.
50
50
 
@@ -183,7 +183,7 @@ Other workflow choices that diverge from the strict spec path may add their own
183
183
 
184
184
  ### 6b. Post-Export Hook (Optional)
185
185
 
186
- Skip this section on a dry run (a dry run fires no hook), when `{onCompleteCommand}` (resolved at SKILL.md On Activation §3 from `workflow.on_complete`) is empty, the bundled default, and when `{result_path}` is null (§6 wrote no per-run record). Otherwise invoke it now, after the emitter wrote both result files:
186
+ Skip this section on a dry run (a dry run fires no hook), when `{onCompleteCommand}` (resolved at SKILL.md On Activation §3 from `workflow.on_complete`) is empty, the bundled default, and when `{result_path}` is null (§6 wrote no per-run record). Otherwise invoke it now, after the emitter wrote the per-run record:
187
187
 
188
188
  ```bash
189
189
  {onCompleteCommand} --result-path="{result_path}"
@@ -53,7 +53,7 @@ These rules apply to every step in this workflow:
53
53
  | **Overrides** | `--language-hint`, `--scope-hint`, `--description`, `--exports`, `--skip-snippet`, `--no-active-pointer`, `--batch <file>`, `--fail-fast`: see On Activation step 4 |
54
54
  | **Gates** | step 1: target input, ambiguous package name [C/n/U/X] (if another registry also holds the name; headless keeps the first registry's pick), multi-language disambiguation [C/n/A] (pick any detected language); step 2: ecosystem match [P/I/A] (none until agentskills.io has a registry API); step 3: repo-shape [C/A] + zero-exports rescue [R/P/A]; step 4: review [C/E/S/Q]; step 5: overwrite [Y/N] |
55
55
  | **Outputs** | SKILL.md, context-snippet.md, metadata.json, active pointer, result contract (timestamped + `-latest` copy), one `SKF_QUICK_SKILL_RESULT_JSON` line (stdout when the run finishes, stderr at a HARD HALT), and under `--batch` the batch summary. Snippet and active pointer can be skipped per overrides. |
56
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; each auto-decision is recorded in the envelope's `headless_decisions`. A caller reads the run's own `SKF_QUICK_SKILL_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and the result contract's `-latest` copy may then describe an earlier run, so it is never read in its place. |
56
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; each auto-decision is recorded in the envelope's `headless_decisions`. A caller reads the run's own `SKF_QUICK_SKILL_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and the result contract's `-latest` copy may then describe an earlier run, so it is never read in its place. Under `--batch`, a caller reads the batch's `batch_summary` event and the summary file its `summary_path` names, never the batch summary's `-latest` copy in its place: that copy holds the last batch summary written, which may be another batch's. A printed `batch_summary` event stays the batch's result even if a halt line follows it. A batch that printed no `batch_summary` event has no batch result, whatever the process exit code: a halt line of its own (phase `batch-mode`, or `on-activation`) says why it stopped, and with no halt line it was cut off or could not print its halt line. A target it printed a `done` or `fail` event for ended as that event says; a target with a `start` event and no end event has no result, and a target with no event did not run in this session. |
57
57
  | **Exit codes** | See `references/halt-contract.md`: the exit-code map and the envelope every HARD HALT emits, except On Activation step 1's check for `python3` and `uv`, which prints none |
58
58
 
59
59
  ## On Activation
@@ -75,6 +75,14 @@ uv run {quickBatchHelper} next --run-dir "{batch_dir}"
75
75
  - **`"status": "record"`**: target `batch` ended, its payload staged, before §3 recorded it (a compaction came between its envelope and §3): go to §3 with that `batch`, never running it again.
76
76
  - **`"status": "done"`**: no target is pending, or `--fail-fast` ended the batch after a failed target. Go to §4.
77
77
 
78
+ If it exits non-zero (the target's run folder cannot be created, or the batch file is gone or unreadable), the batch cannot go on: display "**The batch stopped:** {its `message`}" and HARD HALT with **exit code 4 (write-failure)**: no other target runs and the health check does not run. Emit with nothing to stage in, as §1 does:
79
+
80
+ ```bash
81
+ uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --target stderr <<'SKF_JSON'
82
+ {"phase": "batch-mode", "halt_reason": "write-failure", "reason": "The batch stopped: <its message, one line>", "skill_package": null}
83
+ SKF_JSON
84
+ ```
85
+
78
86
  ### 3. Record the Target
79
87
 
80
88
  Every target ends here: step 6 §4 returns here when the target finishes, and every HARD HALT of steps 1 to 6 returns here after its envelope (`references/halt-contract.md`, In `--batch`), the step 5 ownership halt included. Control returns here even when the step that ended the target reads as the end of the run. Record it:
@@ -83,7 +91,13 @@ Every target ends here: step 6 §4 returns here when the target finishes, and ev
83
91
  uv run {quickBatchHelper} record --run-dir "{batch_dir}" --batch {batch} --target stderr
84
92
  ```
85
93
 
86
- `{batch}` is the number §2's `next` printed. The helper reads how the target ended from what its run folder staged for the emitter (`halt.json`, or step 6's `result-context.json`), appends the outcome to the batch file, removes the run folder of a target that succeeded, and prints the target's `done` or `fail` event on stderr: display it verbatim. Then go back to §2. If it exits non-zero, its `message` names what is missing: stage the payload the target ended with, record again, then go back to §2.
94
+ `{batch}` is the number §2's `next` printed. The helper reads how the target ended from what its run folder staged for the emitter (`halt.json`, or step 6's `result-context.json`), appends the outcome to the batch file, removes the run folder of a target that succeeded, and prints the target's `done` or `fail` event on stderr: display it verbatim. Then go back to §2. If it exits non-zero, its `message` names what is missing: stage the payload the target ended with and record again, once. If it exits non-zero a second time (the batch file cannot be written, or the staged `halt.json` is not JSON or names a `halt_reason` that maps to no exit code), the outcome cannot be kept and the batch cannot go on: display "**The batch stopped:** {its `message`}" and HARD HALT with **exit code 4 (write-failure)**. This halt ends the batch, not only the target: do not go back to §2, which would hand the same target back, no other target runs and the health check does not run. Emit with nothing to stage in, as §1 does:
95
+
96
+ ```bash
97
+ uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --target stderr <<'SKF_JSON'
98
+ {"phase": "batch-mode", "halt_reason": "write-failure", "reason": "The batch stopped: <its message, one line>", "skill_package": null}
99
+ SKF_JSON
100
+ ```
87
101
 
88
102
  ### 4. Batch Summary
89
103
 
@@ -93,6 +107,16 @@ uv run {quickBatchHelper} summarize --run-dir "{batch_dir}" --output-dir "{batch
93
107
 
94
108
  The helper counts the targets, sets the batch status and exit code, writes the summary files below, appends the final status to the batch file (and removes the batch run folder when no target failed), and prints the `batch_summary` event on stderr: display it verbatim. Then print step 7's `start` event and load, read in full and execute `{healthCheckStepFile}`: the health check runs once per batch, here. The batch ends with the event's `exit_code`.
95
109
 
110
+ If `summarize` exits non-zero (a target is still pending, or the batch file is gone or unreadable), it wrote no summary file: display "**The batch stopped:** {its `message`}" and HARD HALT with **exit code 4 (write-failure)**: the health check does not run. When an earlier `summarize` already printed the `batch_summary` event (a compaction came before the health check), that event stays the batch's result (**Reading a batch.** below). Emit with nothing to stage in, as §1 does:
111
+
112
+ ```bash
113
+ uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --target stderr <<'SKF_JSON'
114
+ {"phase": "batch-mode", "halt_reason": "write-failure", "reason": "The batch stopped: <its message, one line>", "skill_package": null}
115
+ SKF_JSON
116
+ ```
117
+
118
+ **Reading a batch.** Under `--batch`, a caller reads the batch's `batch_summary` event and the summary file its `summary_path` names, never `quick-skill-batch-latest.json` in its place: that copy holds the last batch summary written, which may be another batch's. The event comes before the health check, so a caller reads it from the session's output stream (`claude -p --output-format stream-json`) or transcript, not from the final message. A printed `batch_summary` event stays the batch's result even if a halt line follows it. A batch that printed no `batch_summary` event has no batch result, whatever the process exit code: a halt line of its own (phase `batch-mode`, or `on-activation`) says why it stopped, and with no halt line it was cut off or could not print its halt line. A target it printed a `done` or `fail` event for ended as that event says; a target with a `start` event and no end event has no result, and a target with no event did not run in this session. When the event's `summary_path` is null, the per-batch file was not written (its `summary_write_failed` warning says why): the event and `{batch_dir}/batch.jsonl`, which the helper then keeps, are the batch's result, never `quick-skill-batch-latest.json`. A `summary_write_failed` warning beside a set `summary_path` means only the `-latest` copy failed: the file it names is still the batch's result. After a cut-off batch, run the batch again: each run takes a new run folder (SKILL.md On Activation step 1), so it starts over from the first target.
119
+
96
120
  ## Batch summary contract
97
121
 
98
122
  `summarize` writes the batch summary at:
@@ -148,10 +172,14 @@ Batch mode emits per-target boundary events on stderr in addition to the per-ste
148
172
 
149
173
  ```
150
174
  {"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>,"exit_code":<code>,"summary_path":"<path>"}
175
+ {"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>,"exit_code":<code>,"summary_path":null,"warnings":["summary_write_failed: <dir>: <reason>"],"batch_file":"<path>"}
176
+ {"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>,"exit_code":<code>,"summary_path":"<path>","warnings":["<summary_write_failed or batch_file_write_failed>: <dir>: <reason>"]}
151
177
  ```
152
178
 
179
+ When the per-batch summary file cannot be written, `summary_path` is null and `batch_file` names `{batch_dir}/batch.jsonl`, which then holds the batch's result. When `summary_path` is set and the event still carries `warnings`, a later write failed: `summary_write_failed` means only the `-latest` copy failed, and `batch_file_write_failed` that only the summary line could not be added to `{batch_dir}/batch.jsonl`. The file `summary_path` names stays the batch's result.
180
+
153
181
  ## Exit code
154
182
 
155
183
  Without `--fail-fast`, the batch runs every target, whatever an earlier target's outcome, and exits with the batch summary's `exit_code`: `0` when `failed == 0`, otherwise the highest exit code among the failed targets. Automators that branch on the single-target exit-code map read each target's own code in `results[]`. With `--fail-fast`, the batch stops at the first failed target and exits with that target's code.
156
184
 
157
- A batch refused before its first target writes no batch summary. It exits with code `2` when `--description` or `--exports` was passed with `--batch` (or `--language-hint` or `--scope-hint`), §1 cannot read the batch file or `skf-quick-batch.py` is missing, and with code `4` when §1 cannot write the batch run folder.
185
+ A batch refused before its first target writes no batch summary. It exits with code `2` when `--description` or `--exports` was passed with `--batch` (or `--language-hint` or `--scope-hint`), §1 cannot read the batch file or `skf-quick-batch.py` is missing, and with code `4` when §1 cannot write the batch run folder. A batch that stops because `next`, a second `record` or `summarize` fails (§2 to §4) exits with code `4` and writes no batch summary then; a `batch_summary` event an earlier `summarize` already printed stays the batch's result.
@@ -45,6 +45,8 @@ Populate the SKILL.md section structure from `{skillTemplateData}` § "SKILL.md
45
45
 
46
46
  **Skills module** (`repo_shape: skills-module`): Unless `{overrides.exports}` is set, Key Exports lists the skills first, each with its description, then the menu codes, each with its description, display name and the skill it runs, in the inventory's order. Usage Patterns gives one entry per `module-help.csv` usage pattern in the inventory; with no `module-help.csv`, it comes from the README as for a library.
47
47
 
48
+ **Stars from other packages** (`extraction_inventory.external_stars`): after the Key Exports list, one prose sentence per record, not a list item or table row: "`<source statement>` (`{file}` line {line}) also exports every name of `{specifier}`: see that package's documentation." `<source statement>` is the statement as the source writes it at `{file}` line {line}, read from `{run_dir}/src/{file}` without its closing `;`, not the record's `statement`, which writes `export type *` as `export *`. When that line is an `export type *`, which passes on only the package's types, write "every type" for "every name". List none of that package's names.
49
+
48
50
  **Scripts & Assets Note** (add as an optional section when step 3's `{run_dir}/sniff.json` lists a `scripts/`, `bin/`, `assets/`, `templates/` or `schemas/` folder: its `asset_dir_count` is above 0): "This package may include scripts and assets. Run create-skill for full extraction with provenance tracking."
49
51
 
50
52
  **If confidence is low** — include a note: "This skill was generated with limited source data. Consider running create-skill for a more thorough compilation."
@@ -96,7 +96,8 @@ cat > "{run_dir}/result-context.json" <<'SKF_JSON'
96
96
  "detected_languages": {detected_languages},
97
97
  "zero_exports_rescue": {zero_exports_rescue},
98
98
  "active_pointer": "{active_pointer}",
99
- "validation_issues": {validation_issues}
99
+ "validation_issues": {validation_issues},
100
+ "security_result": "{security_result}"
100
101
  },
101
102
  "result_contract": {
102
103
  "skill": "skf-quick-skill",
@@ -111,7 +112,7 @@ SKF_JSON
111
112
  uv run {emitEnvelopeHelper} emit --workflow skf-quick-skill --run-dir "{run_dir}" --result-dir "{skill_package}" < "{run_dir}/result-context.json"
112
113
  ```
113
114
 
114
- This list is the summary, once: `skill_name`, `version`, `language`, `exports_documented` (the export count), `quality_score` and `validation_issues` (step 5, the object `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`), `confidence` and `repo_shape` (step 3, the shape null when it recorded none), `language_resolution` and `detected_languages` (step 1), `zero_exports_rescue` (step 3 §4.5, else null) and `active_pointer` (§1). Write each value as JSON: a string in double quotes with any `"` or `\` escaped, every path absolute with `/`, and `null` for a value the run did not record. Leave the snippet out of both `outputs` under `--skip-snippet`, and `outputs.active_pointer` out when §1 flipped no pointer.
115
+ This list is the summary, once: `skill_name`, `version`, `language`, `exports_documented` (the export count), `quality_score` and `validation_issues` (step 5, the object `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`), `security_result` (step 5 §4: `pass`, `warn` or `skipped`), `confidence` and `repo_shape` (step 3, the shape null when it recorded none), `language_resolution` and `detected_languages` (step 1), `zero_exports_rescue` (step 3 §4.5, else null) and `active_pointer` (§1). Write each value as JSON: a string in double quotes with any `"` or `\` escaped, every path absolute with `/`, and `null` for a value the run did not record. Leave the snippet out of both `outputs` under `--skip-snippet`, and `outputs.active_pointer` out when §1 flipped no pointer.
115
116
 
116
117
  The emitter writes the result contract from `result_contract`, with the payload's own `status` and `summary`, so neither is typed twice (the per-run record `{skill_package}/quick-skill-result-{YYYYMMDD-HHmmss}.json` and its copy `quick-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_QUICK_SKILL_RESULT_JSON` line) and prints one line on stdout, `SKF_QUICK_SKILL_RESULT_JSON: {...}`. In a headless single-target run, bind `{result_envelope_line}` ← that line and do not display it here: the shared health check displays it verbatim as the run's last line, so a `claude -p` caller reads the envelope as the final message. Otherwise (an interactive run, or a `--batch` target) display it verbatim as its own line. If it exits non-zero, fix `result-context.json` once (its `message` names the problem) and run it again; if it still fails, or no path resolved for `{emitEnvelopeHelper}`, leave `{result_envelope_line}` empty, say that the result contract was not written and why, and go on.
117
118
 
@@ -13,7 +13,7 @@ Every HARD HALT in this workflow exits with a stable, documented code so headles
13
13
  | 0 | success | step 7 (terminal) |
14
14
  | 2 | input-invalid | batch mode, before any target runs (`--description`, `--exports`, `--language-hint` or `--scope-hint` passed with `--batch`; §1: a batch file it cannot read, or `skf-quick-batch.py` missing); step 1 §1 (a headless run with no target) |
15
15
  | 3 | resolution-failure | SKILL.md On Activation step 1 (`python3` or `uv` missing); a shipped helper missing, named in the message and in `details.cause` (step 3: `skf-github-fetch.py`, `skf-skills-module.py` §1, `skf-extract-public-api.py` §2; step 5: `skf-atomic-write.py` §2, `skf-validate-frontmatter.py` §4 when skill-check is unavailable, `skf-validate-output.py` §5); step 1 (package resolver missing or failing §1b, a target that is no GitHub repository or package §2, registry chain §3, version tag missing or not checkable §3a, file listing unreadable, language detector missing or failing, language abort or no language found §4); step 3 (non-library shape §1.5, zero-exports §4.5) |
16
- | 4 | write-failure | SKILL.md On Activation step 1 (the run folder cannot be created); batch mode §1 (the batch run folder cannot be written); step 5 §2 (deliverable write failed) |
16
+ | 4 | write-failure | SKILL.md On Activation step 1 (the run folder cannot be created); batch mode §1 (the batch run folder cannot be written) and §2 to §4 (a failing `next`, a second failing `record`, a failing `summarize`); step 5 §2 (deliverable write failed) |
17
17
  | 5 | overwrite-cancelled | step 5 §1 (user selected [N], or a headless run reached a version another SKF workflow built; `details.existing_generator` names its generator) |
18
18
  | 6 | user-cancelled | step 1 §1 ([X] Cancel and exit, or cancel-line affordance) and §3 ([X] at the ambiguous-name gate); step 2 ([A] Abort at ecosystem-match gate); step 4 §6 (user selected [Q]) |
19
19
  | 7 | finalize-blocked | step 6 §1 (the flip helper refused or failed: something that is not a link is at `{skill_group}/active`, or another process holds its lock) |
@@ -31,7 +31,7 @@ SKF_JSON
31
31
  uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"
32
32
  ```
33
33
 
34
- - `phase`: the step's slug (`resolve-target`, `ecosystem-check`, `quick-extract`, `compile`, `write-and-validate`, `finalize`), `on-activation` for a halt before the run starts (SKILL.md On Activation, and batch mode's refusal of `--description` and `--exports` and of the hint flags), or `batch-mode` for batch mode §1.
34
+ - `phase`: the step's slug (`resolve-target`, `ecosystem-check`, `quick-extract`, `compile`, `write-and-validate`, `finalize`), `on-activation` for a halt before the run starts (SKILL.md On Activation, and batch mode's refusal of `--description` and `--exports` and of the hint flags), or `batch-mode` for batch mode §1 to §4.
35
35
  - `halt_reason`: the failure class the halt names: the Meaning of its exit code in the table above, or `not-skf-output` or `flat-layout` for exit 9. The emitter derives `exit_code` from it.
36
36
  - `reason`: the message the step displayed, as one line (its first sentence when it runs longer); it becomes `error.message`. Escape `"` and `\` in it as JSON requires.
37
37
  - `skill_package`: the absolute `{skill_package}` once step 5 §1 computed it, else `null`.
@@ -63,4 +63,4 @@ When `{headless_mode}` is true, each step prints one-line JSON progress events o
63
63
 
64
64
  ## In `--batch`
65
65
 
66
- A HARD HALT in steps 1 to 6 ends the current target, not the batch. After its envelope line and `halt` event, return to `references/batch-mode.md` §3, which records the target and prints its `fail` event: control returns there even when the halt message reads as the end of the run, so a target's halt never exits the process. The batch then takes its next target, or, under `--fail-fast`, writes its summary. A halt before the first target (SKILL.md On Activation, batch mode's refusal of `--description` and `--exports`, or batch mode §1) ends the run itself, with no batch summary.
66
+ A HARD HALT in steps 1 to 6 ends the current target, not the batch. After its envelope line and `halt` event, return to `references/batch-mode.md` §3, which records the target and prints its `fail` event: control returns there even when the halt message reads as the end of the run, so a target's halt never exits the process. The batch then takes its next target, or, under `--fail-fast`, writes its summary. A halt before the first target (SKILL.md On Activation, batch mode's refusal of `--description` and `--exports`, or batch mode §1) ends the run itself, with no batch summary. A batch mode §2 to §4 halt ends the run too and writes no batch summary, though a `batch_summary` event an earlier `summarize` already printed stays the batch's result; the targets recorded before it keep the `done` and `fail` events they printed.
@@ -155,7 +155,7 @@ The helper writes JSON to `{run_dir}/extract.json` with:
155
155
 
156
156
  Read `{run_dir}/extract.json` into the extraction context. The shape of the envelope is the same for every language; §4 builds the inventory from it without per-language branching.
157
157
 
158
- **Read the statements `unlisted[]` still names** before §4. `--follow` read every module it could, so read by eye only the `unlisted[]` records left, whose `module_file` is null or could not be read (a warning says why): list the names each adds, from the README's API section or the module's documentation, and stage them through a quoted heredoc as `{run_dir}/extract-added.json`, `{"exports": [{"name": "<name>", "type": "re-export", "source_file": "<the record's file>"}]}`, which step 4 §4 passes with the other extraction files. Leave the other warnings for §5.
158
+ **Read the statements `unlisted[]` still names** before §4. `--follow` read every module it could, so read by eye only the `unlisted[]` records left, whose `module_file` is null or could not be read (a warning says why): list the names each adds, from the README's API section or the module's documentation, and stage them through a quoted heredoc as `{run_dir}/extract-added.json`, `{"exports": [{"name": "<name>", "type": "re-export", "source_file": "<the record's file>"}]}`, which step 4 §4 passes with the other extraction files. A record whose `statement` is an `export * from` another package (its `specifier` is no relative path; the extractor writes `export type *` as `export *`) adds no name, from the README or anywhere: the recipe runner and Test Skill leave such a star unresolved, so a name listed for it reads as stale. Keep each such record for §4's `external_stars`, and list names only for the others. A named re-export from another package (`export { a, b } from 'pkg'`) is already in `exports[]`. Leave the other warnings for §5.
159
159
 
160
160
  **Multi-module loop:** when `modules[]` is non-empty, fetch every sub-module's manifest in one §2 call, each under its `module_folders[]` entry. Then, per module, run §2's entries call with `--scope "<its module_folders[] entry>"`, its list and output written to `{run_dir}/entries-module-<n>.txt` and `{run_dir}/entries-module-<n>.json`, fetch the files it lists, and run the helper (§3) on them, writing each to `{run_dir}/extract-module-<n>.json`, `<n>` counting from 1 in `modules[]` order (a module is a path such as `modules/core` or a Gradle name such as `core:api`, so it never names the file). Step 4 §4 passes `{run_dir}/extract.json` (the parent manifest, which names the `package_name`) first and then each module's file in that order, and the renderer aggregates `exports[]` and `dependencies[]` across them.
161
161
 
@@ -180,6 +180,7 @@ extraction_inventory:
180
180
  repo_shape: {the §1.5 shape, when one was recorded}
181
181
  skills_root: {skills-module only: the folder that holds the skill folders}
182
182
  exports: [{name, type, brief_description}]
183
+ external_stars: [{file, line, statement, specifier}: each export * from another package §3 kept]
183
184
  usage_patterns: [{pattern from README examples, or the skills-module helper's usage_patterns}]
184
185
  dependencies: [{key deps from manifest}]
185
186
  confidence: {high/medium/low based on data quality}
@@ -15,6 +15,9 @@ skillInventoryProbeOrder:
15
15
  atomicWriteProbeOrder:
16
16
  - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
17
17
  - '{project-root}/src/shared/scripts/skf-atomic-write.py'
18
+ hashContentProbeOrder:
19
+ - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
20
+ - '{project-root}/src/shared/scripts/skf-hash-content.py'
18
21
  ---
19
22
 
20
23
  <!-- Config: communicate in {communication_language}. Generated SKILL.md text in {document_output_language}. -->
@@ -112,30 +115,43 @@ Check:
112
115
  Run: `npx skill-check -h`
113
116
 
114
117
  - If succeeds (returns usage information): Continue to automated validation (section 4)
115
- - If fails (command not found or error): skip to the frontmatter validator in section 4, and log "security scan skipped: skill-check unavailable" in the validation results
118
+ - If fails (command not found or error): skip to the frontmatter validator in section 4, bind `{security_result}` ← `skipped`, and log "security scan skipped: skill-check unavailable" in the validation results
116
119
 
117
120
  ### 4. Validate SKILL.md via skill-check (if available)
118
121
 
119
- **If `npx skill-check` is available**, run validation and its security scan in one call against the skill package written in section 2. `--fix` can also rewrite the frontmatter `description` the user approved in step 4, which would leave SKILL.md out of step with `metadata.json`, so the call runs inside the description guard. Resolve `{descriptionGuardHelper}` ← first existing path in `{descriptionGuardProbeOrder}`, then run the three commands in order:
122
+ **If `npx skill-check` is available**, run validation and its security scan in one call against the skill package written in section 2. `--fix` can also rewrite the frontmatter `description` the user approved in step 4, which would leave SKILL.md out of step with `metadata.json`, so the call runs inside the description guard. Resolve `{descriptionGuardHelper}` ← first existing path in `{descriptionGuardProbeOrder}` and `{hashContentHelper}` ← first existing path in `{hashContentProbeOrder}`, then run the five commands in order. `verify-restore` runs whatever skill-check exits; note that exit code:
120
123
 
121
124
  ```bash
125
+ uv run {hashContentHelper} hash "{skill_package}/SKILL.md" > "{run_dir}/skill-md-hash-before.json"
122
126
  uv run {descriptionGuardHelper} capture "{skill_package}/SKILL.md" > "{run_dir}/description-guard.json"
123
- npx skill-check check "{skill_package}" --fix --format json
127
+ npx skill-check check "{skill_package}" --fix --format json > "{run_dir}/skill-check.json" 2> "{run_dir}/skill-check.stderr"; echo "skill-check exited $?"
124
128
  uv run {descriptionGuardHelper} verify-restore "{skill_package}/SKILL.md" --captured-file "{run_dir}/description-guard.json"
129
+ uv run {hashContentHelper} hash "{skill_package}/SKILL.md" > "{run_dir}/skill-md-hash-after.json"
125
130
  ```
126
131
 
132
+ **If no "skill-check exited" line came back**, the shell tool stopped the block before skill-check finished (a cold `uvx mcp-scan` install can outlast its timeout): run the `verify-restore` command on its own before reading `{run_dir}/skill-check.json`, so a description `--fix` rewrote is restored, then run the second `hash` command.
133
+
127
134
  **Guard outputs.** `capture` stages the approved description in `{run_dir}/description-guard.json` and `verify-restore` reads it there, so it never passes through a shell string. Bind `{guard_restored}` ← `restored` and `{guard_diff_kind}` ← `diff_kind` from `verify-restore`. When `{guard_restored}` is true, the approved description is back in SKILL.md, matching `metadata.json`: record the validation note "description restored after `skill-check --fix` ({guard_diff_kind})", which §6 lists with the auto-fixed issues. `verify-restore` refuses an empty or whitespace-only captured description, and a file it cannot read (exit 1, SKILL.md untouched): re-run it with `--captured-file "{skill_package}/metadata.json"`, whose `description` step 4 rendered, never with an empty value, and if that exits 1 too, record a high-severity issue: the guard could not check the description. When it exits 2 with JSON on stdout (the JSON carries `restore_error`), the restore could not be written, so record a high-severity issue: SKILL.md's description no longer matches `metadata.json`.
128
135
 
129
- **If no `{descriptionGuardProbeOrder}` path exists**, never run `--fix` unguarded. Run `npx skill-check check "{skill_package}" --format json` instead, which validates and scans without fixing, and log the validation issue "description guard unavailable (`skf-description-guard.py` missing): skill-check ran without `--fix`".
136
+ **Rewrite note.** skill-check's JSON lists no fix, so the two `hash` commands tell whether `--fix` changed SKILL.md. Bind `{skill_md_changed}` ← true when the `content_hash` in `{run_dir}/skill-md-hash-before.json` differs from the one in `{run_dir}/skill-md-hash-after.json`, taken after the last `verify-restore` (run the second `hash` command again after a `verify-restore` re-run). When no `{hashContentProbeOrder}` path exists (leave both `hash` commands out) or either file holds no `content_hash`, SKILL.md was not hashed: bind `{skill_md_changed}` ← true as well. When `{skill_md_changed}` is true, record the validation note "skill-check --fix rewrote SKILL.md", or "SKILL.md may have been rewritten (not hashed) by skill-check --fix" when it was not hashed, which §6 lists with the auto-fixed issues. When the block above did not run at all (§3 found no skill-check, or the guard below is missing), `--fix` did not run: hash nothing and bind `{skill_md_changed}` ← false.
137
+
138
+ **If no `{descriptionGuardProbeOrder}` path exists**, never run `--fix` unguarded. Run `npx skill-check check "{skill_package}" --format json` instead, which validates and scans without fixing, with the same capture (`> "{run_dir}/skill-check.json" 2> "{run_dir}/skill-check.stderr"`, its exit code noted), and log the validation issue "description guard unavailable (`skf-description-guard.py` missing): skill-check ran without `--fix`".
139
+
140
+ **If `{run_dir}/skill-check.json` does not start with a JSON object**, skill-check stopped before it validated (exit 2 on a config, option or plugin error): log "skill-check failed: {the stderr line that names the cause, or its last line}" and "security scan skipped: skill-check failed ({that line})", keep `{quality_score}` null, bind `{security_result}` ← `skipped`, and run the frontmatter validator below, as when skill-check is not available. Otherwise stdout can go on after the JSON (a `Wrote <path>` line when the project config sets a report path), so read only the JSON object at its start.
130
141
 
131
- **Parse JSON output** to extract:
142
+ **Parse the JSON in `{run_dir}/skill-check.json`** to extract:
132
143
  - `scores[].score` — overall score (0-100); match the entry by `relativePath`/`skillId`
133
144
  - `diagnostics[]` — remaining issues after auto-fix
134
- - `fixed[]` — issues automatically corrected
135
145
  - `security[]` (when present) — security findings, recorded as advisory warnings (security issues do not block output)
136
146
 
137
147
  Bind `{quality_score}` ← that score, and record the remaining diagnostics and security findings as validation issues. `{quality_score}` is null when skill-check did not run.
138
148
 
149
+ **Read the JSON, not the exit code.** 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 `summary.errorCount` alone is the validation verdict. 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. Bind `{security_result}` from `{run_dir}/skill-check.stderr` and the exit code, the first that applies:
150
+
151
+ - `skipped`: §3 found no skill-check (§3's log is the note), skill-check exits 2, the call stopped before it exited, or the scan exited non-zero and its tail carries no finding code. Log in the validation results "security scan skipped: 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 scan skipped: scanner could not run ({the stderr line that names the cause, or its last line})", or "security scan skipped: scanner exited non-zero with no output" when stderr is blank.
152
+ - `warn`: `security[]` holds findings, or the tail carries finding codes. Record each finding as a validation issue.
153
+ - `pass`: otherwise. The scan exited 0 and skill-check reported no finding. 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).
154
+
139
155
  **If skill-check is not available**, run the shared frontmatter validator. Resolve `{frontmatterValidator}` from `{frontmatterValidatorProbeOrder}`; first existing path wins. If no candidate exists, nothing can check the frontmatter: HARD HALT with **exit code 3 (resolution-failure)**, in interactive mode too: "**`{skill_package}` was written but not validated.** skill-check is unavailable and SKF's frontmatter validator (`skf-validate-frontmatter.py`) is missing from `{project-root}/_bmad/skf/shared/scripts/`, so re-install SKF, then re-run." Stage `{"phase": "write-and-validate", "halt_reason": "resolution-failure", "reason": "The skill was written but not validated: the frontmatter validator is missing.", "skill_package": "{skill_package}", "outputs": {<each deliverable §2 wrote, as "metadata", "skill_md" or "context_snippet" with its path>}, "details": {"cause": "frontmatter-validator-missing"}}` as `{run_dir}/halt.json` and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --run-dir "{run_dir}" --result-dir "{skill_package}" --target stderr < "{run_dir}/halt.json"`.
140
156
 
141
157
  ```bash
@@ -160,9 +176,9 @@ The validator emits JSON with `result` (PASS/FAIL), `validation.skill_md.body[]`
160
176
 
161
177
  "**Validation complete:**
162
178
 
163
- **SKILL.md:** {pass/issues found} (quality score: {score}/100 if skill-check was available)
179
+ **SKILL.md:** {pass/issues found}{ (quality score: {quality_score}/100), only when `{quality_score}` is not null}
164
180
  {list any issues}
165
- {list any auto-fixed issues, and the §4 description-guard note when the guard restored the description}
181
+ {list the auto-fixed issues: the §4 rewrite note when `{skill_md_changed}` is true, and the §4 description-guard note when the guard restored the description}
166
182
 
167
183
  **context-snippet.md:** {pass/issues found}
168
184
  {list any issues}
@@ -170,8 +186,8 @@ The validator emits JSON with `result` (PASS/FAIL), `validation.skill_md.body[]`
170
186
  **metadata.json:** {pass/issues found}
171
187
  {list any issues}
172
188
 
173
- **Security:** {pass/warn/skipped}
174
- {list any security findings}
189
+ **Security:** {security_result}
190
+ {list any security findings, or the §4 skipped note; for `pass`: "scan exited 0; skill-check 1.2.0 does not pass the scan's findings on"}
175
191
 
176
192
  **Overall:** {pass / N issues found}
177
193
 
@@ -180,7 +196,7 @@ These issues are advisory for community-tier skills: fix them in the written fil
180
196
 
181
197
  **Proceeding to finalize...**"
182
198
 
183
- Set `validation_result` with pass/fail status, quality score, and issues list, and `{validation_issues}` ← `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`, each `<n>` the number of entries the validators returned for that heading: `skill_md` counts skill-check's `diagnostics[]` (without skill-check, the frontmatter validator's `issues[]`) plus `validation.skill_md.body[]`; `context_snippet` counts `validation.context_snippet.issues[]`; `metadata` counts `validation.metadata.issues[]`; `security` counts skill-check's `security[]`. A check that did not run counts 0, and `fixed[]` and the guard note count nothing. Step 6 §3's summary carries `{quality_score}` and `{validation_issues}`.
199
+ Set `validation_result` with pass/fail status, quality score, and issues list, and `{validation_issues}` ← `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`, each `<n>` the number of entries the validators returned for that heading: `skill_md` counts skill-check's `diagnostics[]` (without skill-check, the frontmatter validator's `issues[]`) plus `validation.skill_md.body[]`; `context_snippet` counts `validation.context_snippet.issues[]`; `metadata` counts `validation.metadata.issues[]`; `security` counts skill-check's `security[]` (or, without one, the finding codes §4 read from the tail, each occurrence one finding, as skill-check's own scan summary counts them). A check that did not run counts 0, and the §4 rewrite note and the guard note count nothing. Step 6 §3's summary carries `{quality_score}`, `{validation_issues}` and `{security_result}`.
184
200
 
185
201
  ### 7. Auto-Proceed to Finalize
186
202
 
@@ -80,7 +80,7 @@ output_folder_path = ""
80
80
 
81
81
  # Optional post-completion hook: a shell command. When non-empty, step 6 runs
82
82
  # it with `--result-path=<path>` (the per-run result JSON) once the refined
83
- # architecture is promoted and both result files are written, before the run
83
+ # architecture is promoted and the per-run result file is written, before the run
84
84
  # finishes. Use to index the refined doc, notify, or chain the next workflow.
85
85
  # A failure shows as a warning line in step 6's report but never fails the run.
86
86
 
@@ -95,11 +95,11 @@ The shared emitter writes the result contract (per `shared/references/output-con
95
95
  uv run {emitEnvelopeHelper} emit --workflow skf-refine-architecture --run-dir "{run_dir}" --result-dir "{outputFolderPath}" < "{run_dir}/result-context.json"
96
96
  ```
97
97
 
98
- It writes `{outputFolderPath}/refine-architecture-result-{YYYYMMDD-HHmmss}.json` and its `refine-architecture-result-latest.json` copy (a copy, not a symlink: the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line), stamping the time, the run id, the run's warnings and the auto-decisions the gates recorded, and prints the `SKF_REFINE_ARCHITECTURE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. Bind `{result_path}` ← the line's `result_path`: a result file that could not be written leaves it null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run §1's `context` command and the emitter once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`: the run's result contract could not be written.
98
+ It writes `{outputFolderPath}/refine-architecture-result-{YYYYMMDD-HHmmss}.json` and its `refine-architecture-result-latest.json` copy (a copy, not a symlink: the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line), stamping the time, the run id, the run's warnings and the auto-decisions the gates recorded, and prints the `SKF_REFINE_ARCHITECTURE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. Bind `{result_path}` ← the line's `result_path`: a per-run record that could not be written leaves it 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, run §1's `context` command and the emitter once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`: the run's result contract could not be written.
99
99
 
100
100
  ### 5. Post-Completion Hook (optional)
101
101
 
102
- Skip this section when `{onCompleteCommand}` (resolved at SKILL.md On Activation §4 from `workflow.on_complete`) is empty, the bundled default. When `{result_path}` is null, skip it too, with one line saying no result file was written for the hook to read. Otherwise invoke it now, after the emitter wrote both result files, so a notifier, indexer or chained pipeline sees a complete record:
102
+ Skip this section when `{onCompleteCommand}` (resolved at SKILL.md On Activation §4 from `workflow.on_complete`) is empty, the bundled default. When `{result_path}` is null, skip it too, with one line saying no result file was written for the hook to read. Otherwise invoke it now, after the emitter wrote the per-run record, so a notifier, indexer or chained pipeline sees a complete record:
103
103
 
104
104
  ```bash
105
105
  {onCompleteCommand} --result-path="{result_path}"
@@ -78,7 +78,7 @@ If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 fr
78
78
  {onCompleteCommand} --result-path="{result_path}"
79
79
  ```
80
80
 
81
- Log success/failure but never fail the workflow on a hook error: the rename is already committed. The hook runs after the result files are written so an audit-log emit, registry re-index, or notifier sees the completed rename. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely; when `{result_path}` is null, skip it with one line saying no result file was written for it to read.
81
+ Log success/failure but never fail the workflow on a hook error: the rename is already committed. The hook runs after the emitter wrote the per-run record (`result_path` not null) so an audit-log emit, registry re-index, or notifier sees the completed rename. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely; when `{result_path}` is null, skip it with one line saying no result file was written for it to read.
82
82
 
83
83
  ### 4. Release the Run Lock and the Run Folder
84
84
 
@@ -4,13 +4,13 @@ Load this file only from coverage-check.md §2, and follow the one section §2 n
4
4
 
5
5
  ## Quick Tier
6
6
 
7
- - **A skill quick-skill built** (`metadata.json` `generated_by` is `quick-skill`), or **any skill in Python, JavaScript or TypeScript**, **from local source** (State 1): parse the entry points with `{extractPublicApiHelper}` `--mode quick`. quick-skill built its export list with that parser, so both sides count one surface, and in those three languages it gives each name the file that defines it (`defined_in`), so the brief's globs place the name as the recipe runner does. Resolve it ← first existing path in `{extractPublicApiProbeOrder}` and run it once per package in scope, with its manifest and one `--entry-file` for each entry-point file the Source API Surface Definition makes its surface, each path relative to `{source_path}` (`<n>` counts the packages from 1):
7
+ - **A skill quick-skill built** (`metadata.json` `generated_by` is `quick-skill`), or **any skill in Python, JavaScript or TypeScript**, **from local source** (State 1): parse the entry points with `{extractPublicApiHelper}` `--mode quick`. quick-skill built its export list with that parser, so both sides count one surface (except names quick-skill took from a README for a star re-export from another package: the parser gives that star no name, so those names read as stale), and in those three languages it gives each name the file that defines it (`defined_in`), so the brief's globs place the name as the recipe runner does. Resolve it ← first existing path in `{extractPublicApiProbeOrder}` and run it once per package in scope, with its manifest and one `--entry-file` for each entry-point file the Source API Surface Definition makes its surface, each path relative to `{source_path}` (`<n>` counts the packages from 1):
8
8
 
9
9
  ```bash
10
10
  uv run {extractPublicApiHelper} --mode quick --language <language> --source-root "{source_path}" --manifest-file <manifest path> --entry-file <entry path> > "{run_dir}/quick-<n>.json"
11
11
  ```
12
12
 
13
- Its `warnings` name each statement whose names the entry file alone cannot give. Read by eye only the statements its warnings name, save the names they give as a per-file result for the entry file that holds the statement, `{"file": "<entry path>", "exports_found": [<each name>], "signature_mismatches": []}`, and list those names in the Coverage Analysis section as read by eye. Give `surface` one `--quick` per package, one `--per-file` per saved result, the brief when the skill has one (its globs give the `scope.include` and `tier_a_include` sets), the metadata and, when init.md §2 bound one, the provenance map (the baselines of the §2b candidates and guards; they add no name):
13
+ Its `warnings` name each statement whose names the entry file alone cannot give. Read by eye only the statements its warnings name, save the names they give as a per-file result for the entry file that holds the statement, `{"file": "<entry path>", "exports_found": [<each name>], "signature_mismatches": []}`, and list those names in the Coverage Analysis section as read by eye. A warning on an `export *` or `export type *` from another package (a specifier that is no relative path; the warning writes either as `export *`) gives no name, as the recipe runner leaves such a star unresolved: list the statement as the source writes it, `<line>: <statement>`, in the `unresolved_stars` of that entry file's per-file result, and in the Coverage Analysis section as unresolved. Give `surface` one `--quick` per package, one `--per-file` per saved result that lists a name, the brief when the skill has one (its globs give the `scope.include` and `tier_a_include` sets), the metadata and, when init.md §2 bound one, the provenance map (the baselines of the §2b candidates and guards; they add no name):
14
14
 
15
15
  ```bash
16
16
  uv run {coverageInputsScript} surface --quick "{run_dir}/quick-<n>.json" [--per-file "{run_dir}/per-file-<n>.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"
@@ -26,7 +26,7 @@ The recipe runner cannot be used, so subagents read the files in scope one at a
26
26
  uv run {scoreSignaturesScript} plan --inventory "{run_dir}/inventory.json" --output "{run_dir}/signature-plan.json"
27
27
  ```
28
28
 
29
- For each source file that defines public API exports, delegate a subagent that gets that map with the file and the absolute path of `{signatureRulesFile}` (under `{skill-root}`). It gets the `documentedAlternatives` map too, whose forms it compares as that file says. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, compares each with the map's entry by the rules of that file (overload sets and `difference` tags included), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...]}`, each mismatch in the shape §2's signature comparison returns. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
29
+ For each source file that defines public API exports, delegate a subagent that gets that map with the file and the absolute path of `{signatureRulesFile}` (under `{skill-root}`). It gets the `documentedAlternatives` map too, whose forms it compares as that file says. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, gives no name for an `export * from` or `export type * from` another package (a specifier that is no relative path; `export * as ns` still gives `ns`), which the recipe runner leaves unresolved, listing each such statement as `<line>: <statement>` in `unresolved_stars`, compares each with the map's entry by the rules of that file (overload sets and `difference` tags included), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...], "unresolved_stars": [...]}`, each mismatch in the shape §2's signature comparison returns. List each response's `unresolved_stars` in the Coverage Analysis section as unresolved. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
30
30
 
31
31
  ```bash
32
32
  uv run {coverageInputsScript} surface --per-file "{run_dir}/per-file-<n>.txt" --fallback-scan [--extraction "{run_dir}/extract-full.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"
@@ -86,13 +86,13 @@ uv run {coverageInputsScript} floor --skill-dir "{resolved_skill_package}" --out
86
86
 
87
87
  Exit 1 printed its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:inventory`).
88
88
 
89
- Delegate the reading of the skill under test to a subagent, given `{run_dir}` and the path to SKILL.md and to its `references/` directory when one exists: it reads SKILL.md, and every `references/*.md` file too when SKILL.md's `## Full` headings are absent or stubs. It is given `{run_dir}/inventory-floor.json` too, and lists each name there with an origin in a file it reads, choosing its kind from the name's `kindLabel`, the skill's own label, when it has one. For each export two or more of the files it reads document, it compares the SKILL.md body with each reference file, and two reference files with each other only when the body does not settle them (it does not document the export, or agrees with both), by the rules of `{signatureRulesFile}`, whose absolute path (under `{skill-root}`) it is given: parameters, the return type, and descriptions that contradict (brief against detailed is fine). That file alone says what a form may leave out. It writes only this JSON, with no prose and no commentary, to `{run_dir}/inventory-response.txt` with the Write tool, never a shell, and returns that path, or returns the JSON itself when it cannot write files. The JSON's shape and the contract the validator checks are in `{inventoryContractFile}`, whose absolute path (under `{skill-root}`) the subagent is given.
89
+ Delegate the reading of the skill under test to a subagent, given `{run_dir}` and the path to SKILL.md and to its `references/` directory when one exists: it reads SKILL.md, and every `references/*.md` file too when SKILL.md's `## Full` headings are absent or stubs. It is given `{run_dir}/inventory-floor.json` too, and lists each name there with an origin in a file it reads, choosing its kind from the name's `kindLabel`, the skill's own label, when it has one. An origin's `signature` is that place's entry, its non-blank `params` and `return_type` kept as given; a blank one is the reader's. For each export two or more of the files it reads document, it compares the SKILL.md body with each reference file, and two reference files with each other only when the body does not settle them (it does not document the export, or agrees with both), by the rules of `{signatureRulesFile}`, whose absolute path (under `{skill-root}`) it is given: parameters, the return type, and descriptions that contradict (brief against detailed is fine). That file alone says what a form may leave out. It writes only this JSON, with no prose and no commentary, to `{run_dir}/inventory-response.txt` with the Write tool, never a shell, and returns that path, or returns the JSON itself when it cannot write files. The JSON's shape and the contract the validator checks are in `{inventoryContractFile}`, whose absolute path (under `{skill-root}`) the subagent is given.
90
90
 
91
91
  When the subagent returned the JSON rather than the path, or the file is absent or empty afterwards, save its response, as it came back, to `{run_dir}/inventory-response.txt` with the Write tool. The documented inventory is the one §1a validates: do not load SKILL.md or the reference files into the parent context. Without subagents, the parent loads `{inventoryContractFile}` and builds the same JSON from the same files and `inventory-floor.json` and saves it to that file; §1a's checks still run on it.
92
92
 
93
93
  #### 1a. Parent-Side Schema Validation + Spot-Check
94
94
 
95
- Before anything consumes the inventory, `{validateInventoryScript}` checks it against the §1 contract (a wrapping fence stripped), spot-checks sampled names against SKILL.md and the subagent's listed reference files, and refuses it when it leaves out a floor name those files write. The look-up is the script's, never a grep: an export named `$state` or `a.b` is neither a shell variable nor a regex.
95
+ Before anything consumes the inventory, `{validateInventoryScript}` checks it against the §1 contract (a wrapping fence stripped), spot-checks sampled names against SKILL.md and the subagent's listed reference files, and refuses it when it leaves out a floor name those files write, or an origin's `signature`. The look-up is the script's, never a grep: an export named `$state` or `a.b` is neither a shell variable nor a regex.
96
96
 
97
97
  ```bash
98
98
  uv run {validateInventoryScript} --input "{run_dir}/inventory-response.txt" --skill-package "{resolved_skill_package}" --output "{run_dir}/inventory.json" --floor "{run_dir}/inventory-floor.json"
@@ -107,7 +107,7 @@ Only when the validated inventory's `references` is non-empty. Each `cross_check
107
107
 
108
108
  ### 2. Analyze Source Code (Tier-Dependent)
109
109
 
110
- Identify the public API surface from the package entry points (0b), as deep as the tier allows. Whichever branch runs, `{coverageInputsScript} surface` writes it to `{run_dir}/surface.json`, which §2b, §2c, §4 and step 5 read. Names read by eye reach it as per-file results, `{"file": "<path>", "exports_found": [<each name>], "signature_mismatches": []}` for each file, saved to `{run_dir}/per-file-<n>.json` with the Write tool and given to `surface` with one `--per-file` each.
110
+ Identify the public API surface from the package entry points (0b), as deep as the tier allows. Whichever branch runs, `{coverageInputsScript} surface` writes it to `{run_dir}/surface.json`, which §2b, §2c, §4 and step 5 read. Names read by eye reach it as per-file results, `{"file": "<path>", "exports_found": [<each name>], "signature_mismatches": []}` for each file, saved to `{run_dir}/per-file-<n>.json` with the Write tool and given to `surface` with one `--per-file` each. An `export * from` or `export type * from` another package adds no name: list `<line>: <statement>` in `unresolved_stars` and Coverage Analysis.
111
111
 
112
112
  **Exits.** Every `{coverageInputsScript}` and `{scoreSignaturesScript}` command in §2 and §2b exits 0, or exits 1 with its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:surface`). `surface` with a `--per-file`, and `score`, also exit 2 when a subagent response breaks the schema: re-dispatch that subagent once with its `violations[]` appended (or correct a result the main thread wrote), save the new response over the old one and run the command again; a second failure HALTs the same way.
113
113
 
@@ -298,7 +298,7 @@ uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check -
298
298
  uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from guards --input "{run_dir}/surface.json" --metadata "{resolved_skill_package}/metadata.json"
299
299
  uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from numerator --input "{run_dir}/numerator.json" --metadata "{resolved_skill_package}/metadata.json"
300
300
  uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from metadata-coherence --input "{run_dir}/metadata-coherence.json" --metadata "{resolved_skill_package}/metadata.json"
301
- uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from provenance-line --input "{run_dir}/provenance-verify.json"
301
+ uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from provenance-line --input "{run_dir}/provenance-verify.json" [--stale "{run_dir}/stale.json"]
302
302
  ```
303
303
 
304
304
  **A split-body mismatch's Source** is a line inside the skill package: when §1b found one, load `{coverageBranchesFile}` and follow its **Split-Body Sources** section.
@@ -22,6 +22,6 @@ The JSON coverage-check.md §1's reader writes to `{run_dir}/inventory-response.
22
22
  }
23
23
  ```
24
24
 
25
- The contract `validate-inventory.py` checks (coverage-check.md §1a): each `kind` is one of `function`, `class`, `type`, `constant`, `hook`, `interface`, `method`, `struct`, `enum`, `trait`, `macro` or `adapter`; every entry carries the `description` the skill gives it (an empty string when it gives none); a function or method carries `params` (`""` or `[]` when it takes none) and `return_type` as the skill documents them, and leaves a key out when the skill documents nothing for it. A `method`'s `name` is the bare method name and its `owner` the class or object it is called on (`agents` for `cognee.agents.get`, where the skill documents `agents` as a class; the class for one reached through a property, `client.messages`); `owner` is not validated. A function the skill documents in a module is a `function` named bare, never a `method`, even when it is called through the module's name (`register_sql_connection` for `cognee.tools.register_sql_connection`, where the skill documents `tools` as a module). Each `methods` item starts with the method's name, optionally after `async` (`async next_uuid(dep=None)`). A `const` holding a component (a `forwardRef`, `memo` or other wrapper call, or an alias like `const Tabs = TabsPrimitive.Root`) is a `constant` with the declaration or usage form the skill writes as `usage_signature`; a data constant carries no `usage_signature`, `params` or `return_type`. A name documented in several places is listed once per place, each entry with that place's form (`Card` above), and keeps one `kind` in every place. Where the skill documents several names and states members they share once, in the same place (a table row, a paragraph) or over a table for all its rows (a sentence or caption just above it, or its column header), each name's entry carries that shared text in its `params` (in its `usage_signature`, for a usage form), never in a `methods` item, which still starts with the method's name: the shared text first, then `; `, then the name's own members as the skill writes them, without the name, never the name's own members alone (`Note` above, from a row that documents `Note` and `Task`, each with the base fields `id, scope=Scope()`, as `Note(content, title=None)` and `Task(title, done=False)`). An entry whose signature the skill labels partial (simplified, key parameters, a `...` standing in for omitted members, not rest or variadic syntax; a label in a table's column header, such as `Props (simplified)`, labels each of its rows) carries `signature_form: "partial"`, any other `"full"` or no key: copy the label, never judge the form.
25
+ The contract `validate-inventory.py` checks (coverage-check.md §1a): each `kind` is one of `function`, `class`, `type`, `constant`, `hook`, `interface`, `method`, `struct`, `enum`, `trait`, `macro` or `adapter`; every entry carries the `description` the skill gives it (an empty string when it gives none); a function or method carries `params` (`""` or `[]` when it takes none) and `return_type` as the skill documents them, and leaves a key out when the skill documents nothing for it. A name written with empty parentheses in running text or a description cell is a mention, not a form: its entry leaves `params` and `usage_signature` out (a cell reading "`instrument()` wraps functions"). A description cell is any table cell but an export table's first cell and a cell whose column writes the call or signature, by its role (Signature, Props, Params, Parameters, Arguments, Usage, Syntax, Example and the like); a heading or an export table's first cell that writes `name()` is a form, recorded as written. A `method`'s `name` is the bare method name and its `owner` the class or object it is called on (`agents` for `cognee.agents.get`, where the skill documents `agents` as a class; the class for one reached through a property, `client.messages`); `owner` is not validated. A function the skill documents in a module is a `function` named bare, never a `method`, even when it is called through the module's name (`register_sql_connection` for `cognee.tools.register_sql_connection`, where the skill documents `tools` as a module). Each `methods` item starts with the method's name, optionally after `async` (`async next_uuid(dep=None)`). A `const` holding a component (a `forwardRef`, `memo` or other wrapper call, or an alias like `const Tabs = TabsPrimitive.Root`) is a `constant` with the declaration or usage form the skill writes as `usage_signature`; a data constant carries no `usage_signature`, `params` or `return_type`. A name documented in several places is listed once per place, each entry with that place's form (`Card` above), and keeps one `kind` in every place. A place `inventory-floor.json` gives a `signature` (a cited function declaration, a Signature cell, or a heading or an export table's first cell that writes one call) has that form as its entry, whatever its kind, its non-blank `params` and `return_type` kept as given (a `params` may open with the shared text the next sentence gives, then `; `); a blank one is the reader's, and a form the skill writes only in prose is an entry of its own. Where the skill documents several names and states members they share once, in the same place (a table row, a paragraph) or over a table for all its rows (a sentence or caption just above it, or its column header), each name's entry carries that shared text in its `params` (in its `usage_signature`, for a usage form), never in a `methods` item, which still starts with the method's name: the shared text first, then `; `, then the name's own members as the skill writes them, without the name, never the name's own members alone (`Note` above, from a row that documents `Note` and `Task`, each with the base fields `id, scope=Scope()`, as `Note(content, title=None)` and `Task(title, done=False)`). An entry whose signature the skill labels partial (simplified, key parameters, a `...` standing in for omitted members, not rest or variadic syntax; a label in a table's column header, such as `Props (simplified)`, labels each of its rows) carries `signature_form: "partial"`, any other `"full"` or no key: copy the label, never judge the form.
26
26
 
27
27
  A `cross_check_mismatches` entry gives its `export`, `reference_file`, `reference_line` and an `issue` that names each form by its file's path, and the pair's other side: the SKILL.md body's line as `skill_md_line`, or, for two references, the one that sorts first in codepoint order, by path and then line, as `other_file` and `other_line`, with the one that sorts last as `reference_file` and `reference_line`. Each file is its path in the package, such as `references/api-reference.md`.
@@ -59,7 +59,7 @@ When the emitter itself is not installed (init.md §0), no line can be printed:
59
59
 
60
60
  ## Result Files
61
61
 
62
- report.md §4c runs the emitter with `--result-dir "{forge_version}"`. It writes the per-run record `skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name, `-2`, `-3` appended when a run already took that second's name) and then its copy `skf-test-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path (a copy, not a symlink), each atomically. The record holds the envelope's fields, plus `skill`, `outputs[]` (the published report), `summary` (`score`, `threshold`, `result` (`PASS`, `PASS_WITH_DRIFT`, `FAIL` or `INCONCLUSIVE`), `testMode`, `activeCategories[]`, `inconclusiveReasons[]` when present, `threshold_fallback`, `original_threshold` and `evidence_report_path` when the threshold fallback fired, `gapCounts` with the gap ledger's gaps by severity, and `hardGate`), `runId` and `healthCheckDispatched`, and the emitter stamps `timestamp`, `run_id`, `headless_decisions` and `warnings` into it. A run the hard gate blocked writes it too, as a FAIL record, so its `skf-test-skill-result-latest.json` holds that FAIL, not an earlier run's verdict. A HALT writes no result file, and neither does a run cut off before report.md §4c: the copy then still holds an earlier run's verdict.
62
+ report.md §4c runs the emitter with `--result-dir "{forge_version}"`. It writes the per-run record `skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name, `-2`, `-3` appended when a run already took that second's name) and then its copy `skf-test-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path (a copy, not a symlink), each atomically. The record holds the envelope's fields, plus `skill`, `outputs[]` (the published report), `summary` (`score`, `threshold`, `result` (`PASS`, `PASS_WITH_DRIFT`, `FAIL` or `INCONCLUSIVE`), `testMode`, `activeCategories[]`, `inconclusiveReasons[]` when present, `threshold_fallback`, `original_threshold` and `evidence_report_path` when the threshold fallback fired, `gapCounts` with the gap ledger's gaps by severity, and `hardGate`), `runId` and `healthCheckDispatched`, and the emitter stamps `timestamp`, `run_id`, `headless_decisions` and `warnings` into it. A run the hard gate blocked writes it too, as a FAIL record, so its `skf-test-skill-result-latest.json` holds that FAIL, not an earlier run's verdict, unless the run's line holds a `result_file_write_failed` warning (the emitter stops at the first write that fails). A HALT writes no result file, and neither does a run cut off before report.md §4c: the copy then still holds an earlier run's verdict.
63
63
 
64
64
  ## Emitting a Halt
65
65
 
@@ -235,7 +235,7 @@ and bind `{report_file}` ← `{publishedReportFile}`. If the move fails, HALT (`
235
235
  uv run {emitEnvelopeHelper} emit --workflow skf-test-skill --run-dir "{run_dir}" --result-dir "{forge_version}" --target {emit_target} < "{run_dir}/result-context.json"
236
236
  ```
237
237
 
238
- It writes `{forge_version}/skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and then its `skf-test-skill-result-latest.json` copy (the record of the last run whose emitter wrote this copy, at a stable path: a copy, not a symlink; a caller reads its own run's `SKF_TEST_RESULT_JSON` line), each atomically, so a partial write is never observable; stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both; and prints the `SKF_TEST_RESULT_JSON:` line. Bind `{result_line}` ← that line and `{result_path}` ← its `result_path`. A run the hard gate blocked writes the record too, as a FAIL (`status: "error"`, `verdict: "FAIL"`, `halt_reason: "hard-gate-blocked"`, `exit_code` 2), so after a blocked run `skf-test-skill-result-latest.json` holds its FAIL, never an earlier run's verdict. A result file that could not be written leaves `result_path` null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run it once more; if it fails again, HALT (`halt_reason: "write-failed"`, phase `report:result-contract`) with its stderr: the run's result contract could not be written. `references/invocation-contract.md` lists the record's fields.
238
+ It writes `{forge_version}/skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and then its `skf-test-skill-result-latest.json` copy (the record of the last run whose emitter wrote this copy, at a stable path: a copy, not a symlink; a caller reads its own run's `SKF_TEST_RESULT_JSON` line), each atomically, so a partial write is never observable; stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both; and prints the `SKF_TEST_RESULT_JSON:` line. Bind `{result_line}` ← that line and `{result_path}` ← its `result_path`. A run the hard gate blocked writes the record too, as a FAIL (`status: "error"`, `verdict: "FAIL"`, `halt_reason: "hard-gate-blocked"`, `exit_code` 2), so after a blocked run `skf-test-skill-result-latest.json` holds its FAIL, never an earlier run's verdict, unless the run's line holds a `result_file_write_failed` warning (the emitter stops at the first write that fails). 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, run it once more; if it fails again, HALT (`halt_reason: "write-failed"`, phase `report:result-contract`) with its stderr: the run's result contract could not be written. `references/invocation-contract.md` lists the record's fields.
239
239
 
240
240
  Once the result files are written, append `'report'` to `stepsCompleted` in the `{outputFile}` frontmatter (it stays out of the expected set above).
241
241
 
@@ -19,7 +19,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
19
19
  | Severity | Category | Criteria |
20
20
  |----------|-----------------------------|----------|
21
21
  | Critical | `signature-mismatch` | Wrong signature: a documented signature that differs from the source in its parameters (name, type, order, optionality) or its return type, as signature-comparison-rules.md reads each form (a component by its props; an annotation either side leaves out is not compared), in any way but those the Low `signature-mismatch` row and the Info `observation` row grade, such as a renamed Python parameter (the script cannot see which parameters a caller must pass by position), an optional parameter left out of a form the skill does not label partial, or a mismatch with no `difference` (coverage-check §2 and §2b) |
22
- | Critical | `fabricated-signature` | Fabricated signature: a documented export absent at the source line the skill cites for it. Decided only where ast-grep enumerated the exports at the pinned commit: the name is outside that surface, and the cited file is missing or defines no such name (coverage-check §2c) |
22
+ | Critical | `fabricated-signature` | Fabricated signature: a documented export absent at the source line the skill cites for it. Decided only where ast-grep enumerated the exports at the pinned commit: the name is outside that surface, and the cited file is missing or defines no such name, and no item of an export list (`export { L as NAME }`, `export type { ... }`) or of a Python `from X import` clause elsewhere renames to it a name that file declares; any other rename (`export { default as NAME }`, `const NAME = L`, `type NAME = L`, an import then an export, Python `NAME = L`) stays fabricated (coverage-check §2c) |
23
23
  | Critical | `broken-reference` | Broken reference (contextual mode): a reference whose target does not exist, such as a file not at the stated path, a skill not in the skills output folder, a type no module declares, or a `scripts/` or `assets/` file the skill names (coherence-check §4) |
24
24
  | High | `inaccurate-reference` | Inaccurate reference (contextual mode): the target exists but does not match, such as a type the cited module does not export, a signature that differs, or an integration pattern that does not match the implementation (coherence-check §4) |
25
25
  | High | `reference-escape` | A reference whose real path leaves the skill, its source and, for a stack, the skills output folder (coherence-check §4) |
@@ -45,7 +45,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
45
45
  | Low | `metadata` | Missing optional metadata or examples |
46
46
  | Low | `description` | Description trigger optimization recommended (third-person voice, negative triggers, or keyword coverage gaps), a Tessl Review description suggestion included |
47
47
  | Low | `external-validator` | A skill-check diagnostic or a Tessl Review validation finding the run left unresolved (external-validators §5b) |
48
- | Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c). Also a partial signature: a mismatch the signature judge tags `omitted-optional` (defined in signature-comparison-rules.md, Difference tags) on a form the skill labels partial (simplified, key parameters, a `...` that stands in for omitted members, not rest or variadic syntax), which the inventory marks `signature_form: "partial"`, as score-signatures.py checks: `Partial signature: {name}`, at the declaration, counted as matching in Signature Accuracy (coverage-check §2 and §2b). Also a return annotation the body contradicts: a mismatch the signature judge tags `annotation-contradicted` (defined in signature-comparison-rules.md, Difference tags) whose documented signature states a return type and writes the source's parameters, and whose `evidence_line`, the body line that returns the documented type, comes after the declaration and before the next one and, with the local source, starts with `return` (or is a one-line declaration's own expression body), as score-signatures.py checks: `Annotation contradicted: {name}`, at the declaration, counted as matching in Signature Accuracy, with no change to the skill (the annotation is reported upstream) (coverage-check §2 and §2b) Also a documentation error a Deep-tier run sees that no script checks (an import path whose module does not export the name, a type member that differs from its declaration): `Not checked by script: {name}`, at the documenting line, written by hand (coverage-check §2 and §5b) |
48
+ | Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it; an `aliased` name, which a line elsewhere renames from the declaration its cited file holds (an export-list item `L as NAME` (`export { default as NAME } from` included, a whole-string side unquoted, a comment allowed before `export`), a plain `export { NAME }` of an import that renames (`import { L as NAME }`, `import NAME from`), a `const`, `let`, `var` or `type NAME = L` with a bare identifier `L`, a Python `from X import L as NAME` at column 0 or one level in a module-level `try`, `except`, `else`, `if` or `elif` block, or a Python `NAME = L`), at that line, a multi-line list's item at its own line; when the rename's relative or in-package `from` module, or the import that binds `L`, resolves to a file that declares `L`, that file must be the cited one, so a rename of another file's same-named declaration stays a fabricated signature, as do a type assertion, any other expression and an import inside a function or a class), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c). Also a partial signature: a mismatch the signature judge tags `omitted-optional` (defined in signature-comparison-rules.md, Difference tags) on a form the skill labels partial (simplified, key parameters, a `...` that stands in for omitted members, not rest or variadic syntax), which the inventory marks `signature_form: "partial"`, as score-signatures.py checks: `Partial signature: {name}`, at the declaration, counted as matching in Signature Accuracy (coverage-check §2 and §2b). Also a return annotation the body contradicts: a mismatch the signature judge tags `annotation-contradicted` (defined in signature-comparison-rules.md, Difference tags) whose documented signature states a return type and writes the source's parameters, and whose `evidence_line`, the body line that returns the documented type, comes after the declaration and before the next one and, with the local source, starts with `return` (or is a one-line declaration's own expression body), as score-signatures.py checks: `Annotation contradicted: {name}`, at the declaration, counted as matching in Signature Accuracy, with no change to the skill (the annotation is reported upstream) (coverage-check §2 and §2b) Also a documentation error a Deep-tier run sees that no script checks (an import path whose module does not export the name, a type member that differs from its declaration): `Not checked by script: {name}`, at the documenting line, written by hand (coverage-check §2 and §5b) |
49
49
  | Info | `provenance-unverified` | Provenance line not verified: the line-check rules found no definition line, or the recorded line is not a line number (`line-invalid`) (coverage-check §4c) |
50
50
  | Info | `migration-section` | Historical migration content in Section 4b with no T2-future annotation (Case 2 of `migration-section-rules.md`) |
51
51
  | Info | `multi-denominator` | Multi-denominator reporting: barrel and documented-surface clusters diverge by design (>10% cross-cluster) |
@@ -6,9 +6,11 @@ Every signature judge compares by these rules: coverage-check.md §1's split-bod
6
6
 
7
7
  A form claims only what it writes, and only what it claims is compared. Its parameter list claims each parameter it lists, in that order, and no other: a parameter it leaves out is one it says the export does not take (Difference tags say when that is `omitted-optional`). A type or a return type it does not write claims nothing (Annotations).
8
8
 
9
+ A name written with empty parentheses in running text or a description cell is a mention, not a form: it claims nothing and is not compared. A description cell is any table cell but an export table's first cell and a cell whose column writes the call or signature, by its role (Signature, Props, Params, Parameters, Arguments, Usage, Syntax, Example and the like); a heading or an export table's first cell that writes `name()` is a form, an empty call (below).
10
+
9
11
  Three kinds of form do not claim a complete parameter list:
10
12
 
11
- - A call or usage example passes values (literals, variables, `name=value`): `RecursiveSplitter()`, `embed(text, model=m)`, `<Canvas of={X} />`. It is compared as a call: each argument it passes must exist, by position or by name, and every required parameter must be passed; an optional parameter it leaves out is no difference. An empty call `X()` is one too, so it matches a source whose parameters are all optional. A component usage `<X a={x} b="y" />` reads as a list of bare names of the props it sets, and a call `f(a, b)` as its arguments in order, each the parameter at its position (or, written `name=value`, the one it names). A JSX spread (`{...props}`) or children can pass a required prop, and a call's `...args` a required parameter, so neither leaves one out. An entry's `usage_signature` written as a usage is read this way, and each usage in a list of usages is compared as a form of its own, under its entry's `form` index.
13
+ - A call or usage example passes values (literals, variables, `name=value`): `RecursiveSplitter()`, `embed(text, model=m)`, `<Canvas of={X} />`. It is compared as a call: each argument it passes must exist, by position or by name, and every required parameter must be passed; an optional parameter it leaves out is no difference. An empty call `X()` written as a call example (in a heading, an export table's first cell, a call or signature column or a fenced example, never a mention) is one too, so it matches a source whose parameters are all optional. A component usage `<X a={x} b="y" />` reads as a list of bare names of the props it sets, and a call `f(a, b)` as its arguments in order, each the parameter at its position (or, written `name=value`, the one it names). A JSX spread (`{...props}`) or children can pass a required prop, and a call's `...args` a required parameter, so neither leaves one out. An entry's `usage_signature` written as a usage is read this way, and each usage in a list of usages is compared as a form of its own, under its entry's `form` index.
12
14
  - An ellipsis (`...` or `…`) in a list stands for members it does not show and claims nothing about them: `(...)` and `(tasks, data, dataset, ...)` leave out no parameter.
13
15
  - A list of bare names, with no type, default or `?` (`connect(host, port)`, `({ themes, defaultTheme })`), names parameters without claiming the list is complete: an optional member it leaves out is no difference, a required one is a mismatch.
14
16
 
@@ -24,6 +26,8 @@ The names of Python's `*args` and `**kwargs` are never compared (`**kwargs` agai
24
26
 
25
27
  The source's declaration decides: the documented form is compared with the parameters, the optionality and the annotations the signature declares, with one exception, a return annotation the body contradicts (Annotations).
26
28
 
29
+ A const annotated with a function type declares that type's parameters: `export const build: ViteBuilder['build'] = async ({ options }) => ...` takes the one parameter `ViteBuilder['build']` names, and the arrow's destructuring is that same parameter. A form may name it as the type does or destructure it as the arrow does, and a destructuring that names only some of the type's properties, in the arrow or in the form, does not read as a narrower type. A declaration whose parameter list is written only in a package outside the source tree (a binding destructured from a value cast to `typeof domTestingLibrary`, or `const spyOn: typeof vitestSpyOn = (...args) => ...`) has its parameters, their optionality and its return left uncompared and counts as matching, as a component whose props type lives there does (Components).
30
+
27
31
  In the split-body cross-check both forms are documented: each is read by these rules, a type, return type or type arguments one of them leaves out are not compared, and a parameter's optionality is compared only when both forms state it. An ellipsis claims nothing about the members it stands for, required ones included. A call, a list of bare names or a form its own file labels partial (each side's label is read from that side's file, not from the export's one `signature_form`) may leave out a parameter the other form does not state as required, unless leaving it out shifts a caller's positions (a language without keyword arguments, as above); any other form claims its list is complete, so leaving out a parameter the other writes is a mismatch (inventory-contract.md's examples). Each export's form is read alone: what the rows of other exports write, in the same table or file, states nothing about it, so `(action, sink, tracking_record, child_invalidation)` matches `child_invalidation: X | None = None` even when every other row of its table writes its defaults.
28
32
 
29
33
  A name the skill documents in several forms (a brief usage in SKILL.md, a full props list in a reference file) comes with `alternatives`: coverage-check.md §2's check lists them after its `documented` form, and the fallback per-file scan's `documentedAlternatives` holds them by name. Compare the declaration with each form the check offers, each by its own reading rules, and report a mismatch for each form that does not match, with its index as `form` (0 is `documented`, 1 the first alternative), its `documented_sig` written as that form and its `difference` tagged against it. With `candidates`, a form matches when it matches any candidate, and its mismatch is reported at the closest candidate.
@@ -36,7 +40,7 @@ A component is an export the source declares as one: a function that takes one p
36
40
 
37
41
  A component's own props are the members written in its package: its interface or object type, object literals intersected into it, and members computed from its own code (`VariantProps<typeof buttonVariants>`). Members of a type from another package (`React.ComponentProps<"button">`, `HTMLAttributes<...>`, `ComponentProps<typeof Link>`) are inherited, as are the `ref` a `forwardRef` result takes and the optional `children` of `PropsWithChildren<P>`. A prop the form writes must exist on `P`, own or inherited. A component whose props type lives in a package outside the source tree (`const Tabs = TabsPrimitive.Root`) has its props left uncompared: only that it is a component is checked.
38
42
 
39
- A prop the form writes that `P` lacks, or a type or an optionality it writes that the source contradicts, is a mismatch. So is a required own prop it leaves out, and a required inherited one (a `Link`'s `href`) left out of a full form. An optional own prop a typed props list or a table leaves out is an `omitted-optional` difference; one a list of bare names or a usage leaves out, and an optional inherited prop, is no difference. `| null` and `| undefined` on an optional prop are no difference (`VariantProps` adds both). A props type name the form writes that the source does not declare (`Button(props: ButtonProps)` against a source with no `ButtonProps`) is not compared by name, only the props the form lists. Values in a usage example (`variant="subtle"`) are not compared.
43
+ A prop the form writes that `P` lacks, or a type or an optionality it writes that the source contradicts, is a mismatch. So is a required own prop it leaves out, and a required inherited one (a `Link`'s `href`) left out of a full form. An optional own prop a typed props list or a table leaves out is an `omitted-optional` difference; one a list of bare names or a usage leaves out, and an optional inherited prop, is no difference. A rest element in a component's destructured props form (`({ className, variant = "default", ...props }: NativeButtonProps)`) stands for every prop of `P` the form does not name, own or inherited, required ones included, so the form leaves none of them out, and the rest's own name is not compared as a prop. `| null` and `| undefined` on an optional prop are no difference (`VariantProps` adds both). A props type name the form writes that the source does not declare (`Button(props: ButtonProps)` against a source with no `ButtonProps`) is not compared by name, only the props the form lists. Values in a usage example (`variant="subtle"`) are not compared.
40
44
 
41
45
  A props list states optionality as Reading a form says: `({ of, meta, story })`, or a list of names, states none, and in an object type literal (`{ of?: X; meta: M }`) that states it for any member, each member written with no mark claims to be required. A table states optionality by a Required column, a `?` on a prop's name or a Default cell that gives a value; its Type column states types, never optionality. A Default cell with a value marks only its own row optional and an empty one states nothing, so Create Skill's `| Prop | Type | Default | Description |` table never reads a prop with an empty Default cell as required. A destructuring default (`({ of, meta = {} })`) likewise marks only its own member, and the others state no optionality.
42
46
 
@@ -44,6 +48,7 @@ A props list states optionality as Reading a form says: `({ of, meta, story })`,
44
48
 
45
49
  - A parameter written without a type is not compared on type: its name and order are, and its optionality as Reading a form says. Bare `*args` matches `*args: P.args`, and `**kwargs` matches `**kwargs: P.kwargs`.
46
50
  - A return type the form does not state is not compared. A form states one when its entry has a non-blank `return_type` or the form writes one: after `->`, `=>`, `→` or `):`, or in its language's own position (Go's result list after the parameters, the type before the name in C, C++ and Java). A blank `return_type` (`""`) states none, so an entry with `params: ""` and no return type matches `def f() -> None`.
51
+ - A return written as names of values rather than types, a parenthesized list of names such as `→ (nodes, edges)` (after the arrow or in `return_type`), states no return type, so no return is compared. A type name written alone stays a type, lowercase ones included: `-> list` and `→ str` state one.
47
52
  - A type the source leaves unannotated, on a parameter or a return, is not compared, except that a TS/JS parameter with a literal default has that literal's type: `pageSize = 20` is a `number`, so `pageSize?: string` is a mismatch. An unannotated return stays uncompared.
48
53
  - A documented parameter type that names some of the members of the source's union is no mismatch, since a caller passing it makes a valid call (`schema: VectorSchemaProvider` against `VectorSchemaProvider | MultiVectorSchemaProvider`); a documented return type must cover every member the source can return.
49
54
  - Type arguments the form leaves out are not compared: `dict` matches `dict[str, Any]`, and `list[dict]` matches `list[dict[str, Any]]`. Type arguments it writes are: `dict[str, int]` against `dict[str, str]` is a mismatch.