bmad-module-skill-forge 1.0.0 → 1.2.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 +9 -7
- package/docs/_data/pinned.yaml +5 -2
- package/docs/{RELEASING.md → _internal/RELEASING.md} +87 -61
- package/docs/{STABILITY.md → _internal/STABILITY.md} +2 -2
- package/docs/bmad-synergy.md +2 -2
- package/docs/getting-started.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/why-skf.md +5 -4
- package/package.json +2 -7
- package/release-audits/v1.0.0-launch-audit.md +313 -2
- package/src/knowledge/ccc-bridge.md +1 -1
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +134 -0
- package/src/shared/scripts/skf-detect-tools.py +359 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +419 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +413 -0
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +324 -0
- package/src/shared/scripts/skf-preflight.py +14 -4
- package/src/shared/scripts/skf-qmd-classify-collections.py +212 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +9 -3
- package/src/skf-audit-skill/steps-c/step-01-init.md +60 -0
- package/src/skf-audit-skill/steps-c/step-02-re-index.md +7 -1
- package/src/skf-audit-skill/steps-c/step-03-structural-diff.md +19 -1
- package/src/skf-audit-skill/steps-c/step-04-semantic-diff.md +10 -1
- package/src/skf-audit-skill/steps-c/step-05-severity-classify.md +8 -0
- package/src/skf-audit-skill/steps-c/step-06-report.md +8 -0
- package/src/skf-brief-skill/assets/skill-brief-schema.md +36 -18
- package/src/skf-create-skill/steps-c/step-06-validate.md +1 -1
- package/src/skf-drop-skill/steps-c/step-02-execute.md +1 -1
- package/src/skf-export-skill/assets/managed-section-format.md +1 -1
- package/src/skf-export-skill/steps-c/step-04-update-context.md +4 -4
- package/src/skf-rename-skill/steps-c/step-02-execute.md +1 -1
- package/src/skf-setup/SKILL.md +25 -10
- package/src/skf-setup/references/tier-rules.md +2 -2
- package/src/skf-setup/steps-c/step-01-detect-and-tier.md +58 -71
- package/src/skf-setup/steps-c/step-01b-ccc-index.md +49 -66
- package/src/skf-setup/steps-c/step-02-write-config.md +59 -86
- package/src/skf-setup/steps-c/step-03-auto-index.md +73 -91
- package/src/skf-setup/steps-c/step-04-report.md +135 -11
- package/src/skf-setup/steps-c/step-05-health-check.md +4 -2
- package/src/skf-test-skill/steps-c/step-01-init.md +4 -4
- package/src/skf-test-skill/steps-c/step-03-coverage-check.md +1 -1
- package/src/skf-update-skill/steps-c/step-02-detect-changes.md +123 -0
- package/tools/validate-docs-drift.js +42 -2
|
@@ -1,31 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
tierRulesData: 'references/tier-rules.md'
|
|
3
3
|
nextStepFile: './step-05-health-check.md'
|
|
4
|
+
# Resolve `{emitEnvelopeHelper}` by probing `{emitEnvelopeProbeOrder}` in order
|
|
5
|
+
# (installed SKF module path first, src/ dev-checkout fallback); first existing
|
|
6
|
+
# path wins. HALT if neither resolves when the headless envelope must be emitted
|
|
7
|
+
# — the script is the source of truth for the SKF_SETUP_RESULT_JSON contract;
|
|
8
|
+
# never inline-render the envelope from prose (LLM schema drift is the bug
|
|
9
|
+
# this script exists to prevent).
|
|
10
|
+
emitEnvelopeProbeOrder:
|
|
11
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py'
|
|
12
|
+
- '{project-root}/src/shared/scripts/skf-emit-result-envelope.py'
|
|
4
13
|
---
|
|
5
14
|
|
|
15
|
+
<!-- Config: communicate in {communication_language}; emit user-visible report text (FORGE STATUS banner, climb hint, REQUIRED TIER NOT MET block, breadcrumb) in {document_output_language}. The JSON envelope from section 4 is a machine contract — its keys and enum values stay English regardless. -->
|
|
16
|
+
|
|
6
17
|
# Step 4: Forge Status Report
|
|
7
18
|
|
|
8
19
|
## STEP GOAL:
|
|
9
20
|
|
|
10
|
-
Display the forge status report with positive capability framing
|
|
21
|
+
Display the forge status report with positive capability framing, surface tier changes and tool-set deltas on re-runs, prominently flag a required-tier miss, and (when headless) emit the schema-locked `SKF_SETUP_RESULT_JSON` envelope via `{emitEnvelopeHelper}`.
|
|
11
22
|
|
|
12
23
|
## Rules
|
|
13
24
|
|
|
14
|
-
- Focus only on
|
|
25
|
+
- Focus only on display + envelope emission
|
|
15
26
|
- Do not use negative framing ("missing", "lacking", "unavailable")
|
|
16
27
|
- Do not list tools that are not available
|
|
17
28
|
- Use tier capability descriptions from tier-rules.md
|
|
29
|
+
- Never inline-render the envelope JSON — the script owns the schema; drift breaks pipelines
|
|
18
30
|
- Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing status report is NOT the terminal step
|
|
19
31
|
|
|
20
|
-
##
|
|
32
|
+
## Headless Mode Display Rule
|
|
33
|
+
|
|
34
|
+
When `{headless_mode}` is `true`, sections 2 (the human-readable banner) and 3 (the REQUIRED TIER NOT MET human prose block) are **skipped entirely**. The single line of stdout in headless mode is the `SKF_SETUP_RESULT_JSON: {…}` envelope from section 4 — every signal the human banner would have surfaced is already in the envelope (`tier`, `previous_tier`, `tier_changed`, `tools`, `tools_added`, `tools_removed`, `files_written`, `tier_override_*`, `require_tier_satisfied`, `warnings`, `error`). Parent skills and CI pipelines consume one parseable line; they should not be forced to scroll past 30 lines of ASCII-art they cannot use.
|
|
35
|
+
|
|
36
|
+
When `{headless_mode}` is `false`, sections 2 and 3 display normally and section 4 is skipped (interactive runs read the human banner; envelope emission would just be log noise).
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
## MANDATORY SEQUENCE
|
|
23
39
|
|
|
24
40
|
### 1. Load Capability Descriptions
|
|
25
41
|
|
|
26
|
-
Load and read {tierRulesData} for the tier capability descriptions and re-run messages.
|
|
42
|
+
Load and read {tierRulesData} for the tier capability descriptions and re-run messages. Needed by section 2; safe to load unconditionally — the data load itself is silent.
|
|
27
43
|
|
|
28
|
-
### 2. Display Forge Status Report
|
|
44
|
+
### 2. Display Forge Status Report (skip when `{headless_mode}` is true)
|
|
29
45
|
|
|
30
46
|
**Format the report as follows:**
|
|
31
47
|
|
|
@@ -39,6 +55,17 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
|
|
|
39
55
|
|
|
40
56
|
Tools Detected:
|
|
41
57
|
{for each tool that is available, show: tool name — version}
|
|
58
|
+
{if no tools are available: (none yet — see "Climb to next tier" below)}
|
|
59
|
+
|
|
60
|
+
{if calculated_tier is not Deep:}
|
|
61
|
+
Climb to next tier:
|
|
62
|
+
{if not tools.ast_grep: - Install ast-grep (https://ast-grep.github.io) — unlocks AST-backed code analysis (Forge tier)}
|
|
63
|
+
{if tools.ast_grep and not tools.ccc: - Install cocoindex-code (https://github.com/cocoindex-io/cocoindex-code) — adds semantic-guided precision compilation (Forge+ tier)}
|
|
64
|
+
{if tools.ast_grep and not tools.gh_cli: - Install GitHub CLI (https://cli.github.com) — required for Deep tier (cross-repository synthesis)}
|
|
65
|
+
{if tools.ast_grep and not tools.qmd and qmd_status is "absent": - Install qmd (https://github.com/tobi/qmd) — required for Deep tier (knowledge search)}
|
|
66
|
+
{if tools.ast_grep and not tools.qmd and qmd_status is "daemon_stopped": - Start the qmd daemon (already installed) — run `qmd start` (or your distribution's qmd service command) to unlock Deep tier (knowledge search)}
|
|
67
|
+
{if tools.ccc and ccc_daemon is "error": - The ccc daemon is reporting errors — run `ccc doctor` to diagnose. CCC index will fail until resolved (mirrors the qmd daemon-stopped pattern above for parity)}
|
|
68
|
+
{end if}
|
|
42
69
|
|
|
43
70
|
{if hygiene_result is "completed":}
|
|
44
71
|
QMD Registry:
|
|
@@ -63,30 +90,127 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
|
|
|
63
90
|
{if ccc_index_result is "failed": indexing failed — semantic discovery unavailable this session}
|
|
64
91
|
{end if}
|
|
65
92
|
|
|
93
|
+
Files written this run:
|
|
94
|
+
- forge-tier.yaml — {project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml
|
|
95
|
+
{if preferences_yaml_created is true:}
|
|
96
|
+
- preferences.yaml — {project-root}/_bmad/_memory/forger-sidecar/preferences.yaml (first-run defaults)
|
|
97
|
+
{end if}
|
|
98
|
+
{if forge_data_dir_created is true:}
|
|
99
|
+
- {forge_data_folder}/ (directory created)
|
|
100
|
+
{end if}
|
|
101
|
+
{if settings_yml_written is true:}
|
|
102
|
+
- .cocoindex_code/settings.yml — {project-root}/.cocoindex_code/settings.yml ({settings_yml_patterns_added} SKF exclusion pattern(s) merged)
|
|
103
|
+
{end if}
|
|
104
|
+
{if ccc_index_result is "created":}
|
|
105
|
+
- .cocoindex_code/ ccc index — {ccc_file_count} files indexed
|
|
106
|
+
{end if}
|
|
107
|
+
|
|
66
108
|
{if tier_override is active:}
|
|
67
109
|
Note: Tier override active (set in preferences.yaml)
|
|
68
110
|
|
|
111
|
+
{if tier_override_invalid is true:}
|
|
112
|
+
Note: tier_override value "{tier_override_invalid_value}" in preferences.yaml is not valid.
|
|
113
|
+
{if tier_override_invalid_suggestion is non-null: Did you mean "{tier_override_invalid_suggestion}"?}
|
|
114
|
+
Valid values are case-sensitive: Quick, Forge, Forge+, Deep. Using detected tier {calculated_tier}.
|
|
115
|
+
|
|
116
|
+
{if tier_override_unsafe is true:}
|
|
117
|
+
Warning: tier_override is forcing {calculated_tier} but the underlying tool prerequisites are not satisfied.
|
|
118
|
+
Missing: {tier_override_unsafe_missing}. The override is honored, but downstream skills that
|
|
119
|
+
rely on the missing tool(s) will fail at runtime. Install the missing tool(s) or remove
|
|
120
|
+
the override from preferences.yaml.
|
|
121
|
+
|
|
69
122
|
{if re-run with tier change:}
|
|
70
123
|
{appropriate upgrade/downgrade message from tier-rules.md}
|
|
71
124
|
|
|
72
|
-
{if re-run with same tier:}
|
|
125
|
+
{if re-run with same tier and {tools_added} is empty and {tools_removed} is empty:}
|
|
73
126
|
{same-tier message from tier-rules.md}
|
|
74
127
|
|
|
128
|
+
{if re-run with same tier and ({tools_added} or {tools_removed} is non-empty):}
|
|
129
|
+
Tier unchanged: {calculated_tier}.
|
|
130
|
+
{if {tools_added} non-empty:} Newly detected: {comma-separated tool names from tools_added}{if ccc was added and tier is Deep: " — ccc enhances Deep tier transparently."}
|
|
131
|
+
{if {tools_removed} non-empty:} No longer detected: {comma-separated tool names from tools_removed} — re-install to restore those capabilities.
|
|
132
|
+
|
|
75
133
|
═══════════════════════════════════════
|
|
76
134
|
Forge ready. {calculated_tier} tier active.
|
|
77
135
|
═══════════════════════════════════════
|
|
136
|
+
|
|
137
|
+
{if {headless_mode} is false:}
|
|
138
|
+
Next: try [BS] Brief Skill to scope your first compilation target, or [QS] Quick Skill for a fast template-driven path. Already have a skill? [AS] Audit Skill drift-checks an existing skill against current sources.
|
|
78
139
|
```
|
|
79
140
|
|
|
141
|
+
The same-tier-with-tool-deltas branch reads `{tools_added}` and `{tools_removed}` directly from step-01's context (set from `skf-detect-tools.py` output relative to `{previous_tools}`). No LLM-side recomputation.
|
|
142
|
+
|
|
80
143
|
**Tool display rules:**
|
|
144
|
+
|
|
81
145
|
- Only show tools that ARE available with their version strings
|
|
82
146
|
- Do NOT list unavailable tools
|
|
83
147
|
- Do NOT show a "missing" column or section
|
|
84
148
|
|
|
85
|
-
### 3.
|
|
149
|
+
### 3. Display Required-Tier Failure Block (when applicable; skip when `{headless_mode}` is true)
|
|
150
|
+
|
|
151
|
+
If `{require_tier_satisfied}` is `false` AND `{headless_mode}` is `false`, display this block immediately after the status report. In headless mode the failure surfaces via the envelope's `require_tier_satisfied: false` and the synthesized `warnings` entry — the human prose block would be log noise that pipelines have to scroll past.
|
|
152
|
+
|
|
153
|
+
When the block does fire (interactive run with require-tier failure):
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
═══════════════════════════════════════
|
|
157
|
+
REQUIRED TIER NOT MET
|
|
158
|
+
═══════════════════════════════════════
|
|
159
|
+
|
|
160
|
+
Required: {require_tier}
|
|
161
|
+
Detected: {calculated_tier}
|
|
162
|
+
Missing: {require_tier_failure_missing_tools}
|
|
163
|
+
|
|
164
|
+
Install the missing tool(s) and re-run, or relax `--require-tier`.
|
|
165
|
+
See "Climb to next tier" above for install URLs.
|
|
166
|
+
═══════════════════════════════════════
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This block exists to make pipeline failures visible without the operator parsing the JSON envelope.
|
|
170
|
+
|
|
171
|
+
### 4. Emit Headless JSON Envelope
|
|
172
|
+
|
|
173
|
+
When `{headless_mode}` is `true`, build the context payload from this step's accumulated flags and forward it to `{emitEnvelopeHelper}` on stdin. Invoke via `uv run` so the script's PEP 723 dependency declarations are honored (the canonical runtime invocation per `docs/getting-started.md`'s uv prereq — bare `python3` skips the metadata block and breaks on a fresh interpreter). The script computes derived fields (`tools_added`, `tools_removed`, `tier_changed`, `warnings`), validates the assembled envelope against the JSON Schema at `src/shared/scripts/schemas/skf-setup-result-envelope.v1.json`, and emits the single prefixed line `SKF_SETUP_RESULT_JSON: {…}` on stdout.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
echo '{
|
|
177
|
+
"tier": "{calculated_tier}",
|
|
178
|
+
"previous_tier": {previous_tier_or_null},
|
|
179
|
+
"tools": {tools_dict_from_step_01_or_skf_detect_tools_output},
|
|
180
|
+
"previous_tools": {previous_tools_or_null},
|
|
181
|
+
"config_path": "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml",
|
|
182
|
+
"ccc_index": {
|
|
183
|
+
"status": "{ccc_index_result}",
|
|
184
|
+
"indexed_path": {ccc_indexed_path_or_null},
|
|
185
|
+
"file_count": {ccc_file_count_or_null}
|
|
186
|
+
},
|
|
187
|
+
"files_written": {
|
|
188
|
+
"forge-tier.yaml": true,
|
|
189
|
+
"preferences.yaml": {preferences_yaml_created},
|
|
190
|
+
"settings.yml": {settings_yml_written},
|
|
191
|
+
"ccc_index": {ccc_index_result_was_created}
|
|
192
|
+
},
|
|
193
|
+
"tier_override_active": {tier_override_active},
|
|
194
|
+
"tier_override_invalid": {tier_override_invalid},
|
|
195
|
+
"tier_override_invalid_value": {tier_override_invalid_value_or_null},
|
|
196
|
+
"tier_override_unsafe": {tier_override_unsafe},
|
|
197
|
+
"tier_override_unsafe_missing": {tier_override_unsafe_missing_list},
|
|
198
|
+
"require_tier_satisfied": {require_tier_satisfied_or_null},
|
|
199
|
+
"require_tier_failure_missing": {require_tier_failure_missing_tools_list},
|
|
200
|
+
"qmd_status": "{qmd_status}",
|
|
201
|
+
"ccc_exclusion_warnings": {ccc_exclusion_warnings_list},
|
|
202
|
+
"ccc_registry_stale_removed": {ccc_registry_stale_removed_paths_list},
|
|
203
|
+
"ccc_indexing_failed_reason": {ccc_indexing_failed_reason_or_null},
|
|
204
|
+
"error": {error_object_or_null}
|
|
205
|
+
}' | uv run {emitEnvelopeHelper} emit
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The script's documented context-payload shape (see `src/shared/scripts/skf-emit-result-envelope.py` docstring) tolerates two `tools` shapes — bare booleans OR `skf-detect-tools.py`'s `{key: {available: bool, ...}}` output — so either step-01's normalized booleans OR the raw detect-tools output forwarded as-is will produce the correct envelope.
|
|
86
209
|
|
|
87
|
-
|
|
210
|
+
**If the script exits non-zero:** the assembled envelope failed schema validation, which means a context flag from an earlier step is malformed. Surface the error to stderr and continue (the human-readable banner is already displayed; missing JSON envelope on a headless run is a degraded but non-fatal state — the pipeline observer will see no envelope and treat that as "agent did not complete cleanly").
|
|
88
211
|
|
|
89
|
-
|
|
212
|
+
**When `{headless_mode}` is `false`, do NOT invoke the script** — interactive runs read the human-readable banner.
|
|
90
213
|
|
|
91
|
-
|
|
214
|
+
### 5. Chain to Health Check
|
|
92
215
|
|
|
216
|
+
After the forge status report (and any failure block + JSON envelope) has been displayed, load `{nextStepFile}`, read it fully, and execute it — UNLESS `{require_tier_satisfied}` is `false`, in which case halt the workflow here without chaining to step-05. The health-check step is the true terminal step on success — do not stop after the report on a passing run even though it reads as final. step-05 in turn delegates to `shared/health-check.md`; after that returns, the setup workflow is fully done.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
# `shared/health-check.md` resolves relative to the SKF module root
|
|
3
|
-
# (`_bmad/skf/` when installed, `src/` during
|
|
4
|
-
# to this step file.
|
|
3
|
+
# (`{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during
|
|
4
|
+
# development), NOT relative to this step file.
|
|
5
5
|
nextStepFile: 'shared/health-check.md'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
<!-- Config: communicate in {communication_language}. This is a delegation-only step (no user-visible output of its own); shared/health-check.md inherits the language directive on load. -->
|
|
9
|
+
|
|
8
10
|
# Step 5: Workflow Health Check
|
|
9
11
|
|
|
10
12
|
## STEP GOAL:
|
|
@@ -91,10 +91,10 @@ SKF module (`skf init`) or run from a development checkout with src/ present.
|
|
|
91
91
|
|
|
92
92
|
Do not proceed. No partial test report is written.
|
|
93
93
|
|
|
94
|
-
**3b. Run the validator (30s timeout — deterministic validator should finish in <1s; the cap only guards against runaway python)
|
|
94
|
+
**3b. Run the validator (30s timeout — deterministic validator should finish in <1s; the cap only guards against runaway python).** Invoke via `uv run` so the script's PEP 723 PyYAML dependency resolves automatically; bare `python3` would fail with `ModuleNotFoundError: No module named 'yaml'` on a fresh interpreter (`docs/getting-started.md` documents uv as the runtime prereq for exactly this).
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
timeout 30s
|
|
97
|
+
timeout 30s uv run {frontmatterScript} {resolved_skill_package}/SKILL.md --skill-dir-name {skill_name}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
If the command trips the 30s wall-clock (exit code `124`), set
|
|
@@ -116,7 +116,7 @@ Parse the JSON output. Per B2, treat each `status` value explicitly:
|
|
|
116
116
|
This skill will fail `npx skills add` and `npx skill-check check`. {If warn:} Consider fixing frontmatter before proceeding (run `npx skill-check check <skill-dir> --fix` to auto-fix deterministic issues). {If fail:} test-skill cannot proceed — halt and repair frontmatter, then re-run.
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
**3c. Python runtime probe.** Before the first invocation, confirm `python3`
|
|
119
|
+
**3c. Python runtime probe.** Before the first invocation, confirm both `python3` AND `uv` are on `$PATH` (`command -v python3` and `command -v uv`). Both are required: `uv run` shells through to `python3` under the hood AND honors the script's PEP 723 PyYAML dependency declaration that bare `python3` ignores. If either is missing, set `analysis_confidence: degraded` in workflow context and carry a **score cap** into step-05: `capped_score = threshold - 1` → forces auto-FAIL until the runtime is restored. Record the reason in evidence-report and the test report frontmatter (`analysisConfidence: degraded`, `toolingStatus: python3-missing` or `uv-missing` as appropriate). `uv` is a documented runtime prerequisite — see `docs/getting-started.md` for install instructions.
|
|
120
120
|
|
|
121
121
|
### 4. Load Forge Tier State
|
|
122
122
|
|
|
@@ -199,7 +199,7 @@ testResult: ''
|
|
|
199
199
|
score: ''
|
|
200
200
|
threshold: ''
|
|
201
201
|
analysisConfidence: '{full|degraded}'
|
|
202
|
-
toolingStatus: '{ok|python3-missing|frontmatter-validator-missing|frontmatter-validator-timeout}'
|
|
202
|
+
toolingStatus: '{ok|python3-missing|uv-missing|frontmatter-validator-missing|frontmatter-validator-timeout}'
|
|
203
203
|
workspaceDrift: '{not-checked|ok|overridden}'
|
|
204
204
|
testDate: '{run_id timestamp ISO-8601 UTC}'
|
|
205
205
|
stepsCompleted: ['step-01-init']
|
|
@@ -79,7 +79,7 @@ test-skill is a quality gate — it MUST NOT trust subagent output blindly. Befo
|
|
|
79
79
|
|
|
80
80
|
**Schema validation (required keys + types):**
|
|
81
81
|
|
|
82
|
-
1.
|
|
82
|
+
1. Strip wrapping markdown fences before parsing. Subagents frequently return JSON wrapped in a code fence — a line of three backticks (optionally followed by a language tag like `json`) preceding the JSON and a closing line of three backticks after it — despite prompt instructions to return raw JSON. When the first non-empty line of the response is three backticks (optionally with a language tag) and the last non-empty line is three backticks, remove those two fence lines before parsing. Then parse the remaining content as JSON. On parse failure of the inner content → HALT "coverage-check: subagent response not valid JSON".
|
|
83
83
|
2. Required keys present: `exports` (list), `cross_check_mismatches` (list — may be empty). Missing key or wrong type → HALT "coverage-check: subagent JSON schema invalid — missing/typo: {key}". Note: the parent already knows the skill name from workflow context (`{resolved_skill_package}` from step-01) — the subagent is not required to echo it back, and doing so introduces a contract-drift surface without improving verification.
|
|
84
84
|
3. Each `exports[]` entry must be a dict with at minimum `name` (non-empty string) and `kind` (one of `function|class|type|constant|hook|interface|method`). Reject entries violating this; if >0 rejections, HALT "coverage-check: subagent returned malformed export entries — {count} entries do not match schema".
|
|
85
85
|
4. `cross_check_mismatches[]` entries (when non-empty) must carry `export`, `skill_md_line`, `reference_file`, `reference_line`, `issue`. Missing fields → HALT.
|
|
@@ -131,6 +131,80 @@ Read the source directory at `{source_root}` and build a current file inventory:
|
|
|
131
131
|
|
|
132
132
|
**Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, NOT in the change manifest. But §2 Category A ("files in source but not in provenance map → ADDED") would still find the promoted doc files on disk and classify them as ADDED if nothing prevents it. The coordination mechanism is an explicit pre-filter exclusion set built in §2.0 (below) that every Category A subprocess worker receives as an input before it starts scanning. See §2.0 for the exact contract. The exclusion set is the only mechanism guaranteeing that parallel subprocesses cannot double-count `promoted_docs_new[]` paths — prose-level "skip any path" instructions cannot cross subprocess boundaries.
|
|
133
133
|
|
|
134
|
+
### 1c. Major-Version Scope Reconciliation (Pre-Detection)
|
|
135
|
+
|
|
136
|
+
**Purpose:** When upstream undergoes a paradigm shift (rebrand, package restructure, major-version rewrite), the brief's `scope.include` no longer reflects the real public API. §1b handles new authoritative-doc files; §1c handles new **code globs** that fall outside the original scope. Without it, update-skill silently misses the new surface and pays the gap cost on every future update — the cocoindex `0.3.37 → 1.0.0` and cognee `0.5.8 → 1.0.0` runs are existence proofs that this case is real and recurring.
|
|
137
|
+
|
|
138
|
+
**Skip this section entirely if:**
|
|
139
|
+
|
|
140
|
+
- `update_mode == "gap-driven"` (test-report mode — source hasn't drifted), OR
|
|
141
|
+
- `metadata.json.source_type == "docs-only"` (no source tree to scope), OR
|
|
142
|
+
- No audit drift report is available at the path computed in step 1 below.
|
|
143
|
+
|
|
144
|
+
**Procedure:**
|
|
145
|
+
|
|
146
|
+
1. **Locate the most recent audit drift report.** Search `{forge_data_folder}/{skill_name}/{baseline_version}/drift-report-*.md`, sorted by timestamp descending. Pick the latest. If none found, **skip §1c entirely** — the post-detection deletion-ratio trigger in §2.2 still catches major restructures even without an audit pass.
|
|
147
|
+
|
|
148
|
+
2. **Parse the report for an "Out-of-Scope Observations" section.** Look for either a top-level `## Out-of-Scope Observations` heading or a `### Out-of-Scope New Public API` subsection under `## Remediation Suggestions`. Each entry must expose:
|
|
149
|
+
|
|
150
|
+
- `path` — literal file path or directory glob (e.g., `python/cocoindex/_internal/api.py` or `python/cocoindex/resources/**`)
|
|
151
|
+
- `evidence` — short one-liner from the report (export count, rationale)
|
|
152
|
+
|
|
153
|
+
**Note:** This section is an optional audit-skill output. `skf-audit-skill` does not currently discover new files (per `src/skf-audit-skill/steps-c/step-02-re-index.md` — new-file detection is the responsibility of update-skill). The section is a forward-looking integration point: manual additions to the drift report or a future audit-skill enhancement populate it. If absent, no candidates from this trigger — proceed to step 6.
|
|
154
|
+
|
|
155
|
+
3. **Reconcile against existing amendments.** For each candidate, consult `brief.scope.amendments[]`:
|
|
156
|
+
|
|
157
|
+
- `action: "promoted"` AND `path` matches → already in scope, skip silently.
|
|
158
|
+
- `action: "skipped"` AND `path` matches → user previously declined, honor silently.
|
|
159
|
+
- `action: "demoted-include"` or `action: "demoted-exclude"` AND `path` matches → user previously narrowed scope on this path, do not re-prompt; record as `pre_decided`.
|
|
160
|
+
- No matching amendment → continue to user prompt.
|
|
161
|
+
|
|
162
|
+
4. **Prompt for each unresolved candidate.** Present the same menu shape as §1b:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
**Out-of-scope new public API discovered**
|
|
166
|
+
|
|
167
|
+
Path: {candidate.path}
|
|
168
|
+
Evidence: {evidence from drift report}
|
|
169
|
+
Drift report: {report relative path}
|
|
170
|
+
|
|
171
|
+
This path was not in the brief's `scope.include` when the skill was created. How should update-skill handle it?
|
|
172
|
+
|
|
173
|
+
[P] Promote — add to scope.include AND extract in this run
|
|
174
|
+
[S] Skip — leave out of scope AND record skip in amendments (no re-prompt)
|
|
175
|
+
[U] Update — halt this run and return to skf-brief-skill to refine scope
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
5. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate — record `action: "skipped"`, `category: "scope-expansion"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently expand scope.
|
|
179
|
+
|
|
180
|
+
6. **Apply decision:**
|
|
181
|
+
|
|
182
|
+
- **[P] Promote:**
|
|
183
|
+
1. Append `candidate.path` to `brief.scope.include` as a literal glob (preserve any wildcards from the drift report).
|
|
184
|
+
2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "out-of-scope new public API — drift report {report basename}"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
|
|
185
|
+
3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
|
|
186
|
+
4. Display: `"Promoted {path} — brief amended; §2 Category A will pick up matching files as ADDED."`
|
|
187
|
+
5. **No `promoted_docs_new[]` entry and no `change_detection_excludes` write** — promoted code globs flow through the standard §2 Category A → §3 extraction path, unlike §1b's promoted docs which bypass extraction.
|
|
188
|
+
|
|
189
|
+
- **[S] Skip:**
|
|
190
|
+
1. Do NOT modify `scope.include` or `scope.exclude`.
|
|
191
|
+
2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1c"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
|
|
192
|
+
3. **Write the amended brief back to disk** so neither §1c nor a future run will re-prompt.
|
|
193
|
+
4. Display: `"Skipped {path} — decision recorded in amendments."`
|
|
194
|
+
|
|
195
|
+
- **[U] Update:**
|
|
196
|
+
1. Halt the workflow immediately.
|
|
197
|
+
2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
|
|
198
|
+
3. Exit with status `halted-for-brief-refinement`. Change manifest is not yet built — no partial writes to provenance.
|
|
199
|
+
|
|
200
|
+
7. **Summary:** After all candidates are resolved (or none were found):
|
|
201
|
+
|
|
202
|
+
- `"Scope reconciliation: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments."`
|
|
203
|
+
- If N = 0 (section absent or empty): `"Scope reconciliation: no out-of-scope observations in drift report."`
|
|
204
|
+
- If §1c was skipped entirely (no drift report): omit this line; §2.2 will still run.
|
|
205
|
+
|
|
206
|
+
**Record for evidence report:** the update-skill evidence report appends `scope_reconciliation_pre: {drift_report: path, candidates: N, promoted: P, skipped: S, pre_decided: A, decisions: [{path, action, evidence}]}` (omit when §1c was skipped).
|
|
207
|
+
|
|
134
208
|
### 2. Compare Against Provenance Map
|
|
135
209
|
|
|
136
210
|
**If normal mode (provenance map available):**
|
|
@@ -183,6 +257,55 @@ Aggregate all subprocess results into a unified change manifest.
|
|
|
183
257
|
- All exports will be fully re-extracted in step 03
|
|
184
258
|
- Skip export-level comparison
|
|
185
259
|
|
|
260
|
+
#### 2.2 — Major-Version Scope Reconciliation (Post-Detection)
|
|
261
|
+
|
|
262
|
+
**Purpose:** §1c catches the major-version case when an audit drift report supplies explicit candidates. §2.2 is the safety net that fires when no audit was run (or audit emitted no out-of-scope section): it inspects the just-built Category A/B results for the deletion-ratio signature of a major-version restructure and gives the user an off-ramp before §3 commits the change manifest.
|
|
263
|
+
|
|
264
|
+
**Skip this section entirely if:**
|
|
265
|
+
|
|
266
|
+
- `update_mode == "gap-driven"` (test-report mode), OR
|
|
267
|
+
- `degraded_mode == true` (no provenance baseline to compare against — every export looks "modified", deletion ratio is meaningless), OR
|
|
268
|
+
- Provenance map's tracked export count is zero.
|
|
269
|
+
|
|
270
|
+
**Trigger:** Compute
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
deleted_export_count = (sum of exports across Category A DELETED files)
|
|
274
|
+
+ (DELETED_EXPORT count from Category B)
|
|
275
|
+
total_provenance_exports = provenance_map.entries.length
|
|
276
|
+
deletion_ratio = deleted_export_count / total_provenance_exports
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If `deletion_ratio >= 0.50`, present the prompt below. Otherwise skip §2.2 silently and continue to §3.
|
|
280
|
+
|
|
281
|
+
**Prompt:**
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
**Major-version scope shift detected**
|
|
285
|
+
|
|
286
|
+
Deleted exports: {deleted_export_count} of {total_provenance_exports} ({percent}%)
|
|
287
|
+
Deleted files: {deleted_file_count}
|
|
288
|
+
Added files (in scope): {added_in_scope_count}
|
|
289
|
+
Renamed/moved exports: {renamed_or_moved_count}
|
|
290
|
+
|
|
291
|
+
The upstream surface appears to have been substantially replaced. The brief's
|
|
292
|
+
`scope.include` patterns may no longer reflect the real public API.
|
|
293
|
+
|
|
294
|
+
[C] Continue — proceed with re-extraction; the deletion is intentional
|
|
295
|
+
[B] Brief — halt and re-run skf-brief-skill to refine scope first
|
|
296
|
+
[A] Audit — halt and run skf-audit-skill to map the new surface, then re-run update-skill
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Headless mode (`{headless_mode}` is true):** auto-select `[C] Continue`, log a WARN-level entry to the evidence report (`scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "headless-continue"}`), and surface the warning in step-07's report. A non-interactive run must not silently halt, but the user must be able to see the signal post-hoc.
|
|
300
|
+
|
|
301
|
+
**Apply decision:**
|
|
302
|
+
|
|
303
|
+
- **[C] Continue:** record `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "continue"}` and proceed to §3.
|
|
304
|
+
- **[B] Brief:** halt with status `halted-for-brief-refinement`. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` Change manifest discarded — no partial writes.
|
|
305
|
+
- **[A] Audit:** halt with status `halted-for-audit`. Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface — its drift report will feed §1c on the next update-skill run."` Change manifest discarded.
|
|
306
|
+
|
|
307
|
+
**Why both §1c and §2.2:** §1c is precise (per-path P/S/U) but requires upstream signal from audit-skill. §2.2 is coarse (single halt/continue) but self-contained — it fires even when the user runs update-skill directly without audit. Together they cover the major-version case across the two real workflows.
|
|
308
|
+
|
|
186
309
|
### 3. Build Change Manifest
|
|
187
310
|
|
|
188
311
|
Compile the change manifest with structured entries:
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* every version number, commit SHA, and library reference the docs cite.
|
|
6
6
|
*
|
|
7
7
|
* What it checks:
|
|
8
|
+
* - The anchor `skf_version` matches `package.json.version` (self-reference invariant
|
|
9
|
+
* declared in pinned.yaml's header; enforced here at CI/pre-commit time AND bumped
|
|
10
|
+
* by release.yaml on every release)
|
|
8
11
|
* - Every skill listed in `docs/_data/pinned.yaml` actually exists at the
|
|
9
12
|
* claimed version and commit in $OMS/skills/<name>/<version>/<name>/metadata.json
|
|
10
13
|
* - The metadata.json `version` field matches the anchor `version`
|
|
@@ -32,6 +35,7 @@ const yaml = require('js-yaml');
|
|
|
32
35
|
const SKF_ROOT = path.resolve(__dirname, '..');
|
|
33
36
|
const DOCS_DIR = path.join(SKF_ROOT, 'docs');
|
|
34
37
|
const ANCHORS_PATH = path.join(DOCS_DIR, '_data', 'pinned.yaml');
|
|
38
|
+
const PACKAGE_JSON_PATH = path.join(SKF_ROOT, 'package.json');
|
|
35
39
|
|
|
36
40
|
function loadAnchors() {
|
|
37
41
|
if (!fs.existsSync(ANCHORS_PATH)) {
|
|
@@ -46,6 +50,38 @@ function loadAnchors() {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function checkSkfVersionAgainstPackageJson(anchors) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (!fs.existsSync(PACKAGE_JSON_PATH)) {
|
|
56
|
+
errors.push(`CRITICAL: package.json not found at ${PACKAGE_JSON_PATH}`);
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
59
|
+
let pkg;
|
|
60
|
+
try {
|
|
61
|
+
pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
errors.push(`CRITICAL: could not parse ${PACKAGE_JSON_PATH}: ${error.message}`);
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
const anchorVersion = anchors.skf_version;
|
|
67
|
+
const packageVersion = pkg.version;
|
|
68
|
+
if (!anchorVersion) {
|
|
69
|
+
errors.push(`MISSING_ANCHOR: pinned.yaml has no skf_version key`);
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
if (!packageVersion) {
|
|
73
|
+
errors.push(`MISSING_PKG_VERSION: package.json has no version field`);
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
76
|
+
if (anchorVersion !== packageVersion) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`SKF_VERSION_DRIFT: pinned.yaml skf_version "${anchorVersion}" !== package.json version "${packageVersion}" ` +
|
|
79
|
+
`— bump pinned.yaml or check that release.yaml's "Update pinned.yaml skf_version" step fired`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
|
84
|
+
|
|
49
85
|
function resolveOmsPath(anchors) {
|
|
50
86
|
if (process.env.OMS) {
|
|
51
87
|
return path.resolve(process.env.OMS);
|
|
@@ -172,7 +208,11 @@ function main() {
|
|
|
172
208
|
const anchors = loadAnchors();
|
|
173
209
|
const omsPath = resolveOmsPath(anchors);
|
|
174
210
|
|
|
175
|
-
const errors = [
|
|
211
|
+
const errors = [
|
|
212
|
+
...checkSkfVersionAgainstPackageJson(anchors),
|
|
213
|
+
...checkCanonicalFiles(anchors, omsPath),
|
|
214
|
+
...checkDocsForStaleVersions(anchors),
|
|
215
|
+
];
|
|
176
216
|
|
|
177
217
|
if (errors.length > 0) {
|
|
178
218
|
console.error('DRIFT DETECTED:\n');
|
|
@@ -186,7 +226,7 @@ function main() {
|
|
|
186
226
|
}
|
|
187
227
|
|
|
188
228
|
const skillCount = Object.keys(anchors.skills).length;
|
|
189
|
-
console.log(`OK: ${skillCount} skills checked against ${omsPath}, no drift.`);
|
|
229
|
+
console.log(`OK: skf_version ${anchors.skf_version} matches package.json; ${skillCount} skills checked against ${omsPath}, no drift.`);
|
|
190
230
|
process.exit(0);
|
|
191
231
|
}
|
|
192
232
|
|