bmad-module-skill-forge 1.8.0 → 2.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 +10 -5
- package/docs/_data/pinned.yaml +2 -2
- package/docs/agents.md +11 -2
- package/docs/architecture.md +5 -4
- package/docs/bmad-synergy.md +42 -3
- package/docs/campaign.md +172 -0
- package/docs/examples.md +7 -3
- package/docs/forge-auto.md +90 -0
- package/docs/getting-started.md +9 -0
- package/docs/how-it-works.md +6 -4
- package/docs/index.md +4 -3
- package/docs/skill-model.md +1 -1
- package/docs/troubleshooting.md +17 -1
- package/docs/verifying-a-skill.md +9 -3
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +73 -27
- package/package.json +2 -2
- package/src/module-help.csv +2 -1
- package/src/shared/_known-workarounds.yaml +155 -0
- package/src/shared/data/language-corpora.json +32 -0
- package/src/shared/references/pipeline-contracts.md +14 -2
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -1
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +417 -0
- package/src/shared/scripts/skf-detect-language.py +5 -3
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +12 -1
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- package/src/shared/scripts/skf-language-corpora.py +100 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
- package/src/shared/scripts/skf-preapply.py +192 -0
- package/src/shared/scripts/skf-shape-detect.py +1114 -0
- package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
- package/src/shared/scripts/skf-validate-pins.py +368 -0
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +26 -20
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/init.md +30 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +677 -0
- package/src/skf-analyze-source/references/step-shape-detect.md +86 -0
- package/src/skf-audit-skill/SKILL.md +1 -0
- package/src/skf-audit-skill/assets/drift-report-template.md +6 -0
- package/src/skf-audit-skill/references/init.md +1 -1
- package/src/skf-audit-skill/references/report.md +4 -0
- package/src/skf-audit-skill/references/severity-classify.md +1 -1
- package/src/skf-audit-skill/references/step-doc-drift.md +147 -0
- package/src/skf-brief-skill/SKILL.md +7 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/references/analyze-target.md +7 -4
- package/src/skf-brief-skill/references/confirm-brief.md +0 -1
- package/src/skf-brief-skill/references/gather-intent.md +22 -2
- package/src/skf-brief-skill/references/scope-definition.md +2 -1
- package/src/skf-brief-skill/references/step-auto-brief.md +188 -0
- package/src/skf-brief-skill/references/step-auto-validate.md +209 -0
- package/src/skf-brief-skill/references/write-brief.md +2 -3
- package/src/skf-campaign/SKILL.md +190 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +73 -0
- package/src/skf-campaign/manifest.yaml +11 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +122 -0
- package/src/skf-campaign/references/step-02-strategy.md +97 -0
- package/src/skf-campaign/references/step-03-pins.md +56 -0
- package/src/skf-campaign/references/step-04-provenance.md +63 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
- package/src/skf-campaign/references/step-06-batch.md +87 -0
- package/src/skf-campaign/references/step-07-capstone.md +63 -0
- package/src/skf-campaign/references/step-08-verify.md +75 -0
- package/src/skf-campaign/references/step-09-refine.md +83 -0
- package/src/skf-campaign/references/step-10-export.md +106 -0
- package/src/skf-campaign/references/step-11-maintenance.md +84 -0
- package/src/skf-campaign/references/step-resume.md +114 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +235 -0
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
- package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
- package/src/skf-campaign/scripts/campaign-report.py +249 -0
- package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
- package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
- package/src/skf-campaign/templates/campaign-report-template.md +54 -0
- package/src/skf-campaign/templates/kickoff-template.md +48 -0
- package/src/skf-create-skill/SKILL.md +4 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
- package/src/skf-create-skill/assets/skill-sections.md +2 -0
- package/src/skf-create-skill/references/compile.md +5 -2
- package/src/skf-create-skill/references/extract.md +9 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -1
- package/src/skf-create-skill/references/generate-artifacts.md +11 -2
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/report.md +17 -1
- package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
- package/src/skf-create-skill/references/step-auto-shard.md +110 -0
- package/src/skf-create-skill/references/step-doc-rot.md +109 -0
- package/src/skf-create-skill/references/step-doc-sources.md +123 -0
- package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
- package/src/skf-create-skill/references/validate.md +12 -3
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-drop-skill/references/execute.md +20 -9
- package/src/skf-drop-skill/references/report.md +2 -0
- package/src/skf-drop-skill/references/select.md +7 -2
- package/src/skf-forger/SKILL.md +15 -4
- package/src/skf-refine-architecture/SKILL.md +2 -1
- package/src/skf-refine-architecture/references/compile.md +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-rename-skill/references/execute.md +5 -4
- package/src/skf-rename-skill/references/rebuild-context.md +2 -2
- package/src/skf-rename-skill/references/select.md +3 -3
- package/src/skf-setup/SKILL.md +1 -1
- package/src/skf-setup/references/auto-index.md +3 -1
- package/src/skf-setup/references/detect-and-tier.md +4 -0
- package/src/skf-setup/references/report.md +4 -1
- package/src/skf-setup/references/tier-rules.md +1 -1
- package/src/skf-test-skill/SKILL.md +8 -7
- package/src/skf-test-skill/customize.toml +2 -1
- package/src/skf-test-skill/references/coverage-check.md +10 -0
- package/src/skf-test-skill/references/external-validators.md +1 -1
- package/src/skf-test-skill/references/init.md +16 -1
- package/src/skf-test-skill/references/report.md +5 -1
- package/src/skf-test-skill/references/score.md +95 -6
- package/src/skf-test-skill/references/source-access-protocol.md +13 -3
- package/src/skf-test-skill/references/step-hard-gate.md +73 -0
- package/src/skf-test-skill/scripts/compute-score.py +4 -3
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/customize.toml +0 -9
- package/src/skf-update-skill/references/detect-changes.md +33 -3
- package/src/skf-update-skill/references/health-check.md +2 -2
- package/src/skf-update-skill/references/init.md +1 -0
- package/src/skf-update-skill/references/re-extract.md +15 -1
- package/src/skf-verify-stack/references/report.md +1 -1
- package/tools/cli/lib/ui.js +3 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextStepFile: 'step-auto-validate.md'
|
|
3
|
+
validateBriefSchemaProbeOrder:
|
|
4
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
|
|
5
|
+
- '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
|
|
6
|
+
writeSkillBriefProbeOrder:
|
|
7
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
|
|
8
|
+
- '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
|
|
9
|
+
emitBriefEnvelopeProbeOrder:
|
|
10
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
11
|
+
- '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
12
|
+
mergeDocUrlsProbeOrder:
|
|
13
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-merge-doc-urls.py'
|
|
14
|
+
- '{project-root}/src/shared/scripts/skf-merge-doc-urls.py'
|
|
15
|
+
detectDocsProbeOrder:
|
|
16
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-detect-docs.py'
|
|
17
|
+
- '{project-root}/src/shared/scripts/skf-detect-docs.py'
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<!-- Config: communicate in {communication_language}. -->
|
|
21
|
+
|
|
22
|
+
# Step 1a: Auto-Brief Generation
|
|
23
|
+
|
|
24
|
+
## STEP GOAL:
|
|
25
|
+
|
|
26
|
+
To enrich an upstream skill brief (produced by AN auto-scope) with documentation URLs discovered via `skf-detect-docs.py`, validate the enriched brief, and write it through the canonical writer. Envelope emission is deferred to step-auto-validate.md, which presents the brief for user approval before continuing. This step replaces the interactive gather-intent → analyze-target → scope-definition → confirm-brief → write-brief chain when `[auto]` mode is active.
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
|
|
30
|
+
- Auto-proceed step — no user interaction required
|
|
31
|
+
- This step is conditional — only loaded when `[auto]` flag is present in the pipeline context
|
|
32
|
+
- Must produce the same output artifact as the interactive chain: a validated `skill-brief.yaml`
|
|
33
|
+
- Doc detection is best-effort — failures do not halt the pipeline
|
|
34
|
+
- Do NOT re-derive scope fields from the upstream brief — AN already set them correctly
|
|
35
|
+
- Do NOT render YAML or JSON envelopes in the LLM — delegate to deterministic scripts
|
|
36
|
+
|
|
37
|
+
## MANDATORY SEQUENCE
|
|
38
|
+
|
|
39
|
+
### 1. Load Upstream Brief
|
|
40
|
+
|
|
41
|
+
Read the upstream brief path from `{brief_path}` (passed by the forger from AN's `SKF_ANALYZE_RESULT_JSON` `brief_paths[]`).
|
|
42
|
+
|
|
43
|
+
**IF `{brief_path}` is not set or the file does not exist:**
|
|
44
|
+
- HARD HALT with exit code 2 (`input-missing`): "**Auto-brief requires an upstream brief — `brief_path` is missing or the file does not exist at `{brief_path}`.**"
|
|
45
|
+
- Emit error envelope per §6 with `halt_reason: "input-missing"`.
|
|
46
|
+
|
|
47
|
+
**Resolve `{validateBriefSchemaHelper}`** from `{validateBriefSchemaProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
48
|
+
|
|
49
|
+
Validate the upstream brief against the schema:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run {validateBriefSchemaHelper} {brief_path}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The script returns JSON `{valid, errors[], warnings[], halt_reason, brief}`.
|
|
56
|
+
|
|
57
|
+
- **`valid: false`** — the upstream brief is malformed. HARD HALT with exit code 2 (`input-invalid`): "**Upstream brief at `{brief_path}` is invalid: {first error message}.**" Emit error envelope per §6 with `halt_reason: "input-invalid"`.
|
|
58
|
+
- **`valid: true`** — proceed with the parsed `brief` payload. Surface any non-empty `warnings[]` to the log.
|
|
59
|
+
|
|
60
|
+
Extract from the parsed brief:
|
|
61
|
+
- `skill_name` ← `brief.name`
|
|
62
|
+
- `version` ← `brief.version`
|
|
63
|
+
- `source_repo` ← `brief.source_repo`
|
|
64
|
+
- `language` ← `brief.language`
|
|
65
|
+
- `scope_type` ← `brief.scope.type`
|
|
66
|
+
- `forge_tier` ← `brief.forge_tier`
|
|
67
|
+
- `description` ← `brief.description`
|
|
68
|
+
- `created` ← `brief.created`
|
|
69
|
+
- `created_by` ← `brief.created_by`
|
|
70
|
+
- All scope fields: `scope.include`, `scope.exclude`, `scope.notes`, `scope.rationale`, `scope.amendments`, `scope.tier_a_include`
|
|
71
|
+
- Optional fields: `source_type`, `source_authority`, `doc_urls`, `target_version`, `target_ref`, `source_ref`, `scripts_intent`, `assets_intent`
|
|
72
|
+
|
|
73
|
+
**Docs-only check:** If `source_type` is `docs-only` in the parsed brief, skip §2 (Run Doc Detection) and §3 (Enrich Brief with Detected Docs) — the doc URL is already in the brief's `doc_urls`. Log: "Docs-only brief — skipping repo-based doc detection. Doc URLs provided by upstream." Proceed directly to §4 (Validate Enriched Brief). All brief fields (`source_type`, `source_authority`, `doc_urls`, `scope_type`) must pass through unmodified.
|
|
74
|
+
|
|
75
|
+
### 2. Run Doc Detection
|
|
76
|
+
|
|
77
|
+
**Resolve `{detectDocsHelper}`** from `{detectDocsProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
78
|
+
|
|
79
|
+
Invoke doc detection to discover documentation URLs for the source repo:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run {detectDocsHelper} --repo-url {source_repo}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`--repo-url` is always required (the script uses it for GitHub API calls). If a local clone is also available at `{local_clone_path}`, add `--local-path {local_clone_path}` to enable docs-folder scanning in addition to API-based detection.
|
|
86
|
+
|
|
87
|
+
**Handle exit codes:**
|
|
88
|
+
|
|
89
|
+
- **Exit 0 (found docs):** Parse the JSON output array. Each entry has `{url, detected_via, content_hash, content_type}`. Proceed to §3 with the detected docs.
|
|
90
|
+
- **Exit 1 (none found):** Log: "No external documentation found — brief generated from source analysis only." Proceed to §4 with no doc enrichment.
|
|
91
|
+
- **Exit 2 (error):** Log warning: "Doc detection failed — proceeding without doc enrichment." Do NOT halt — doc enrichment is best-effort. Proceed to §4 with no doc enrichment.
|
|
92
|
+
|
|
93
|
+
### 3. Enrich Brief with Detected Docs
|
|
94
|
+
|
|
95
|
+
For each detected doc entry, create a brief `doc_urls` entry:
|
|
96
|
+
|
|
97
|
+
- `url` ← `url` (direct copy)
|
|
98
|
+
- `label` ← derive from `content_type` if available:
|
|
99
|
+
- `"api-docs"` → `"API Documentation"`
|
|
100
|
+
- `"guide"` → `"Guide"`
|
|
101
|
+
- `"reference"` → `"Reference"`
|
|
102
|
+
- Otherwise derive from `detected_via`:
|
|
103
|
+
- `"homepageUrl"` → `"Homepage"`
|
|
104
|
+
- `"readme_link"` → `"README Link"`
|
|
105
|
+
- `"pages_api"` → `"GitHub Pages"`
|
|
106
|
+
- `"docs_folder"` → `"Docs Folder"`
|
|
107
|
+
- `source` ← coarse provenance derived from `detected_via` (per the `skill-brief.v1.json` `doc_urls[].source` enum): `homepageUrl` → `homepage`, `readme_link` → `readme-detection`, `pages_api` → `pages-api`, `docs_folder` → `docs-folder`. This marks the entry as opportunistically detected, distinct from a registry-guaranteed corpus.
|
|
108
|
+
|
|
109
|
+
**Merge via the canonical helper.** Resolve `{mergeDocUrlsHelper}` from `{mergeDocUrlsProbeOrder}` (first existing path wins; HALT if neither exists). Pass the upstream brief's `doc_urls` as `existing` and the entries just mapped above as `detected`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
echo '{"scope_type": "{scope_type}", "existing": {upstream brief doc_urls JSON, [] if none}, "detected": {mapped detected entries JSON}}' \
|
|
113
|
+
| uv run {mergeDocUrlsHelper}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The helper returns `{"doc_urls": [...], "suppressed": [...]}`. It deduplicates by **normalized** URL (lowercase host, strip a trailing `/index.html` and any trailing `/`), so a seeded `…/book/` and a README's `…/book/index.html` collapse to one entry; existing/corpora-seeded entries always win and keep their `source: language-registry`, so the registry-vs-detected distinction survives the merge. For a **whole-language reference** (`scope_type == "full-library"` AND ≥1 `existing` entry has `source: language-registry`) it additionally suppresses README noise on a corpus host: non-corpus path segments (`/whatsnew/`, `/contribute`, `/wiki/`) and non-primary-locale duplicates of a kept page (`/ja/master/` when `/en/master/` is kept). Ordinary skills (any other `scope_type`, or no registry corpora) pass through with dedup only — no suppression. Use the returned `doc_urls` as the brief's merged list.
|
|
117
|
+
|
|
118
|
+
**Log suppressed entries.** When `suppressed` is non-empty, log one line per entry — `"info: suppressed {url} ({reason})"` — so the operator can see what the whole-language noise filter dropped (never drop silently). The N==0 DEGRADED case (a whole-language repo whose registry returned no corpora) carries no `language-registry` entry, so suppression stays inactive and its README docs are kept — this is intentional (there is no canonical corpus host to filter against).
|
|
119
|
+
|
|
120
|
+
### 4. Validate Enriched Brief
|
|
121
|
+
|
|
122
|
+
Assemble the enriched brief context as a flat JSON object following the write-brief §3 contract:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"name": "{skill_name}",
|
|
127
|
+
"target_version": "{target_version or null}",
|
|
128
|
+
"detected_version": null,
|
|
129
|
+
"source_type": "{source_type or 'source'}",
|
|
130
|
+
"source_repo": "{source_repo}",
|
|
131
|
+
"language": "{language}",
|
|
132
|
+
"description": "{description}",
|
|
133
|
+
"forge_tier": "{forge_tier}",
|
|
134
|
+
"created": "{created}",
|
|
135
|
+
"created_by": "{created_by}",
|
|
136
|
+
"scope_type": "{scope_type}",
|
|
137
|
+
"scope_include": ["{scope.include patterns}"],
|
|
138
|
+
"scope_exclude": ["{scope.exclude patterns}"],
|
|
139
|
+
"scope_notes": "{scope.notes or ''}",
|
|
140
|
+
"scope_rationale": null,
|
|
141
|
+
"scope_tier_a_include": null,
|
|
142
|
+
"scope_amendments": null,
|
|
143
|
+
"doc_urls": [{"url": "...", "label": "...", "source": "..."}],
|
|
144
|
+
"scripts_intent": "{scripts_intent or null}",
|
|
145
|
+
"assets_intent": "{assets_intent or null}",
|
|
146
|
+
"source_authority": "{source_authority or null}",
|
|
147
|
+
"target_ref": "{target_ref or null}",
|
|
148
|
+
"source_ref": "{source_ref or null}",
|
|
149
|
+
"version_resolved": "{version}"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The `version_resolved` key pins the output to the upstream brief's version — without it, the writer's precedence logic falls through to `1.0.0` since `target_version` and `detected_version` are both null on the auto path.
|
|
154
|
+
|
|
155
|
+
### 5. Write Enriched Brief
|
|
156
|
+
|
|
157
|
+
**Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
158
|
+
|
|
159
|
+
Write the enriched brief through the canonical writer:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
echo '<context-json>' | uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --from-flat
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**On script failure (non-zero exit):**
|
|
166
|
+
- Exit 1 (validation/invariant): Emit error envelope per §6 with `halt_reason: "input-invalid"`, then HARD HALT.
|
|
167
|
+
- Exit 2 (I/O failure): Emit error envelope per §6 with `halt_reason: "write-failed"`, then HARD HALT.
|
|
168
|
+
|
|
169
|
+
**On success:** Capture `brief_path` and `version` from the response envelope for step-auto-validate's envelope emission.
|
|
170
|
+
|
|
171
|
+
### 6. Error Envelope (Canonical)
|
|
172
|
+
|
|
173
|
+
Every HARD HALT in this step emits the error envelope on stderr:
|
|
174
|
+
|
|
175
|
+
**Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
echo '{"status":"error","skill_name":"{skill_name or unknown}","halt_reason":"{reason}","mode":"auto"}' | \
|
|
179
|
+
uv run {emitBriefEnvelopeHelper} emit --target stderr
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 7. Chain to Auto-Validate
|
|
183
|
+
|
|
184
|
+
Load, read fully, then execute {nextStepFile} to present the auto-brief validation gate, where the user can approve, edit, or reject the brief before the pipeline continues.
|
|
185
|
+
|
|
186
|
+
## CRITICAL STEP COMPLETION NOTE
|
|
187
|
+
|
|
188
|
+
ONLY WHEN the enriched brief has been written and validated will you load and read fully {nextStepFile} to begin the auto-brief validation gate.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
nextStepFile: 'health-check.md'
|
|
3
|
+
rejectTargetFile: 'confirm-brief.md'
|
|
4
|
+
validateBriefSchemaProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
|
|
7
|
+
writeSkillBriefProbeOrder:
|
|
8
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
|
|
9
|
+
- '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
|
|
10
|
+
emitBriefEnvelopeProbeOrder:
|
|
11
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
12
|
+
- '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<!-- Config: communicate in {communication_language}. -->
|
|
16
|
+
|
|
17
|
+
# Step 1b: Auto-Brief Validation
|
|
18
|
+
|
|
19
|
+
## STEP GOAL:
|
|
20
|
+
|
|
21
|
+
To present the user with a concise summary of the auto-generated brief and offer three actions — approve, edit, or reject — before the pipeline continues. On approve or edit, the result envelope is emitted and the pipeline chains to the health check. On reject, the pipeline falls back to the interactive brief review cycle with pre-populated fields.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- This step is conditional — only loaded from step-auto-brief.md when `[auto]` mode is active
|
|
26
|
+
- The brief MUST already exist on disk (written by step-auto-brief §5) before this step runs
|
|
27
|
+
- Do NOT render YAML or JSON envelopes in the LLM — delegate to deterministic scripts
|
|
28
|
+
- Do NOT modify confirm-brief.md or write-brief.md — the [R]eject path reuses them as-is
|
|
29
|
+
- The 10-line summary is always displayed, even in headless mode, for logging transparency
|
|
30
|
+
|
|
31
|
+
## MANDATORY SEQUENCE
|
|
32
|
+
|
|
33
|
+
### 1. Load Auto-Brief
|
|
34
|
+
|
|
35
|
+
Read the brief from `{forge_data_folder}/{skill_name}/skill-brief.yaml` (written by step-auto-brief §5).
|
|
36
|
+
|
|
37
|
+
**Resolve `{validateBriefSchemaHelper}`** from `{validateBriefSchemaProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
38
|
+
|
|
39
|
+
Validate the brief against the schema:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run {validateBriefSchemaHelper} {forge_data_folder}/{skill_name}/skill-brief.yaml
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The script returns JSON `{valid, errors[], warnings[], halt_reason, brief}`.
|
|
46
|
+
|
|
47
|
+
- **`valid: false`** — HARD HALT with exit code 2 (`input-invalid`): "**Auto-brief at `{forge_data_folder}/{skill_name}/skill-brief.yaml` is invalid: {first error message}.**" Emit error envelope per §7 with `halt_reason: "input-invalid"`.
|
|
48
|
+
- **`valid: true`** — proceed with the parsed `brief` payload. Surface any non-empty `warnings[]` to the log.
|
|
49
|
+
|
|
50
|
+
**IF the file does not exist:**
|
|
51
|
+
- HARD HALT with exit code 2 (`input-missing`): "**Auto-brief not found at `{forge_data_folder}/{skill_name}/skill-brief.yaml` — step-auto-brief must write the brief before this step runs.**" Emit error envelope per §7 with `halt_reason: "input-missing"`.
|
|
52
|
+
|
|
53
|
+
Extract from the parsed brief:
|
|
54
|
+
- `skill_name` ← `brief.name`
|
|
55
|
+
- `version` ← `brief.version`
|
|
56
|
+
- `source_repo` ← `brief.source_repo`
|
|
57
|
+
- `language` ← `brief.language`
|
|
58
|
+
- `scope_type` ← `brief.scope.type`
|
|
59
|
+
- `scope_include` ← `brief.scope.include`
|
|
60
|
+
- `scope_exclude` ← `brief.scope.exclude`
|
|
61
|
+
- `forge_tier` ← `brief.forge_tier`
|
|
62
|
+
- `description` ← `brief.description`
|
|
63
|
+
- `doc_urls` ← `brief.doc_urls`
|
|
64
|
+
|
|
65
|
+
### 2. Present 10-Line Summary
|
|
66
|
+
|
|
67
|
+
Render a concise summary from the brief fields for rapid scanning:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Auto-Brief Summary: {skill_name}
|
|
71
|
+
─────────────────────────────────
|
|
72
|
+
Source: {source_repo}
|
|
73
|
+
Language: {language}
|
|
74
|
+
Scope: {scope_type} ({N} include, {M} exclude patterns)
|
|
75
|
+
Docs: {doc_urls count} sources detected | "None detected"
|
|
76
|
+
Version: {version}
|
|
77
|
+
Forge Tier: {forge_tier}
|
|
78
|
+
Pipeline: forge-auto ({forge_tier} tier)
|
|
79
|
+
Description: "{description}"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Where `{N}` is the count of `scope_include` patterns and `{M}` is the count of `scope_exclude` patterns. If `doc_urls` is null or empty, display "None detected". The `Pipeline` line names the auto pipeline and the resolved `{forge_tier}` — it carries no numeric quality target, which would be an unverified guarantee an automator might parse as fact.
|
|
83
|
+
|
|
84
|
+
### 3. Validation Gate
|
|
85
|
+
|
|
86
|
+
**GATE: [A]pprove** — Present `[A]pprove` / `[E]dit` / `[R]eject` to user.
|
|
87
|
+
|
|
88
|
+
"**Review the auto-generated brief above.**
|
|
89
|
+
|
|
90
|
+
Select an action:
|
|
91
|
+
[A] Approve — accept the brief as-is and continue the pipeline
|
|
92
|
+
[E] Edit — modify specific fields before continuing
|
|
93
|
+
[R] Reject — fall back to interactive brief review with pre-populated fields"
|
|
94
|
+
|
|
95
|
+
If `{headless_mode}`: auto-proceed with `[A]pprove`, log: "headless: auto-approve auto-brief".
|
|
96
|
+
|
|
97
|
+
Wait for user response. Branch on the response:
|
|
98
|
+
|
|
99
|
+
- `[A]` or `approve` → §4 ([A]pprove path)
|
|
100
|
+
- `[E]` or `edit` → §5 ([E]dit path)
|
|
101
|
+
- `[R]` or `reject` → §6 ([R]eject path)
|
|
102
|
+
- Any other input → answer briefly, re-display the menu
|
|
103
|
+
|
|
104
|
+
### 4. [A]pprove Path
|
|
105
|
+
|
|
106
|
+
**Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
107
|
+
|
|
108
|
+
Emit the `SKF_BRIEF_RESULT_JSON` envelope with `mode: "auto"`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
echo '{"status":"success","brief_path":"{brief_path}","skill_name":"{skill_name}","version":"{version}","language":"{language}","scope_type":"{scope_type}","halt_reason":null,"mode":"auto"}' | \
|
|
112
|
+
uv run {emitBriefEnvelopeHelper} emit
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Where `{brief_path}` is `{forge_data_folder}/{skill_name}/skill-brief.yaml`.
|
|
116
|
+
|
|
117
|
+
Chain to {nextStepFile} (health-check.md) — load, read fully, then execute.
|
|
118
|
+
|
|
119
|
+
### 5. [E]dit Path
|
|
120
|
+
|
|
121
|
+
Present each brief field with its current value and accept natural language modification requests.
|
|
122
|
+
|
|
123
|
+
"**Editable fields:**
|
|
124
|
+
|
|
125
|
+
1. **Name:** {skill_name}
|
|
126
|
+
2. **Source:** {source_repo}
|
|
127
|
+
3. **Language:** {language}
|
|
128
|
+
4. **Scope type:** {scope_type}
|
|
129
|
+
5. **Include patterns:** {scope_include}
|
|
130
|
+
6. **Exclude patterns:** {scope_exclude}
|
|
131
|
+
7. **Description:** {description}
|
|
132
|
+
8. **Doc URLs:** {doc_urls or "None"}
|
|
133
|
+
9. **Version:** {version}
|
|
134
|
+
10. **Forge tier:** {forge_tier}
|
|
135
|
+
|
|
136
|
+
Tell me what to change (e.g. 'change scope type to public-api', 'add doc URL https://...')."
|
|
137
|
+
|
|
138
|
+
Wait for user response. Apply changes to the brief context.
|
|
139
|
+
|
|
140
|
+
**Re-write the modified brief** through the canonical writer:
|
|
141
|
+
|
|
142
|
+
**Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins.
|
|
143
|
+
|
|
144
|
+
Assemble the modified brief context as a flat JSON object (same format as step-auto-brief §4):
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
echo '<modified-flat-json>' | uv run {writeSkillBriefHelper} write --target {forge_data_folder}/{skill_name}/skill-brief.yaml --from-flat
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The canonical writer validates the brief internally — on non-zero exit, surface the error and re-prompt for corrections. The edit loop allows multiple modifications — each write re-validates before accepting.
|
|
151
|
+
|
|
152
|
+
**Re-validate the written brief** against the schema to confirm correctness:
|
|
153
|
+
|
|
154
|
+
**Resolve `{validateBriefSchemaHelper}`** from `{validateBriefSchemaProbeOrder}`; first existing path wins.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
uv run {validateBriefSchemaHelper} {forge_data_folder}/{skill_name}/skill-brief.yaml
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
If validation fails (the writer missed a constraint), surface the error and re-prompt for corrections.
|
|
161
|
+
|
|
162
|
+
Re-present the 10-line summary (§2 format) with updated values so the user can verify the change.
|
|
163
|
+
|
|
164
|
+
"Updated brief written. **Select:** [A] Approve and continue · [E] Edit more · [R] Reject"
|
|
165
|
+
|
|
166
|
+
- `[A]` → emit envelope per §4, chain to {nextStepFile}
|
|
167
|
+
- `[E]` → repeat §5 edit loop
|
|
168
|
+
- `[R]` → §6 ([R]eject path)
|
|
169
|
+
|
|
170
|
+
### 6. [R]eject Path
|
|
171
|
+
|
|
172
|
+
"**Falling back to interactive brief — fields pre-populated from auto-detection.**"
|
|
173
|
+
|
|
174
|
+
Hydrate brief context variables from the auto-brief on disk, using the same field mapping as the ratify path in gather-intent §3.1a:
|
|
175
|
+
|
|
176
|
+
- `name` ← `brief.name`; `version` ← `brief.version`; `target_version` ← `brief.target_version`
|
|
177
|
+
- `target_ref` ← `brief.target_ref`; `source_ref` ← `brief.source_ref` (optional git refs; preserve when present)
|
|
178
|
+
- `source_repo` ← `brief.source_repo`; `source_type` ← `brief.source_type`; `source_authority` ← `brief.source_authority`; `doc_urls` ← `brief.doc_urls`
|
|
179
|
+
- `language` ← `brief.language`; `description` ← `brief.description`; `forge_tier` ← `brief.forge_tier`
|
|
180
|
+
- `created` ← `brief.created`; `created_by` ← `brief.created_by`
|
|
181
|
+
- `scope.type` / `scope.include` / `scope.exclude` / `scope.tier_a_include` / `scope.notes` / `scope.rationale` / `scope.amendments` ← `brief.scope.*` (preserve `tier_a_include` and the `amendments` log verbatim — do not re-derive or drop them)
|
|
182
|
+
- `scripts_intent` ← `brief.scripts_intent`; `assets_intent` ← `brief.assets_intent`
|
|
183
|
+
|
|
184
|
+
Set `ratify_mode: true` and `ratify_source_path: {forge_data_folder}/{skill_name}/skill-brief.yaml` in workflow context.
|
|
185
|
+
|
|
186
|
+
Chain to {rejectTargetFile} (confirm-brief.md) — load, read fully, then execute. The user gets the full interactive review experience: view, adjust fields inline, revise scope via [R], or approve via [C] → write-brief.md → health-check.md.
|
|
187
|
+
|
|
188
|
+
The interactive chain's write-brief.md handles its own envelope emission with `mode: null` (interactive), which is correct since the user explicitly chose to leave auto mode.
|
|
189
|
+
|
|
190
|
+
### 7. Error Envelope (Canonical)
|
|
191
|
+
|
|
192
|
+
Every HARD HALT in this step emits the error envelope on stderr:
|
|
193
|
+
|
|
194
|
+
**Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
echo '{"status":"error","skill_name":"{skill_name or unknown}","halt_reason":"{reason}","mode":"auto"}' | \
|
|
198
|
+
uv run {emitBriefEnvelopeHelper} emit --target stderr
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 8. Chain
|
|
202
|
+
|
|
203
|
+
Load, read fully, then execute the appropriate next step file based on the user's choice:
|
|
204
|
+
- [A]pprove or [E]dit (after final approve): {nextStepFile} (health-check.md)
|
|
205
|
+
- [R]eject: {rejectTargetFile} (confirm-brief.md)
|
|
206
|
+
|
|
207
|
+
## CRITICAL STEP COMPLETION NOTE
|
|
208
|
+
|
|
209
|
+
ONLY WHEN the user has made their choice (approve, edit-then-approve, or reject) and the appropriate action has been taken (envelope emitted or context hydrated) will you load and read fully the next step file.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
briefSchemaFile: 'assets/skill-brief-schema.md'
|
|
3
2
|
versionResolutionFile: 'references/version-resolution.md'
|
|
4
3
|
qmdRegistrationFile: 'references/qmd-collection-registration.md'
|
|
5
4
|
nextStepFile: 'health-check.md'
|
|
@@ -98,7 +97,7 @@ Assemble the brief context as a **flat** JSON object — every approved value is
|
|
|
98
97
|
"scope_rationale": null | {"recommended":"...","chosen":"...","accepted_recommendation":true|false,"heuristic":"...","reason":"...","recorded":"YYYY-MM-DD"},
|
|
99
98
|
"scope_tier_a_include": null | ["{tier-A authoring-surface patterns — from step 03 §3c capture, or hydrated on a ratify run}"],
|
|
100
99
|
"scope_amendments": null | [{"path":"...","action":"...","reason":"...","date":"YYYY-MM-DD","workflow":"..."}],
|
|
101
|
-
"doc_urls": null | [{"url": "...", "label": "..."}],
|
|
100
|
+
"doc_urls": null | [{"url": "...", "label": "...", "source": "{optional: language-registry|readme-detection|homepage|pages-api|docs-folder}"}],
|
|
102
101
|
"scripts_intent": null | "{detect|none|free-text}",
|
|
103
102
|
"assets_intent": null | "{detect|none|free-text}",
|
|
104
103
|
"source_authority": null | "{official|community|internal}",
|
|
@@ -163,7 +162,7 @@ The script derives `exit_code` deterministically from `halt_reason` (null→0, i
|
|
|
163
162
|
|
|
164
163
|
The script enforces the success/error halt_reason invariant (success requires null halt_reason; error requires non-null). The `user-cancelled` halt_reason is accepted for completeness (interactive `[X]` Cancel sites in step 1/03/04) but never appears on the headless code path.
|
|
165
164
|
|
|
166
|
-
Invocation sites (each pointed at this block, not duplicated): step 1 §1 (write-failed pre-resolution), step 1 §8 (input-missing/input-invalid GATE), step 5 §2b (overwrite-cancelled), step 5 §3 (input-invalid/write-failed from script).
|
|
165
|
+
Invocation sites (each pointed at this block, not duplicated): step 1 §1 (write-failed pre-resolution; forge-tier-missing), step 1 §8 (input-missing/input-invalid GATE), step 2 §1 (target-inaccessible/gh-auth-failed), step 5 §2b (overwrite-cancelled), step 5 §3 (input-invalid/write-failed from script). The step 1 §1 forge-tier-missing and step 2 §1 target-inaccessible/gh-auth-failed sites emit through this block too, so every headless HALT class surfaces a `SKF_BRIEF_RESULT_JSON` envelope — there are no envelope-silent failure classes.
|
|
167
166
|
|
|
168
167
|
When `{headless_mode}` is false, skip this section silently — no envelope is emitted.
|
|
169
168
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skf-campaign
|
|
3
|
+
description: Campaign orchestration — multi-library skill production with dependency tracking, file-based state, and resume. Use when the user asks to "run a campaign" or "orchestrate skills."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Campaign
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Orchestrates the production of 15+ skills across multiple sessions by driving them through the full SKF pipeline (brief, generate, compile, test, export) in dependency order. Campaign sits at the top of the pipeline ladder — it does not produce skill artifacts directly but sequences the workflows that do. File-based state (`_campaign-state.yaml`) survives context death, enabling resume from any point.
|
|
11
|
+
|
|
12
|
+
## Conventions
|
|
13
|
+
|
|
14
|
+
- Bare paths (e.g. `references/step-01-setup.md`) resolve from the skill root.
|
|
15
|
+
- `references/` holds step files chained by stage number, plus the directive contract at `references/campaign-directive-spec.md` (the canonical spec for `_campaign-directive.md`); `templates/` holds workflow-loaded templates; `scripts/` and `assets/` hold deterministic helpers and the state schema.
|
|
16
|
+
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
|
17
|
+
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
18
|
+
- `{skill-name}` resolves to the skill directory's basename.
|
|
19
|
+
|
|
20
|
+
## Role
|
|
21
|
+
|
|
22
|
+
You are a campaign orchestrator operating in Ferris's Management mode. You sequence workflows, track per-skill state, enforce quality gates, and ensure every skill reaches its target tier — while the individual pipeline workflows handle the actual artifact production.
|
|
23
|
+
|
|
24
|
+
## On Activation
|
|
25
|
+
|
|
26
|
+
Run these steps once, in order, before dispatching to Mode Routing.
|
|
27
|
+
|
|
28
|
+
1. **Load config.** Read `{project-root}/_bmad/skf/config.yaml` and `{sidecar_path}/preferences.yaml` in one batched message (independent files). From config resolve `project_name`, `user_name`, `communication_language`, `document_output_language`, `skills_output_folder`, `forge_data_folder`, `sidecar_path`. From preferences resolve `headless_mode` (default false). If the config file is missing, fall back to `forge_data_folder = forge-data`.
|
|
29
|
+
|
|
30
|
+
2. **Resolve `{headless_mode}`** — true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `preferences.yaml`. Default: false.
|
|
31
|
+
|
|
32
|
+
3. **Resolve workflow customization.** Run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The script merges three layers (scalars override, arrays append):
|
|
39
|
+
|
|
40
|
+
- `{skill-root}/customize.toml` — bundled defaults
|
|
41
|
+
- `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)
|
|
42
|
+
- `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)
|
|
43
|
+
|
|
44
|
+
If it fails or is missing, fall back to `{skill-root}/customize.toml` directly. Apply the fallback for each scalar now so step files never repeat the conditional, and stash as workflow-context variables:
|
|
45
|
+
|
|
46
|
+
- `{campaignWorkspacePath}` ← `workflow.campaign_workspace_path` if non-empty, else `{forge_data_folder}/_campaign`
|
|
47
|
+
- `{qualityGateHard}` / `{qualityGateSoftTarget}` / `{qualityGateSoftFallback}` ← the `quality_gate_*` scalars (defaults `zero-critical-high` / `90` / `80`)
|
|
48
|
+
- `{reportTemplatePath}` ← `workflow.report_template_path` if non-empty, else `templates/campaign-report-template.md`
|
|
49
|
+
- `{kickoffTemplatePath}` ← `workflow.kickoff_template_path` if non-empty, else `templates/kickoff-template.md`
|
|
50
|
+
- `{briefTemplatePath}` ← `workflow.brief_template_path` if non-empty, else `templates/campaign-brief-template.yaml`
|
|
51
|
+
- `{onComplete}` ← `workflow.on_complete` (empty = no-op)
|
|
52
|
+
|
|
53
|
+
Load `workflow.persistent_facts` (literal sentences and `file:` references, globs expanded) and keep them in mind for the whole campaign — they are injected into every per-skill kickoff. Run any `activation_steps_prepend` before step 1 and any `activation_steps_append` after this step.
|
|
54
|
+
|
|
55
|
+
Every step file resolves its frontmatter path vars (`stateFile`, `backupFile`, …) under `{campaignWorkspacePath}` — overriding `campaign_workspace_path` relocates the whole campaign workspace without editing a single step.
|
|
56
|
+
|
|
57
|
+
4. **Parse CLI overrides** into the workflow context:
|
|
58
|
+
|
|
59
|
+
| Flag | Effect |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `--headless` / `-H` | Force `{headless_mode} = true` (see step 2). |
|
|
62
|
+
| `--brief <file>` | Seed step-01 targets from a `campaign-brief.yaml` instead of interactive prompts. Implies `--headless`. |
|
|
63
|
+
| `--manifest <file>` | Seed step-01 targets from a plain-text `name,repo_url,tier,pin` manifest. Implies `--headless`. |
|
|
64
|
+
| `--from <skill>` | Resume override — see Mode Routing. |
|
|
65
|
+
|
|
66
|
+
If `--brief` or `--manifest` is set, force `{headless_mode} = true` (log "headless: coerced by --brief/--manifest" if it was false).
|
|
67
|
+
|
|
68
|
+
**`--manifest` format:** one target per line, comma-separated `name,repo_url,tier,pin` (omit `pin` or leave it empty for latest); add a trailing `;dep1,dep2` segment for `depends_on`. Blank lines and `#` comments are skipped. If any line fails to parse, HALT in step-01 listing the offending line numbers rather than silently dropping targets — a partial target set is never run.
|
|
69
|
+
|
|
70
|
+
5. **Dispatch** per Mode Routing below.
|
|
71
|
+
|
|
72
|
+
## Workflow Rules
|
|
73
|
+
|
|
74
|
+
These rules apply to every step in this workflow:
|
|
75
|
+
|
|
76
|
+
- State-first — write state to disk before chaining to the next step or workflow
|
|
77
|
+
- Read-backup-modify-write for all state mutations (see State Contract below)
|
|
78
|
+
- Validate `_campaign-state.yaml` on every load by running `uv run scripts/campaign-validate-state.py --state-file {stateFile}` and HALT (exit code 3, `invalid-state`) on non-zero — never hand-validate the schema
|
|
79
|
+
- Zero memory dependency (NFR-2) — campaign state is 100% recoverable from disk; never rely on conversation context for progress tracking
|
|
80
|
+
- Treat a missing or unparseable `SKF_*_RESULT_JSON` envelope from any sub-skill as a sub-skill failure; never write partial state from an unparsed envelope
|
|
81
|
+
- Append a one-line entry to the campaign decision log (`{campaignWorkspacePath}/_campaign-decision-log.md`, append-only) at every operator decision or auto-decision (skip/force, architecture-doc skip, export cancel/proceed, campaign overwrite, `.bak` recovery, user-cancel) so rationale survives compaction and resume
|
|
82
|
+
- **Universal cancel affordance** — at any interactive gate the operator may type `cancel`, `exit`, or `:q` to leave cleanly. HARD HALT with **exit code 12 (`user-cancelled`)**, log the cancellation to the decision log, and leave state intact and resumable. This gives the operator a documented way out at every prompt between Setup and the Export gate. **The Export gate is the one exception:** it has its own `[C]ancel` with exit code 11 (`export-cancelled`), which remains canonical there — do not also emit 12 at the export gate, so an automator's exit-code branch stays deterministic. These keywords are recognized only as a response *to an interactive prompt*; a skill or campaign named `cancel`/`exit` supplied as data is never treated as a cancel.
|
|
83
|
+
- Always communicate in `{communication_language}`
|
|
84
|
+
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
|
|
85
|
+
- If `{headless_mode}` is true, emit a single-line JSON progress event to **stderr** at each step's entry and exit so schedulers stream live progress instead of post-mortem-parsing the final envelope:
|
|
86
|
+
- entry: `{"stage":N,"name":"<slug>","status":"start"}`
|
|
87
|
+
- exit (just before chaining): `{"stage":N,"name":"<slug>","status":"done"}`
|
|
88
|
+
- on HARD HALT: `{"stage":N,"name":"<slug>","status":"halt","exit":<code>}` instead of "done"
|
|
89
|
+
|
|
90
|
+
`N` is the stage number (0–10, per the Stages table) and `<slug>` is the kebab portion of the step filename. For the non-numbered routing/terminal steps (`resume`, `health-check`) emit `"stage":null` with the slug. One line per event; do not pretty-print.
|
|
91
|
+
|
|
92
|
+
## Stages
|
|
93
|
+
|
|
94
|
+
| # | Step | File | Auto-proceed |
|
|
95
|
+
|---|------|------|--------------|
|
|
96
|
+
| 0 | Setup | references/step-01-setup.md | Yes |
|
|
97
|
+
| 1 | Strategy | references/step-02-strategy.md | Yes |
|
|
98
|
+
| 2 | Pin Validation | references/step-03-pins.md | Yes |
|
|
99
|
+
| 3 | Provenance | references/step-04-provenance.md | Yes |
|
|
100
|
+
| 4 | Skill Loop | references/step-05-skill-loop.md | Yes |
|
|
101
|
+
| 5 | Tier B Batch | references/step-06-batch.md | Yes |
|
|
102
|
+
| 6 | Capstone | references/step-07-capstone.md | Yes |
|
|
103
|
+
| 7 | Verification | references/step-08-verify.md | Yes |
|
|
104
|
+
| 8 | Refinement | references/step-09-refine.md | Yes |
|
|
105
|
+
| 9 | Export | references/step-10-export.md | No (write-gate HALT) |
|
|
106
|
+
| 10 | Maintenance | references/step-11-maintenance.md | Yes |
|
|
107
|
+
|
|
108
|
+
**Stage numbering:** step files are 1-indexed (`step-01` … `step-11`); `campaign.current_stage` in state is 0-indexed, so step-`NN` runs stage `NN − 1` (e.g. step-01 = stage 0, step-11 = stage 10). The Resume Routing table in `references/step-resume.md` maps a resolved stage number to its step file; because `current_stage` records the highest *completed* stage, resume without an active skill advances to `current_stage + 1` (the stage that still needs to run) before consulting the table.
|
|
109
|
+
|
|
110
|
+
## Invocation Contract
|
|
111
|
+
|
|
112
|
+
| Aspect | Detail |
|
|
113
|
+
|--------|--------|
|
|
114
|
+
| **Inputs** | `campaign` to start a new campaign; `campaign resume [--from=<skill>]` to resume from last active or specified skill; `campaign status` for a read-only progress summary |
|
|
115
|
+
| **Overrides** | `--headless` / `-H`, `--brief <file>`, `--manifest <file>`, `--from <skill>` — see On Activation step 4 |
|
|
116
|
+
| **Gates** | Step 9 (Export): write-gate HALT — requires explicit user approval before writing exported skills to disk |
|
|
117
|
+
| **Outputs** | `_campaign-state.yaml` (state), `campaign-brief.yaml` (machine-generated brief), `campaign-report.md` (post-campaign summary), `_campaign-decision-log.md` (append-only rationale), `SKF_CAMPAIGN_RESULT_JSON` (headless envelope) — all under `{campaignWorkspacePath}` |
|
|
118
|
+
| **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
|
|
119
|
+
| **Exit codes** | See "Exit Codes" below |
|
|
120
|
+
|
|
121
|
+
## Exit Codes
|
|
122
|
+
|
|
123
|
+
Every HARD HALT exits with a stable, documented code so headless automators can branch on the failure class without grepping message text:
|
|
124
|
+
|
|
125
|
+
| Code | Meaning | Raised by |
|
|
126
|
+
| ---- | -------------------- | ----------------------------------------------------------- |
|
|
127
|
+
| 0 | success | step-11 (terminal) |
|
|
128
|
+
| 3 | invalid-state | any step §1 (`campaign-validate-state.py` non-zero on load) |
|
|
129
|
+
| 4 | circular-deps | step-02 §5 |
|
|
130
|
+
| 5 | invalid-pin | step-03 §5 |
|
|
131
|
+
| 6 | inaccessible-repo | step-04 §5 |
|
|
132
|
+
| 7 | dependency-deadlock | step-05 §4 (no skill ready and no recovery chosen) |
|
|
133
|
+
| 8 | missing-brief | step-03/04/05 §2 (brief missing or unreadable) |
|
|
134
|
+
| 9 | corrupt-state | step-resume §1 (primary unrecoverable, `.bak` also invalid) |
|
|
135
|
+
| 10 | report-failure | step-11 §2 — **degraded only**: the report could not be generated; the campaign still completes and state stays intact (never a hard halt that discards a finished campaign) |
|
|
136
|
+
| 11 | export-cancelled | step-10 §4 (operator chose `[C]ancel` — graceful, resumable) |
|
|
137
|
+
| 12 | user-cancelled | any interactive gate (operator typed `cancel` / `exit` / `:q` — graceful, resumable; see Workflow Rules) |
|
|
138
|
+
|
|
139
|
+
## Result Contract on HARD HALT
|
|
140
|
+
|
|
141
|
+
In addition to the success-variant envelope (see Campaign Headless Envelope), every HARD HALT emits an **error variant** so automators don't silently break. Emit one line on **stderr**:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
SKF_CAMPAIGN_RESULT_JSON: {"status":"error","exit_code":<N>,"phase":"<slug>","error":{"code":"<class>","message":"<short>"},"skills_completed":N,"skills_failed":N,"campaign_report_path":null,"decision_log":"<path-or-null>"}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`<class>` is the Exit Codes meaning (e.g. `circular-deps`, `inaccessible-repo`); `<slug>` is the step where the HALT occurred. One line, no pretty-print.
|
|
148
|
+
|
|
149
|
+
## Mode Routing
|
|
150
|
+
|
|
151
|
+
On invocation:
|
|
152
|
+
|
|
153
|
+
1. **`campaign resume [--from=<skill>]`** — load `references/step-resume.md`. Validates state integrity, checks backup consistency, and chains to the appropriate stage step file. If `--from=<skill>` is provided, override the resume point to the named skill.
|
|
154
|
+
2. **`campaign`** (new, no existing state) — run from stage 0 (Setup).
|
|
155
|
+
3. **`campaign`** (state exists) — detect existing `{campaignWorkspacePath}/_campaign-state.yaml` and prompt: **resume** via `references/step-resume.md`, or **overwrite** with a new campaign. On overwrite, first archive the existing `_campaign-state.yaml` and `campaign-brief.yaml` to `{campaignWorkspacePath}/archive/{name}-{timestamp}/`, log the archive to the decision log, and show a one-line summary of what is being set aside before chaining to step-01. In headless mode, default to **resume** (never silently clobber); archive-and-overwrite only when `--brief`/`--manifest` explicitly seeds a new campaign.
|
|
156
|
+
4. **`campaign status`** (read-only) — load `{campaignWorkspacePath}/_campaign-state.yaml`, validate it via `campaign-validate-state.py`, display the Resume Detection summary block followed by the last ~15 lines of `{campaignWorkspacePath}/_campaign-decision-log.md` (so the operator sees the recent decision trail without flooding the terminal on a long campaign), then stop. No backup, no mutation, no chaining. Exit 0 (or 9 if the state is unrecoverable).
|
|
157
|
+
|
|
158
|
+
## Resume Detection
|
|
159
|
+
|
|
160
|
+
When resuming:
|
|
161
|
+
|
|
162
|
+
1. Read `_campaign-state.yaml`
|
|
163
|
+
2. Validate via `campaign-validate-state.py` (halt on invalid; attempt `.bak` recovery per step-resume)
|
|
164
|
+
3. Find last active or completed stage from `campaign.current_stage` (the highest completed stage); when no skill is active, resume continues from the stage *after* it (`current_stage + 1`, terminal-capped)
|
|
165
|
+
4. Skip completed skills (status = `completed` or `skipped`)
|
|
166
|
+
5. If `--from=<skill>` is provided, find the named skill and resume from its stage
|
|
167
|
+
6. Continue from the next incomplete skill in `dependency_graph.execution_order`
|
|
168
|
+
|
|
169
|
+
## State Contract
|
|
170
|
+
|
|
171
|
+
All state mutations follow the read-backup-modify-write pattern:
|
|
172
|
+
|
|
173
|
+
1. **Read** `_campaign-state.yaml`
|
|
174
|
+
2. **Validate** via `uv run scripts/campaign-validate-state.py --state-file {stateFile}` (halt on invalid)
|
|
175
|
+
3. **Backup** — copy current `_campaign-state.yaml` to `_campaign-state.yaml.bak`
|
|
176
|
+
4. **Modify** in memory
|
|
177
|
+
5. **Update** `campaign.last_updated` to current ISO-8601 timestamp
|
|
178
|
+
6. **Write** modified state back to `_campaign-state.yaml`
|
|
179
|
+
|
|
180
|
+
The `.bak` file is one-deep (overwritten on every write). If the primary file is corrupted (crash during write), the `.bak` file contains the last valid state — step-resume §1 recovers from it automatically rather than dead-halting.
|
|
181
|
+
|
|
182
|
+
## Campaign Headless Envelope
|
|
183
|
+
|
|
184
|
+
When `{headless_mode}` is true, the final step emits a single-line JSON envelope on stdout:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
SKF_CAMPAIGN_RESULT_JSON: {"status":"success|error","skills_completed":0,"skills_failed":0,"quality_scores":{},"campaign_report_path":"","decision_log":"","duration":""}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`status` is `"success"` when the campaign completes normally, `"error"` on any unrecoverable halt (with `exit_code` per the Exit Codes table — see Result Contract on HARD HALT). `skills_completed` and `skills_failed` count per-skill outcomes. `quality_scores` maps skill names to their test-skill scores. `campaign_report_path` points to the generated `campaign-report.md`. `decision_log` points to `_campaign-decision-log.md`. `duration` is the wall-clock time of the campaign run. Populate the counts, `quality_scores`, and `duration` directly from the `campaign-report.py` result JSON (step-11 §2) — do not recompute them by hand.
|