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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/docs/_data/pinned.yaml +9 -10
- package/docs/campaign.md +2 -2
- package/docs/skill-model.md +3 -3
- package/docs/verifying-a-skill.md +17 -18
- package/docs/workflows.md +10 -10
- package/package.json +1 -1
- package/src/shared/references/description-guard-protocol.md +12 -10
- package/src/shared/references/output-contract-schema.md +1 -1
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +38 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +5 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +8 -3
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +6 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +7 -2
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +1 -1
- package/src/shared/scripts/skf-parse-gaps.py +4 -1
- package/src/shared/scripts/skf-quick-batch.py +24 -7
- package/src/shared/scripts/skf-verify-provenance-completeness.py +809 -85
- package/src/skf-analyze-source/customize.toml +3 -2
- package/src/skf-analyze-source/references/auto-docs-only.md +2 -2
- package/src/skf-analyze-source/references/generate-briefs.md +5 -3
- package/src/skf-analyze-source/references/headless-contract.md +2 -2
- package/src/skf-analyze-source/references/step-auto-scope.md +2 -2
- package/src/skf-audit-skill/customize.toml +4 -3
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
- package/src/skf-campaign/references/campaign-contracts.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +2 -2
- package/src/skf-campaign/references/step-07-capstone.md +3 -3
- package/src/skf-campaign/references/step-08-verify.md +3 -3
- package/src/skf-campaign/references/step-09-refine.md +3 -3
- package/src/skf-campaign/references/step-11-maintenance.md +1 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
- package/src/skf-campaign/scripts/campaign-report.py +60 -4
- package/src/skf-campaign/scripts/campaign-state.py +82 -17
- package/src/skf-campaign/templates/campaign-report-template.md +4 -0
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/skill-sections.md +2 -2
- package/src/skf-create-skill/customize.toml +4 -4
- package/src/skf-create-skill/references/batch-mode.md +3 -1
- package/src/skf-create-skill/references/report.md +9 -4
- package/src/skf-create-skill/references/validate.md +49 -29
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/report.md +3 -3
- package/src/skf-create-stack-skill/references/validate.md +21 -8
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-quick-skill/SKILL.md +1 -1
- package/src/skf-quick-skill/references/batch-mode.md +30 -2
- package/src/skf-quick-skill/references/compile.md +2 -0
- package/src/skf-quick-skill/references/finalize.md +3 -2
- package/src/skf-quick-skill/references/halt-contract.md +3 -3
- package/src/skf-quick-skill/references/quick-extract.md +2 -1
- package/src/skf-quick-skill/references/write-and-validate.md +27 -11
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +2 -2
- package/src/skf-rename-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +4 -4
- package/src/skf-test-skill/references/inventory-contract.md +1 -1
- package/src/skf-test-skill/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/scoring-rules.md +2 -2
- package/src/skf-test-skill/references/signature-comparison-rules.md +7 -2
- package/src/skf-test-skill/scripts/gap-ledger.py +196 -46
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +253 -17
- package/src/skf-test-skill/scripts/score-signatures.py +9 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +91 -12
- package/src/skf-update-skill/customize.toml +1 -1
- package/src/skf-update-skill/references/gap-driven.md +1 -1
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +1 -1
- package/src/skf-update-skill/references/report.md +27 -9
- package/src/skf-update-skill/references/write.md +24 -12
- package/src/skf-verify-stack/references/report.md +1 -1
|
@@ -59,8 +59,9 @@ analysis_report_template_path = "templates/analysis-report-template.md"
|
|
|
59
59
|
# written the result files, at the end of every run that writes them
|
|
60
60
|
# (generate-briefs.md, step-auto-scope.md and auto-docs-only.md), as:
|
|
61
61
|
# <on_complete> --result-path=<forge_data_folder>/analyze-source-result-latest.json
|
|
62
|
-
# A
|
|
63
|
-
#
|
|
62
|
+
# A HALT calls no hook, nor does a run whose result file or its -latest
|
|
63
|
+
# copy could not be written (result_path null, or a result_file_write_failed
|
|
64
|
+
# warning naming the copy, in the envelope). Useful for Slack notifications,
|
|
64
65
|
# dashboard ingest, CI hooks, or chaining into a brief linter. A hook
|
|
65
66
|
# failure is displayed and never fails the run.
|
|
66
67
|
#
|
|
@@ -145,9 +145,9 @@ Add `"coexistence": "alongside"` when `{coexistence_suffix}` is non-empty ([A]lo
|
|
|
145
145
|
uv run {emitEnvelopeHelper} emit --workflow skf-analyze-source --run-dir "{run_dir}" --result-dir "{forge_data_folder}" < "{run_dir}/result-context.json"
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
The emitter stamps the UTC time and the run id into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy, and prints the `SKF_ANALYZE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. A
|
|
148
|
+
The emitter stamps the UTC time and the run id into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy, and prints the `SKF_ANALYZE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. A per-run record that could not be written leaves `result_path` null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, correct `result-context.json` from the message on its stderr and run it once more; if it fails again, HARD HALT (exit code 4, `halt_reason: "write-failed"`, phase `auto-docs-only:5`, path `{forge_data_folder}`): "The result contract could not be written: {its message}."
|
|
149
149
|
|
|
150
|
-
If `{onCompleteCommand}` is non-empty
|
|
150
|
+
If `{onCompleteCommand}` is non-empty, the line's `result_path` is not null and its `warnings` hold no `result_file_write_failed` entry naming `analyze-source-result-latest.json`, invoke it now: `{onCompleteCommand} --result-path={forge_data_folder}/analyze-source-result-latest.json`. Display a hook failure; it never fails the run. When `{onCompleteCommand}` is non-empty but `result_path` is null or a `result_file_write_failed` warning names `analyze-source-result-latest.json` (the copy alone failed), skip the hook and say so: "on_complete skipped: `analyze-source-result-latest.json` was not written."
|
|
151
151
|
|
|
152
152
|
### 6. Chain to health check
|
|
153
153
|
|
|
@@ -287,11 +287,11 @@ The shared emitter writes the result files and prints the envelope; this section
|
|
|
287
287
|
uv run {emitEnvelopeHelper} emit --workflow skf-analyze-source --run-dir "{run_dir}" --result-dir "{forge_data_folder}" < "{run_dir}/result-context.json"
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
The emitter stamps the UTC time and the run id into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy (the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_ANALYZE_RESULT_JSON` line), and prints the `SKF_ANALYZE_RESULT_JSON:` line on stdout (field rules in `references/headless-contract.md`). When `{headless_mode}` is true, display the line verbatim: it is the success signal of a headless run. A
|
|
290
|
+
The emitter stamps the UTC time and the run id into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy (the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_ANALYZE_RESULT_JSON` line), and prints the `SKF_ANALYZE_RESULT_JSON:` line on stdout (field rules in `references/headless-contract.md`). When `{headless_mode}` is true, display the line verbatim: it is the success signal of a headless run. A per-run record that could not be written leaves `result_path` null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, correct `result-context.json` from the message on its stderr and run it once more; if it fails again, HARD HALT (exit code 4, `halt_reason: "write-failed"`, phase `generate-briefs:9`, path `{forge_data_folder}`, with `brief_paths` and `unit_counts` from the payload): "The result contract could not be written: {its message}."
|
|
291
291
|
|
|
292
292
|
### 9b. On-Complete Hook (pipeline integration)
|
|
293
293
|
|
|
294
|
-
If `{onCompleteCommand}` is non-empty
|
|
294
|
+
If `{onCompleteCommand}` is non-empty, the §9 line's `result_path` is not null and its `warnings` hold no `result_file_write_failed` entry naming `analyze-source-result-latest.json` (the emitter wrote the timestamped result JSON and its `analyze-source-result-latest.json` copy), invoke it now:
|
|
295
295
|
|
|
296
296
|
```
|
|
297
297
|
{onCompleteCommand} --result-path={forge_data_folder}/analyze-source-result-latest.json
|
|
@@ -303,8 +303,10 @@ The stable `-latest` path is preferred over the timestamped copy so downstream c
|
|
|
303
303
|
- On non-zero exit / process error: display the hook failure (`on_complete hook failed (exit {code}): {stderr_snippet}`); it never fails the run.
|
|
304
304
|
- **Never fail the workflow on hook errors** — the hook is for pipeline integration (Slack, dashboards, CI), not for gating skill-brief production.
|
|
305
305
|
|
|
306
|
+
When `{onCompleteCommand}` is non-empty but `result_path` is null or a `result_file_write_failed` warning names `analyze-source-result-latest.json` (the copy alone failed), skip the hook and say so: "on_complete skipped: `analyze-source-result-latest.json` was not written." The hook never gets a `-latest` copy this run failed to write.
|
|
307
|
+
|
|
306
308
|
If `{onCompleteCommand}` is empty, skip this section entirely (default behavior — no hook configured).
|
|
307
309
|
|
|
308
310
|
### 10. Chain to Health Check
|
|
309
311
|
|
|
310
|
-
After the briefs are written (or skipped, or none was confirmed), the report updated, the summary presented, the run ended in §9 and the on-complete hook invoked (or skipped
|
|
312
|
+
After the briefs are written (or skipped, or none was confirmed), the report updated, the summary presented, the run ended in §9 and the on-complete hook invoked (or skipped as §9b says), delete the run folder, whose payloads the emitter has read: `rm -rf "{run_dir}"`. Then load, read the full file, and execute `{nextStepFile}`. The health-check step is the true terminal step: continue past the summary even though it reads as final.
|
|
@@ -17,7 +17,7 @@ SKF_ANALYZE_RESULT_JSON: {"status":"success|error|redirect|skipped","report_path
|
|
|
17
17
|
- `brief_paths`: absolute path of every `skill-brief.yaml` the run wrote (empty array if none). A brief is written only after it passed the brief writer and, outside the docs-only branch, the schema gate.
|
|
18
18
|
- `unit_counts` — `confirmed` (units approved for briefs) and `skipped` (rejected in step 5, or the skipped target in coexistence §0c) counts; auto mode reports `confirmed:N, skipped:0`. `maybe` is a reserved slot, currently always `0`.
|
|
19
19
|
- `mode`: `"auto"` when the invocation carries `[auto]`, from activation through step 1a and the docs-only branch; `"interactive"` otherwise, also in the step-by-step chain an `[auto]` run falls back to (step 1a §3, exit 1).
|
|
20
|
-
- `result_path`: the per-run result file the emitter wrote, stamped by it; `null` for a HALT, which writes no result file, and when the
|
|
20
|
+
- `result_path`: the per-run result file the emitter wrote, stamped by it; `null` for a HALT, which writes no result file, when `--result-dir` names no folder (with no warning), and when the per-run record could not be written (a `result_file_write_failed` warning then says why); a `-latest` copy that could not be written leaves it set, and a `result_file_write_failed` warning names the copy.
|
|
21
21
|
- `error`: on a HALT only, `{"phase", "reason"}` plus `path` when the halt names one.
|
|
22
22
|
- `headless_decisions`: present when the run recorded an auto-decision: step 1a's §3b merge-or-split decision (gate `auto-scope.cohesion`), its §4 choices of shape for a repository on a framework or a parser library (gate `auto-scope.shape`) and its §5 choice of the documented language (gate `auto-scope.language`), and each gate default a headless run of the step-by-step chain took: `scan-project.scan`, `identify-units.classifications`, `map-and-detect.findings` (with the composite merges it accepted), `recommend.recommendations` and `generate-briefs.write`.
|
|
23
23
|
- `warnings`: present when the run recorded at least one.
|
|
@@ -32,7 +32,7 @@ The step that ends the run stages `{run_dir}/result-context.json` with the envel
|
|
|
32
32
|
uv run {emitEnvelopeHelper} emit --workflow skf-analyze-source --run-dir "{run_dir}" --result-dir "{forge_data_folder}" < "{run_dir}/result-context.json"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The emitter stamps the UTC time, the run id, the decisions and warnings the run recorded into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy, and prints the line. A
|
|
35
|
+
The emitter stamps the UTC time, the run id, the decisions and warnings the run recorded into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy, and prints the line. 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, the step corrects the payload from the message on its stderr and runs it once more; if it fails again, it HARD HALTs with exit code 4 (`write-failed`). Then `{onCompleteCommand}` runs, when it is set, `result_path` is not null and no `result_file_write_failed` warning names `analyze-source-result-latest.json`, with `--result-path={forge_data_folder}/analyze-source-result-latest.json`, and a hook failure is displayed and never fails the run; otherwise a set hook is skipped with "on_complete skipped: `analyze-source-result-latest.json` was not written." Last, the step deletes `{run_dir}` and chains to step 7.
|
|
36
36
|
|
|
37
37
|
## Halt Envelope
|
|
38
38
|
|
|
@@ -447,8 +447,8 @@ Add `"coexistence": "alongside"` when `{coexistence_suffix}` is non-empty ([A]lo
|
|
|
447
447
|
uv run {emitEnvelopeHelper} emit --workflow skf-analyze-source --run-dir "{run_dir}" --result-dir "{forge_data_folder}" < "{run_dir}/result-context.json"
|
|
448
448
|
```
|
|
449
449
|
|
|
450
|
-
The emitter stamps the UTC time, the run id and the decisions and warnings the run recorded into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy, and prints the `SKF_ANALYZE_RESULT_JSON:` line on stdout (field rules in `references/headless-contract.md`). When `{headless_mode}` is true, display the line verbatim. A
|
|
450
|
+
The emitter stamps the UTC time, the run id and the decisions and warnings the run recorded into the record, writes `{forge_data_folder}/analyze-source-result-{YYYYMMDD-HHmmss}.json` and its `analyze-source-result-latest.json` copy, and prints the `SKF_ANALYZE_RESULT_JSON:` line on stdout (field rules in `references/headless-contract.md`). When `{headless_mode}` is true, display the line verbatim. A per-run record that could not be written leaves `result_path` null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, correct `result-context.json` from the message on its stderr and run it once more; if it fails again, HARD HALT (exit code 4, `halt_reason: "write-failed"`, phase `step-auto-scope:9`, path `{forge_data_folder}`): "The result contract could not be written: {its message}."
|
|
451
451
|
|
|
452
|
-
If `{onCompleteCommand}` is non-empty
|
|
452
|
+
If `{onCompleteCommand}` is non-empty, the line's `result_path` is not null and its `warnings` hold no `result_file_write_failed` entry naming `analyze-source-result-latest.json`, invoke it now: `{onCompleteCommand} --result-path={forge_data_folder}/analyze-source-result-latest.json`. Display a hook failure; it never fails the run. When `{onCompleteCommand}` is non-empty but `result_path` is null or a `result_file_write_failed` warning names `analyze-source-result-latest.json` (the copy alone failed), skip the hook and say so: "on_complete skipped: `analyze-source-result-latest.json` was not written."
|
|
453
453
|
|
|
454
454
|
Then delete the run folder, whose payloads the emitter has read: `rm -rf "{run_dir}"`. Load, read fully, then execute {nextStepFile} to run the shared workflow health check.
|
|
@@ -62,10 +62,11 @@ drift_report_template_path = "assets/drift-report-template.md"
|
|
|
62
62
|
|
|
63
63
|
# Optional post-audit hook: a shell command run from {project-root}, not an
|
|
64
64
|
# agent instruction. Step 6 (references/report.md) runs it on a finished
|
|
65
|
-
# audit, after the emitter wrote the
|
|
65
|
+
# audit, after the emitter wrote the per-run record (`result_path` not null),
|
|
66
|
+
# as:
|
|
66
67
|
# <on_complete> --result-path=<{forge_version}/audit-skill-result-<YYYYMMDD-HHmmss>.json>
|
|
67
|
-
# It never runs on a halt, nor when the
|
|
68
|
-
# failure is shown to the user and never fails the audit.
|
|
68
|
+
# It never runs on a halt, nor when the per-run record could not be written.
|
|
69
|
+
# A failure is shown to the user and never fails the audit.
|
|
69
70
|
#
|
|
70
71
|
# Empty string = no-op (default).
|
|
71
72
|
|
|
@@ -196,7 +196,7 @@ When On Activation step 3 fell back to the bundled `customize.toml`, add `"custo
|
|
|
196
196
|
uv run {emitEnvelopeHelper} emit --workflow skf-audit-skill --run-dir "{run_dir}" --result-dir "{forge_version}" < "{run_dir}/result-context.json"
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
It writes `{forge_version}/audit-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and its `audit-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_AUDIT_RESULT_JSON` line), stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both, and prints the `SKF_AUDIT_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, bind `{result_envelope_line}` ← the 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 `drift_score`, `report_path`, `next_workflow`, `upstream_moved` and `upstream_ref` from the final message. A
|
|
199
|
+
It writes `{forge_version}/audit-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and its `audit-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_AUDIT_RESULT_JSON` line), stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both, and prints the `SKF_AUDIT_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, bind `{result_envelope_line}` ← the 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 `drift_score`, `report_path`, `next_workflow`, `upstream_moved` and `upstream_ref` from the final message. A per-run record that could not be written leaves `result_path` null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, correct `result-context.json` from the message on its stderr and run it once more; if it fails again, HALT with **exit 4**, `halt_reason: "write-failed"`, phase `report:result-contract`: the run's result contract could not be written.
|
|
200
200
|
|
|
201
201
|
**Hard-halt envelope (headless only).** Every hard halt prints its envelope through the emitter on **stderr** with `status: "error"` and the `exit_code` / `halt_reason` for its failure class, as each step's **Halt envelope** paragraph shows: it is the only failure signal a wrapping pipeline receives. `drift_score` carries its last known value (`null` if classification never ran); `report_path` is `null` when the report write failed.
|
|
202
202
|
|
|
@@ -115,6 +115,64 @@
|
|
|
115
115
|
"type": ["integer", "null"]
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
},
|
|
119
|
+
"stage_outcomes": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"description": "What the capstone (Stage 6), verification (Stage 7) and refinement (Stage 8) stages did, written by campaign-state.py set-campaign beside each summary: completed, skipped (a stage skipped on purpose, --no-<field>) or failed, with the failure's halt_reason (no-result when the sub-skill printed no line, result-unreadable when its line could not be read) and exit_code (null when it gave none). Optional: a stage is absent until set-campaign records it, so older states still validate.",
|
|
123
|
+
"properties": {
|
|
124
|
+
"capstone": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"required": ["outcome"],
|
|
128
|
+
"properties": {
|
|
129
|
+
"outcome": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"enum": ["completed", "skipped", "failed"]
|
|
132
|
+
},
|
|
133
|
+
"halt_reason": {
|
|
134
|
+
"type": ["string", "null"]
|
|
135
|
+
},
|
|
136
|
+
"exit_code": {
|
|
137
|
+
"type": ["integer", "null"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"verification": {
|
|
142
|
+
"type": "object",
|
|
143
|
+
"additionalProperties": false,
|
|
144
|
+
"required": ["outcome"],
|
|
145
|
+
"properties": {
|
|
146
|
+
"outcome": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"enum": ["completed", "skipped", "failed"]
|
|
149
|
+
},
|
|
150
|
+
"halt_reason": {
|
|
151
|
+
"type": ["string", "null"]
|
|
152
|
+
},
|
|
153
|
+
"exit_code": {
|
|
154
|
+
"type": ["integer", "null"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"refinement": {
|
|
159
|
+
"type": "object",
|
|
160
|
+
"additionalProperties": false,
|
|
161
|
+
"required": ["outcome"],
|
|
162
|
+
"properties": {
|
|
163
|
+
"outcome": {
|
|
164
|
+
"type": "string",
|
|
165
|
+
"enum": ["completed", "skipped", "failed"]
|
|
166
|
+
},
|
|
167
|
+
"halt_reason": {
|
|
168
|
+
"type": ["string", "null"]
|
|
169
|
+
},
|
|
170
|
+
"exit_code": {
|
|
171
|
+
"type": ["integer", "null"]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
118
176
|
}
|
|
119
177
|
}
|
|
120
178
|
},
|
|
@@ -51,7 +51,7 @@ Every write to `_campaign-state.yaml` goes through `scripts/campaign-state.py`.
|
|
|
51
51
|
| `set-skill` | step-05, step-10, step-resume | a skill's `status`, `quality_score`, `skill_path`, `brief_path` or `export` outcome (`--export`); `active` stamps `started_at` once, `completed` stamps `completed_at`, `pending` clears both and the `export` outcome |
|
|
52
52
|
| `append-workarounds` | step-05 | entries of a skill's `workarounds_applied`, each once |
|
|
53
53
|
| `apply-batch` | step-06 | the Tier B batch from its line-to-skill map: `--start`, `--results-file` or `--no-results` |
|
|
54
|
-
| `set-campaign` | steps 07 to 09 | `architecture_doc_path`, and the capstone, verification or refinement summary read from the sub-skill's envelope line (`capstone.verified` with the verification) |
|
|
54
|
+
| `set-campaign` | steps 07 to 09 | `architecture_doc_path`, and the capstone, verification or refinement summary read from the sub-skill's envelope line (`capstone.verified` with the verification); null for a stage skipped on purpose (`--no-<field>`), a sub-skill that printed no line (`--no-result <field>`, a `not_recorded` row with halt reason `no-result`) or a line it cannot read (one that does not parse, or has no `status` or `exit_code`: a `not_recorded` row with halt reason `result-unreadable`, exit 0); and `stage_outcomes`, each stage `completed`, `skipped` or `failed` with its halt reason and exit code |
|
|
55
55
|
| `set-stage` | steps 05, 10, 11 | `current_stage` alone |
|
|
56
56
|
| `recover` | step-resume | `.bak` over the primary, when `.bak` validates (exit 9 otherwise) |
|
|
57
57
|
| `archive` | SKILL.md (overwrite) | nothing: it moves the state, its `.bak` and the brief into a new folder under `archive/` and returns it |
|
|
@@ -66,7 +66,7 @@ Invoke QS in `--batch` mode with the generated batch file:
|
|
|
66
66
|
skf-quick-skill --batch {batchFile}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
QS `--batch` implies `--headless`. When the batch ends it prints a `batch_summary` event on stderr: keep its `summary_path`, the batch summary file that holds each target's result.
|
|
69
|
+
QS `--batch` implies `--headless`. When the batch ends it prints a `batch_summary` event on stderr: keep its `summary_path`, the batch summary file that holds each target's result. When its `summary_path` is null, QS could not write that file: keep the event's `batch_file` (QS's `batch.jsonl`, which holds each target's result) as `<summary_path>` instead, and log the event's `summary_write_failed` warning, type `event`.
|
|
70
70
|
|
|
71
71
|
### §5: Record Results
|
|
72
72
|
|
|
@@ -84,7 +84,7 @@ uv run {stateScript} apply-batch --state-file {stateFile} --map-file {batchMapFi
|
|
|
84
84
|
|
|
85
85
|
A `completed` result sets the skill `completed`, with `completed_at` from the clock, its `quality_score` (the skill-check score QS records, not a test-skill score) and its `skill_path`; a `failed` one sets it `failed`. Log each entry of the helper's `failed[]` with its `error_code` (`no-batch-result` when the summary holds no result for its line), type `event`.
|
|
86
86
|
|
|
87
|
-
When QS printed no `batch_summary` event (run no record call), or the record call exits 2 (a missing summary or map, or
|
|
87
|
+
When QS printed no `batch_summary` event (run no record call), or the record call exits 2 (a missing or unreadable summary, batch file or map, or results that belong to another batch file), fail every skill the map lists instead, and log why: never guess which target built which skill.
|
|
88
88
|
|
|
89
89
|
```
|
|
90
90
|
uv run {stateScript} apply-batch --state-file {stateFile} --map-file {batchMapFile} --no-results --stage 5
|
|
@@ -50,7 +50,7 @@ Invoke `skf-create-stack-skill` with `--headless` and these inputs, so it asks n
|
|
|
50
50
|
- `stack_name`: the `stack_name` that `uv run {manifestScript} --stack-name {stateFile}` prints, the campaign name as a skill name cut to leave room for the `-stack` SS appends; when it prints null, leave the input out and SS names the stack `{project_name}-stack`
|
|
51
51
|
- `architecture_doc_path`: `campaign.architecture_doc_path`, only when state sets it
|
|
52
52
|
|
|
53
|
-
Keep the `SKF_STACK_RESULT_JSON` line it prints for §4, which records it. On `status` `error`,
|
|
53
|
+
Keep the `SKF_STACK_RESULT_JSON` line it prints for §4, which records it: on stdout, or on stderr when SS halts. On `status` `error`, log its `halt_reason` (type `event`); a run with that line on neither stream is recorded in §4. Verification and the campaign report still run either way.
|
|
54
54
|
|
|
55
55
|
### §4: Record the Capstone and Complete the Stage
|
|
56
56
|
|
|
@@ -69,10 +69,10 @@ From a `success` envelope it sets `campaign.capstone`, the campaign-level summar
|
|
|
69
69
|
- `campaign.capstone.verified`: `null` for now; set by the verify stage (step-08) once the stack is checked
|
|
70
70
|
- `campaign.capstone.completed_at`: the time of the write, from the clock
|
|
71
71
|
|
|
72
|
-
An `error` envelope sets `campaign.capstone` to `null` (the helper names it in `not_recorded`). When §2 found no skill to compose,
|
|
72
|
+
An `error` envelope sets `campaign.capstone` to `null` (the helper names it in `not_recorded`). When §2 found no skill to compose, record no capstone instead: `uv run {stateScript} set-campaign --state-file {stateFile} --no-capstone --stage 6`. When SS printed no `SKF_STACK_RESULT_JSON` line on stdout or stderr, there is no result to read, whatever its exit code: record it with `uv run {stateScript} set-campaign --state-file {stateFile} --no-result capstone --stage 6`, log "no SKF_STACK_RESULT_JSON line (exit <code>)" with SS's exit code (type `event`), and go on. When the helper's `not_recorded` row gives the halt reason `result-unreadable` (SS printed a line that does not parse, or one with no `status` or `exit_code`), log "unparseable SKF_STACK_RESULT_JSON line" (type `event`) and go on.
|
|
73
73
|
|
|
74
74
|
The capstone is a derived artifact: it is **not** tracked as a skill entry in the `skills[]` array. The constituent skill list and any verbose detail are reported in the step output and are available to downstream steps (verify, refine).
|
|
75
75
|
|
|
76
76
|
## OUTPUT
|
|
77
77
|
|
|
78
|
-
Display capstone summary: stack skill name, path, quality score, and the list of constituent skills. Chain to `{nextStepFile}`.
|
|
78
|
+
Display capstone summary: stack skill name, path, quality score, and the list of constituent skills, or "skipped" when §2 found no skill to compose, or "failed (<halt_reason>)" from the helper's `not_recorded` row. That row's `halt_reason` fills it (`failed (no-result)` when SS printed no line); when that is null, show `failed (exit <exit_code>)`, or `failed (no exit code)` when the exit code is null too. Chain to `{nextStepFile}`.
|
|
@@ -46,7 +46,7 @@ Once resolved (steps 2 and 3), persist the path at once, so the refine stage and
|
|
|
46
46
|
|
|
47
47
|
Invoke `skf-verify-stack` with `--headless --architecture-doc <path>`, where `<path>` is the architecture doc §3 resolved.
|
|
48
48
|
|
|
49
|
-
VS discovers skills from its own configured `{skills_output_folder}`; the campaign does NOT pass individual skill paths. Keep the `SKF_VERIFY_STACK_RESULT_JSON` line it prints on stdout
|
|
49
|
+
VS discovers skills from its own configured `{skills_output_folder}`; the campaign does NOT pass individual skill paths. Keep the `SKF_VERIFY_STACK_RESULT_JSON` line it prints for §5: on stdout, or on stderr when VS halts. Its fields are defined once, by `shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json` (in the SKF module: `{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during development); §5 reads `report_path`, `overall_verdict`, `coverage_percentage` and `recommendation_count` from it.
|
|
50
50
|
|
|
51
51
|
### §5: Record the VS Outcome and Complete the Stage
|
|
52
52
|
|
|
@@ -67,8 +67,8 @@ SKF_RESULT
|
|
|
67
67
|
|
|
68
68
|
It also sets `campaign.capstone.verified` to `true` when `overall_verdict` is `FEASIBLE`, otherwise `false` (only if a `campaign.capstone` entry exists from step-07).
|
|
69
69
|
|
|
70
|
-
**On VS failure** (a
|
|
70
|
+
**On VS failure** (a line whose `status` is `error` or whose `exit_code` is non-zero): the helper sets `campaign.verification` to `null` and names it in `not_recorded`; log that line's `exit_code` and `halt_reason` (type `event`). Verification failure does NOT block the campaign: it produces diagnostic information for operator review. When §3 skipped VS, record no verification instead: `uv run {stateScript} set-campaign --state-file {stateFile} --no-verification --stage 7`. When VS printed no `SKF_VERIFY_STACK_RESULT_JSON` line on stdout or stderr, there is no result to read, whatever its exit code: record it with `uv run {stateScript} set-campaign --state-file {stateFile} --no-result verification --stage 7`, log "no SKF_VERIFY_STACK_RESULT_JSON line (exit <code>)" with VS's exit code (type `event`), and go on. When the helper's `not_recorded` row gives the halt reason `result-unreadable` (VS printed a line that does not parse, or one with no `status` or `exit_code`), log "unparseable SKF_VERIFY_STACK_RESULT_JSON line" (type `event`) and go on.
|
|
71
71
|
|
|
72
72
|
## OUTPUT
|
|
73
73
|
|
|
74
|
-
Display verification summary: overall verdict (or "skipped" if architecture doc was not found), report path (if produced), and coverage percentage. Chain to `{nextStepFile}`.
|
|
74
|
+
Display verification summary: overall verdict (or "skipped" if architecture doc was not found, or "failed (<halt_reason>)" from the helper's `not_recorded` row), report path (if produced), and coverage percentage. That row's `halt_reason` fills it (`failed (no-result)` when VS printed no line); when that is null, show `failed (exit <exit_code>)`, or `failed (no exit code)` when the exit code is null too. Chain to `{nextStepFile}`.
|
|
@@ -56,7 +56,7 @@ skf-refine-architecture --headless --architecture-doc <arch_path> --vs-report-pa
|
|
|
56
56
|
- `--vs-report-path`: `campaign.verification.report_path` from §3, or `none` when state holds no report path.
|
|
57
57
|
- `--scope-skills`: comma-separated names of completed campaign skills (from `skills[]` where `status == "completed"`). Optional but improves focus by limiting refinement scope to campaign-relevant skills.
|
|
58
58
|
|
|
59
|
-
Keep the `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line it prints on stdout
|
|
59
|
+
Keep the `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line it prints for §5: on stdout, or on stderr when RA halts. Its fields are defined once, by `shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json` (in the SKF module: `{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during development); §5 reads `status`, `refined_path`, `gap_count`, `issue_count` and `improvement_count` from it.
|
|
60
60
|
|
|
61
61
|
### §5: Record the RA Outcome and Complete the Stage
|
|
62
62
|
|
|
@@ -75,8 +75,8 @@ SKF_RESULT
|
|
|
75
75
|
- `campaign.refinement.issue_count`: from the envelope
|
|
76
76
|
- `campaign.refinement.improvement_count`: from the envelope
|
|
77
77
|
|
|
78
|
-
**On RA failure** (a
|
|
78
|
+
**On RA failure** (a line whose `status` is `error` or whose `exit_code` is non-zero): the helper sets `campaign.refinement` to `null` and names it in `not_recorded`; log that line's `exit_code` and `halt_reason` (type `event`). Refinement failure does NOT block the campaign: the campaign continues to export with whatever state exists. When §3 skipped RA, record no refinement instead: `uv run {stateScript} set-campaign --state-file {stateFile} --no-refinement --stage 8`. When RA printed no `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line on stdout or stderr, there is no result to read, whatever its exit code: record it with `uv run {stateScript} set-campaign --state-file {stateFile} --no-result refinement --stage 8`, log "no SKF_REFINE_ARCHITECTURE_RESULT_JSON line (exit <code>)" with RA's exit code (type `event`), and go on. When the helper's `not_recorded` row gives the halt reason `result-unreadable` (RA printed a line that does not parse, or one with no `status` or `exit_code`), log "unparseable SKF_REFINE_ARCHITECTURE_RESULT_JSON line" (type `event`) and go on.
|
|
79
79
|
|
|
80
80
|
## OUTPUT
|
|
81
81
|
|
|
82
|
-
Display refinement summary: refined architecture path (or "skipped" if architecture doc was not found), gap count, issue count, and improvement count. Chain to `{nextStepFile}`.
|
|
82
|
+
Display refinement summary: refined architecture path (or "skipped" if architecture doc was not found, or "failed (<halt_reason>)" from the helper's `not_recorded` row), gap count, issue count, and improvement count. That row's `halt_reason` fills it (`failed (no-result)` when RA printed no line); when that is null, show `failed (exit <exit_code>)`, or `failed (no exit code)` when the exit code is null too. Chain to `{nextStepFile}`.
|
|
@@ -44,7 +44,7 @@ uv run {reportScript} \
|
|
|
44
44
|
--decision-log {decisionLogFile}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Its JSON result on stdout carries `skills_completed`, `skills_failed`, `quality_scores`, `export_verdicts` (each completed skill's quality-gate verdict at Export), `skills_excluded` (the completed skills the gate kept from export), `skills_exported` and `export_failures` (what step-10 recorded: the skills `skf-export-skill` exported, and each skill whose export failed, with its `halt_reason` and `exit_code`; both null when Export did not run) and `duration`, already computed: §6 displays them, and §5 builds the envelope from `{resultContextFile}`, the payload the script also writes, on success and on failure alike.
|
|
47
|
+
Its JSON result on stdout carries `skills_completed`, `skills_failed`, `quality_scores`, `export_verdicts` (each completed skill's quality-gate verdict at Export), `skills_excluded` (the completed skills the gate kept from export), `skills_exported` and `export_failures` (what step-10 recorded: the skills `skf-export-skill` exported, and each skill whose export failed, with its `halt_reason` and `exit_code`; both null when Export did not run), `stage_outcomes` (whether the capstone, verification and refinement stages completed, were skipped or failed, with a failure's `halt_reason` and `exit_code`; null before the capstone stage ran) and `duration`, already computed: §6 displays them, and §5 builds the envelope from `{resultContextFile}`, the payload the script also writes, on success and on failure alike.
|
|
48
48
|
|
|
49
49
|
**On success** (exit code 0): log the report path and the summary stats from the result JSON (type `event`).
|
|
50
50
|
|
|
@@ -47,12 +47,17 @@ Record mode (--record): joins the QS batch summary (`summary_path` of the
|
|
|
47
47
|
`quality_score`) or `failed` (with QS's `error_code`, or `no-batch-result`
|
|
48
48
|
when the summary holds no result for its line). A result whose target is not
|
|
49
49
|
the mapped line's target means the summary belongs to another batch file, and
|
|
50
|
-
nothing is joined.
|
|
50
|
+
nothing is joined. When QS could not write the summary (`summary_path` null),
|
|
51
|
+
the event's `batch_file` names the batch run folder's `batch.jsonl`, and
|
|
52
|
+
--record takes that file instead: a path named `batch.jsonl` is read through
|
|
53
|
+
the shared skf-quick-batch.py's `load` (a later result line for a batch
|
|
54
|
+
number wins, a spoiled line is skipped), and its results are joined as a
|
|
55
|
+
summary's are.
|
|
51
56
|
|
|
52
57
|
CLI:
|
|
53
58
|
uv run campaign-render-batch.py --state-file <p> --brief-file <p> \
|
|
54
59
|
[-o <batchFile>] [--map <mapFile>] [--directive-file <p>]
|
|
55
|
-
uv run campaign-render-batch.py --record <summary_path> --map <mapFile>
|
|
60
|
+
uv run campaign-render-batch.py --record <summary_path|batch_file> --map <mapFile>
|
|
56
61
|
|
|
57
62
|
Output:
|
|
58
63
|
render: batch text (one target per line) to {batchFile} via -o, else to
|
|
@@ -68,8 +73,8 @@ Output:
|
|
|
68
73
|
Exit codes:
|
|
69
74
|
0 batch file written (0+ targets), or results joined
|
|
70
75
|
2 state file missing / unreadable / bad YAML, a directive that cannot be
|
|
71
|
-
read, a missing or unreadable map or
|
|
72
|
-
match the map (SUMMARY_MISMATCH)
|
|
76
|
+
read, a missing or unreadable map, summary or batch.jsonl, or a summary
|
|
77
|
+
or batch.jsonl that does not match the map (SUMMARY_MISMATCH)
|
|
73
78
|
8 brief missing / unreadable (missing-brief HALT), or a selected Tier B skill
|
|
74
79
|
has no matching brief target with a repo_url (unmatched-target)
|
|
75
80
|
"""
|
|
@@ -87,18 +92,25 @@ import yaml
|
|
|
87
92
|
|
|
88
93
|
GATE_SCRIPT = Path(__file__).resolve().with_name("campaign-quality-gate.py")
|
|
89
94
|
RESOLVE_PACKAGE_SCRIPT = Path(__file__).resolve().parent.parent.parent / "shared" / "scripts" / "skf-resolve-package.py"
|
|
95
|
+
SHARED_SCRIPTS = RESOLVE_PACKAGE_SCRIPT.parent
|
|
90
96
|
SELECTED_STATUSES = ("pending", "active")
|
|
91
97
|
|
|
92
98
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
95
|
-
spec = importlib.util.spec_from_file_location("skf_resolve_package", RESOLVE_PACKAGE_SCRIPT)
|
|
99
|
+
def _shared(file_name: str) -> Any:
|
|
100
|
+
spec = importlib.util.spec_from_file_location(file_name[:-3].replace("-", "_"), SHARED_SCRIPTS / file_name)
|
|
96
101
|
module = importlib.util.module_from_spec(spec)
|
|
97
102
|
spec.loader.exec_module(module)
|
|
98
|
-
return module
|
|
103
|
+
return module
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _version_re() -> Any:
|
|
107
|
+
"""skf-resolve-package.py's _VERSION_RE: the pins parse-target reads after `@`."""
|
|
108
|
+
return _shared(RESOLVE_PACKAGE_SCRIPT.name)._VERSION_RE
|
|
99
109
|
|
|
100
110
|
|
|
101
111
|
VERSION_RE = _version_re()
|
|
112
|
+
# QS's batch helper: --record reads a batch's batch.jsonl through its own load.
|
|
113
|
+
qb = _shared("skf-quick-batch.py")
|
|
102
114
|
|
|
103
115
|
|
|
104
116
|
def _err(message: str, code: str, exit_code: int, **extra: Any) -> int:
|
|
@@ -365,11 +377,30 @@ def _load_json(path: str, label: str) -> Tuple[Optional[Dict[str, Any]], int]:
|
|
|
365
377
|
return data, 0
|
|
366
378
|
|
|
367
379
|
|
|
380
|
+
def _load_batch_file(path: str) -> Tuple[Optional[Dict[str, Any]], int]:
|
|
381
|
+
"""A QS batch run folder's batch.jsonl, read as a summary: its results, by batch number.
|
|
382
|
+
|
|
383
|
+
Read through skf-quick-batch.py's own load, so a later result line for a
|
|
384
|
+
batch number replaces an earlier one and a spoiled line is skipped.
|
|
385
|
+
"""
|
|
386
|
+
if not Path(path).is_file():
|
|
387
|
+
return None, _err(f"Batch result file not found: {path}", "SUMMARY_NOT_FOUND", 2)
|
|
388
|
+
try:
|
|
389
|
+
state = qb.load(Path(path).parent)
|
|
390
|
+
except qb.BatchError as exc:
|
|
391
|
+
return None, _err(f"Batch result file unreadable: {exc}", "SUMMARY_PARSE_ERROR", 2)
|
|
392
|
+
return {"results": [state["results"][n] for n in sorted(state["results"])]}, 0
|
|
393
|
+
|
|
394
|
+
|
|
368
395
|
def record(summary_file: str, map_file: str) -> int:
|
|
369
396
|
batch_map, rc = _load_json(map_file, "map")
|
|
370
397
|
if batch_map is None:
|
|
371
398
|
return rc
|
|
372
|
-
|
|
399
|
+
if Path(summary_file).name == qb.BATCH_FILE:
|
|
400
|
+
# the event's batch_file: QS could not write the summary, so batch.jsonl holds the results
|
|
401
|
+
batch_summary, rc = _load_batch_file(summary_file)
|
|
402
|
+
else:
|
|
403
|
+
batch_summary, rc = _load_json(summary_file, "summary")
|
|
373
404
|
if batch_summary is None:
|
|
374
405
|
return rc
|
|
375
406
|
try:
|
|
@@ -392,7 +423,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
392
423
|
parser.add_argument("-o", "--output", dest="output", help="batch file path (default: stdout)")
|
|
393
424
|
parser.add_argument("--map", dest="map_file", help="line-to-skill map path (written, or read by --record)")
|
|
394
425
|
parser.add_argument("--directive-file", help="campaign directive whose Skip List is applied")
|
|
395
|
-
parser.add_argument("--record", dest="summary_file",
|
|
426
|
+
parser.add_argument("--record", dest="summary_file",
|
|
427
|
+
help="QS batch summary to join to --map, or the batch's batch.jsonl when summary_path "
|
|
428
|
+
"is null")
|
|
396
429
|
args = parser.parse_args(argv)
|
|
397
430
|
if args.summary_file:
|
|
398
431
|
if not args.map_file:
|
|
@@ -14,7 +14,8 @@ Output (JSON on stdout):
|
|
|
14
14
|
"quality_scores":{"skill":score,...},"duration":"...",
|
|
15
15
|
"export_verdicts":{"skill":"pass|fallback|fail",...},"skills_excluded":["skill",...],
|
|
16
16
|
"skills_exported":["skill",...],
|
|
17
|
-
"export_failures":[{"skill","halt_reason","exit_code"},...]
|
|
17
|
+
"export_failures":[{"skill","halt_reason","exit_code"},...],
|
|
18
|
+
"stage_outcomes":{"capstone|verification|refinement":{"outcome","halt_reason","exit_code"}}}
|
|
18
19
|
|
|
19
20
|
`export_verdicts` holds each completed skill's quality-gate verdict at Export
|
|
20
21
|
and `skills_excluded` the completed skills the gate kept from export, both
|
|
@@ -30,6 +31,14 @@ halt_reason and exit code. Both are null when the Export stage did not run
|
|
|
30
31
|
(`campaign.current_stage` below 9). The report's Export Gate table shows the
|
|
31
32
|
same outcome per skill in its Export column.
|
|
32
33
|
|
|
34
|
+
`stage_outcomes` is what the capstone, verification and refinement stages
|
|
35
|
+
did: the `campaign.stage_outcomes` entry campaign-state.py set-campaign
|
|
36
|
+
recorded for each one, as {"outcome", "halt_reason", "exit_code"} with
|
|
37
|
+
`outcome` completed, skipped or failed (null for a stage it holds no
|
|
38
|
+
outcome for). It is null when the capstone stage did not run
|
|
39
|
+
(`campaign.current_stage` below 6). The report's Stages section shows the
|
|
40
|
+
same outcome per stage, a failure with its halt_reason, else its exit code.
|
|
41
|
+
|
|
33
42
|
--context-file also writes the payload skf-emit-result-envelope.py turns into
|
|
34
43
|
the campaign's SKF_CAMPAIGN_RESULT_JSON line (schemas/
|
|
35
44
|
skf-campaign-result-envelope.v1.json): the result above, with the report as
|
|
@@ -116,14 +125,19 @@ def _export_record(skill: Any) -> Optional[Dict[str, Any]]:
|
|
|
116
125
|
return record if isinstance(record, dict) and record.get("status") in ("exported", "failed") else None
|
|
117
126
|
|
|
118
127
|
|
|
128
|
+
def _failed_cell(record: Dict[str, Any]) -> str:
|
|
129
|
+
"""A failure as the report shows it: its halt_reason, else its exit code, else none."""
|
|
130
|
+
why = record.get("halt_reason") or (
|
|
131
|
+
f"exit {record['exit_code']}" if record.get("exit_code") is not None else "no exit code")
|
|
132
|
+
return f"failed ({why})"
|
|
133
|
+
|
|
134
|
+
|
|
119
135
|
def _export_cell(record: Optional[Dict[str, Any]], verdict: str) -> str:
|
|
120
136
|
if record is None:
|
|
121
137
|
return "not exported" if verdict == "fail" else "N/A"
|
|
122
138
|
if record["status"] == "exported":
|
|
123
139
|
return "exported"
|
|
124
|
-
|
|
125
|
-
f"exit {record['exit_code']}" if record.get("exit_code") is not None else "no exit code")
|
|
126
|
-
return f"failed ({why})"
|
|
140
|
+
return _failed_cell(record)
|
|
127
141
|
|
|
128
142
|
|
|
129
143
|
def _export_outcomes(state: Any) -> Dict[str, Any]:
|
|
@@ -146,6 +160,44 @@ def _export_outcomes(state: Any) -> Dict[str, Any]:
|
|
|
146
160
|
return {"skills_exported": exported, "export_failures": failures}
|
|
147
161
|
|
|
148
162
|
|
|
163
|
+
STAGES = (("capstone", "Capstone"), ("verification", "Verification"), ("refinement", "Refinement"))
|
|
164
|
+
STAGE_OUTCOMES = ("completed", "skipped", "failed")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _stage_record(entry: Any) -> Optional[Dict[str, Any]]:
|
|
168
|
+
"""A stage outcome campaign-state.py recorded, with its three keys, or None."""
|
|
169
|
+
if not isinstance(entry, dict) or entry.get("outcome") not in STAGE_OUTCOMES:
|
|
170
|
+
return None
|
|
171
|
+
return {"outcome": entry["outcome"], "halt_reason": entry.get("halt_reason"), "exit_code": entry.get("exit_code")}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _stage_outcomes(state: Any) -> Optional[Dict[str, Any]]:
|
|
175
|
+
"""What the capstone, verification and refinement stages did; null before stage 6 ran."""
|
|
176
|
+
campaign = state.get("campaign") if isinstance(state, dict) else None
|
|
177
|
+
stage = campaign.get("current_stage") if isinstance(campaign, dict) else None
|
|
178
|
+
if not isinstance(stage, int) or isinstance(stage, bool) or stage < 6:
|
|
179
|
+
return None
|
|
180
|
+
held = campaign.get("stage_outcomes")
|
|
181
|
+
held = held if isinstance(held, dict) else {}
|
|
182
|
+
return {field: _stage_record(held.get(field)) for field, _label in STAGES}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _stages_section(outcomes: Optional[Dict[str, Any]]) -> str:
|
|
186
|
+
if outcomes is None:
|
|
187
|
+
return "The capstone, verification and refinement stages have not run."
|
|
188
|
+
lines = ["| Stage | Outcome |", "|-------|---------|"]
|
|
189
|
+
for field, label in STAGES:
|
|
190
|
+
record = outcomes[field]
|
|
191
|
+
if record is None:
|
|
192
|
+
cell = "not recorded"
|
|
193
|
+
elif record["outcome"] == "failed":
|
|
194
|
+
cell = _failed_cell(record)
|
|
195
|
+
else:
|
|
196
|
+
cell = record["outcome"]
|
|
197
|
+
lines.append(f"| {label} | {cell} |")
|
|
198
|
+
return "\n".join(lines)
|
|
199
|
+
|
|
200
|
+
|
|
149
201
|
def _export_gate_section(
|
|
150
202
|
classification: Optional[Dict[str, Any]], state: Optional[Dict[str, Any]] = None
|
|
151
203
|
) -> str:
|
|
@@ -273,6 +325,7 @@ def _compute_aggregates(
|
|
|
273
325
|
"duration_table": "\n".join(duration_table_rows),
|
|
274
326
|
"failed_skipped_section": "\n".join(failed_skipped_lines),
|
|
275
327
|
"export_gate_section": _export_gate_section(classification, state),
|
|
328
|
+
"stages_section": _stages_section(_stage_outcomes(state)),
|
|
276
329
|
}
|
|
277
330
|
|
|
278
331
|
|
|
@@ -324,6 +377,7 @@ def run(
|
|
|
324
377
|
"halt_reason": "report-failure",
|
|
325
378
|
**_counts(state),
|
|
326
379
|
**_export_outcomes(state),
|
|
380
|
+
"stage_outcomes": _stage_outcomes(state),
|
|
327
381
|
"campaign_report_path": None,
|
|
328
382
|
"decision_log": decision_log,
|
|
329
383
|
})
|
|
@@ -377,6 +431,7 @@ def run(
|
|
|
377
431
|
None if classification is None else [e["name"] for e in classification["excluded"]]
|
|
378
432
|
),
|
|
379
433
|
**_export_outcomes(state),
|
|
434
|
+
"stage_outcomes": _stage_outcomes(state),
|
|
380
435
|
}
|
|
381
436
|
_write_context(context_file, {
|
|
382
437
|
"status": "success",
|
|
@@ -387,6 +442,7 @@ def run(
|
|
|
387
442
|
"skills_excluded": result["skills_excluded"],
|
|
388
443
|
"skills_exported": result["skills_exported"],
|
|
389
444
|
"export_failures": result["export_failures"],
|
|
445
|
+
"stage_outcomes": result["stage_outcomes"],
|
|
390
446
|
"campaign_report_path": result["report_path"],
|
|
391
447
|
"decision_log": decision_log,
|
|
392
448
|
"duration": result["duration"],
|