bmad-module-skill-forge 4.0.0 → 6.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 +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/agents.md +1 -1
- package/docs/architecture.md +5 -5
- package/docs/campaign.md +1 -1
- package/docs/concepts.md +4 -4
- package/docs/examples.md +32 -25
- package/docs/getting-started.md +3 -3
- package/docs/how-it-works.md +2 -2
- package/docs/index.md +1 -1
- package/docs/skill-model.md +41 -39
- package/docs/troubleshooting.md +9 -3
- package/docs/verifying-a-skill.md +42 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +6 -6
- package/package.json +1 -1
- package/src/knowledge/tool-resolution.md +6 -6
- package/src/shared/references/headless-gate-convention.md +4 -1
- package/src/shared/references/output-contract-schema.md +3 -3
- package/src/shared/references/pipeline-contracts.md +2 -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 +11 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +1180 -189
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +64 -20
- package/src/shared/scripts/skf-extract-public-api.py +472 -120
- package/src/shared/scripts/skf-extraction-inventory.py +10 -6
- package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
- package/src/shared/scripts/skf-parse-gaps.py +136 -26
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-resolve-customization.py +197 -0
- package/src/shared/scripts/skf-shard-body.py +239 -41
- package/src/shared/scripts/skf-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +574 -45
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +3 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/generate-briefs.md +1 -1
- package/src/skf-analyze-source/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/SKILL.md +4 -3
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +3 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +3 -2
- package/src/skf-campaign/references/campaign-contracts.md +2 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +8 -3
- package/src/skf-campaign/references/step-10-export.md +3 -2
- package/src/skf-create-skill/SKILL.md +4 -3
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/ccc-index-check.md +1 -1
- package/src/skf-create-skill/references/component-extraction.md +1 -1
- package/src/skf-create-skill/references/extract.md +3 -2
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +8 -3
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +3 -2
- package/src/skf-create-stack-skill/references/generate-output.md +1 -1
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
- package/src/skf-create-stack-skill/references/report.md +1 -1
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +3 -2
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +1 -1
- package/src/skf-export-skill/SKILL.md +3 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +3 -2
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +4 -3
- package/src/skf-quick-skill/references/finalize.md +1 -1
- package/src/skf-quick-skill/references/halt-contract.md +2 -2
- package/src/skf-quick-skill/references/write-and-validate.md +1 -1
- package/src/skf-refine-architecture/SKILL.md +8 -9
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +3 -2
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +3 -2
- package/src/skf-setup/references/ccc-index.md +1 -1
- package/src/skf-setup/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/SKILL.md +3 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +11 -8
- package/src/skf-test-skill/references/coverage-check-branches.md +81 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +60 -97
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/inventory-contract.md +27 -0
- package/src/skf-test-skill/references/invocation-contract.md +3 -3
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +5 -5
- package/src/skf-test-skill/references/scoring-rules.md +8 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
- package/src/skf-test-skill/references/source-access-protocol.md +14 -10
- package/src/skf-test-skill/references/step-hard-gate.md +1 -0
- package/src/skf-test-skill/scripts/build-result-context.py +20 -3
- package/src/skf-test-skill/scripts/gap-ledger.py +456 -176
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +516 -79
- package/src/skf-test-skill/scripts/locate-export-segments.py +685 -76
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +1205 -65
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +350 -35
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +3 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +54 -33
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +15 -10
- package/src/skf-update-skill/references/re-extract.md +3 -1
- package/src/skf-update-skill/references/report.md +6 -5
- package/src/skf-update-skill/references/write.md +42 -20
- package/src/skf-verify-stack/SKILL.md +4 -3
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
# tested the versions SKF has run on, as strings
|
|
49
49
|
# basis where minimum and tested come from
|
|
50
50
|
# version_command the command that prints the installed version
|
|
51
|
+
# (skill_check and tessl have none: the check never runs
|
|
52
|
+
# them, and reads skill-check's package.json and the name
|
|
53
|
+
# of the binary tessl installs)
|
|
51
54
|
# install_url where to install it from
|
|
52
55
|
# upgrade how to upgrade it
|
|
53
56
|
# acknowledged_as its row in the README Acknowledgements table (required
|
|
@@ -180,7 +183,9 @@ tools:
|
|
|
180
183
|
minimum: null
|
|
181
184
|
tested: ["0.111.0"]
|
|
182
185
|
basis: No minimum yet, since no older tessl has run Tessl Review. The tests stub tessl. Pull request 517 (unreleased v3 development, merged 2026-09-28 as 2b3b5113) records live create-skill and test-skill runs on tessl 0.111.0 in which Tessl Review returned a score. On 2026-10-04 the v3.0.0 pre-release tool check (recorded in the pull request that added these versions) ran `skf status` from main b5150b08 (the release candidate), which read tessl 0.111.0, and ran skf-tessl-review.py collect against it signed out, which starts no review.
|
|
183
|
-
|
|
186
|
+
# tessl is not run: on a tessl that has never run, any command accepts its
|
|
187
|
+
# Terms for the user. The check reads the version from the name of the
|
|
188
|
+
# binary tessl installs (tessl-<version>-<platform> in tessl/versions/).
|
|
184
189
|
install_url: https://tessl.io
|
|
185
190
|
upgrade: tessl cli update
|
|
186
191
|
acknowledged_as: tessl
|
|
@@ -195,7 +200,8 @@ tools:
|
|
|
195
200
|
minimum: null
|
|
196
201
|
tested: ["1.2.0"]
|
|
197
202
|
basis: SKF runs it through npx at whatever version npx resolves, so there is no floor it can check. In the v3.0.0 pre-release tool check (recorded in the pull request that added these versions) on 2026-10-04, on main b5150b08 (the release candidate), npx resolved skill-check 1.2.0, npm's latest since 2026-02-21, and test-skill's check (`npx skill-check check <skill-dir> --format json --no-security-scan`) on a copy of an oh-my-skills skill returned every field SKF reads. No other version has a recorded run.
|
|
198
|
-
|
|
203
|
+
# skill-check has no --version (1.2.0 exits 2 on it): the check reads the
|
|
204
|
+
# version from the package.json of the installed skill-check package.
|
|
199
205
|
install_url: https://github.com/thedaviddias/skill-check
|
|
200
206
|
upgrade: npm install -g skill-check@latest
|
|
201
207
|
acknowledged_as: skill-check
|
|
@@ -28,6 +28,7 @@ You are a source code analyst and decomposition architect collaborating with a d
|
|
|
28
28
|
These rules apply to every step in this workflow:
|
|
29
29
|
|
|
30
30
|
- Only load one step file at a time — never preload future steps
|
|
31
|
+
- Run every subagent in the foreground (parallel ones as several calls in one message, never with `run_in_background`) and wait for every long-running command to exit before the next instruction; in a headless run, never end a turn while either still runs, since under `claude -p` a run stops when its turn ends.
|
|
31
32
|
- Always communicate in `{communication_language}`; the text persisted into `skill-brief.yaml` (each recommendation's `description` and `scope.notes`) is written in `{document_output_language}`
|
|
32
33
|
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action, and record each one in the run sink as the gate says
|
|
33
34
|
|
|
@@ -80,10 +81,10 @@ These rules apply to every step in this workflow:
|
|
|
80
81
|
|
|
81
82
|
3. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
|
|
82
83
|
|
|
83
|
-
4. **Resolve workflow customization.**
|
|
84
|
+
4. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
84
85
|
|
|
85
86
|
```bash
|
|
86
|
-
uv run {
|
|
87
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-analyze-source.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for step 5 to record in the run sink.
|
|
@@ -31,7 +31,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
|
|
|
31
31
|
| `constituent_refs` | object | map of path→ref strings | Optional. Per-constituent git ref overrides for composite (multi-repo or multi-ref) sources. Keys must match entries in the analyze-source `project_paths` array. When absent, a single `target_ref` (or auto-detected ref) applies to all paths. Mutually exclusive with `target_ref`. |
|
|
32
32
|
| `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
|
|
33
33
|
| `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
|
|
34
|
-
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional narrower tier-A include list for a curated-subset skill
|
|
34
|
+
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional narrower tier-A include list for a curated-subset skill: stratified monorepo packages, single-crate `specific-modules` scopes, and multi-entry exports-map packages. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`. **With it, the denominator deflation guard measures the stored `stats.effective_denominator` against this list's set, in either direction, instead of against the coarse `scope.include` union**, so supply it only when it genuinely narrows the surface, never as an empty list, and never as a way to widen. See `skf-test-skill/references/source-access-protocol.md`. |
|
|
35
35
|
| `scope.registry_path` | string | Path from the source root (non-empty) | Optional, for `scope.type: "component-library"`. The component registry file. `skf-create-skill` step 3d reads it instead of detecting a registry, and writes it back, with a `demo-and-registry` amendment, when the user confirms a detected registry or gives its path. |
|
|
36
36
|
| `scope.ui_variants` | array | `{name, package?}` objects (`name` non-empty) | Optional, for `scope.type: "component-library"`. The design system variants, the first one primary. |
|
|
37
37
|
| `scope.demo_patterns` | array | Glob patterns (non-empty strings) | Optional, for `scope.type: "component-library"`. The demo and example files `skf-create-skill` step 3d leaves out. Step 3d reads them instead of detecting demo files, and writes them back, with a `demo-and-registry` amendment per glob, when the user confirms or adjusts the detected ones. |
|
|
@@ -287,7 +287,7 @@ 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 stable path
|
|
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 result file that could not be written leaves the line's `result_path` null and a `result_file_write_failed` warning in it, 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
|
|
|
@@ -4,7 +4,7 @@ Canonical headless/pipeline contract for skf-analyze-source. The shared emitter,
|
|
|
4
4
|
|
|
5
5
|
## Result Envelope
|
|
6
6
|
|
|
7
|
-
The step that ends a run (step 6 §9, step 1a §9, or the docs-only branch §5) prints the envelope on **stdout** before chaining to step 7, and displays it when `{headless_mode}` is true. Every HARD HALT of a headless run prints it on **stderr** with `status: "error"`, so an automator branches on the failure class without grepping message text.
|
|
7
|
+
The step that ends a run (step 6 §9, step 1a §9, or the docs-only branch §5) prints the envelope on **stdout** before chaining to step 7, and displays it when `{headless_mode}` is true. Every HARD HALT of a headless run prints it on **stderr** with `status: "error"`, so an automator branches on the failure class without grepping message text. A caller reads the run's own `SKF_ANALYZE_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and `analyze-source-result-latest.json` may then describe an earlier run, so it is never read in its place.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
SKF_ANALYZE_RESULT_JSON: {"status":"success|error|redirect|skipped","report_path":"…|null","brief_paths":["…"],"unit_counts":{"confirmed":N,"skipped":N,"maybe":N},"exit_code":0,"halt_reason":null,"mode":"interactive|auto","result_path":"…|null"}
|
|
@@ -28,6 +28,7 @@ You are a skill auditor in Ferris Audit mode: a deterministic drift-detection wo
|
|
|
28
28
|
|
|
29
29
|
- Never fabricate findings — all data must trace to source code with file:line citations
|
|
30
30
|
- Only load one step file at a time — never preload future steps
|
|
31
|
+
- Run every subagent in the foreground (parallel ones as several calls in one message, never with `run_in_background`) and wait for every long-running command to exit before the next instruction; in a headless run, never end a turn while either still runs, since under `claude -p` a run stops when its turn ends.
|
|
31
32
|
- Update `stepsCompleted` in output file frontmatter before loading next step
|
|
32
33
|
- Always communicate in `{communication_language}`
|
|
33
34
|
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision; step 1's gates (manifest-vs-link, upstream-drift, baseline confirm) also record theirs in the run sink as they decide
|
|
@@ -59,7 +60,7 @@ Stage 1b returns to init.md at §5b's **Record for report**, then §6. Step 1 §
|
|
|
59
60
|
| **Inputs** | `skill_name` [required], `skill_path` [optional: the skill folder, bypassing manifest/symlink resolution], `tier_override` [optional: Quick / Forge / Forge+ / Deep, in place of the detected tier], `upstream_drift_choice` [optional: C / S / X; answers the upstream-drift gate in references/upstream-checkout.md; C, auditing the upstream ref from a private tree, when unset] |
|
|
60
61
|
| **Gates** | step 1: Manifest-vs-Symlink Gate [N/M/X] · Upstream-Drift Gate [C/S/X] (references/upstream-checkout.md, only when upstream moved) · Baseline Confirm Gate [C/X] (only on a doubt: the forge tier is below the compile tier, or the provenance map is more than 90 days old) |
|
|
61
62
|
| **Outputs** | The drift report, the JSON it was built from and the result contract, in `{forge_version}/` (the audited version's folder): `references/headless-contract.md` names each file |
|
|
62
|
-
| **Headless** | Every gate takes its default action, a pre-supplied `upstream_drift_choice` answers the upstream-drift gate, `tier_override` sets the tier (step 1 §2), and each gate's decision lands in the envelope's `headless_decisions` |
|
|
63
|
+
| **Headless** | Every gate takes its default action, a pre-supplied `upstream_drift_choice` answers the upstream-drift gate, `tier_override` sets the tier (step 1 §2), and each gate's decision lands in the envelope's `headless_decisions`. A caller reads the run's own `SKF_AUDIT_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and `audit-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
|
|
63
64
|
| **Exit codes** | `references/headless-contract.md`: each halt class's exit code and `halt_reason`, and the `SKF_AUDIT_RESULT_JSON` envelope |
|
|
64
65
|
|
|
65
66
|
## On Activation
|
|
@@ -72,10 +73,10 @@ Stage 1b returns to init.md at §5b's **Record for report**, then §6. Step 1 §
|
|
|
72
73
|
|
|
73
74
|
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
|
|
74
75
|
|
|
75
|
-
3. **Resolve workflow customization.**
|
|
76
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
76
77
|
|
|
77
78
|
```bash
|
|
78
|
-
uv run {
|
|
79
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-audit-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Then keep the reason as `{customization_resolver_unavailable}`, which step 6 hands to the emitter as a warning.
|
|
@@ -56,7 +56,7 @@ Based on forge tier detected in Step 01:
|
|
|
56
56
|
**Forge tier (ast-grep available):**
|
|
57
57
|
- The recipe runner extracts, as the **AST Extraction Protocol** in `{extractionPatternsData}` says: one `skf-extract-public-api.py --mode full` call over the bounded scan list from §2 (§3 gives the command) runs every recipe for the language in one process, reads only the files on the list, and keeps every match (`--head-cap 0`: a match past a cap would read as a removed export in step 3). While it can run, never run the recipes one at a time, batch them, or merge and dedupe their matches by hand
|
|
58
58
|
- Each export it returns is T1 as it stands: its name (`$NAME`), type, file, line (`$NAME`'s line, as create-skill records it, not the match's first line, which a decorator or an `export` line can hold), `ast_node_type` and `signature`, the declaration on one line even when a formatter split its parameters over several
|
|
59
|
-
- Read by eye only what the runner leaves (§3): the forms the recipes leave out (Known Limitation #11 in `{extractionPatternsData}`) when a file uses them, the names an entry point exports that no recipe found, the provenance map's entries the runner leaves out (
|
|
59
|
+
- Read by eye only what the runner leaves (§3): the forms the recipes leave out (Known Limitation #11 in `{extractionPatternsData}`) when a file uses them, the names an entry point exports that no recipe found, the provenance map's entries the runner leaves out (a Python or Rust underscore name such as `__version__`, a module) that the source still declares, and the files it could not parse or read. Label each export by the tool that produced it: T1 with `extraction_method: ast-grep` for a runner export, T1-low with `extraction_method: source-read` and `ast_node_type: null` for an export read by eye
|
|
60
60
|
|
|
61
61
|
**Tier degradation handling (Forge/Forge+/Deep):** If the runner cannot run and ast-grep is unavailable, follow `{tierDegradationRulesData}` (AST Tool Unavailable, Per-File AST Failure) for the fallback and the user notification. A file ast-grep cannot parse falls back to reading that file only: its exports are T1-low with `extraction_method: source-read` and `ast_node_type: null`, and exports the runner matched in other files stay T1. Silent degradation is forbidden: the snapshot names each file's status, and the run names each file read after an ast-grep failure, and why (§3, §4); when ast-grep is unavailable for the whole run, every export is T1-low.
|
|
62
62
|
|
|
@@ -137,7 +137,7 @@ Pass `--extraction` when the runner wrote its JSON, `--scope-type public-api` wh
|
|
|
137
137
|
**3. What the runner leaves.** Read by eye (T1-low, `extraction_method: source-read`, `ast_node_type: null`) only:
|
|
138
138
|
|
|
139
139
|
- each file in `to_read`: a `parse-failed` one (its `issue`: a syntax error, where a recipe can miss an export; not UTF-8; unreadable), with the warning "**AST fallback:** ast-grep could not parse {file} ({issue}); its exports were read from source and labeled T1-low.", and an `unread` one (an incomplete run, or no runner: then every file, by text pattern matching at Quick tier);
|
|
140
|
-
- each name in `extraction_gaps`, in its `file` at its `line`: a gap with an `entry` is a name that entry point exports and no recipe found; a gap whose `entry` is null is a provenance map entry the runner leaves out (
|
|
140
|
+
- each name in `extraction_gaps`, in its `file` at its `line`: a gap with an `entry` is a name that entry point exports and no recipe found; a gap whose `entry` is null is a provenance map entry the runner leaves out (a Python or Rust underscore name such as `__version__`, a module) that its file still declares;
|
|
141
141
|
- the forms the recipes leave out (Known Limitation #11 in `{extractionPatternsData}`) in a file that uses them.
|
|
142
142
|
|
|
143
143
|
For each file to read, launch a subprocess (Pattern 2) that loads it and returns ONLY `{"files": [...], "exports": [...]}`, with no prose and no fences: `files` lists the file when the worker read the whole of it (also when it holds no export), and each export gives `name`, `type`, `signature` (the declaration on one line), `file` (relative to `{source_root}`, with forward slashes), `line` (`$NAME`'s line), `confidence: "T1-low"`, `extraction_method: "source-read"` and `ast_node_type: null`. For a gap whose `entry` is null, record its `name`, `file` and `line`; as `type`, its `export_type` when the declaration at that line still has that kind, else the kind it has now (a `variable` now declared by a `def` is a `function`, which step 3 reports as a changed kind), and when the gap gives no `export_type`, the kind the declaration has; as `signature`, the declaration on one line, the import line for an alias that renames (`from cognee.api.v1.visualize import visualize_graph as visualize`), and for a module or package (a gap at line 1 of a file the name is the module or package of) `module` and the file's path from `{source_root}` without its extension or a final `__init__` or `index`, dotted for Python (`module cognee.modules.pipelines`). Save each worker's JSON as it returned it, as `{auditDataFolder}/export-details-{n}.json` (`{n}` counting from 1), never merged by hand; without subprocesses, read the files in the main thread, one by one, and save one such file. A stack's export needs no library: the snapshot helper gives each one its file's `source_library` from the provenance map (the map `normalize` returns as `source_library_by_file`).
|
|
@@ -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 (
|
|
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 result file that could not be written leaves `result_path` null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, 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
|
|
|
@@ -32,6 +32,7 @@ You are a skill scoping architect collaborating with a developer who wants to cr
|
|
|
32
32
|
These rules apply to every step in this workflow:
|
|
33
33
|
|
|
34
34
|
- Only load one step file at a time — never preload future steps
|
|
35
|
+
- Run every subagent in the foreground (parallel ones as several calls in one message, never with `run_in_background`) and wait for every long-running command to exit before the next instruction; in a headless run, never end a turn while either still runs, since under `claude -p` a run stops when its turn ends.
|
|
35
36
|
- **Lazy-load references and assets:** `references/*.md` and `assets/*.md` files are loaded inside the section that needs them, not at step entry. If a section is skipped (e.g. `version-resolution.md` when step 2's extractor ran, `scope-templates.md` for the `docs-only` branch that bypasses §2c), do not load that file. Each unnecessary load costs context (~5-10 KB per reference) and biases the LLM toward consulting material the current path does not need.
|
|
36
37
|
- Always communicate in `{communication_language}` (the language for user-facing prose). Written artifact text — the `description`, `notes`, and other free-form fields persisted into `skill-brief.yaml` — is in `{document_output_language}`; per-step rules call this out where it applies (see step 5). The two values may be the same.
|
|
37
38
|
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
|
|
@@ -58,10 +59,10 @@ Give the halt's own `halt_reason`: the helper derives `exit_code` from it. `skil
|
|
|
58
59
|
|
|
59
60
|
**Resolve `{auto_mode}`**: true when the invocation carries the `[auto]` flag (a pipeline's `BS[auto]`, which step 1 §1b routes to the auto stages), else false.
|
|
60
61
|
|
|
61
|
-
3. **Resolve workflow customization.**
|
|
62
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
62
63
|
|
|
63
64
|
```bash
|
|
64
|
-
uv run {
|
|
65
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-brief-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case add `customization_resolver_unavailable: <reason>` to `workflow_warnings[]` as the run's first warning.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| `target_ref` | string | Git ref (tag or branch) | Optional. Explicit git ref used verbatim as the resolved `source_ref`, bypassing version-to-tag matching. Escape hatch for monorepo crate tags whose prefix differs from the skill name (e.g. tag `livekit/v0.7.42` for skill `livekit-rust`). Remote sources only. |
|
|
27
27
|
| `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
|
|
28
28
|
| `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
|
|
29
|
-
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional. Narrower tier-A include list for a curated-subset skill
|
|
29
|
+
| `scope.tier_a_include` | array | Glob patterns (min 1, non-empty) | Optional. Narrower tier-A include list for a curated-subset skill: stratified monorepo packages, single-crate `specific-modules` scopes, and multi-entry exports-map packages. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`, so the denominator reflects the authoring surface rather than incidentally-matched internal infrastructure. **With it, the denominator deflation guard measures the stored `stats.effective_denominator` against this list's set, in either direction, instead of against the coarse `scope.include` union**, so supply it only when it genuinely narrows the surface, never as an empty list, and never as a way to widen (listing files outside `scope.include` inverts its meaning). See `skf-test-skill/references/source-access-protocol.md` stratified-scope resolution. |
|
|
30
30
|
| `scope.registry_path` | string | Path from the source root (non-empty) | Optional, for `scope.type: "component-library"`. The component registry file. `skf-create-skill` step 3d reads it instead of detecting a registry, and writes it back, with a `demo-and-registry` amendment, when the user confirms a detected registry or gives its path. |
|
|
31
31
|
| `scope.ui_variants` | array | `{name, package?}` objects (`name` non-empty) | Optional, for `scope.type: "component-library"`. The design system variants, the first one primary. |
|
|
32
32
|
| `scope.demo_patterns` | array | Glob patterns (non-empty strings) | Optional, for `scope.type: "component-library"`. The demo and example files `skf-create-skill` step 3d leaves out. Step 3d reads them instead of detecting demo files, and writes them back, with a `demo-and-registry` amendment per glob, when the user confirms or adjusts the detected ones. |
|
|
@@ -156,7 +156,7 @@ scope:
|
|
|
156
156
|
|
|
157
157
|
**Demotion (scope-expansion only):** `demoted-include` removes a previously-promoted path from `scope.include` — used when a prior `[P]` decision is reversed. `demoted-exclude` removes a path from `scope.exclude` — used when a previously excluded path needs to be re-evaluated. Both write the structural change and append the amendment so future runs see the rationale. Demotion is not valid for `category: "auth-doc"`: auth-doc skips already prevent re-prompting without scope mutation.
|
|
158
158
|
|
|
159
|
-
**Exclusion (scope-expansion only):** `excluded` adds a path to `scope.exclude`, written by `skf-update-skill` gap-driven rescope (gap-driven.md §1 rule R1) when a coverage gap's remediation is removal (the export is internal, `#[doc(hidden)]`, or out of scope). The amendment is the audit trail; the `scope.exclude` write is what shrinks the source barrel, so the legitimate scope reduction is expressed in the brief rather than by editing `metadata.stats`. This keeps the reduction visible to `skf-test-skill`'s denominator-deflation check, which re-derives the barrel from `scope.include` filtered by `scope.exclude
|
|
159
|
+
**Exclusion (scope-expansion only):** `excluded` adds a path to `scope.exclude`, written by `skf-update-skill` gap-driven rescope (gap-driven.md §1 rule R1) when a coverage gap's remediation is removal (the export is internal, `#[doc(hidden)]`, or out of scope). The amendment is the audit trail; the `scope.exclude` write is what shrinks the source barrel, so the legitimate scope reduction is expressed in the brief rather than by editing `metadata.stats`. This keeps the reduction visible to `skf-test-skill`'s denominator-deflation check, which re-derives the barrel from `scope.include` filtered by `scope.exclude` (with a `scope.tier_a_include` list, the tier A set within it). Not valid for `category: "auth-doc"`.
|
|
160
160
|
|
|
161
161
|
**Backward compatibility:** `scope.amendments` is optional. Briefs without this field validate unchanged. Treat missing as an empty list. Existing entries without `category` are equivalent to `category: "auth-doc"` — readers must default the field when absent.
|
|
162
162
|
|
|
@@ -9,8 +9,8 @@ Full argument set, gate map, exit codes, and headless result envelope for `skf-b
|
|
|
9
9
|
| **Inputs** | The headless arguments, each with its default and the halt it can raise, are the Arguments table of `references/headless-args.md`, which `skf-validate-brief-inputs.py` enforces: `target_repo` and `skill_name` are required, every other one is optional. Two of them route the run elsewhere: `from_brief` [optional: path to a pre-authored `skill-brief.yaml` to *ratify*: when supplied it is the source of truth, `target_repo`/`skill_name` become optional/derived-from-brief, and the run mirrors the interactive §3.1a ratify path: schema-validate, skip analyze-target/scope-definition, write through the canonical writer to `{forge_data_folder}/{name}/skill-brief.yaml`: no `force` needed when the brief already lives at that path; otherwise `force` applies as on the derive route], `[auto]` [optional: bracket modifier passed via pipeline context: when present, BS loads the upstream brief from `brief_path` in pipeline data, enriches it with doc detection, and writes through the canonical writer; requires `brief_path` from AN's `SKF_ANALYZE_RESULT_JSON`] |
|
|
10
10
|
| **Gates** | step 1: Input Gate [use args] (`references/headless-args.md`) | step 1 ratify: Choice Gate [R] | step 2: Choice Gate [P] (a truncated GitHub tree), and the workspace of a monorepo [the repo root, or the workspace an `include` glob names] | step 3: Choice Gate [the `scope_type` argument, else the recommendation] | step 3: Input Gate [the `include` and `exclude` arguments, else the scope type's boundary default; a `reference-app` scope, or a `specific-modules` `scope_type` argument naming no module, halts] | step 3: Confirm Gate [C] | step 4: Confirm Gate [C] | step 5: Overwrite Gate [Y/N] (the brief exists; headless halts `overwrite-cancelled` unless `force` was supplied) |
|
|
11
11
|
| **Outputs** | `skill-brief.yaml` at `{forge_data_folder}/{skill-name}/skill-brief.yaml`; one `SKF_BRIEF_RESULT_JSON` line on stdout at the end of a successful run when `{headless_mode}` is true or the run is in `[auto]` mode (see Result Contract below); for the run itself, a scratch folder under `{project-root}/_bmad-output/.skf-run/` holding what it hands its helpers (the repository's file list, fetched files, JSON payloads), which the run deletes once the brief is written or the user cancels (any other halt leaves it in place, less a clone of the repository) |
|
|
12
|
-
| **Headless** | All gates auto-resolve with heuristic-driven or default action when `{headless_mode}` is true; step 1's input gate (`references/headless-args.md`) validates the pre-supplied inputs before any stage uses them, and the gates that would otherwise prompt consume them; absent `source_authority` and `scope_type` are resolved by signal-driven detection (see `references/headless-args.md`); with no `include`, step 3 takes the scope type's boundary default and names it in the envelope's `warnings` (a `specific-modules` recommendation that only the module count made becomes `full-library`), while a `reference-app` scope, or a `specific-modules` `scope_type` argument whose `intent` and `scope_hint` name no module, halts with `input-missing`; existing briefs are preserved unless `--force` was supplied (HALT with `overwrite-cancelled` otherwise); supplying `from_brief <path>` instead routes step 1 to the ratify path described in the `from_brief` Inputs cell (schema-validate, skip analyze/scope, write to the forge path: no `force` needed when the brief already lives at that path; otherwise `force` applies as on the derive route) rather than deriving a new brief |
|
|
13
|
-
| **Transient-failure retry** | This workflow does **not** auto-retry network or subprocess failures. A failed call the run can go on without (a doc URL that does not answer, a failed doc detection or QMD step, an extraction script that falls back to the prompt path) is logged, added to the run's warnings and reported in the result envelope's `warnings`, and the workflow continues with whatever signal it has; a failure it cannot go on without, such as step 2 unable to read the target, is a HARD HALT. Headless pipelines that want retry semantics should wrap the invocation at their orchestrator level (e.g. CI re-runner on
|
|
12
|
+
| **Headless** | All gates auto-resolve with heuristic-driven or default action when `{headless_mode}` is true; step 1's input gate (`references/headless-args.md`) validates the pre-supplied inputs before any stage uses them, and the gates that would otherwise prompt consume them; absent `source_authority` and `scope_type` are resolved by signal-driven detection (see `references/headless-args.md`); with no `include`, step 3 takes the scope type's boundary default and names it in the envelope's `warnings` (a `specific-modules` recommendation that only the module count made becomes `full-library`), while a `reference-app` scope, or a `specific-modules` `scope_type` argument whose `intent` and `scope_hint` name no module, halts with `input-missing`; existing briefs are preserved unless `--force` was supplied (HALT with `overwrite-cancelled` otherwise); supplying `from_brief <path>` instead routes step 1 to the ratify path described in the `from_brief` Inputs cell (schema-validate, skip analyze/scope, write to the forge path: no `force` needed when the brief already lives at that path; otherwise `force` applies as on the derive route) rather than deriving a new brief. A caller reads the run's own `SKF_BRIEF_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result. |
|
|
13
|
+
| **Transient-failure retry** | This workflow does **not** auto-retry network or subprocess failures. A failed call the run can go on without (a doc URL that does not answer, a failed doc detection or QMD step, an extraction script that falls back to the prompt path) is logged, added to the run's warnings and reported in the result envelope's `warnings`, and the workflow continues with whatever signal it has; a failure it cannot go on without, such as step 2 unable to read the target, is a HARD HALT. Headless pipelines that want retry semantics should wrap the invocation at their orchestrator level (e.g. a CI re-runner on an error envelope, or on a run that printed none). Rationale: brief-skill is read-mostly with one terminal write (the YAML at step 5); a partial-signal retry has more failure modes than just re-running the whole workflow, which is cheap. |
|
|
14
14
|
| **Exit codes** | See "Exit Codes" below |
|
|
15
15
|
|
|
16
16
|
## Exit Codes
|
|
@@ -173,10 +173,10 @@ Using the boundary definitions from `{scopeTemplatesPath}`, present the appropri
|
|
|
173
173
|
|
|
174
174
|
- **A module include** is `<path>/**`, where `<path>` is the module's `path` in `{run_dir}/snapshot.json` (its `workspaces` or `module_candidates` entry, or the entry of `{run_dir}/package-snapshot.json` when §4.3 picked from it): repo-relative, with any workspace prefix already in it. For Maven and Gradle it is the §4.1 `modules` entry, prefixed with `{monorepo_workspace}/` when step 2 picked a workspace. A `named_module_subset` name maps to the §4.3 module it names. With no module picked, the include is `{monorepo_workspace}/**` when step 2 picked a workspace (§3b), else `**`.
|
|
175
175
|
- **The default exclusions** are the Full Library template's test globs (`**/*.test.*`, `**/*.spec.*`, `**/test/**`, `**/tests/**`) and build globs (`**/dist/**`, `**/build/**`, `**/target/**`). The template's configuration and documentation exclusions name no glob, and the module includes already leave the root configuration files and the docs folders out.
|
|
176
|
-
- **The public API files** are the files that define the names the package's entry points export. Run §3c's recipe runner (its helper, source folder, clone and clone removal) with the `full-library` default as its globs and `-o "{run_dir}/public-api.json"`, then print the file of each public name, an entry point's own definitions, the files its re-exports lead to
|
|
176
|
+
- **The public API files** are the files that define the names the package's entry points export. Run §3c's recipe runner (its helper, source folder, clone and clone removal) with the `full-library` default as its globs and `-o "{run_dir}/public-api.json"`, then print the file of each public name, an entry point's own definitions, the files its re-exports lead to, the files that define a namespace's `members` (the names a submodule the package exports passes on) and each other in-scope file that defines a public name (`entry_point_diff.other_definitions`, such as a name two packages of a monorepo each define):
|
|
177
177
|
|
|
178
178
|
```bash
|
|
179
|
-
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); print("\n".join(sorted({f for p in d["entry_point_diff"]["public"] for f in [p.get("file")] + [m.get("file") for m in p.get("members") or []] if f})))' "{run_dir}/public-api.json"
|
|
179
|
+
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); print("\n".join(sorted({f for p in d["entry_point_diff"]["public"] + (d["entry_point_diff"].get("other_definitions") or []) for f in [p.get("file")] + [m.get("file") for m in p.get("members") or []] if f})))' "{run_dir}/public-api.json"
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
At Quick tier, when the runner exits 1 to 3 or the clone fails, or when it prints no file, take the `full-library` default instead and give the reason in the warn line: `warn: headless boundary default public-api: full-library boundaries ({the reason})`.
|
|
@@ -215,10 +215,10 @@ Head this off by capturing the authoring surface as **`scope.tier_a_include`**:
|
|
|
215
215
|
uv run {extractPublicApiHelper} --mode full --source-root "<source folder>" --include "<include glob>" --exclude "<exclude glob>" --language "{language}" -o "{run_dir}/tier-a.json"
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
-
The candidates are the files that define a name an entry point passes on (`reexport_targets`,
|
|
218
|
+
The candidates are the files that define a name an entry point passes on (`reexport_targets`, the `members` of a namespace in `entry_point_diff.public`, and `entry_point_diff.other_definitions`, each other in-scope file that defines a public name), less the entry points themselves (`entry_points.files[].file`) and the files of the names the runner lists outside the scope (`entry_point_diff.outside_scope`), since a tier A name counts only in a file `scope.include` matches. A file outside `scope.include` that a named re-export (`export { x } from`) or a barrel reaches has no `outside_scope` row and stays a candidate, which Create Skill's runner then warns about. Print them, one per line:
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); entries = {f["file"] for f in d["entry_points"]["files"]}; diff = d.get("entry_point_diff") or {}; members = {m.get("file") for p in diff.get("public") or [] for m in p.get("members") or []}; outside = {o.get("file") for o in diff.get("outside_scope") or []}; print("\n".join(sorted(({t["file"] for t in d["reexport_targets"]} | members) - {None} - entries - outside)))' "{run_dir}/tier-a.json"
|
|
221
|
+
uv run python -c 'import json, sys; d = json.load(open(sys.argv[1], encoding="utf-8")); entries = {f["file"] for f in d["entry_points"]["files"]}; diff = d.get("entry_point_diff") or {}; members = {m.get("file") for p in diff.get("public") or [] for m in p.get("members") or []}; others = {o.get("file") for o in diff.get("other_definitions") or []}; outside = {o.get("file") for o in diff.get("outside_scope") or []}; print("\n".join(sorted(({t["file"] for t in d["reexport_targets"]} | members | others) - {None} - entries - outside)))' "{run_dir}/tier-a.json"
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
Once the runner has run, whatever its exit, remove a clone this section made (`rm -rf "{run_dir}/clone"`).
|
|
@@ -31,10 +31,10 @@ Run these steps once, in order, before dispatching to Mode Routing.
|
|
|
31
31
|
|
|
32
32
|
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
|
|
33
33
|
|
|
34
|
-
3. **Resolve workflow customization.**
|
|
34
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
uv run {
|
|
37
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-campaign.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case, unless the invocation is `campaign status` (which writes nothing), log `customization_resolver_unavailable: <reason>` as an `event` in the decision log once `{campaignWorkspacePath}` is bound below (`references/campaign-contracts.md`, Decision Log).
|
|
@@ -73,6 +73,7 @@ These rules apply to every step in this workflow:
|
|
|
73
73
|
- 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
|
|
74
74
|
- Zero memory dependency: campaign state is 100% recoverable from disk; never rely on conversation context for progress tracking
|
|
75
75
|
- A sub-skill's result is what the step that runs it reads (an `SKF_*_RESULT_JSON` envelope or a verdict): a missing, unparseable or error result is a sub-skill failure, and never write partial state from an unparsed envelope
|
|
76
|
+
- Run every subagent in the foreground (parallel ones as several calls in one message, never with `run_in_background`) and wait for every long-running command to exit before the next instruction; in a headless run, never end a turn while either still runs, since under `claude -p` a run stops when its turn ends.
|
|
76
77
|
- Log every operator decision, headless default and event (skip/force, overwrite, export cancel/proceed, `.bak` recovery, user-cancel, a failed skill) as one typed entry in the append-only `{campaignWorkspacePath}/_campaign-decision-log.md` through `campaign-state.py log` (`references/campaign-contracts.md`), so rationale survives compaction and resume
|
|
77
78
|
- **Universal cancel affordance:** at any interactive gate between Setup and the Export gate, `cancel`/`exit`/`:q` triggers a HARD HALT with **exit code 12 (`user-cancelled`)**: log it and leave state intact and resumable. Exception: the Export gate's own `[C]ancel` stays exit code 11 (`export-cancelled`); never also emit 12 there, so an automator's exit-code branch stays deterministic.
|
|
78
79
|
- Always communicate in `{communication_language}`
|
|
@@ -36,6 +36,8 @@ SKF_CAMPAIGN_HALT
|
|
|
36
36
|
|
|
37
37
|
`<class>` is the Exit Codes meaning (e.g. `circular-deps`, `inaccessible-repo`), from which the emitter derives `exit_code`; `<step slug>` is the kebab portion of the step file where the HALT occurred. `halt-payload` adds the `completed` and `failed` counts of the state (0 before Setup wrote it) and the decision log (null before the campaign workspace exists). The emitter prints one line on stderr, with `status` `error`, `exit_code`, `halt_reason`, `phase` and an `error` object holding `code` and `message`. If either command exits non-zero, display the halt message alone.
|
|
38
38
|
|
|
39
|
+
A caller reads the run's own `SKF_CAMPAIGN_RESULT_JSON` line, at the end or at a HARD HALT. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result. Campaign writes no result file of its own, so nothing stands in for the line; its state stays on disk for `@Ferris campaign resume`.
|
|
40
|
+
|
|
39
41
|
## State Contract
|
|
40
42
|
|
|
41
43
|
Every write to `_campaign-state.yaml` goes through `scripts/campaign-state.py`. Each write reads the state and validates it (an invalid state is never rotated into `.bak`, and nothing is written), applies the operation, stamps `campaign.last_updated` (and any `started_at` or `completed_at` the operation sets) from the clock in UTC, validates the result (a change that would make the state invalid is refused), copies the valid primary it read to `_campaign-state.yaml.bak`, and writes the new state to a temporary file renamed over the old one. A crash therefore leaves the old file or the new one, never half of one, and `.bak` always holds the last valid state. `--stage N` on a write also sets `campaign.current_stage`: each stage writes its number only in its final write, after every gate, because resume starts at `current_stage + 1`.
|
|
@@ -12,6 +12,10 @@ kickoffScript: 'scripts/campaign-render-kickoff.py'
|
|
|
12
12
|
gateScript: 'scripts/campaign-quality-gate.py'
|
|
13
13
|
stateScript: 'scripts/campaign-state.py'
|
|
14
14
|
validateScript: 'scripts/campaign-validate-state.py'
|
|
15
|
+
# The customization helper the kickoff pipe runs (§5).
|
|
16
|
+
resolveCustomizationProbeOrder:
|
|
17
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py'
|
|
18
|
+
- '{project-root}/src/shared/scripts/skf-resolve-customization.py'
|
|
15
19
|
---
|
|
16
20
|
|
|
17
21
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -86,16 +90,17 @@ For each ready Tier A skill:
|
|
|
86
90
|
(During development, with no `{project-root}/_bmad/skf/shared/scripts/skf-preapply.py` installed, the helper lives at `{project-root}/src/shared/scripts/skf-preapply.py`.) It writes its `applied[]` workarounds to `{preapplyLogFile}`, which the kickoff script reads. Pre-apply is best-effort: if the helper is missing or exits non-zero, log a warning and proceed (it is not a gate), and leave `--workarounds-file` out of the kickoff call below, so an earlier skill's log is never listed for this one.
|
|
87
91
|
- **Kickoff emit:** render the whole kickoff message with the kickoff script, which fills every placeholder of the template. The customization resolver pipes the persistent facts into it:
|
|
88
92
|
```
|
|
89
|
-
uv run {
|
|
93
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow.persistent_facts | uv run {kickoffScript} --state-file {stateFile} --brief-file {briefFile} --skill {skill_name} --template {kickoffTemplate} [--workarounds-file {preapplyLogFile}] --facts-json - --project-root {project-root} [--directive-file <campaign.directive_path>]
|
|
90
94
|
```
|
|
95
|
+
- `{resolveCustomizationHelper}` is resolved from `{resolveCustomizationProbeOrder}` when it is not bound.
|
|
91
96
|
- `--facts-json -` takes `workflow.persistent_facts` from the resolver unchanged. When the resolver is missing or fails, write the `persistent_facts` list of `{skill-root}/customize.toml`, as written, to `{factsFile}` as a JSON list with your file-writing tool, and replace the pipe with `< {factsFile}`: no fact or workaround ever passes through shell quoting.
|
|
92
97
|
- `--workarounds-file` is passed when pre-apply exited 0 for this skill; the script lists each workaround it applied (the file, the fingerprint and its fix, the severity).
|
|
93
98
|
- `--directive-file` is passed when state sets `campaign.directive_path`.
|
|
94
99
|
|
|
95
100
|
Present the script's output unchanged as the context for the skill's pipeline run, and fill nothing in by hand. An exit 2 with no JSON on stderr is a usage error from a mangled call, not invalid input: fix the call and run it again. An exit 2 with JSON names its `code`: on `BAD_WORKAROUNDS` or `BAD_FACTS` this call's input is wrong, so correct it and run the call again; on `BRIEF_NOT_FOUND` or `BRIEF_UNREADABLE`, HALT (exit code 8, `missing-brief`); on any other code, HALT (exit code 2, `invalid-input`) with its `error`. Correct a call once only: when the corrected call fails too, HALT (exit code 2, `invalid-input`).
|
|
96
101
|
- **BS → CS → TS**: the forge pipeline for a named skill, the chain the forger's `forge` alias runs. Run each sub-skill with `--headless`, so no nested gate waits and each prints its result envelope. A sub-skill that fails ends the pipeline, and the skill fails (§5.3) with its `halt_reason`. Campaign runs no analyze-source pass: brief-skill scopes the named skill itself, while analyze-source names its briefs from the repository, so brief-skill would overwrite or never read them.
|
|
97
|
-
- **BS** (`skf-brief-skill`): `--headless --force` with each non-null field of §4's `brief_skill` as the input of that name (`target_repo`, `skill_name`, `target_version`, `language_hint`, `scope_hint`); copy them, never read them out of the kickoff. `target_version` is the skill's pin: Setup accepts only an X.Y.Z version as a Tier A pin, and brief-skill refuses any other shape with `input-invalid`, so a pin never silently drops out of the build. `--force` replaces a brief an earlier attempt wrote under this name. Record the `brief_path` of its SKF_BRIEF_RESULT_JSON at once, so resume and the report have it: `uv run {stateScript} set-skill --state-file {stateFile} --skill {skill_name} --brief-path <brief_path>`. An envelope with `status` `error
|
|
98
|
-
- **CS** (`skf-create-skill`): `--headless` with that `brief_path`. CS prints
|
|
102
|
+
- **BS** (`skf-brief-skill`): `--headless --force` with each non-null field of §4's `brief_skill` as the input of that name (`target_repo`, `skill_name`, `target_version`, `language_hint`, `scope_hint`); copy them, never read them out of the kickoff. `target_version` is the skill's pin: Setup accepts only an X.Y.Z version as a Tier A pin, and brief-skill refuses any other shape with `input-invalid`, so a pin never silently drops out of the build. `--force` replaces a brief an earlier attempt wrote under this name. Record the `brief_path` of its SKF_BRIEF_RESULT_JSON at once, so resume and the report have it: `uv run {stateScript} set-skill --state-file {stateFile} --skill {skill_name} --brief-path <brief_path>`. An envelope with `status` `error` fails the skill with its `halt_reason`; a BS run that printed none fails it with halt reason `no-result`, logged as "no SKF_BRIEF_RESULT_JSON line".
|
|
103
|
+
- **CS** (`skf-create-skill`): `--headless` with that `brief_path`. CS prints one SKF_CREATE_SKILL_RESULT_JSON line: on stdout when it finishes the brief, on stderr when it halts. A `failed` line fails the skill with its `summary.halt_reason`. A CS run that printed no line fails the skill too, with halt reason `no-result`, logged as "no SKF_CREATE_SKILL_RESULT_JSON line". A `partial` line (CS finished a source brief whose extraction found no export, `summary.warning` `zero-exports`) goes on to TS like a `success` one. On success, keep the `{skill_package}` its report names (the compiled skill's folder): §5.3 records it.
|
|
99
104
|
- **TS** (`skf-test-skill`): first resolve the gate again, right before the test, so a directive edit made during this skill counts and the threshold never has to survive the nested runs in conversation context: `uv run {gateScript} resolve --state-file {stateFile} --skill {skill_name} [--directive-file <campaign.directive_path>]` (exit 2 HALTs as at §4). Then run `{skill_name} --headless --threshold={threshold}` with that call's `threshold`, and `pipeline_alias` set to `campaign` in its data context (the explicit threshold wins over test-skill's per-pipeline default). TS settles its verdict, caps and 80% floor fallback itself; §5.3 reads the result.
|
|
100
105
|
- **Doc-rot check:** **record** what CS step 5c already produced; do not re-derive it. Because the pipeline body runs inline (see the RULES note on inline execution), that step's context is directly readable here. Its `correction_matches` holds only the corrections step 5c's judgment pass kept and wrote as `## CORRECTION` blocks, each with `source`, `pattern`, `candidate_category`, `context_line` and `affected`; the candidates the pass rejected are not corrections, so they are never recorded. Append one entry per `correction_matches` record to the skill's `workarounds_applied` array, written as `[doc-rot] {affected}: {candidate_category}`, so they survive in state: `uv run {stateScript} append-workarounds --state-file {stateFile} --skill {skill_name} --entry '<entry>'`, one single-quoted `--entry` per record (an entry the skill already holds is not added twice, so a resumed skill records each correction once). When step 5c took its skip branch (`doc_rot_triggered: false`, `corrections_added: 0`), there is nothing to record: append nothing and move on.
|
|
101
106
|
|
|
@@ -94,8 +94,9 @@ skf-export-skill {export_name} --headless
|
|
|
94
94
|
|
|
95
95
|
Capture its result envelope `SKF_EXPORT_RESULT_JSON`, then record the outcome under the campaign `name`, so step-11's report and result envelope read it from state:
|
|
96
96
|
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
97
|
+
- **No envelope, or a line that does not parse or has no `exit_code`:** there is no result to read. Record it with `uv run {stateScript} set-skill --state-file {stateFile} --skill {name} --export failed --export-halt-reason <reason>` (no exit code), `no-result` for no line, logged as "no SKF_EXPORT_RESULT_JSON line" (type `event`), and `result-unreadable` for a line that does not parse or has no `exit_code`, the term the gate scripts use, logged as "unparseable SKF_EXPORT_RESULT_JSON line", and continue with the remaining skills.
|
|
98
|
+
- **An envelope with `exit_code` 0:** `uv run {stateScript} set-skill --state-file {stateFile} --skill {name} --export exported`, and log the result (type `event`).
|
|
99
|
+
- **Any other `exit_code`:** `uv run {stateScript} set-skill --state-file {stateFile} --skill {name} --export failed --export-exit-code <exit_code> [--export-halt-reason <halt_reason>]`, with the envelope's `halt_reason`, and log the error (type `event`: exit code, envelope or stderr). Continue with the remaining skills: one failure does not block the other exports.
|
|
99
100
|
|
|
100
101
|
After all exports complete, display a summary:
|
|
101
102
|
|
|
@@ -30,6 +30,7 @@ These rules apply to every step in this workflow:
|
|
|
30
30
|
- Never include content in SKILL.md without a provenance citation: `[AST:]` or `[SRC:]` for code (T1 and T1-low), `[QMD:]` for T2 context or `[EXT:]` for T3 documentation, in the forms `assets/skill-sections.md` gives
|
|
31
31
|
- Never write into a skill folder SKF did not generate — generate-artifacts §1 runs the inventory's write check before creating any directory; without the helper it writes only into a skill folder that does not exist yet
|
|
32
32
|
- Only load one step file at a time — never preload future steps
|
|
33
|
+
- Run every subagent in the foreground (parallel ones as several calls in one message, never with `run_in_background`) and wait for every long-running command to exit before the next instruction; in a headless run, never end a turn while either still runs, since under `claude -p` a run stops when its turn ends.
|
|
33
34
|
- Once step 3 §2b has bound `{source_tree}` (the private tree a remote source is read from), every HALT after it first runs `uv run {sourceTreeHelper} close --tree "{source_tree}"` from `{project-root}` and goes on whatever it prints. Step 7 removes the tree on a run that finishes, and a later run removes one a stopped run left behind, seven days on
|
|
34
35
|
- Every HARD HALT in steps 1 to 7 emits the result envelope before it stops, in every mode. Each HALT names its exit code, `halt_reason` and phase where it fires, and its emit command. Stage `{"phase": "<phase>", "halt_reason": "<halt_reason>", "reason": "<the halt message, one line>", "summary": {"halt_reason": "<halt_reason>", "evidence_report": null}}` as `{run_dir}/halt.json` (add `"path"` when the halt names a file or folder; once step 7 has promoted the package, add `"skill_package"` and `"outputs"` and set `summary.evidence_report` to `{forge_version}/evidence-report.md`), then run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`, adding `--result-dir "{forge_version}"` once step 7 has created `{forge_version}`, so the emitter also writes the halt's `create-skill-result-*.json` and its `-latest.json` copy there. Display the line it prints, then stop with the exit code; under `--batch` the halt ends only its brief: go to `references/batch-mode.md` §3, which records it and takes the next brief. If `{emitEnvelopeHelper}` resolved no path, display the halt message alone. The schema, `shared/scripts/schemas/skf-create-skill-result-envelope.v1.json`, lists every `halt_reason` with its exit code
|
|
35
36
|
- Always communicate in `{communication_language}`
|
|
@@ -65,7 +66,7 @@ These rules apply to every step in this workflow:
|
|
|
65
66
|
| **Inputs** | brief_path (a skill-brief.yaml path or a skill name) [optional: without it, step 1 offers the briefs in `{forge_data_folder}`, and a headless run halts unless there is one], --batch [optional: the briefs or folders of briefs after it, `{forge_data_folder}` when none is given, each validated before the first compiles] |
|
|
66
67
|
| **Gates** | step 1: Brief Selection (no brief named and several in `{forge_data_folder}`; headless: HARD HALT `brief-missing`, exit 2; one brief: picked and recorded as gate `brief-selection`) | step 3: Tag Choice Gate (several tags match the brief's version; headless: the first in priority order) | step 3 §2a: Authoritative-File Gate [P]/[S]/[U] (an authoritative AI documentation file outside the brief's scope; headless: deferred-headless, one auto-decision per path) | step 3: Review Gate [C]/[R] (with the zero-export warning when a source brief without `doc_urls` extracted no export; [R] with it or the head-cap warning) | step 3c: Zero-Export Gate [C]/[R] (a source brief with no export whose every `doc_urls` fetch failed) | step 3d: Demo-Exclusion Gate [Y], then Registry Gate [Y] (candidate found) or [S] (none found), component-library scope only, each skipped once the brief holds `scope.demo_patterns` or `scope.registry_path`, which step 3d writes when a user confirms or gives the patterns or the registry path |
|
|
67
68
|
| **Outputs** | SKILL.md, context-snippet.md, metadata.json, provenance-map.json, evidence-report.md, extraction-rules.yaml, references/; per brief, the result contract (`create-skill-result-{YYYYMMDD-HHmmss}.json` and its `-latest.json` copy in `{forge_version}`) and one `SKF_CREATE_SKILL_RESULT_JSON` line on stdout, on stderr at a HARD HALT |
|
|
68
|
-
| **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; each auto-decision reaches the envelope's and the result contract's `headless_decisions` |
|
|
69
|
+
| **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; each auto-decision reaches the envelope's and the result contract's `headless_decisions`. A caller reads the run's own `SKF_CREATE_SKILL_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and `create-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
|
|
69
70
|
| **Exit codes** | 0 for a finished brief; at a HARD HALT 2 (the brief is missing, malformed or invalid), 3 (no forge config, source, helper or documentation), 4 (a write failed), 5 (a refused or broken package), 6 (the user chose to refine the scope). A `--batch` run exits with the highest code among its briefs, 0 when every brief finished |
|
|
70
71
|
|
|
71
72
|
## On Activation
|
|
@@ -75,10 +76,10 @@ These rules apply to every step in this workflow:
|
|
|
75
76
|
|
|
76
77
|
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
|
|
77
78
|
|
|
78
|
-
3. **Resolve workflow customization.**
|
|
79
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
79
80
|
|
|
80
81
|
```bash
|
|
81
|
-
uv run {
|
|
82
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
82
83
|
```
|
|
83
84
|
|
|
84
85
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for step 1 §0 to record in each brief's run folder.
|
|
@@ -315,4 +315,4 @@ t2_future_count: {N}
|
|
|
315
315
|
|
|
316
316
|
**Frontmatter — pinned detection contract:** the `t2_future_count` field is the authoritative forward-looking-annotation count for downstream gate checks (e.g. skf-test-skill §2b migration-section rule). Emit **always**, even when 0 — omission is indistinguishable from "no T2-future data" and silently flips the gate into Case 2/3 for a Case-1 skill. `generated` and `forge_tier` mirror the narrative header for consumers that read only the frontmatter. Downstream gate rules parse `t2_future_count` from frontmatter, not prose — prose drift (heading renames, alternate phrasings like "forward-looking annotations") silently breaks grep-based detection.
|
|
317
317
|
|
|
318
|
-
**Description Guard slot:** populated by step 6 §0 (create-skill) and §0 (update-skill) when the guard protocol fires. `Restored: true` indicates that an external tool (typically `skill-check --fix
|
|
318
|
+
**Description Guard slot:** populated by step 6 §0 (create-skill) and §0 (update-skill) when the guard protocol fires. `Restored: true` indicates that an external tool (typically `skill-check --fix` or the `skf-shard-body.py` body split, which create-skill and update-skill both run; update-skill runs `split-body` only as its fallback when that helper is missing) rewrote the frontmatter `description` and the guard restored the pre-tool value. When `Restored: false`, leave `Triggering tool`, `Original description preserved`, and `Notes` as the no-fire dash the template above shows. When `Restored: true`, fill all four fields: tool name, whether the original was successfully written back, and a one-sentence note describing what the tool had changed (e.g., "replaced with generic summary", "truncated at 80 chars", "angle-bracket tokens re-introduced"). Downstream test-skill assertions can grep for `Restored: true` to detect unintended tool rewrites without parsing free-form warning prose.
|
|
@@ -9,7 +9,7 @@ Step 3 §2b (deferred ccc discovery on SKF's workspace clone) and step 7 §6b (t
|
|
|
9
9
|
|
|
10
10
|
## 1. Wait for the Pass
|
|
11
11
|
|
|
12
|
-
Run `cd "{ccc_root}" && ccc status`. If it prints an `Indexing in progress:` line, a pass is still running and the counts below it are not final: run `cd "{ccc_root}" && ccc index` again, which waits for the running pass to finish and then makes a quick incremental pass, then run `ccc status` again. If the line is still there after 3 such runs, return **index unverified** to the caller and skip §2 and §3.
|
|
12
|
+
Run `cd "{ccc_root}" && ccc status`. If it prints an `Indexing in progress:` line, a pass is still running and the counts below it are not final: run `cd "{ccc_root}" && ccc index` again, which waits for the running pass to finish and then makes a quick incremental pass, then run `ccc status` again. Run both in the foreground with the shell's longest timeout. If the line is still there after 3 such runs, return **index unverified** to the caller and skip §2 and §3.
|
|
13
13
|
|
|
14
14
|
## 2. Check the Languages
|
|
15
15
|
|
|
@@ -176,7 +176,7 @@ For each Props contract found (a `*Props` interface, or a Vue props type):
|
|
|
176
176
|
|
|
177
177
|
The component exports are those `react-component-functions` (its `javascript` form for `.js` and `.jsx` files), `react-component-arrow-functions` and `react-wrapped-components` (for `memo`, `forwardRef` and `lazy` components) find, and the items of a local list such as the `export { Button, buttonVariants }` that ends a shadcn/ui component file, which `js-local-exports` finds. Sort a local list's items by what they name: the runner records an item's `export_type` as the kind of the declaration it names (`interface`, `type` or `enum` for a type). A type-only item (one in an `export type { ... }` list, or a `type X` specifier) is a type: one whose name ends in `Props` is a Props contract for Steps 1 and 3 (read its fields from the interface or type the list names), and any other is a shared type for Step 4. A PascalCase value item is a component, and any other value item (such as `buttonVariants`) is a shared export: record it with the shared types of Step 4.
|
|
178
178
|
|
|
179
|
-
For each component export: record name, source file, line number. Do not document the function signature in detail (
|
|
179
|
+
For each component export: record name, source file, line number. Do not document the function signature in detail (a component's contract is its Props interface: Test Skill compares the props a form writes, never its return type or wrapper).
|
|
180
180
|
|
|
181
181
|
**Step 3 (link Props to components):**
|
|
182
182
|
|
|
@@ -144,7 +144,7 @@ Then index SKF's clone and search it. The clone persists across forges, so an in
|
|
|
144
144
|
|
|
145
145
|
**Note:** Brief-specific `include_patterns` and `exclude_patterns` are not written to `settings.yml`. The CCC index is general-purpose: it indexes everything (minus standard artifacts). Brief-specific filtering happens at search result time, not index time. This allows a single workspace CCC index to serve multiple briefs with different scope filters.
|
|
146
146
|
|
|
147
|
-
2. **Index the clone:** run `cd "{remote_clone_path}" && ccc index` in the foreground with an
|
|
147
|
+
2. **Index the clone:** run `cd "{remote_clone_path}" && ccc index` in the foreground with the shell's longest timeout (in an interactive run, an index that can outlast that timeout may run in the background instead, the step waiting for it to exit before it goes on; a headless run keeps it in the foreground); a call the shell stops at that timeout is a `ccc index` that failed, below. Indexing can take several minutes on large codebases (1000+ files); a reused index processes only what changed. Then load `{cccIndexCheckData}` and run it with `{ccc_root}` = `{remote_clone_path}` and `{ccc_settings_owner}` = `skf`. On **index unverified**, display "CCC index still building (index unverified), skipping the language check." and continue to step 3. When `ccc index` fails, or the check returns **degraded**, set `{ccc_discovery: []}` and continue: this is not an error.
|
|
148
148
|
|
|
149
149
|
3. **Construct semantic query:** Build from brief data: `"{brief.name} {brief.scope}"`. Truncate to 80 characters: keep the full skill name and trim `brief.scope` from the end. If `brief.scope` is very short (< 10 chars), append terms from `brief.description` to fill the remaining space.
|
|
150
150
|
|
|
@@ -240,11 +240,12 @@ After extraction, validate the collected exports against the package's actual pu
|
|
|
240
240
|
**When the recipe runner extracted (§4: its JSON's `status` is `ok` or `incomplete`, and `scope.languages` is not empty),** its `entry_point_diff` is this check: do not read the entry points or diff the sets yourself. It read each package's entry points, traced each re-exported name to the file that defines it, and diffed those names with the recipe matches (`skf-extract-public-api.py --help` lists the entry points it reads):
|
|
241
241
|
|
|
242
242
|
- `public`: the package's public names, which `metadata.json`'s `exports[]` lists. In a language with no entry point, or only empty ones (`entry_points.by_language`), every export the recipes found is public.
|
|
243
|
+
- `other_definitions`: each other in-scope definition of a public name, at its `file` and `line`, such as a name two packages of a monorepo each define (its `public` row keeps one of them). No `extraction_gaps` or `outside_scope` row reads it, and `exports_public_api` never counts it; `effective_denominator` counts its name when its file is counted.
|
|
243
244
|
- `internal`: exports the recipes found that no entry point exports or makes reachable. §5's `init` marks each `internal: true` in the inventory, and step 5 keeps them out of `metadata.json`'s `exports[]`.
|
|
244
245
|
- `extraction_gaps`: names an entry point exports that no recipe found: read each by eye at its `file` and `line` (T1-low), or trace it (§4) when it has no `file`.
|
|
245
246
|
- `outside_scope`: names an entry point exports that are defined in files outside the brief's scope. Display each of the runner's `warnings` (one names each `package.json` `exports` subpath whose entry point is outside the files in scope) and list these names with them: widen `scope.include` before extraction, or this surface stays undocumented while `exports_public_api` still counts it, so `public_api_coverage` drops (only `effective_denominator`, for the curated-subset shapes, leaves it out).
|
|
246
247
|
|
|
247
|
-
**A `public-api` scope documents only the public surface.** Under the brief's `scope.type: public-api`, §5's `init` also marks each export of a language family whose `entry_points.by_language` status is `barrel` `public: true` or `public: false` by this diff, as update-skill's `records` marks an update's exports (`skf-extraction-inventory.py --help` gives the rule): an export is public when `public` or `extraction_gaps` names it at its file (by its own name or the `local` one), or when a `public` name with no `file` is its name. A `namespace` item (a submodule the package exports, or `export * as ns`) with a `members` list stands for its members, the names its module passes on, which users call as `pkg.sub.name`: each member is public at its file (by its name or its `local` one), and the module itself is not. Step 5 leaves each export marked `public: false` out of SKILL.md, `references/`, `metadata.json`'s `exports[]` and the provenance map, and one warning of the inventory names them, each with its file (the first ten, then how many more). An export §5's `add` sends later is marked by the same rule. When the diff cannot be read (no `entry_point_diff`, an incomplete run, one with `errors`, or a non-empty `entry_points.unresolved`), nothing is marked and every export is documented, with a warning that says why. Any other scope type, and a family with no barrel, marks nothing.
|
|
248
|
+
**A `public-api` scope documents only the public surface.** Under the brief's `scope.type: public-api`, §5's `init` also marks each export of a language family whose `entry_points.by_language` status is `barrel` `public: true` or `public: false` by this diff, as update-skill's `records` marks an update's exports (`skf-extraction-inventory.py --help` gives the rule): an export is public when `public`, `other_definitions` or `extraction_gaps` names it at its file (by its own name or the `local` one), or when a `public` name with no `file` is its name. A `namespace` item (a submodule the package exports, or `export * as ns`) with a `members` list stands for its members, the names its module passes on, which users call as `pkg.sub.name`: each member is public at its file (by its name or its `local` one), and the module itself is not. Step 5 leaves each export marked `public: false` out of SKILL.md, `references/`, `metadata.json`'s `exports[]` and the provenance map, and one warning of the inventory names them, each with its file (the first ten, then how many more). An export §5's `add` sends later is marked by the same rule. When the diff cannot be read (no `entry_point_diff`, an incomplete run, one with `errors`, or a non-empty `entry_points.unresolved`), nothing is marked and every export is documented, with a warning that says why. Any other scope type, and a family with no barrel, marks nothing.
|
|
248
249
|
|
|
249
250
|
§5's `init` records its `counts` (`exports_public_api`, `exports_internal`, `effective_denominator` and `effective_denominator_basis`) and `arms` for step 5 §4. Files you read by eye because no recipe reads their language (`files_without_recipes_list`) are outside this diff and these counts: count their entry points' names by steps 2 to 4 of `{entryPointsByHandData}`, with `--extraction "{extraction_json}"` (its names join them, each once), and send the `counts` its call writes with §5's `set`.
|
|
250
251
|
|
|
@@ -110,7 +110,7 @@ When AST tools are available (Forge, Forge+ and Deep tiers) and the step that ru
|
|
|
110
110
|
- **Head cap:** each recipe keeps at most 200 matches, or at Forge+ and Deep 500 for `scope.type: "full-library"` and 300 for `scope.type: "component-library"`, in file and line order. `truncated` is true, and so is that recipe's `truncated` in `recipes[]`, when a recipe matched more: the matches past the cap are missing, so warn about them.
|
|
111
111
|
- **Each export** is one name in one file, and gives `export_name`, `source_file`, `source_line` (`$NAME`'s line), `signature_line` (the source line holding it), `citation` (`[AST:{file}:L{line}]`), `ast_recipe`, `ast_node_type` (the `kind` its recipe declares), `export_type`, `from` (a re-export's module), `signature` (the declaration on one line), `params`, `return_type`, `confidence: T1` and `extraction_method: ast-grep`. Record them as they are.
|
|
112
112
|
- **Signatures:** a function's `params` lists each parameter (its name, type, default and whether it is optional), and is null for any other export and for a declaration the runner does not parse; `return_type` is null when the declaration states none.
|
|
113
|
-
- **Entry points:** `entry_point_diff` diffs the names the
|
|
113
|
+
- **Entry points:** `entry_point_diff` diffs the names the entry points export with the recipe matches (`public`, `other_definitions`, `internal`, `extraction_gaps`, `outside_scope`; a `namespace` item's `members` stand in for it), `counts` gives `exports_public_api`, `exports_internal` and `effective_denominator`, and `arms` the monorepo, specific-modules and multi-subpath `exports` flags.
|
|
114
114
|
- **Read by eye (T1-low)** only what it leaves: each file in `file_issues` (a syntax error where a recipe can miss an export, or a file that is not UTF-8, unreadable or missing), the files `files_without_recipes` counts that hold public API (by the Quick tier Strategy), the forms Known Limitation #11 below lists, the `unread` files of a rerun, and the `extraction_gaps` names.
|
|
115
115
|
|
|
116
116
|
Act on its `status`, not its exit code: `ok`, every file in scope was read; `incomplete`, an ast-grep run failed or timed out: run the step's command once more, and when it is still incomplete keep its exports and read by eye each file an `errors[]` item lists in `unread`; `no-ast-grep`, no ast-grep it can run, so no recipe ran.
|
|
@@ -135,9 +135,9 @@ Read the forms #11 lists by eye whether or not the runner ran:
|
|
|
135
135
|
11. **Forms the recipes deliberately do not cover (ast-grep 0.45.3):** read these by eye at T1-low when the source uses them, or follow them with the Re-Export Tracing protocol in `extraction-patterns-tracing.md` where noted.
|
|
136
136
|
- **Every JS/TS `export_statement` recipe:** `export` followed by a line break before the declaration (tree-sitter reads the bare `export` as a statement of its own, so there is no `export_statement` to match; formatters never write it); an export nested in a `namespace`, `declare module` or `declare global` block, a `.d.ts` file's `declare module "pkg" { ... }` included, since it is a member, not a module export.
|
|
137
137
|
- **`js-exported-functions`, `react-component-functions`:** an anonymous `export default function () {}` (#3); a function exported through a list (`export { f }` and `export { f as g }`, which `js-local-exports` finds at the list's line), `export default f;` or `export = f`; a function bound to a `const` (the constants and arrow-function recipes find it); a wrapped default such as `export default memo(function Name() {})` or `forwardRef(...)`, whose value is a call, not a declaration; class, object-literal and interface methods; the implementation of an overloaded function (its signatures match instead). The React recipes also skip generator functions (an overload signature of one still matches, since a signature does not show it) and class components and keep a PascalCase function that is not a component (such as a Next.js `GET` handler).
|
|
138
|
-
- **`js-exported-constants`, `js-exported-arrow-functions`, `react-component-arrow-functions`, `react-wrapped-components`:** every declarator after the first one a recipe accepts in `export const a = 1, b = () => 2` (one match per statement); destructuring (`export const { x } = obj
|
|
138
|
+
- **`js-exported-constants`, `js-exported-arrow-functions`, `react-component-arrow-functions`, `react-wrapped-components`:** every declarator after the first one a recipe accepts in `export const a = 1, b = () => 2` (one match per statement); destructuring (`export const { x } = obj`: the runner lists in `extraction_gaps` the public names of a pattern that opens the statement; read a later or unreadable pattern by eye, as quick mode asks); `let`, `var` and `export declare const`. The arrow-function recipes also skip an arrow wrapped in parentheses, `as`, `satisfies` or a call, and a `function` expression; the constants recipe reports them, and `react-wrapped-components` reports a PascalCase `memo`, `forwardRef` or `lazy` call. `react-wrapped-components` skips any other wrapper (`observer(...)`, `styled.div`, `connect(...)(Component)`), a wrapped call cast with `as` or `satisfies`, and `export default memo(...)`, whose exposed name is `default`.
|
|
139
139
|
- **`js-exported-classes`, `react-props-interfaces`:** `export default class` and `export default interface` (their exposed name is `default`); `export declare class`, and `export declare interface` (`ts-exported-types` finds it); a class expression bound to a `const` (the constants recipe reports it); a props type written as a type alias (`export type XProps = {...}`, which `ts-exported-types` finds); a type exported through a list (`export type { T }`, which `js-local-exports` or `js-reexports` finds).
|
|
140
|
-
- **`ts-exported-types`:** `export default interface` (its exposed name is `default`);
|
|
140
|
+
- **`ts-exported-types`:** `export default interface` (its exposed name is `default`); a type exported through a list (`export type { T }`, `export { type T }`), which `js-local-exports` finds, or `js-reexports` when the list has a `from`.
|
|
141
141
|
- **`js-reexports`, `js-namespace-reexports`, `js-local-exports`:** a bare `export * from '...'`, which exposes no name of its own; an `export { a } from '...' with { type: 'json' }` statement, or the legacy `export { a } from '...' assert { type: 'json' }`, which parse as ERROR nodes; `export default x;` and `export = x`; a list inside a `namespace`, `declare module` or `declare global` block, a member rather than a module export; in a `.js` file, `default` as a list item's name or alias (`export { default } from '...'`, `export { x as default }`), a keyword token there that no `$NAME` captures (TypeScript parses it as a name). Follow these with the Re-Export Tracing protocol.
|
|
142
142
|
- **`rust-public-functions`:** a `pub fn` in an `impl` block (#8); trait methods, which carry no `pub`; `pub fn` imports in an `extern "C" { }` block; functions a macro generates (`macro_rules!` and `cfg_if!` bodies are token trees, which ast-grep does not parse). One file cannot show reachability across files: a `pub fn` at the top of `foo.rs` matches whether or not `mod foo;` is `pub`, so check `pub use` re-exports with the Re-Export Tracing protocol. `#[cfg]`-gated and `#[doc(hidden)]` functions still match, and a raw identifier keeps its prefix (`r#match`).
|
|
143
143
|
- **`go-exported-functions`:** methods; an exported function-typed variable (`var F = func() {}`). `Test*`, `Benchmark*` and `Example*` functions in `_test.go` files, and the exported functions of `package main` or an `internal/` package, match but are not importable API: exclude them through `scope.exclude`.
|
|
@@ -241,7 +241,7 @@ Otherwise, `ccc index` requires the directory to be initialized first:
|
|
|
241
241
|
- `{ccc_settings_owner}` `skf`: resolve `{mergeCccExclusionsHelper}` from `{mergeCccExclusionsProbeOrder}` and, from `{project-root}`, run `uv run {mergeCccExclusionsHelper} --clone-root "{ccc_root}"`. It runs `ccc init -f` in the clone when its `settings.yml` is missing and adds the standard build and dependency exclusions the file lacks, as step 3 §2b does, so it changes nothing once step 3 prepared the clone. When no candidate resolves, the command fails or `settings_ready` is false, log it and skip the rest of this section, including the registry update.
|
|
242
242
|
- `{ccc_settings_owner}` `user`: run `cd "{ccc_root}" && ccc init` (idempotent: a no-op once initialized).
|
|
243
243
|
|
|
244
|
-
Then run `cd "{ccc_root}" && ccc index
|
|
244
|
+
Then run `cd "{ccc_root}" && ccc index` in the foreground with the shell's longest timeout (in an interactive run, an index that can outlast that timeout may run in the background instead, the step waiting for it to exit before it goes on; a headless run keeps it in the foreground), or use the ccc MCP tool; a call the shell stops at that timeout is ccc indexing that failed, which **Error handling** at the end of this section covers. `ccc_bridge.ensure_index` is a conceptual interface, not a callable function. Indexing is a no-op if the source was already indexed during setup, step 2b or step 3.
|
|
245
245
|
|
|
246
246
|
**Nested project marker:** when `{ccc_root}` is a local source inside a git checkout whose `.git` is a folder, or inside a folder that holds `.cocoindex_code/settings.yml` (a local source such as `./packages/lib`, or a linked worktree or submodule inside such a checkout, whose `.git` file ccc does not count as a marker), `ccc init` exits non-zero with `A parent directory has a project marker`. This is expected: re-run as `cd "{ccc_root}" && ccc init -f` to initialize at the subtree anyway, then `ccc index`. Do not treat the parent-marker warning as fatal. ccc writes no `.gitignore` entry for such a project; **Keep the index out of git** below covers it.
|
|
247
247
|
|