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.
Files changed (128) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +3 -3
  3. package/docs/_data/pinned.yaml +19 -13
  4. package/docs/agents.md +1 -1
  5. package/docs/architecture.md +5 -5
  6. package/docs/campaign.md +1 -1
  7. package/docs/concepts.md +4 -4
  8. package/docs/examples.md +32 -25
  9. package/docs/getting-started.md +3 -3
  10. package/docs/how-it-works.md +2 -2
  11. package/docs/index.md +1 -1
  12. package/docs/skill-model.md +41 -39
  13. package/docs/troubleshooting.md +9 -3
  14. package/docs/verifying-a-skill.md +42 -21
  15. package/docs/why-skf.md +1 -1
  16. package/docs/workflows.md +6 -6
  17. package/package.json +1 -1
  18. package/src/knowledge/tool-resolution.md +6 -6
  19. package/src/shared/references/headless-gate-convention.md +4 -1
  20. package/src/shared/references/output-contract-schema.md +3 -3
  21. package/src/shared/references/pipeline-contracts.md +2 -1
  22. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
  23. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  25. package/src/shared/scripts/skf-build-change-manifest.py +1180 -189
  26. package/src/shared/scripts/skf-classify-changed-files.py +15 -10
  27. package/src/shared/scripts/skf-count-tokens.py +54 -10
  28. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  29. package/src/shared/scripts/skf-emit-result-envelope.py +64 -20
  30. package/src/shared/scripts/skf-extract-public-api.py +472 -120
  31. package/src/shared/scripts/skf-extraction-inventory.py +10 -6
  32. package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
  33. package/src/shared/scripts/skf-parse-gaps.py +136 -26
  34. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  35. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  36. package/src/shared/scripts/skf-shard-body.py +239 -41
  37. package/src/shared/scripts/skf-validate-output.py +12 -3
  38. package/src/shared/scripts/skf-verify-provenance-completeness.py +574 -45
  39. package/src/shared/tool-requirements.yaml +8 -2
  40. package/src/skf-analyze-source/SKILL.md +3 -2
  41. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  42. package/src/skf-analyze-source/references/generate-briefs.md +1 -1
  43. package/src/skf-analyze-source/references/headless-contract.md +1 -1
  44. package/src/skf-audit-skill/SKILL.md +4 -3
  45. package/src/skf-audit-skill/references/re-index.md +2 -2
  46. package/src/skf-audit-skill/references/report.md +1 -1
  47. package/src/skf-brief-skill/SKILL.md +3 -2
  48. package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
  49. package/src/skf-brief-skill/references/invocation-contract.md +2 -2
  50. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  51. package/src/skf-campaign/SKILL.md +3 -2
  52. package/src/skf-campaign/references/campaign-contracts.md +2 -0
  53. package/src/skf-campaign/references/step-05-skill-loop.md +8 -3
  54. package/src/skf-campaign/references/step-10-export.md +3 -2
  55. package/src/skf-create-skill/SKILL.md +4 -3
  56. package/src/skf-create-skill/assets/skill-sections.md +1 -1
  57. package/src/skf-create-skill/references/ccc-index-check.md +1 -1
  58. package/src/skf-create-skill/references/component-extraction.md +1 -1
  59. package/src/skf-create-skill/references/extract.md +3 -2
  60. package/src/skf-create-skill/references/extraction-patterns.md +3 -3
  61. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  62. package/src/skf-create-skill/references/report.md +8 -3
  63. package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
  64. package/src/skf-create-skill/references/validate.md +1 -1
  65. package/src/skf-create-stack-skill/SKILL.md +3 -2
  66. package/src/skf-create-stack-skill/references/generate-output.md +1 -1
  67. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  68. package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
  69. package/src/skf-create-stack-skill/references/report.md +1 -1
  70. package/src/skf-create-stack-skill/references/validate.md +1 -1
  71. package/src/skf-drop-skill/SKILL.md +3 -2
  72. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  73. package/src/skf-drop-skill/references/report.md +1 -1
  74. package/src/skf-export-skill/SKILL.md +3 -2
  75. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  76. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  77. package/src/skf-export-skill/references/invocation-contract.md +1 -1
  78. package/src/skf-export-skill/references/load-skill.md +1 -1
  79. package/src/skf-export-skill/references/summary.md +1 -1
  80. package/src/skf-export-skill/references/token-report.md +29 -9
  81. package/src/skf-forger/SKILL.md +3 -2
  82. package/src/skf-forger/references/pipeline-mode.md +2 -2
  83. package/src/skf-quick-skill/SKILL.md +4 -3
  84. package/src/skf-quick-skill/references/finalize.md +1 -1
  85. package/src/skf-quick-skill/references/halt-contract.md +2 -2
  86. package/src/skf-quick-skill/references/write-and-validate.md +1 -1
  87. package/src/skf-refine-architecture/SKILL.md +8 -9
  88. package/src/skf-refine-architecture/customize.toml +1 -1
  89. package/src/skf-refine-architecture/references/report.md +1 -1
  90. package/src/skf-rename-skill/SKILL.md +3 -2
  91. package/src/skf-rename-skill/references/invocation-contract.md +2 -2
  92. package/src/skf-setup/SKILL.md +3 -2
  93. package/src/skf-setup/references/ccc-index.md +1 -1
  94. package/src/skf-setup/references/invocation-contract.md +1 -1
  95. package/src/skf-test-skill/SKILL.md +3 -2
  96. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  97. package/src/skf-test-skill/references/coherence-check.md +11 -8
  98. package/src/skf-test-skill/references/coverage-check-branches.md +81 -0
  99. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  100. package/src/skf-test-skill/references/coverage-check.md +60 -97
  101. package/src/skf-test-skill/references/init.md +2 -1
  102. package/src/skf-test-skill/references/inventory-contract.md +27 -0
  103. package/src/skf-test-skill/references/invocation-contract.md +3 -3
  104. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  105. package/src/skf-test-skill/references/report.md +5 -5
  106. package/src/skf-test-skill/references/scoring-rules.md +8 -7
  107. package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
  108. package/src/skf-test-skill/references/source-access-protocol.md +14 -10
  109. package/src/skf-test-skill/references/step-hard-gate.md +1 -0
  110. package/src/skf-test-skill/scripts/build-result-context.py +20 -3
  111. package/src/skf-test-skill/scripts/gap-ledger.py +456 -176
  112. package/src/skf-test-skill/scripts/load-coverage-inputs.py +516 -79
  113. package/src/skf-test-skill/scripts/locate-export-segments.py +685 -76
  114. package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
  115. package/src/skf-test-skill/scripts/score-signatures.py +1205 -65
  116. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  117. package/src/skf-test-skill/scripts/validate-inventory.py +350 -35
  118. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  119. package/src/skf-update-skill/SKILL.md +3 -2
  120. package/src/skf-update-skill/references/detect-changes.md +1 -1
  121. package/src/skf-update-skill/references/gap-driven.md +54 -33
  122. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  123. package/src/skf-update-skill/references/merge.md +15 -10
  124. package/src/skf-update-skill/references/re-extract.md +3 -1
  125. package/src/skf-update-skill/references/report.md +6 -5
  126. package/src/skf-update-skill/references/write.md +42 -20
  127. package/src/skf-verify-stack/SKILL.md +4 -3
  128. package/tools/cli/lib/tool-check.js +158 -2
@@ -11,6 +11,10 @@ atomicWriteProbeOrder:
11
11
  emitEnvelopeProbeOrder:
12
12
  - '{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py'
13
13
  - '{project-root}/src/shared/scripts/skf-emit-result-envelope.py'
14
+ # The customization helper §5's post-completion hook runs.
15
+ resolveCustomizationProbeOrder:
16
+ - '{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py'
17
+ - '{project-root}/src/shared/scripts/skf-resolve-customization.py'
14
18
  ---
15
19
 
16
20
  <!-- Config: communicate in {communication_language}. -->
@@ -25,6 +29,7 @@ To display the final compilation summary (skill name, version, source, export co
25
29
 
26
30
  - Write only the brief's result files (through the emitter, §5) and, outside `--batch`, the removal of the brief's run folder. Change no skill artifact, except in §5's fallback when no path in `{emitEnvelopeProbeOrder}` exists: `validation_status` in `{skill_package}/metadata.json` and one warning appended to `{forge_version}/evidence-report.md`
27
31
  - §5 emits through `{emitEnvelopeHelper}`, resolved from `{emitEnvelopeProbeOrder}` when it is not bound.
32
+ - §5's post-completion hook runs `{resolveCustomizationHelper}`, resolved from `{resolveCustomizationProbeOrder}` when it is not bound.
28
33
  - Deliver structured report with confidence breakdown
29
34
  - The user-facing report is not the end of the run: §5 and §6 follow it in order, and §6 chains to the health check via `{nextStepFile}`, or under `--batch` back to `references/batch-mode.md`, which runs it once the last brief is recorded
30
35
 
@@ -96,7 +101,7 @@ To use this skill immediately, add the context snippet to your CLAUDE.md:
96
101
 
97
102
  Every brief that reaches this step gets its own result contract and `on_complete` call, each brief of a `--batch` run included. A HARD HALT in steps 1 to 7 never reaches this step: it emits its envelope where it fires, as its HALT line says, and writes its result file there once step 7 has created `{forge_version}`.
98
103
 
99
- **Write the result contract through the emitter.** `{emitEnvelopeHelper}` writes `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` and its `create-skill-result-latest.json` copy (a stable path for pipeline consumers; a copy, not a symlink), stamps the timestamp, the run id and the run sink's decisions and warnings into them, checks the brief's `SKF_CREATE_SKILL_RESULT_JSON` line against `skf-create-skill-result-envelope.v1.json` and prints it. First read the run sink, from `{project-root}`:
104
+ **Write the result contract through the emitter.** `{emitEnvelopeHelper}` writes `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` and its `create-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_CREATE_SKILL_RESULT_JSON` line), stamps the timestamp, the run id and the run sink's decisions and warnings into them, checks the brief's `SKF_CREATE_SKILL_RESULT_JSON` line against `skf-create-skill-result-envelope.v1.json` and prints it. First read the run sink, from `{project-root}`:
100
105
 
101
106
  ```bash
102
107
  grep -c . "{run_dir}/headless-decisions.jsonl" 2>/dev/null; grep -c '"gate":"zero-exports"' "{run_dir}/headless-decisions.jsonl" 2>/dev/null
@@ -113,12 +118,12 @@ uv run {emitEnvelopeHelper} emit --workflow skf-create-skill --run-dir "{run_dir
113
118
 
114
119
  The evidence report in `outputs` carries the `## Auto-Decisions` audit table: a pipeline consumer follows its path to audit the run. Display the line the emitter prints. If it exits non-zero, fix `result-context.json` once (its `message` names the problem) and run it again. When it fails again, display its `message`: this brief has no result contract.
115
120
 
116
- **If no path in `{emitEnvelopeProbeOrder}` exists** (an incomplete install), write no result contract: append "Result contract skipped: skf-emit-result-envelope.py is missing." under `{forge_version}/evidence-report.md`'s `## Remaining Warnings`, and set `validation_status: 'schema-unavailable'` in `{skill_package}/metadata.json`, rewritten through `python3 {atomicWriteHelper} write --target {skill_package}/metadata.json` when `{atomicWriteHelper}` resolves. Pipeline consumers observe the missing `-latest.json` and the metadata flag.
121
+ **If no path in `{emitEnvelopeProbeOrder}` exists** (an incomplete install), write no result contract: append "Result contract skipped: skf-emit-result-envelope.py is missing." under `{forge_version}/evidence-report.md`'s `## Remaining Warnings`, and set `validation_status: 'schema-unavailable'` in `{skill_package}/metadata.json`, rewritten through `python3 {atomicWriteHelper} write --target {skill_package}/metadata.json` when `{atomicWriteHelper}` resolves. A caller sees no `SKF_CREATE_SKILL_RESULT_JSON` line for this brief and the metadata flag; a `create-skill-result-latest.json` already there is an earlier compile's.
117
122
 
118
123
  **Post-completion hook.** When `{onCompleteCommand}` is not bound, bind it to the `workflow.on_complete` value this prints, or to the one in `{skill-root}/customize.toml` when the resolver cannot run:
119
124
 
120
125
  ```bash
121
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow.on_complete
126
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow.on_complete
122
127
  ```
123
128
 
124
129
  When `{onCompleteCommand}` is non-empty and the emitter wrote this brief's result contract (its `emit` exited 0), run it as a shell command from `{project-root}`, for this brief:
@@ -51,7 +51,7 @@ Read `ccc_index` from forge-tier.yaml and branch on `ccc_index.status`:
51
51
  - `"none"` or `"failed"`: attempt lazy indexing via `ccc_bridge.ensure_index(source_root)`. If indexing succeeds, continue to section 3. If indexing fails, set `{ccc_discovery: []}` and auto-proceed to section 5.
52
52
  - Any other status, for example a `"stale"` an older SKF recorded: treat it like `"fresh"`.
53
53
 
54
- **Tool resolution for ccc_bridge.ensure_index:** Use `/ccc` skill indexing (Claude Code), ccc MCP server (Cursor), or the CLI: `cd "{source_root}" && ccc init`, and when it exits non-zero with `A parent directory has a project marker` (a local source inside a git checkout or below another ccc project), `cd "{source_root}" && ccc init -f`, the same handling as step 7 §6b. Run `cd "{source_root}" && ccc index` only once `{source_root}/.cocoindex_code/settings.yml` exists: without it, `ccc index` initializes the enclosing git checkout with ccc's defaults and none of the SKF exclusions, so treat indexing as failed instead. Note: `ccc init` takes no positional arguments — it initializes the index for the current working directory. See `knowledge/tool-resolution.md`. Exception: when `{source_root}` is `{project-root}` and `{project-root}/.cocoindex_code/settings.yml` does not exist, do not run `ccc init` or `ccc index` — `/skf-setup` initializes the project index with the SKF exclusions. Set `{ccc_discovery: []}`, note that re-running `/skf-setup` enables semantic discovery, and auto-proceed to section 5.
54
+ **Tool resolution for ccc_bridge.ensure_index:** Use `/ccc` skill indexing (Claude Code), ccc MCP server (Cursor), or the CLI: `cd "{source_root}" && ccc init`, and when it exits non-zero with `A parent directory has a project marker` (a local source inside a git checkout or below another ccc project), `cd "{source_root}" && ccc init -f`, the same handling as step 7 §6b. Run `cd "{source_root}" && ccc index` in the foreground with the shell's longest timeout (a call the shell stops at that timeout is indexing that failed), and only once `{source_root}/.cocoindex_code/settings.yml` exists: without it, `ccc index` initializes the enclosing git checkout with ccc's defaults and none of the SKF exclusions, so treat indexing as failed instead. Note: `ccc init` takes no positional arguments: it initializes the index for the current working directory. See `knowledge/tool-resolution.md`. Exception: when `{source_root}` is `{project-root}` and `{project-root}/.cocoindex_code/settings.yml` does not exist, do not run `ccc init` or `ccc index`: `/skf-setup` initializes the project index with the SKF exclusions. Set `{ccc_discovery: []}`, note that re-running `/skf-setup` enables semantic discovery, and auto-proceed to section 5.
55
55
 
56
56
  **Keep a lazy index out of git:** after lazy indexing has run any ccc command, whichever tool ran it and whether it succeeded or not, return to `{project-root}`, resolve `{cccGitHygieneHelper}` from `{cccGitHygieneProbeOrder}` and run `uv run {cccGitHygieneHelper} nested --dir "{source_root}" --project-root "{project-root}"` before continuing to section 3 or section 5. It is the same check as step 7 §6b: when git does not ignore `{source_root}/.cocoindex_code/`, it writes a `.gitignore` holding `*` inside that folder, and SKF never edits a project's own `.gitignore`. Bind `{ccc_ignore_notice}` ← `notice` and display `{ccc_ignore_notice}` verbatim when it is not null: its remedy command is quoted for paths with spaces. If no candidate exists, or the helper exits non-zero or prints no JSON, continue: discovery never blocks.
57
57
 
@@ -333,7 +333,7 @@ Fix the findings that have one answer, in this order, then run the command above
333
333
  --skill-dir <staging-skill-dir>
334
334
  ```
335
335
 
336
- It gives each mismatched citation its `expected_prefix`, moves each `source_line` that has exactly one value in `definition_lines` there, with the citations of the old line in SKILL.md and `references/` (a range by both ends, each citation at most once), writes each changed file through the atomic writer, and prints `applied[]`, `left_as_warn[]` (each finding it left, with its `why`), `files_written[]` and `summary`, on exit 0 and on exit 1 (it left a WARN) alike. Exit 2 prints no JSON (its stderr line names any file it already wrote): list a WARN, `not fixed: verifier error` with that line, and go on to item 2.
336
+ It gives each mismatched citation its `expected_prefix`, moves each `source_line` that has exactly one value in `definition_lines` there, with the citations of the old line in SKILL.md and `references/` (a range by both ends, each citation at most once), writes each changed file through the atomic writer, and prints `applied[]`, `left_as_warn[]` (each finding it left, with its `why`), `files_written[]` and `summary`, on exit 0 and on exit 1 (it left a WARN) alike. A `stale[]` item that carries `folded_to` is a bookkeeping variant (`X_def`, `X_exact`, `a11y_X`) its file never names: its line was checked against that base, so its `definition_lines` are the base's. Exit 2 prints no JSON (its stderr line names any file it already wrote): list a WARN, `not fixed: verifier error` with that line, and go on to item 2.
337
337
  2. **Node kinds.** For each `node_kinds[]` item (`reason` is `invalid-kind` or `error-kind`), take the entry at its `entry_index` in the staged provenance map (its `export_name` and `source_file` confirm it; item 1's `fix` may have moved its `source_line`) and set its `ast_node_type` to the node kind the Relabel Rule in `{relabelRuleData}` gives: the kind of the recipe the export's `ast_recipe` names in the extraction inventory step 3 §5 wrote, `{project-root}/_bmad-output/.skf-stage/{skill-name}.inventory.json`, else its `kind-at` lookup at the entry's current `source_line`. When the rule gives no kind, list a WARN with its reason. Never invent a kind, and never change `extraction_method` to clear the finding. Write the map with `python3 {atomicWriteHelper} write --target <staging-skill-dir>/provenance-map.json`.
338
338
 
339
339
  If that second run leaves no JSON at `{verify_json}`, record the fixes already applied and `second run: verifier error`, and continue to §8. Fix nothing else, and triage what is left:
@@ -30,6 +30,7 @@ These rules apply to every step in this workflow:
30
30
  - Zero hallucination — all extracted content must trace to actual source code (compose-mode inferences must be labeled)
31
31
  - Only load one step file at a time — never preload future steps
32
32
  - If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread, with two exceptions: never decide by hand whether SKF generated a folder (the ownership check), and never work out by hand what a shared helper computes where its step names a `helper-missing` HALT: that helper resolving to no path stops the run with exit 3 `helper-missing` (`references/invocation-contract.md` lists every such halt). Where a step says what to do without its helper (an advisory check, a default value), do as it says
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
  - Never write into a skill folder SKF did not generate — generate-output §1 runs the inventory's write check before any prior metadata is read and again before staging
34
35
  - Always communicate in `{communication_language}`
35
36
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action, and record each auto-decision in the run sink the moment the gate takes it, with the `record --decision` command the gate shows
@@ -80,10 +81,10 @@ The sink, `{run_dir}/warnings.jsonl`, is the list: step 7 §8b lists it in `evid
80
81
  3. **Preferences fallback.** Otherwise, read `headless_mode` from `{sidecar_path}/preferences.yaml` (`true` or `false`).
81
82
  4. **Default:** `false`.
82
83
 
83
- 3. **Resolve workflow customization.** Run:
84
+ 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:
84
85
 
85
86
  ```bash
86
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
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-create-stack-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): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
@@ -188,7 +188,7 @@ uv run {verifyProvenanceCompletenessHelper} fix \
188
188
  --skill-dir {skill_staging}
189
189
  ```
190
190
 
191
- Read the JSON, not the exit code, ignoring `missing` and `orphaned` (a stack's `exports` is `[]`). Record a `workflow_warnings[]` entry (`severity: "info"`, `code: "provenance-lines-fixed"`) naming the lines `fix` moved, and one (`code: "provenance-line-unverified"`, `message`: the item's `export_name`, `source_file`, `source_line`, `reason` and `definition_lines`) per `stale[]` item of the last `verify`. The check is advisory (`provenance-lines-unchecked`) and ends at a `summary.stale_check` of `skipped-no-source-root`. Delete `{forge_version}/provenance-verify.skf-tmp` at the end.
191
+ Read the JSON, not the exit code, ignoring `missing` and `orphaned` (a stack's `exports` is `[]`). Record a `workflow_warnings[]` entry (`severity: "info"`, `code: "provenance-lines-fixed"`) naming the lines `fix` moved, and one (`code: "provenance-line-unverified"`, `message`: the item's `export_name`, `source_file`, `source_line`, `reason` and `definition_lines`, and, when it carries `folded_to`, that its line was checked against that base name) per `stale[]` item of the last `verify`. The check is advisory (`provenance-lines-unchecked`) and ends at a `summary.stale_check` of `skipped-no-source-root`. Delete `{forge_version}/provenance-verify.skf-tmp` at the end.
192
192
 
193
193
  ### 8. Pre-Commit Gate
194
194
 
@@ -7,7 +7,7 @@ The headless contract of Stack Skill: its inputs, gates and outputs, the exit co
7
7
  | **Inputs** | All optional; step 1 binds and checks each one, and halts with `input-invalid` on a value it cannot use. `project_path`: the folder code mode scans (default: the project root; an interactive run asks once when the manifests sit in several folders). `skills`: the libraries to rank, or the constituent skills in compose mode. `stack_name`: the stack's skill name (default `{project_name}-stack`). `scope_overrides`: step 3 scope overrides. `architecture_doc_path`: the document compose mode reads. `mode`: `code` or `compose` (default: step 1 detects it) |
8
8
  | **Gates** | step 1: compose suggestion [accept] and scan scope [A] (each asked only in an interactive run; headless takes the default and records it); step 2: no manifest found (headless: HALT `no-manifests`, exit 2); step 3: scope gate [C]; step 6: review gate [C] |
9
9
  | **Outputs** | SKILL.md (stack), context-snippet.md, metadata.json; `provenance-map.json` and `evidence-report.md` in `{forge_version}`; the result contract, `create-stack-skill-result-{YYYYMMDD-HHmmss}.json` and its `create-stack-skill-result-latest.json` copy in `{forge_version}`, with the `-latest` copy also at the stack group root `{forge_data_folder}/{stack_name}/`; a run folder under `{project-root}/_bmad-output/.skf-run/`, deleted when the run finishes or the user cancels and kept after any other HALT |
10
- | **Headless** | All gates auto-resolve with their default action when `{headless_mode}` is true, and each records its decision in the run sink, so the envelope's `headless_decisions` lists every one |
10
+ | **Headless** | All gates auto-resolve with their default action when `{headless_mode}` is true, and each records its decision in the run sink, so the envelope's `headless_decisions` lists every one. A caller reads the run's own `SKF_STACK_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 each `create-stack-skill-result-latest.json` copy may then describe an earlier run, so neither is read in its place. |
11
11
  | **Exit codes** | See "Exit Codes" below |
12
12
 
13
13
  ## Exit Codes
@@ -213,7 +213,7 @@ Each subprocess:
213
213
 
214
214
  **If parallel subprocess unavailable:** Process libraries sequentially in main thread. Report progress after each library.
215
215
 
216
- **Per-subprocess timeout (S6):** Apply a 60-second wall-clock timeout to each library's extraction subprocess. On timeout, mark the library as `partial-failure` with `warnings: ["extraction timeout after 60s"]`, store whatever partial data was returned (if any), and continue with the remaining libraries. Do NOT abort the batch on a single timeout.
216
+ **Per-subprocess budget (S6):** Tell each library's extraction subprocess to stop and return what it has after about 20 tool calls, adding `"extraction budget reached"` to its `warnings` when it stops there, and wait for it in the foreground. A library whose subprocess returned partial data is a `partial-failure`: store that data and continue with the remaining libraries. One whose subprocess returned no usable data is a complete failure (§3): it goes in `failed[]` with an `extraction-failed` warning and counts toward the `all-extractions-failed` halt. Do NOT abort the batch on a single library that reached its budget or failed.
217
217
 
218
218
  ### 3. Handle Extraction Failures
219
219
 
@@ -221,7 +221,7 @@ For each library extraction:
221
221
 
222
222
  **Success:** Store the extraction result as the library's `per_library_extractions[]` entry.
223
223
 
224
- **Partial failure:** Store partial result with warnings, continue with other libraries.
224
+ **Partial failure:** Store partial result with warnings, append a `workflow_warnings[]` entry (`step: "step-04"`, `severity: "warn"`, `code: "extraction-partial"`, `message`: the library and its warnings, `extraction budget reached` among them when it stopped on its budget), and continue with other libraries.
225
225
 
226
226
  **Complete failure:** Log failure reason, exclude from stack skill, note in report.
227
227
 
@@ -71,7 +71,7 @@ uv run {emitEnvelopeHelper} emit --workflow skf-create-stack-skill --run-dir "{r
71
71
 
72
72
  The emitter stamps `timestamp`, `run_id`, the run's auto-decisions and its warnings into the record, writes `create-stack-skill-result-<YYYYMMDD-HHmmss>.json` (it picks the name, with `-2` for a run that ends in the same second as another) and its `create-stack-skill-result-latest.json` copy in `{forge_version}`, and prints the success line on stdout: display it when `{headless_mode}` is true. A file it cannot write becomes a `result_file_write_failed` warning in the envelope, with `result_path` `null` when the per-run record is the one that failed. If the emitter exits non-zero, fix the payload its stderr names and run it once more; if it fails again, print its error and continue: the result contract is advisory and never halts the report.
73
73
 
74
- **Stable latest pointer at the stack group root.** Pipeline consumers read `{forge_data_folder}/{stack_name}/create-stack-skill-result-latest.json`, above the version folder, without knowing the current version. Copy the record there (a copy, not a symlink, so no consumer chases a link across version folders):
74
+ **Stable latest pointer at the stack group root.** `{forge_data_folder}/{stack_name}/create-stack-skill-result-latest.json`, above the version folder, is a copy of the version folder's `-latest` copy, at a path that does not name the current version; a caller reads its own run's `SKF_STACK_RESULT_JSON` line. Copy the record there (a copy, not a symlink, so no consumer chases a link across version folders):
75
75
 
76
76
  ```bash
77
77
  python3 {atomicWriteHelper} write --target {forge_data_folder}/{stack_name}/create-stack-skill-result-latest.json < {forge_version}/create-stack-skill-result-latest.json
@@ -114,7 +114,7 @@ Take the first-line format, `|IMPORTANT:` second-line, and token-estimate checks
114
114
 
115
115
  Then verify the two stack-specific rows the generic validator does not cover:
116
116
  - [ ] Stack and integrations lines present
117
- - [ ] Token estimate lands near the ~80-120 design target from step 7 §5 (the validator flags only its wider <40 / >200 bounds; an ~80-150 snippet with an overflow-strategy `workflow_warning` is expected, not a defect)
117
+ - [ ] Token estimate lands near the ~80-120 design target from step 7 §5 (the validator flags only its wider <40 / >300 bounds; an ~80-150 snippet with an overflow-strategy `workflow_warning` is expected, not a defect)
118
118
 
119
119
  Record format violations as **WARNING** findings.
120
120
 
@@ -33,6 +33,7 @@ These rules apply to every step in this workflow:
33
33
  - Never purge content SKF did not generate: select.md §8b takes the purge verdict for the skill folder and its forge folder from the inventory helper; a forge folder SKF did not generate is left in place, and a purge SKF cannot check is refused
34
34
  - Only load one step file at a time — never preload future steps
35
35
  - If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread, with two exceptions. A helper On-Activation §4 resolves is never replaced by hand (a missing one halts the run before the first prompt): never type an envelope or edit the manifest or a context file yourself. And never decide by hand whether SKF generated a folder: without the inventory helper, select.md offers manifest skills only and refuses every purge
36
+ - 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.
36
37
  - Always communicate in `{communication_language}`
37
38
  - At any interactive prompt, the inputs `cancel`, `exit`, `[X]`, `q`, or `:q` exit cleanly with exit code 6 (`halt_reason: "user-cancelled"`)
38
39
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
@@ -59,10 +60,10 @@ Headless callers and pipelines: the inputs, flags, gate map, exit codes and `SKF
59
60
 
60
61
  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.
61
62
 
62
- 3. **Resolve workflow customization.** Run:
63
+ 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:
63
64
 
64
65
  ```bash
65
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
66
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
66
67
  ```
67
68
 
68
69
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-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. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
@@ -10,7 +10,7 @@ The arguments, gates, exit codes and headless result envelope of `skf-drop-skill
10
10
  | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--dry-run` (run selection and display the §10 confirmation block, then exit with `status="dry-run"`: no manifest mutation, no file deletion) |
11
11
  | **Gates** | step 1: Input Gate [use args] (§4 skill), Scope Gate [use args] (§6 version), Mode Gate [use args] (§8 mode), Confirm Gate [Y] |
12
12
  | **Outputs** | Updated manifest, rebuilt context files, (purge: deleted directories), `drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC) and `drop-skill-result-latest.json` in `{skills_output_folder}` (none for a dry run or a HALT); a run folder under `{project-root}/_bmad-output/.skf-run/`, kept only after a HALT, except an interactive HALT in step 1 or 2, which deletes it |
13
- | **Headless** | Gates auto-resolve with their default action (SKILL.md Workflow Rules); select.md §6 and §8 halt when the `version` or `mode` argument a headless run must pass is missing. Any non-empty `forbid_purge_in_headless`, set under `[workflow]` in a team or personal override (never in the bundled `customize.toml`, which every update overwrites), stops a headless `mode=purge` at On-Activation §4 (exit code 6, `halt_reason: "headless-purge-forbidden"`) when the customization resolver runs. When it cannot, including an override file that fails to parse, the run warns `customization_resolver_unavailable` and the bundled empty value leaves the guard off. A key written without the `[workflow]` line is ignored with no warning, and the guard stays off. |
13
+ | **Headless** | Gates auto-resolve with their default action (SKILL.md Workflow Rules); select.md §6 and §8 halt when the `version` or `mode` argument a headless run must pass is missing. Any non-empty `forbid_purge_in_headless`, set under `[workflow]` in a team or personal override (never in the bundled `customize.toml`, which every update overwrites), stops a headless `mode=purge` at On-Activation §4 (exit code 6, `halt_reason: "headless-purge-forbidden"`) when the customization resolver runs. When it cannot, including an override file that fails to parse, the run warns `customization_resolver_unavailable` and the bundled empty value leaves the guard off. A key written without the `[workflow]` line is ignored with no warning, and the guard stays off. A caller reads the run's own `SKF_DROP_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 `drop-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
14
14
  | **Exit codes** | See "Exit Codes" below |
15
15
 
16
16
  ## Exit Codes
@@ -121,7 +121,7 @@ Then run, in every mode:
121
121
  uv run {emitEnvelopeHelper} emit --workflow skf-drop-skill --run-dir "{run_dir}" --result-dir "{skills_output_folder}" < "{run_dir}/result-context.json"
122
122
  ```
123
123
 
124
- The emitter stamps the UTC time, the run id, the run's warnings and the auto-decisions select.md recorded (`headless_decisions`) into the record, writes it as `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json`, copies it to `{skills_output_folder}/drop-skill-result-latest.json` (the stable path pipelines read), and prints the `SKF_DROP_SKILL_RESULT_JSON:` line on stdout (field rules in `references/invocation-contract.md`). Bind `{result_json_path}` ← that line's `result_path`: null when the record could not be written, and the line's `warnings` then say why. 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 the envelope as the final message. If the helper exits non-zero, correct `result-context.json` from the `message` of its stderr JSON and run it once more; if it fails again, leave `{result_envelope_line}` empty, display "The drop result record could not be written: {message}" and go on, since the drop itself is complete.
124
+ The emitter stamps the UTC time, the run id, the run's warnings and the auto-decisions select.md recorded (`headless_decisions`) into the record, writes it as `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json`, copies it to `{skills_output_folder}/drop-skill-result-latest.json` (the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_DROP_SKILL_RESULT_JSON` line), and prints the `SKF_DROP_SKILL_RESULT_JSON:` line on stdout (field rules in `references/invocation-contract.md`). Bind `{result_json_path}` ← that line's `result_path`: null when the record could not be written, and the line's `warnings` then say why. 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 the envelope as the final message. If the helper exits non-zero, correct `result-context.json` from the `message` of its stderr JSON and run it once more; if it fails again, leave `{result_envelope_line}` empty, display "The drop result record could not be written: {message}" and go on, since the drop itself is complete.
125
125
 
126
126
  ### Post-drop hook (optional)
127
127
 
@@ -27,6 +27,7 @@ You are Ferris in Delivery mode, a packaging specialist: you check a developer's
27
27
  ## Workflow Rules
28
28
 
29
29
  - Only load one step file at a time — never preload future steps
30
+ - 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.
30
31
  - Always communicate in `{communication_language}`
31
32
  - At any interactive prompt, the inputs `cancel`, `exit`, `[X]`, `q`, or `:q` exit cleanly with exit code 6 (`halt_reason: "user-cancelled"`)
32
33
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and record each auto-decision in the run sink (`references/result-envelope.md`)
@@ -58,10 +59,10 @@ Headless callers and pipelines: the inputs, flags, gates, outputs and exit codes
58
59
 
59
60
  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.
60
61
 
61
- 3. **Resolve workflow customization.** Run:
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 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
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-export-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. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
@@ -34,9 +34,9 @@ The root path in context-snippet.md on disk uses the **`skill_root` of the first
34
34
  - **Line 1:** Skill name + version + platform-aware root path — version signals training data staleness
35
35
  - **Line 2 (IMPORTANT):** Retrieval instruction — always present
36
36
  - **Line 3 (quick-start):** Anchor pointer to Quick Start section
37
- - **Line 4 (api):** Top exports from metadata.json `exports` array (up to 10, with `()` for functions)
37
+ - **Line 4 (api):** The ranked exports (see the api line rule below), with `()` for functions
38
38
  - **Line 5 (key-types):** Anchor pointer + inline summary of most important type/enum values
39
- - **Line 6 (gotchas):** 2-3 most critical pitfalls — derived from T2-future annotations, async requirements, breaking changes
39
+ - **Line 6 (gotchas):** At most 3 pitfalls, separated by `; `, derived from the current sources (see the gotchas line rule below)
40
40
 
41
41
  ## Stack Skill Snippet Template
42
42
 
@@ -50,12 +50,12 @@ The root path in context-snippet.md on disk uses the **`skill_root` of the first
50
50
 
51
51
  ## Rules
52
52
 
53
- - **api line**: Top exports from metadata.json `exports` array (up to 10 for Deep tier, up to 5 for all other tiers). Append `()` to function names. Comma-separated.
53
+ - **api line**: Ranked, so the line stays the same from export to export: the prior snippet's `|api:` names in order, then the names of SKILL.md's `## Key API Summary` rows in row order (a component library's `## Component Catalog` rows, a reference app's `## Pattern Surface` rows), then metadata.json `exports` in order. A row's name is matched without backticks, a module qualifier or generic parameters, and a cell naming several is split on `/`. Each name once, only when `exports` lists it, and never one the current SKILL.md marks deprecated or removed (a deprecation, migration or breaking-changes bullet gives it as the old name, or its Key API Summary row's purpose says deprecated or removed). The first 10 for Deep tier, 5 for all other tiers; above the 300-token ceiling, names drop from the end. Append `()` to a name whose provenance `export_type` is a function, or else when the prior `|api:` line wrote it with `()`. Comma-separated.
54
54
  - **key-types line**: Anchor to `#key-types` section + inline summary (~10 words) of the most important type values
55
- - **gotchas line**: Derived from: T2-future annotations (breaking changes), async requirements, version-specific behavior. If no gotchas available, omit the line.
55
+ - **gotchas line**: At most 3 pitfalls, separated by `; ` (the separator step 3 splits a prior line on). Derived from the current sources: a T2-future annotation in the evidence report (breaking changes, async requirements, version-specific behavior); the current SKILL.md, through a bullet of a section whose heading names gotchas, deprecations, migration, breaking changes or import corrections, a `## Key API Summary` row whose purpose says deprecated or removed, and an async requirement its Quick Start shows; and a prior pitfall either of them still states, kept in its prior wording. The prior pitfalls still stated come first, in their prior order, then the new ones in source order. `[CARRIED]` marks only a prior pitfall no current source backs, which takes a free slot for one export (one-cycle expiry). If no gotchas are available, omit the line.
56
56
  - **stack line**: Component versions from metadata.json `components` for stack skills
57
57
  - **integrations line**: Co-import patterns from metadata.json `integrations` for stack skills
58
58
  - If fewer exports than the limit, list all available
59
59
  - If no exports data available, omit the api line
60
- - Section anchors must be verified against actual SKILL.md headings during generation. For split-body skills (where `references/` exists and `## Full` headings are stubs), if a heading is missing from SKILL.md, rewrite the anchor to point to the reference file path (preferred). Omit the anchor line only if the heading cannot be found in either SKILL.md or `references/*.md`
60
+ - Section anchors must be verified against actual SKILL.md headings during generation. Keep `SKILL.md#{slug}` when SKILL.md has the heading. Only when SKILL.md lacks it and the skill is split-body (`references/` exists and the `## Full` headings are absent or stubs; not a stack skill's structural references), point the anchor at the `references/{file}.md#{slug}` that holds it. Omit the anchor line when neither SKILL.md nor `references/*.md` holds the heading
61
61
  - Skill path is relative to project root and uses the IDE's `skill_root` prefix (see **Skill Root Path** above, and the IDE → Context File Mapping table in `managed-section-format.md`)
@@ -18,7 +18,7 @@ To generate or update context-snippet.md for the skill in the Vercel-aligned ind
18
18
 
19
19
  - Focus only on generating the context-snippet.md content — T1-now only, no T2 annotations
20
20
  - If `passive_context: false` was detected in step 1, skip this step entirely
21
- - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), iterate sections 2 to 5 per skill. Each skill has its own prior-gotchas carry-forward state (§2.5): do not share state across skills. §2.7 resolves `{skill_root}`, the snippet's root only, and §2.8 creates the stage folder once for the run (neither depends on the skill). See step 1 §1c.
21
+ - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), iterate sections 2 to 5 per skill. Each skill has its own prior snippet state (§2.5: its `prior_gotchas` carry-forward and its `prior_api`): do not share state across skills. §2.7 resolves `{skill_root}`, the snippet's root only, and §2.8 creates the stage folder once for the run (neither depends on the skill). See step 1 §1c.
22
22
  - Every HALT names its exit code, `halt_reason` and phase; in headless mode it first emits its envelope as `references/result-envelope.md` states
23
23
 
24
24
  ## MANDATORY SEQUENCE
@@ -43,11 +43,12 @@ Before generating new snippet content, check for a prior snippet:
43
43
 
44
44
  1. Read `{resolved_skill_package}/context-snippet.md` if it exists (resolved in step 1 — see `knowledge/version-paths.md`)
45
45
  2. If it exists, extract the `|gotchas:` line (if any). Trim leading whitespace and the `|gotchas:` prefix, then capture the remaining content as `prior_gotchas_content`.
46
- 3. **Detect the carry-forward marker:** If `prior_gotchas_content` starts with the token `[CARRIED]` (whitespace-insensitive), set `prior_gotchas_already_carried = true` and strip the marker before storing the remainder. Otherwise set `prior_gotchas_already_carried = false`.
46
+ 3. **Split it into pitfalls and detect the carry-forward marker:** split `prior_gotchas_content` on the pitfall separator {snippetFormatData} gives (`; `), and keep the pitfalls, in order, as `prior_gotchas`. A pitfall that starts with the token `[CARRIED]` (whitespace-insensitive) is **already carried**: strip the marker and record that. A line an older export marked once at its start (`[CARRIED] a, b`) reads as one pitfall, already carried.
47
47
  4. **Distinguish empty from absent:** If the `|gotchas:` line exists but has no non-whitespace content after the prefix, treat it as **absent** — set `prior_gotchas = null`. Only a non-empty value counts as a prior gotchas line worth carrying forward.
48
- 5. If no prior snippet exists at all, set `prior_gotchas = null` and `prior_gotchas_already_carried = false`.
48
+ 5. **Capture the prior api names:** if the prior snippet has an `|api:` line, split its value on commas, drop each name's trailing `()` and surrounding spaces, and capture the names, in order, as `prior_api` (no `()`, no spaces), noting each name the line wrote with `()`. With no `|api:` line, `prior_api` is empty.
49
+ 6. If no prior snippet exists at all, set `prior_gotchas = null` and `prior_api` to an empty list.
49
50
 
50
- These values will be used as a fallback in §3a if new gotchas cannot be derived. The `[CARRIED]` marker provides a **hard one-cycle expiry**: gotchas that were already carried once will be dropped on the next carry-forward attempt rather than preserved indefinitely.
51
+ §3a gives the gotchas line's free slots to the prior pitfalls no current source states, and §3b ranks the `prior_api` names first. The `[CARRIED]` marker provides a **hard one-cycle expiry**: a pitfall already carried once is dropped on the next export rather than preserved indefinitely.
51
52
 
52
53
  ### 2.7. Resolve Skill Root Path
53
54
 
@@ -71,28 +72,34 @@ python3 -c "import tempfile; print(tempfile.mkdtemp(prefix='skf-export-'))"
71
72
 
72
73
  #### 3a. Gotchas (Both Skill Types)
73
74
 
74
- Derive new gotchas from the T2-future annotations in the evidence report at `{forge_evidence_report}` (breaking changes), async requirements and version-specific behavior. Step 1 §2 bound that path; when it is null, the skill has no evidence report to read.
75
+ Derive the gotchas (breaking changes, deprecations, async requirements and version-specific behavior) from the current sources. A gotcha is **derived** when a current source states it:
76
+
77
+ 1. **The evidence report:** a T2-future annotation in the evidence report at `{forge_evidence_report}`, when step 1 §2 bound that path (when it is null, the skill has no evidence report to read).
78
+ 2. **The current SKILL.md** (`{resolved_skill_package}/SKILL.md`, the published, test-verified artifact): a bullet of a section whose heading names gotchas, deprecations, migration, breaking changes or import corrections (such as `## Migration & Deprecation Warnings`), a `## Key API Summary` row whose purpose says deprecated or removed, and an async requirement its Quick Start shows (an API it calls with `await`).
79
+ 3. **A prior pitfall still stated:** a pitfall of `prior_gotchas` (§2.5) that source 1 or 2 still states, kept in its prior wording without the `[CARRIED]` marker.
80
+
81
+ A gotcha the current SKILL.md states is therefore derived even when the evidence report records no T2-future annotation. `[CARRIED]` applies only to a prior pitfall no current source backs.
75
82
 
76
83
  **Detect first-export state before applying carry-forward logic.** The `[CARRIED]` one-cycle expiry is meaningful only on a *re-export*. On a first export, the prior `context-snippet.md` was written by the workflow that built or updated the skill (create-skill, create-stack-skill, quick-skill or update-skill) inside the same forge cycle: those gotchas are freshly derived, not "left over from a previous export." Treating them as carry-forward primes them for premature expiry on the second export.
77
84
 
78
85
  Read the skill's manifest entry: `python3 {manifestOpsHelper} {skills_output_folder} get {skill-name}`. When it returns `not_found`, or an `entry` none of whose `versions` records a `last_exported`, this is a first export: set `is_first_export = true`. Otherwise `is_first_export = false`. The helper returns the entry in the v2 shape whatever is on disk, so a v1 manifest reads the same here as in step 4.
79
86
 
80
- Resolve the gotchas line by this decision tree:
87
+ Resolve the gotchas line by this decision tree. The line holds at most 3 pitfalls, joined by the separator {snippetFormatData} gives (`|gotchas: {pitfall-1}; {pitfall-2}; {pitfall-3}`). A prior pitfall no current source states is **unbacked**.
81
88
 
82
- - **If new gotchas are derived:** Use them (they supersede any prior gotchas). Write as `|gotchas: {pitfall-1}, {pitfall-2}` with no marker.
83
- - **If NO new gotchas are derived AND `is_first_export == true` AND `prior_gotchas` exists:** Treat the prior gotchas as **freshly derived** by the workflow that wrote them, and write them **without** the `[CARRIED]` marker. (The marker only applies to re-exports.) No warning needed; this is the normal first-export shape.
84
- - **If NO new gotchas are derived BUT `prior_gotchas` exists AND `is_first_export == false` AND `prior_gotchas_already_carried == false`:** First carry-forward cycle on a re-export: preserve the prior gotchas line, prefixing the value with `[CARRIED]` so the next export can detect that expiry has been reached. Write as `|gotchas: [CARRIED] {prior gotchas content}`. Emit warning: "**Gotchas preserved from prior export (one-cycle carry-forward).** These gotchas will be DROPPED on the next export unless new gotchas are derived or you manually refresh them. Review now if they are still applicable."
85
- - **If NO new gotchas are derived AND `prior_gotchas` exists AND `prior_gotchas_already_carried == true`:** Expiry reached (re-export only: the first-export branch above takes precedence). Drop the gotchas line entirely. Emit warning: "**Stale gotchas dropped:** the prior gotchas were already carried forward once and cannot be derived from the current evidence report. The snippet now has no gotchas line. If the prior gotchas are still relevant, re-add them to the evidence report's T2-future section and re-run export."
86
- - **If NO new gotchas derived AND no `prior_gotchas`:** Omit the gotchas line.
89
+ - **If gotchas are derived:** Write them first, with no marker: the prior pitfalls a current source still states, in their prior order, then the new ones, in source order (source 1, then source 2 in SKILL.md order), the first 3 of them. Each slot left free goes to an unbacked prior pitfall, in prior order, by the next three branches; when no gotcha is derived, all 3 slots are free.
90
+ - **If an unbacked prior pitfall gets a free slot AND `is_first_export == true`:** Treat it as **freshly derived** by the workflow that wrote it, and write it **without** the `[CARRIED]` marker. (The marker only applies to re-exports.) No warning needed; this is the normal first-export shape.
91
+ - **If an unbacked prior pitfall gets a free slot AND `is_first_export == false` AND it is not already carried:** First carry-forward cycle on a re-export: write it as `[CARRIED] {pitfall}`, beside any derived ones, so the next export can detect that expiry has been reached. Emit warning: "**Gotchas preserved from prior export (one-cycle carry-forward):** {the pitfalls}. They will be DROPPED on the next export unless the current SKILL.md or evidence report states them. Review now if they are still applicable."
92
+ - **If an unbacked prior pitfall is already carried (expiry reached; re-export only, as the first-export branch above takes precedence) or gets no free slot:** Drop it. Emit warning: "**Stale gotchas dropped:** {the pitfalls} no longer appear in the current SKILL.md or evidence report (each was already carried forward once, or the line's 3 slots went to current gotchas). If one is still relevant, re-forge the skill with update-skill so its SKILL.md states it, or add it to the evidence report's T2-future section, then re-run export."
93
+ - **If no pitfall is written:** Omit the gotchas line.
87
94
 
88
95
  #### 3b. Single Skills (`skill_type: "single"`)
89
96
 
90
97
  1. Read metadata.json for `version`, `exports` array
91
- 2. Select top exports (up to 10 for Deep tier, 5 otherwise). Append `()` to function names.
98
+ 2. **Rank the api names**, so the `|api:` line stays the same from export to export while the surface does: first the `prior_api` names (the prior snippet's `|api:` line, §2.5), in order; then the names of the rows of SKILL.md's `## Key API Summary` table, in row order (for a component library, the Key Components of its `## Component Catalog` rows; for a reference app, the surfaces of its `## Pattern Surface` rows); then the metadata.json `exports`, in order. Match a row's name to `exports` without backticks, `()`, a module qualifier (`cognee.add` is `add`) or generic parameters (`Meta<T = Args>` is `Meta`), and split a cell that names several on `/` (a Key Components cell on commas). Take each name once, only when metadata.json `exports` lists it, and never one the current SKILL.md marks deprecated or removed: a bullet of a section whose heading names deprecations, migration or breaking changes gives it as the deprecated, removed or renamed name (not as its replacement), or its `## Key API Summary` row's purpose says deprecated or removed. Keep the first 10 for the Deep tier, 5 for the other tiers. Append `()` to a name whose `export_type` in the provenance map at `{forge_provenance_map}` is a function; when that map is null or gives no kind for the name, keep the `()` the prior `|api:` line gave it.
92
99
  3. Read SKILL.md to extract: heading slugs for `#quick-start` and `#key-types`, inline summary of key types (~10 words)
93
- 4. **Anchor verification (split-body awareness):** For each section anchor (`#quick-start`, `#key-types`), verify the heading exists in SKILL.md. If a `references/` directory exists and `## Full` headings in SKILL.md are absent or stubs (indicating split-body, not a stack skill's structural references), rewrite the anchor to point to the reference file path (e.g., `references/{file}.md#key-types`). If the heading cannot be resolved in either location, omit that anchor line from the snippet.
100
+ 4. **Anchor verification (split-body awareness):** For each section anchor (`#quick-start`, `#key-types`): when SKILL.md has the heading, keep `SKILL.md#{slug}`. Only when SKILL.md lacks the heading and the skill is split-body (a `references/` directory exists and the `## Full` headings in SKILL.md are absent or stubs; not a stack skill's structural references), point the anchor at the `references/{file}.md#{slug}` that holds the heading. When neither holds it, omit that anchor line from the snippet.
94
101
 
95
- Emit the single-skill template from {snippetFormatData} (loaded in §2), filling `api` from the exports selected above (list all if fewer than the limit; omit the line if there are none), `key-types` from the inline summary extracted above, and `gotchas` per the §3a decision tree (omit the line when it resolves to none).
102
+ Emit the single-skill template from {snippetFormatData} (loaded in §2), filling `api` from the names ranked above (all of them when fewer than the limit; omit the line when there are none), `key-types` from the inline summary extracted above, and `gotchas` per the §3a decision tree (omit the line when it resolves to none).
96
103
 
97
104
  #### 3c. Stack Skills (`skill_type: "stack"`)
98
105
 
@@ -112,7 +119,7 @@ Bind `{count}` ← the `tokens` value of its `context-snippet.md` row (`len(text
112
119
 
113
120
  - Target: ~80-120 tokens per skill (aspirational for Quick/Forge tiers)
114
121
  - Hard ceiling: 300 tokens (Deep tier may legitimately exceed 120 when gotchas carry load-bearing breaking-change notices)
115
- - If `{count}` is above 300, trim the description, the exports list or the refs, write the draft again and measure it again, until `{count}` is 300 or below. **Do NOT drop gotchas to fit**: gotchas exist precisely to deliver the "do not rely on training data" signal and are the last thing to cut
122
+ - If `{count}` is above 300, drop api names from the end of the §3b ranking, one at a time, writing the draft again and measuring it again after each, until `{count}` is 300 or below; only when the api line is gone and `{count}` is still above 300, shorten the key-types summary the same way. **Do NOT drop gotchas to fit**: gotchas exist precisely to deliver the "do not rely on training data" signal and are the last thing to cut
116
123
 
117
124
  When the helper exits non-zero, delete the `{export_stage_dir}` folder and HALT (exit code 4, `halt_reason: "context-rebuild-failed"`, phase `generate-snippet §4`): "SKF cannot measure the snippet: {the helper's error}. Re-install SKF and re-run the export."
118
125
 
@@ -11,5 +11,5 @@ The inputs, flags, gates and outputs of `skf-export-skill`, for headless callers
11
11
  | **Gates** | step 1 §1b, only when config.yaml sets no `snippet_skill_root_override`: the layout question when no earlier export chose a snippet root (headless: [I], the IDE skill folder), or the mismatch gate when an earlier export's root differs from the IDE mapping (headless: checks the disk, and takes (d), the earlier root for this run, when only that folder holds the skills, else (b), the IDE mapping); step 1 §6: one Confirm Gate [C] for the whole batch (headless: [C]); step 4 §3b orphaned context files (headless: keep) and §4c.1 orphaned managed-section rows (headless: preserve); step 4 §8: one Confirm Gate [C] for the whole batch (headless: [C]) |
12
12
  | **Outputs** | Updated `.export-manifest.json` (every skill in the batch, also when `passive_context` is off), updated context files (CLAUDE.md/AGENTS.md/.cursorrules), per-skill `context-snippet.md` (written last, after the step-4 gate), per-run result contract `export-skill-result-{YYYYMMDD-HHmmss}.json` and `export-skill-result-latest.json` (none on a dry run); in a headless run, a scratch run folder under `{project-root}/_bmad-output/.skf-run/`, which step 6 deletes (a HARD HALT keeps it) |
13
13
  | **Multi-skill mode** | Activated when more than one skill is selected (via `--all`, multi-selection, or multi-argument invocation). See `references/load-skill.md` §1c for the per-step iteration map. |
14
- | **Headless** | Every gate takes its default action and records it as it decides (SKILL.md Workflow Rules), and the run ends with one `SKF_EXPORT_RESULT_JSON` line: `references/result-envelope.md` |
14
+ | **Headless** | Every gate takes its default action and records it as it decides (SKILL.md Workflow Rules), and the run ends with one `SKF_EXPORT_RESULT_JSON` line: `references/result-envelope.md`. A caller reads that 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 `export-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
15
15
  | **Exit codes** | 0 on success and on a dry run. Each HARD HALT names its exit code and `halt_reason` where it occurs (3 and `not-skf-output` for a flat skill SKF did not generate, for example); `references/result-envelope.md` maps every halt site to its code |
@@ -99,7 +99,7 @@ The inventory helper chooses the version to export and binds its paths by the Re
99
99
  uv run {skillInventoryHelper} resolve "{skills_output_folder}" --skill {skill-name} --forge-data-folder "{forge_data_folder}"
100
100
  ```
101
101
 
102
- Bind from its `resolve` object `{resolved_version}` ← `chosen_version`, `{resolved_skill_package}` ← `skill_package`, `{forge_version}` ← `forge_version` (the version's forge folder) and `{forge_evidence_report}` ← `paths.evidence_report.path` (that folder's `evidence-report.md`, else the flat copy an older skill keeps, else null), which step 3 derives the gotchas from. Log each `resolve.errors` entry, and `resolve.manifest_error` when it is set, as an Info note, then act on `reason`:
102
+ Bind from its `resolve` object `{resolved_version}` ← `chosen_version`, `{resolved_skill_package}` ← `skill_package`, `{forge_version}` ← `forge_version` (the version's forge folder), `{forge_evidence_report}` ← `paths.evidence_report.path` (that folder's `evidence-report.md`, else the flat copy an older skill keeps, else null), which step 3 derives the gotchas from, and `{forge_provenance_map}` ← `paths.provenance_map.path` (that folder's `provenance-map.json`, else the flat copy, else null), whose `export_type` gives step 3 the api line's `()`. Log each `resolve.errors` entry, and `resolve.manifest_error` when it is set, as an Info note, then act on `reason`:
103
103
 
104
104
  1. `manifest-and-link`, `manifest` or `link`: the export manifest or the `active` link names the version, and its package is on disk. Continue.
105
105
  2. `manifest-lags-link` (the Manifest-lag guard): the `active` link names a version the manifest has not caught up with, as when a create or update run flipped `active` after the last export (the SS→TS→EX order). Continue with the link's version, and emit the helper's `detail` as an Info note, followed by "Exporting v{resolved_version}: the manifest's active_version advances to it on this export." Step 4 publishes the version in `{resolved_skill_package}/metadata.json`, so the manifest catches up here.
@@ -139,7 +139,7 @@ SKF_JSON
139
139
  ```
140
140
 
141
141
  - Pass `--run-dir` in a headless run: the emitter folds the auto-decisions the gates recorded there into `headless_decisions`, in the line and in the result file.
142
- - **Without `--dry-run`:** pass `--result-dir`, and give `status` `"success"`, `skills` (every skill in `skill_batch`), `context_files_updated` (the files step 4 §9 wrote; `[]` with passive context off), `manifest_path` (the `{manifest_path}` step 4 §9b bound) and `result_contract` (below). The emitter writes the per-run record `export-skill-result-{YYYYMMDD-HHmmss}.json` in `{skills_output_folder}` (its name from the clock, with `-2`, `-3` and so on when a run already took that second), then its copy `export-skill-result-latest.json`, the stable path pipelines read.
142
+ - **Without `--dry-run`:** pass `--result-dir`, and give `status` `"success"`, `skills` (every skill in `skill_batch`), `context_files_updated` (the files step 4 §9 wrote; `[]` with passive context off), `manifest_path` (the `{manifest_path}` step 4 §9b bound) and `result_contract` (below). The emitter writes the per-run record `export-skill-result-{YYYYMMDD-HHmmss}.json` in `{skills_output_folder}` (its name from the clock, with `-2`, `-3` and so on when a run already took that second), then its copy `export-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_EXPORT_RESULT_JSON` line.
143
143
  - **With `--dry-run`:** leave out `--result-dir` and `result_contract`, and give `status` `"dry-run"`, `skills`, `context_files_updated: []` and `manifest_path: null`. The emitter writes no file, so a dry run leaves `export-skill-result-latest.json` as the last real export wrote it.
144
144
 
145
145
  The emitter derives `exit_code` (0) and `halt_reason` (null) and sets `result_path`: leave them out. Bind `{result_path}` ← the line's `result_path` (null on a dry run). 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 the envelope as the final message. If the emitter exits non-zero, correct the payload field its stderr message names and run it once more; if it fails again, leave `{result_envelope_line}` empty, display "**Result contract not written:** {message}" and continue with `{result_path}` null.
@@ -17,7 +17,7 @@ To calculate approximate token counts for all exported artifacts and present a c
17
17
 
18
18
  - Focus only on token counting and reporting — read-only measurement
19
19
  - Auto-proceed when complete
20
- - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), run the helper once per skill, then present one aggregate table with one row per skill (context-snippet.md, SKILL.md, metadata.json, references/, package total). The `managed_section` value is identical across runs (same target files) — measure and report it once for the run. See step 1 §1c.
20
+ - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), run the helper once per skill, then present one aggregate table with one row per skill (context-snippet.md, SKILL.md, metadata.json, references/, package total) and the Context Budget Impact per skill (its `context_budget.on_trigger`, `full_disclosure` and `snippet`). The `managed_section` and `context_budget.always_on` values are identical across runs (same target files): measure and report them once for the run. See step 1 §1c.
21
21
 
22
22
  ## MANDATORY SEQUENCE
23
23
 
@@ -37,44 +37,64 @@ The helper emits JSON:
37
37
  {
38
38
  "files": [{"path": "…", "role": "…", "exists": true, "words": 0, "tokens": 0}],
39
39
  "references_total": {"count": 0, "words": 0, "tokens": 0},
40
- "managed_section": {"present": true, "source_file": "…", "words": 0, "tokens": 0},
41
- "package_total": {"words": 0, "tokens": 0}
40
+ "managed_section": {"present": true, "source_file": "…", "target_files": ["…"], "skills": 0, "words": 0, "tokens": 0},
41
+ "package_total": {"words": 0, "tokens": 0},
42
+ "context_budget": {"always_on": 0, "on_trigger": 0, "full_disclosure": 0, "snippet": 0}
42
43
  }
43
44
  ```
44
45
 
45
46
  **Read the JSON — do not re-count words in-prompt.** Each `tokens` value is a char-over-four estimate (`len(text)//4`, the SKF-wide convention shared with `skf-validate-output.py`); each `words` value is the whitespace-split count for the report's Words column. The `files` array carries one row per artifact — `role` is one of `context-snippet`, `skill-md`, `metadata`, `reference`. `package_total` already sums context-snippet.md + SKILL.md + metadata.json + `references_total` and **excludes** the `managed_section` row: the managed section is a shared all-skills cost (it bundles this skill's snippet plus every other skill's snippet), so folding it in would double-count this skill's snippet and pull in unrelated skills. It is reported separately under Context Budget Impact.
46
47
 
47
- **If `passive_context` was disabled:** the helper reports the `context-snippet.md` row with `exists: false` and `managed_section.present: false` — render both as "N/A (disabled)".
48
+ `managed_section.skills` counts the skill rows (each `[name v…]` row header, as the managed-section rebuild counts them) in the measured block. `managed_section.target_files` lists every target file that holds a block, each once, and `managed_section.source_file` names the one the helper measured, the first of them; a target file that cannot be read is skipped. `context_budget` gives the Context Budget Impact values, in tokens: `always_on` is the managed section's (null when no target file holds a block: render it as N/A), `on_trigger` SKILL.md's, `full_disclosure` SKILL.md's plus `references_total.tokens` (what an agent loads when it reads every reference too: neither the snippet nor metadata.json, so it is not `package_total`), and `snippet` the context-snippet.md row's.
49
+
50
+ **If `passive_context` was disabled:** the helper reports the `context-snippet.md` row with `exists: false` and `managed_section.present: false`: render both rows, the always-on cost and the benchmark as "N/A (disabled)".
48
51
 
49
52
  ### 2. Present Token Report
50
53
 
51
- Render the table straight from the helper JSON — no re-counting.
54
+ Render the report straight from the helper JSON, with no re-counting. In each table row, `{n}` ← the row's `words` and `{t}` ← its `tokens`:
52
55
 
53
56
  - `context-snippet.md` row → `files[].tokens` / `.words` for `role: context-snippet`
54
- - `Managed section` row → `managed_section.words` / `.tokens` (report once for the run)
57
+ - `Managed section` row → `managed_section.words` / `.tokens`; `{target-file-list}` ← `managed_section.target_files`, joined with commas; `{skill-count}` ← `managed_section.skills` (report the row once for the run)
55
58
  - `SKILL.md` row → `files[]` for `role: skill-md`
56
59
  - `metadata.json` row → `files[]` for `role: metadata`
57
- - `references/` row → `references_total.words` / `.tokens`; `{count}` files = `references_total.count`
60
+ - `references/` row → `references_total.words` / `.tokens`; `{count}` ← `references_total.count`
58
61
  - `Package total` row → `package_total.words` / `.tokens`
59
62
 
63
+ The Context Budget Impact lines, per skill in multi-skill mode (the always-on cost once for the run):
64
+
65
+ - `{managed-section-tokens}` ← `context_budget.always_on`, the managed section's tokens (N/A when null)
66
+ - `{measured-file}` ← `managed_section.source_file`, the target file the helper measured
67
+ - `{skill-tokens}` ← `context_budget.on_trigger`, SKILL.md's tokens
68
+ - `{total-tokens}` ← `context_budget.full_disclosure`, SKILL.md's tokens plus `references_total.tokens` (never `package_total`, which also counts the snippet and metadata.json)
69
+ - `{snippet-tokens}` ← `context_budget.snippet`, the context-snippet.md row's tokens
70
+ - `{skill-name}` ← `skill_package`, its last folder (multi-skill mode only)
71
+
60
72
  "**Token Report**
61
73
 
62
74
  | Artifact | Words | Est. Tokens | Notes |
63
75
  |----------|-------|-------------|-------|
64
76
  | context-snippet.md | {n} | ~{t} | Passive context (always-on) |
65
- | Managed section (all skills) | {n} | ~{t} | In {target-file-list}, all {count} skills |
77
+ | Managed section (all skills) | {n} | ~{t} | In {target-file-list}, all {skill-count} skills |
66
78
  | SKILL.md | {n} | ~{t} | Active skill (on-trigger) |
67
79
  | metadata.json | {n} | ~{t} | Machine-readable |
68
80
  | references/ | {n} | ~{t} | {count} files |
69
81
  | **Package total** | **{n}** | **~{t}** | **This skill's own artifacts (snippet + SKILL.md + metadata + references); excludes the shared Managed section row** |
70
82
 
71
83
  **Context Budget Impact:**
72
- - **Always-on cost:** ~{managed-section-tokens} tokens (managed section in {target-file-list})
84
+ - **Always-on cost:** ~{managed-section-tokens} tokens (measured in {measured-file}; the managed section is in {target-file-list})
73
85
  - **On-trigger cost:** ~{skill-tokens} tokens (when SKILL.md is loaded)
74
86
  - **Full disclosure cost:** ~{total-tokens} tokens (if references/ also loaded)
75
87
 
76
88
  **Benchmark:** Target is ~80-120 tokens per skill in managed section. Current: ~{snippet-tokens} tokens."
77
89
 
90
+ **In multi-skill mode**, render the Context Budget Impact and the Benchmark this way instead: the always-on cost once for the run, then one line per skill, each from that skill's own run:
91
+
92
+ "**Context Budget Impact:**
93
+ - **Always-on cost:** ~{managed-section-tokens} tokens (measured in {measured-file}; the managed section is in {target-file-list})
94
+ - **{skill-name}:** on-trigger ~{skill-tokens} tokens, full disclosure ~{total-tokens} tokens, snippet ~{snippet-tokens} tokens
95
+
96
+ **Benchmark:** Target is ~80-120 tokens per skill in managed section."
97
+
78
98
  ### 3. Proceed to Summary
79
99
 
80
100
  Display: "**Proceeding to export summary...**"
@@ -67,6 +67,7 @@ Say "dismiss" or "exit persona" to leave Ferris at any time.
67
67
  - Write state files only to `{sidecar_path}` and, for a pipeline, to its own run folder under `{project-root}/_bmad-output/.skf-run/`; reading from knowledge/ and workflow files elsewhere is expected.
68
68
  - When a workflow step directs knowledge consultation, consult `{project-root}/_bmad/skf/knowledge/skf-knowledge-index.csv` to select the relevant fragment(s) and load only those files. If the CSV is missing or empty, inform the user and continue without knowledge augmentation
69
69
  - Load the referenced fragment(s) from `{project-root}/_bmad/skf/` using the path in the `fragment_file` column (e.g., `knowledge/overview.md` resolves to `{project-root}/_bmad/skf/knowledge/overview.md`) before giving recommendations on the topic the step directed
70
+ - 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.
70
71
 
71
72
  ## On Activation
72
73
 
@@ -86,10 +87,10 @@ Run these steps once, in order, before the first reply.
86
87
  - No JSON object with a `status`: HARD HALT with what the call printed. When `uv` itself is missing, add that SKF runs its scripts with `uv`, which installs from <https://docs.astral.sh/uv/getting-started/installation/>.
87
88
  - `status` `ok`: bind `{project_name}`, `{user_name}`, `{communication_language}` and `{document_output_language}` from `config`, and each folder from its absolute path: `{output_folder}` from `config.output_folder_resolved`, `{skills_output_folder}` from `config.skills_output_folder_resolved`, `{forge_data_folder}` from `config.forge_data_folder_resolved` and `{sidecar_path}` from `config.sidecar_path_resolved`. The greeting reads `derived`. `sidecar.preferences_error` and `sidecar.forge_tier_error` appear only for a file that is there but did not load: name that file in one line of the greeting and go on with its defaults.
88
89
 
89
- 3. **Resolve agent customization.** Run:
90
+ 3. **Resolve agent 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:
90
91
 
91
92
  ```bash
92
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key agent
93
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key agent
93
94
  ```
94
95
 
95
96
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-forger.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. Ferris keeps no run log of his own, so that line opens the greeting.
@@ -55,7 +55,7 @@ Paths here resolve as the skf-forger SKILL.md **Conventions** say: each `uv run
55
55
  SKF_GATE
56
56
  ```
57
57
 
58
- After any other workflow, or when the gate printed no JSON, give the step's status: `completed`, or `halted` with the workflow's own halt reason (or what the gate printed):
58
+ After any other workflow, or when the gate printed no JSON, give the step's status: `completed`, or `halted` with the workflow's own halt reason (or what the gate printed). An ungated workflow (CS, BS, QS, US, EX, SS, RA, DS, RS) 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, so record it `halted` with `--reason no-result`, as the gate halts a gated one:
59
59
 
60
60
  ```bash
61
61
  uv run scripts/pipeline-journal.py step --journal "<journal>" --code <code> --status <completed|halted> [--reason '<reason>'] [--set "<name>=<value>"] [--output "<path>"]
@@ -86,4 +86,4 @@ uv run scripts/pipeline-journal.py reopen --journal "<journal>"
86
86
 
87
87
  ## Special behaviors
88
88
 
89
- - **`SF` in a sequence:** SF writes no result file; the one `SKF_SETUP_RESULT_JSON` envelope line it displays is its output. Continue on `status` `success`. Any other status halts the pipeline, with that status and any `error.reason` as the halt reason; so does a run that displays no envelope line, with the line it did display as the reason.
89
+ - **`SF` in a sequence:** SF writes no result file; the one `SKF_SETUP_RESULT_JSON` envelope line it displays is its output. Continue on `status` `success`. Any other status halts the pipeline, with that status and any `error.reason` as the halt reason; so does a run that displays no envelope line, with the line it did display as the reason, or `no-result` when it displayed none.