bmad-module-skill-forge 5.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 (91) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/agents.md +1 -1
  4. package/docs/architecture.md +3 -3
  5. package/docs/campaign.md +1 -1
  6. package/docs/troubleshooting.md +9 -3
  7. package/docs/verifying-a-skill.md +9 -9
  8. package/docs/workflows.md +4 -4
  9. package/package.json +1 -1
  10. package/src/knowledge/tool-resolution.md +6 -6
  11. package/src/shared/references/headless-gate-convention.md +4 -1
  12. package/src/shared/references/output-contract-schema.md +3 -3
  13. package/src/shared/references/pipeline-contracts.md +2 -1
  14. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
  15. package/src/shared/scripts/skf-build-change-manifest.py +332 -54
  16. package/src/shared/scripts/skf-classify-changed-files.py +15 -10
  17. package/src/shared/scripts/skf-emit-result-envelope.py +58 -15
  18. package/src/shared/scripts/skf-extract-public-api.py +33 -13
  19. package/src/shared/scripts/skf-parse-gaps.py +3 -2
  20. package/src/shared/scripts/skf-verify-provenance-completeness.py +108 -11
  21. package/src/skf-analyze-source/SKILL.md +1 -0
  22. package/src/skf-analyze-source/references/generate-briefs.md +1 -1
  23. package/src/skf-analyze-source/references/headless-contract.md +1 -1
  24. package/src/skf-audit-skill/SKILL.md +2 -1
  25. package/src/skf-audit-skill/references/report.md +1 -1
  26. package/src/skf-brief-skill/SKILL.md +1 -0
  27. package/src/skf-brief-skill/references/invocation-contract.md +2 -2
  28. package/src/skf-campaign/SKILL.md +1 -0
  29. package/src/skf-campaign/references/campaign-contracts.md +2 -0
  30. package/src/skf-campaign/references/step-05-skill-loop.md +2 -2
  31. package/src/skf-campaign/references/step-10-export.md +3 -2
  32. package/src/skf-create-skill/SKILL.md +2 -1
  33. package/src/skf-create-skill/references/ccc-index-check.md +1 -1
  34. package/src/skf-create-skill/references/component-extraction.md +1 -1
  35. package/src/skf-create-skill/references/extract.md +1 -1
  36. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  37. package/src/skf-create-skill/references/report.md +2 -2
  38. package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
  39. package/src/skf-create-skill/references/validate.md +1 -1
  40. package/src/skf-create-stack-skill/SKILL.md +1 -0
  41. package/src/skf-create-stack-skill/references/generate-output.md +1 -1
  42. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  43. package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
  44. package/src/skf-create-stack-skill/references/report.md +1 -1
  45. package/src/skf-drop-skill/SKILL.md +1 -0
  46. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  47. package/src/skf-drop-skill/references/report.md +1 -1
  48. package/src/skf-export-skill/SKILL.md +1 -0
  49. package/src/skf-export-skill/references/invocation-contract.md +1 -1
  50. package/src/skf-export-skill/references/summary.md +1 -1
  51. package/src/skf-forger/SKILL.md +1 -0
  52. package/src/skf-forger/references/pipeline-mode.md +2 -2
  53. package/src/skf-quick-skill/SKILL.md +2 -1
  54. package/src/skf-quick-skill/references/finalize.md +1 -1
  55. package/src/skf-quick-skill/references/halt-contract.md +2 -2
  56. package/src/skf-quick-skill/references/write-and-validate.md +1 -1
  57. package/src/skf-refine-architecture/SKILL.md +2 -1
  58. package/src/skf-refine-architecture/customize.toml +1 -1
  59. package/src/skf-refine-architecture/references/report.md +1 -1
  60. package/src/skf-rename-skill/SKILL.md +1 -0
  61. package/src/skf-rename-skill/references/invocation-contract.md +2 -2
  62. package/src/skf-setup/SKILL.md +1 -0
  63. package/src/skf-setup/references/ccc-index.md +1 -1
  64. package/src/skf-setup/references/invocation-contract.md +1 -1
  65. package/src/skf-test-skill/SKILL.md +1 -0
  66. package/src/skf-test-skill/assets/output-section-formats.md +4 -4
  67. package/src/skf-test-skill/references/coherence-check.md +8 -6
  68. package/src/skf-test-skill/references/coverage-check-branches.md +23 -3
  69. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  70. package/src/skf-test-skill/references/coverage-check.md +30 -43
  71. package/src/skf-test-skill/references/init.md +1 -1
  72. package/src/skf-test-skill/references/inventory-contract.md +27 -0
  73. package/src/skf-test-skill/references/invocation-contract.md +3 -3
  74. package/src/skf-test-skill/references/report.md +3 -3
  75. package/src/skf-test-skill/references/scoring-rules.md +6 -6
  76. package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
  77. package/src/skf-test-skill/references/source-access-protocol.md +12 -8
  78. package/src/skf-test-skill/references/step-hard-gate.md +1 -0
  79. package/src/skf-test-skill/scripts/build-result-context.py +20 -3
  80. package/src/skf-test-skill/scripts/gap-ledger.py +78 -13
  81. package/src/skf-test-skill/scripts/load-coverage-inputs.py +345 -9
  82. package/src/skf-test-skill/scripts/locate-export-segments.py +591 -63
  83. package/src/skf-test-skill/scripts/score-signatures.py +939 -74
  84. package/src/skf-test-skill/scripts/validate-inventory.py +245 -29
  85. package/src/skf-update-skill/SKILL.md +1 -0
  86. package/src/skf-update-skill/references/gap-driven.md +28 -22
  87. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  88. package/src/skf-update-skill/references/merge.md +4 -4
  89. package/src/skf-update-skill/references/report.md +1 -1
  90. package/src/skf-update-skill/references/write.md +5 -4
  91. package/src/skf-verify-stack/SKILL.md +2 -1
@@ -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`.
@@ -99,8 +99,8 @@ For each ready Tier A skill:
99
99
 
100
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`).
101
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.
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`, or none, fails the skill.
103
- - **CS** (`skf-create-skill`): `--headless` with that `brief_path`. CS prints an SKF_CREATE_SKILL_RESULT_JSON line (on stderr) only when it halts; that line fails the skill with its `summary.halt_reason`. On success, keep the `{skill_package}` its report names (the compiled skill's folder): §5.3 records it.
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.
104
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.
105
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.
106
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
- - **Exit code 0:** `uv run {stateScript} set-skill --state-file {stateFile} --skill {name} --export exported`, and log the result (type `event`).
98
- - **Non-zero exit:** `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` when it printed one, and log the error (type `event`: exit code, envelope or stderr). Continue with the remaining skills: one failure does not block the other exports.
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
@@ -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 (it's always `(props: XProps) => JSX.Element`).
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 extended timeout. 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.
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
 
@@ -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`, or use the ccc MCP tool. `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.
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
 
@@ -101,7 +101,7 @@ To use this skill immediately, add the context snippet to your CLAUDE.md:
101
101
 
102
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}`.
103
103
 
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 (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}`:
105
105
 
106
106
  ```bash
107
107
  grep -c . "{run_dir}/headless-decisions.jsonl" 2>/dev/null; grep -c '"gate":"zero-exports"' "{run_dir}/headless-decisions.jsonl" 2>/dev/null
@@ -118,7 +118,7 @@ uv run {emitEnvelopeHelper} emit --workflow skf-create-skill --run-dir "{run_dir
118
118
 
119
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.
120
120
 
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. 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.
122
122
 
123
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:
124
124
 
@@ -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
@@ -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
@@ -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
@@ -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`)
@@ -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 |
@@ -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.
@@ -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
 
@@ -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.
@@ -27,6 +27,7 @@ These rules apply to every step in this workflow:
27
27
  - Never fabricate content — all data must come from source extraction or user input
28
28
  - Never write into a skill folder SKF did not generate
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
  - **Universal cancel-line affordance**: at any interactive prompt the user may type `cancel`, `exit`, `:q`, or select the `[X] Cancel and exit` menu option (where surfaced) to leave cleanly. HARD HALT with **exit code 6 (user-cancelled)**: stage `{"phase": "<the step's slug>", "halt_reason": "user-cancelled", "reason": "Cancelled. No files were written.", "skill_package": null}` as `{run_dir}/halt.json` and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"` (`references/halt-contract.md`). In step 4 §6 the equivalent affordance is `[Q] Quit without writing`, with the same exit code and envelope.
32
33
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action, and record each auto-decision in the run folder the moment its gate decides, so the envelope's `headless_decisions` carries it: stage `{"gate": "<the gate's name>", "default_action": "<X>", "taken_action": "<X>", "reason": "headless: <why>"}` as `{run_dir}/decision.json` and run `uv run {emitEnvelopeHelper} record --workflow skf-quick-skill --run-dir "{run_dir}" --decision < "{run_dir}/decision.json"`. Each gate names its gate and default where it decides.
@@ -52,7 +53,7 @@ These rules apply to every step in this workflow:
52
53
  | **Overrides** | `--language-hint`, `--scope-hint`, `--description`, `--exports`, `--skip-snippet`, `--no-active-pointer`, `--batch <file>`, `--fail-fast`: see On Activation step 4 |
53
54
  | **Gates** | step 1: target input, ambiguous package name [C/n/U/X] (if another registry also holds the name; headless keeps the first registry's pick), multi-language disambiguation [C/n/A] (pick any detected language); step 2: ecosystem match [P/I/A] (none until agentskills.io has a registry API); step 3: repo-shape [C/A] + zero-exports rescue [R/P/A]; step 4: review [C/E/S/Q]; step 5: overwrite [Y/N] |
54
55
  | **Outputs** | SKILL.md, context-snippet.md, metadata.json, active pointer, result contract (timestamped + `-latest` copy), one `SKF_QUICK_SKILL_RESULT_JSON` line (stdout when the run finishes, stderr at a HARD HALT), and under `--batch` the batch summary. Snippet and active pointer can be skipped per overrides. |
55
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; each auto-decision is recorded in the envelope's `headless_decisions` |
56
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; each auto-decision is recorded in the envelope's `headless_decisions`. A caller reads the run's own `SKF_QUICK_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 the result contract's `-latest` copy may then describe an earlier run, so it is never read in its place. |
56
57
  | **Exit codes** | See `references/halt-contract.md`: the exit-code map and the envelope every HARD HALT emits, except On Activation step 1's check for `python3` and `uv`, which prints none |
57
58
 
58
59
  ## On Activation
@@ -113,7 +113,7 @@ uv run {emitEnvelopeHelper} emit --workflow skf-quick-skill --run-dir "{run_dir}
113
113
 
114
114
  This list is the summary, once: `skill_name`, `version`, `language`, `exports_documented` (the export count), `quality_score` and `validation_issues` (step 5, the object `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`), `confidence` and `repo_shape` (step 3, the shape null when it recorded none), `language_resolution` and `detected_languages` (step 1), `zero_exports_rescue` (step 3 §4.5, else null) and `active_pointer` (§1). Write each value as JSON: a string in double quotes with any `"` or `\` escaped, every path absolute with `/`, and `null` for a value the run did not record. Leave the snippet out of both `outputs` under `--skip-snippet`, and `outputs.active_pointer` out when §1 flipped no pointer.
115
115
 
116
- The emitter writes the result contract from `result_contract`, with the payload's own `status` and `summary`, so neither is typed twice (the per-run record `{skill_package}/quick-skill-result-{YYYYMMDD-HHmmss}.json` and its copy `quick-skill-result-latest.json`, the stable path for pipeline consumers) and prints one line on stdout, `SKF_QUICK_SKILL_RESULT_JSON: {...}`. In a headless single-target run, bind `{result_envelope_line}` ← that 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. Otherwise (an interactive run, or a `--batch` target) display it verbatim as its own line. If it exits non-zero, fix `result-context.json` once (its `message` names the problem) and run it again; if it still fails, or no path resolved for `{emitEnvelopeHelper}`, leave `{result_envelope_line}` empty, say that the result contract was not written and why, and go on.
116
+ The emitter writes the result contract from `result_contract`, with the payload's own `status` and `summary`, so neither is typed twice (the per-run record `{skill_package}/quick-skill-result-{YYYYMMDD-HHmmss}.json` and its copy `quick-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_QUICK_SKILL_RESULT_JSON` line) and prints one line on stdout, `SKF_QUICK_SKILL_RESULT_JSON: {...}`. In a headless single-target run, bind `{result_envelope_line}` ← that 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. Otherwise (an interactive run, or a `--batch` target) display it verbatim as its own line. If it exits non-zero, fix `result-context.json` once (its `message` names the problem) and run it again; if it still fails, or no path resolved for `{emitEnvelopeHelper}`, leave `{result_envelope_line}` empty, say that the result contract was not written and why, and go on.
117
117
 
118
118
  **Post-completion hook (optional).** If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 from `workflow.on_complete`) and the emitter wrote the result contract (the line it printed has a non-null `result_path` and no `result_file_write_failed` warning naming `quick-skill-result-latest.json`), invoke it after the result contract is finalized:
119
119
 
@@ -22,7 +22,7 @@ Every HARD HALT in this workflow exits with a stable, documented code so headles
22
22
 
23
23
  ## Result Contract on HARD HALT
24
24
 
25
- Every HARD HALT emits an **error envelope** through the shared emitter, `{emitEnvelopeHelper}`, so headless automators never meet a failed run with no result, except SKILL.md On Activation's halt for a missing `python3` or `uv`, which the emitter needs: that halt displays its message alone. SKILL.md On Activation step 1 resolved the emitter; when a compaction lost it, it is the first existing path of `{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py` and `{project-root}/src/shared/scripts/skf-emit-result-envelope.py`. Stage the halt payload as `{run_dir}/halt.json` through a quoted heredoc, then run the emitter:
25
+ Every HARD HALT emits an **error envelope** through the shared emitter, `{emitEnvelopeHelper}`, so no HARD HALT leaves a headless automator without a result line, except SKILL.md On Activation's halt for a missing `python3` or `uv`, which the emitter needs: that halt displays its message alone. SKILL.md On Activation step 1 resolved the emitter; when a compaction lost it, it is the first existing path of `{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py` and `{project-root}/src/shared/scripts/skf-emit-result-envelope.py`. Stage the halt payload as `{run_dir}/halt.json` through a quoted heredoc, then run the emitter:
26
26
 
27
27
  ```bash
28
28
  cat > "{run_dir}/halt.json" <<'SKF_JSON'
@@ -47,7 +47,7 @@ The emitter prints one line on stderr, `SKF_QUICK_SKILL_RESULT_JSON: {...}`: dis
47
47
  {skill_package}/quick-skill-result-latest.json (copy, not symlink)
48
48
  ```
49
49
 
50
- so consumers that hardcode the `-latest.json` path see a deterministic file even on failed runs. Before step 5 §1 computes `{skill_package}`, the stderr line and the exit code are the whole contract. A HALT while `{skill_package}` has no `metadata.json` (a failed first write in step 5 §2, for example) writes nothing on disk either: a package holding only result files is not SKF output, so the next run's ownership check would refuse it.
50
+ so the halt's record also becomes the `-latest.json` copy; a caller still reads the run's own `SKF_QUICK_SKILL_RESULT_JSON` line, never the copy in its place. Before step 5 §1 computes `{skill_package}`, the stderr line and the exit code are the whole contract. A HALT while `{skill_package}` has no `metadata.json` (a failed first write in step 5 §2, for example) writes nothing on disk either: a package holding only result files is not SKF output, so the next run's ownership check would refuse it.
51
51
 
52
52
  **Schema:** `shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json` (installed under `{project-root}/_bmad/skf/`).
53
53
 
@@ -72,7 +72,7 @@ If `{skill_package}/metadata.json` exists, confirm with user before overwriting:
72
72
  ```
73
73
 
74
74
  Proceed to section 2.
75
- - **If user selects N:** HARD HALT with **exit code 5 (overwrite-cancelled)**: "Overwrite cancelled. Existing skill preserved. Quick Skill names a skill after its target, so to create it under another name, brief it with `@Ferris BS` and compile it with `@Ferris CS`; or remove the existing directory yourself and re-run." Stage `{"phase": "write-and-validate", "halt_reason": "overwrite-cancelled", "reason": "Overwrite cancelled. Existing skill preserved.", "skill_package": "{skill_package}", "details": {"existing_generator": "{existing_generator}"}}` as `{run_dir}/halt.json` (`existing_generator` null when none names it) and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --run-dir "{run_dir}" --result-dir "{skill_package}" --target stderr < "{run_dir}/halt.json"`: `{skill_package}` holds `metadata.json`, so the emitter also writes the `-latest.json` envelope there, and consumers that hardcode that path see a deterministic file even on this cancelled run.
75
+ - **If user selects N:** HARD HALT with **exit code 5 (overwrite-cancelled)**: "Overwrite cancelled. Existing skill preserved. Quick Skill names a skill after its target, so to create it under another name, brief it with `@Ferris BS` and compile it with `@Ferris CS`; or remove the existing directory yourself and re-run." Stage `{"phase": "write-and-validate", "halt_reason": "overwrite-cancelled", "reason": "Overwrite cancelled. Existing skill preserved.", "skill_package": "{skill_package}", "details": {"existing_generator": "{existing_generator}"}}` as `{run_dir}/halt.json` (`existing_generator` null when none names it) and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --run-dir "{run_dir}" --result-dir "{skill_package}" --target stderr < "{run_dir}/halt.json"`: `{skill_package}` holds `metadata.json`, so the emitter also writes the `-latest.json` envelope there; a caller still reads the run's own `SKF_QUICK_SKILL_RESULT_JSON` line, never that copy in its place.
76
76
 
77
77
  **GATE [default: Y]** when `{existing_generator}` is `quick-skill` or null: if `{headless_mode}` is true, auto-proceed with Y, log "headless: overwriting existing `{skill_package}`", and record the decision: stage `{"gate": "write-and-validate.overwrite", "default_action": "Y", "taken_action": "Y", "reason": "headless: overwrote the existing package", "evidence": {"skill_package": "{skill_package}"}}` as `{run_dir}/decision.json` and run `uv run {emitEnvelopeHelper} record --workflow skf-quick-skill --run-dir "{run_dir}" --decision < "{run_dir}/decision.json"`.
78
78
 
@@ -26,6 +26,7 @@ You are an architecture refinement analyst in Ferris Architect mode; the user br
26
26
 
27
27
  - Never speculate — every gap, issue, or improvement must cite specific APIs, types, or function signatures from the generated skills
28
28
  - Only load one step file at a time — never preload future steps
29
+ - 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.
29
30
  - Always communicate in `{communication_language}`
30
31
  - At any interactive prompt, the inputs `cancel`, `exit`, `[X]`, `q`, or `:q` exit cleanly with exit code 6 (`halt_reason: "user-cancelled"`)
31
32
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action; a gate that picks its default records that decision in the run sink the moment it decides
@@ -52,7 +53,7 @@ You are an architecture refinement analyst in Ferris Architect mode; the user br
52
53
  | **Flags** | `--headless` / `-H`; `--architecture-doc <path>` and `--vs-report-path <path>` answer the step 1 prompts (`--vs-report-path none` refines without a report and skips the search for one); `--scope-skills <names>` (comma-separated skill names that replace the derived scope, so step 2 asks no scope confirmation) |
53
54
  | **Gates** | step 1: Input Gate [use args] | step 2: Scope Confirm Gate [C] continue / [X] cancel (only when a derived scope drops skills or keeps an ambiguous one) | step 5: Review Gate [R] review each refinement / [C] approve and replace the refined document / [X] cancel |
54
55
  | **Outputs** | `refined-architecture-{arch_project_name}.md` at `{outputFolderPath}` (the document's frontmatter `project_name`, else config's), promoted from a draft once the step 5 review approves it; an earlier one is first renamed to `refined-architecture-{arch_project_name}-{timestamp}.md`. Also `refine-architecture-result-{YYYYMMDD-HHmmss}.json`, its `-latest.json` copy and, once an approved review drops a finding, `.ra-dismissed-{arch_project_name}.json` |
55
- | **Headless** | Every gate takes its default when `{headless_mode}` is true: `--scope-skills` skips the step 2 scope confirmation, and without `--vs-report-path` step 1 uses the [VS] report it finds. |
56
+ | **Headless** | Every gate takes its default when `{headless_mode}` is true: `--scope-skills` skips the step 2 scope confirmation, and without `--vs-report-path` step 1 uses the [VS] report it finds. A caller reads the run's own `SKF_REFINE_ARCHITECTURE_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 `refine-architecture-result-latest.json` may then describe an earlier run, so it is never read in its place. |
56
57
 
57
58
  ## Result Contract (Headless)
58
59
 
@@ -71,7 +71,7 @@ refinement_rules_path = ""
71
71
  # Override the destination directory for the refined architecture document
72
72
  # and the files beside it: the earlier refined document a promotion renames,
73
73
  # the result files (refine-architecture-result-<YYYYMMDD-HHmmss>.json and its
74
- # -latest.json copy, the stable path pipelines read) and the record of
74
+ # -latest.json copy, the record of the last run whose emitter wrote it) and the record of
75
75
  # findings a review dropped (.ra-dismissed-<project>.json). Empty = use
76
76
  # {output_folder} from config.yaml. Useful for orgs that want
77
77
  # refined-architecture artifacts to land under a curated docs tree.
@@ -95,7 +95,7 @@ The shared emitter writes the result contract (per `shared/references/output-con
95
95
  uv run {emitEnvelopeHelper} emit --workflow skf-refine-architecture --run-dir "{run_dir}" --result-dir "{outputFolderPath}" < "{run_dir}/result-context.json"
96
96
  ```
97
97
 
98
- It writes `{outputFolderPath}/refine-architecture-result-{YYYYMMDD-HHmmss}.json` and its `refine-architecture-result-latest.json` copy (a copy, not a symlink: the stable path pipelines read), stamping the time, the run id, the run's warnings and the auto-decisions the gates recorded, and prints the `SKF_REFINE_ARCHITECTURE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. Bind `{result_path}` ← the line's `result_path`: a result file that could not be written leaves it null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run §1's `context` command and the emitter once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`: the run's result contract could not be written.
98
+ It writes `{outputFolderPath}/refine-architecture-result-{YYYYMMDD-HHmmss}.json` and its `refine-architecture-result-latest.json` copy (a copy, not a symlink: the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line), stamping the time, the run id, the run's warnings and the auto-decisions the gates recorded, and prints the `SKF_REFINE_ARCHITECTURE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. Bind `{result_path}` ← the line's `result_path`: a result file that could not be written leaves it null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run §1's `context` command and the emitter once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`: the run's result contract could not be written.
99
99
 
100
100
  ### 5. Post-Completion Hook (optional)
101
101
 
@@ -32,6 +32,7 @@ These rules apply to every step in this workflow:
32
32
  - Never allow a rename to collide with an existing skill name
33
33
  - Never rename a folder SKF did not generate: select.md §4a takes that verdict from the inventory helper
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
  - Every helper is required: select.md §1, §5's validator call and execute.md §0 halt (exit 4) without one, and no stage does a helper's write, scan or check by hand
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"`)
@@ -10,8 +10,8 @@ The inputs, flags, gate map, outputs and headless result envelope of `skf-rename
10
10
  | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--dry-run` (run selection + validation + display the step 1 §8 confirmation block, then exit with `status="dry-run"`: no run lock, no copy, no manifest re-key, no delete, no result file); `--acknowledge-official` (answers the step 1 §6 source-authority warning for this run only, so a headless run may rename a skill whose `source_authority` is `"official"`; the decision is recorded in `headless_decisions[]`). |
11
11
  | **Gates** | step 1: Input Gate [use args] x2 (§4 old name, §5 new name); Source-Authority Gate [Y/N] (§6, only for an `"official"` skill; headless default: HALT `source-authority-blocked` unless `--acknowledge-official` or the `force_source_authority_in_headless` setting); Confirm Gate [Y] (§8). Step 2 asks nothing. |
12
12
  | **Outputs** | Renamed skill directories, updated manifest, updated context files, `{new_name}/rename-skill-result-{YYYYMMDD-HHmmss}.json` (UTC) and `{new_name}/rename-skill-result-latest.json`; a halt keeps its run folder under `{project-root}/_bmad-output/.skf-run/` |
13
- | **Concurrency** | step 1 §4b takes the run lock `{forge_data_folder}/.skf-rename-{old_name}.lock` through `skf-run-lock.py`. While another rename of the same `old_name` holds a lock that has not gone stale (60 minutes after it was taken or renewed), the run HALTs with `halt_reason: "halted-for-concurrent-run"` (exit 5). Every exit releases it; `--dry-run` takes none. |
14
- | **Headless** | All gates auto-resolve with their default action when `{headless_mode}` is true. The §6 source-authority warning HALTs by default when `source_authority="official"`: pass `--acknowledge-official` to proceed for one run. The `force_source_authority_in_headless = "true"` setting, in a team or personal override of this skill (SKILL.md On Activation step 3 names the files), is a standing approval for every headless run. Either way the decision is recorded in `headless_decisions[]`. |
13
+ | **Concurrency** | step 1 §4b takes the run lock `{forge_data_folder}/.skf-rename-{old_name}.lock` through `skf-run-lock.py`. While another rename of the same `old_name` holds a lock that has not gone stale (60 minutes after it was taken or renewed), the run HALTs with `halt_reason: "halted-for-concurrent-run"` (exit 5). Every exit releases it, except a run cut off before it ends, whose lock stays until it goes stale; `--dry-run` takes none. |
14
+ | **Headless** | All gates auto-resolve with their default action when `{headless_mode}` is true. The §6 source-authority warning HALTs by default when `source_authority="official"`: pass `--acknowledge-official` to proceed for one run. The `force_source_authority_in_headless = "true"` setting, in a team or personal override of this skill (SKILL.md On Activation step 3 names the files), is a standing approval for every headless run. Either way the decision is recorded in `headless_decisions[]`. A caller reads the run's own `SKF_RENAME_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 `rename-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
15
15
  | **Exit codes** | Stable per-failure-class codes: see `references/exit-codes.md` |
16
16
 
17
17
  ## Result Contract (Headless)
@@ -27,6 +27,7 @@ You are a system executor: run each step in sequence, write the configuration fi
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
  - Communicate in `{communication_language}`.
31
32
  - If `{orphan_action}` is non-null or `{quiet_mode}` is true, resolve the step 3 orphan-removal gate without prompting; step 3 records the decision for the envelope's `warnings`.
32
33
  - When `{quiet_mode}` is true, the one line setup displays is the `SKF_SETUP_RESULT_JSON: {…}` envelope that step 4 builds, or the blocked envelope from a halt. In a standalone run that line is the run's final message, so it is all `claude -p` prints (step 4 holds it until then).
@@ -39,7 +39,7 @@ If `{ccc}` (from step 1) is false, go to section 3 with no output. Otherwise con
39
39
 
40
40
  Unless `{quiet_mode}` is true, display: "**Preparing the ccc settings, then the semantic index if it needs building: a first index can take several minutes on large codebases (1000+ files). Run `ccc status` in another terminal to monitor progress.**"
41
41
 
42
- Pass `{ccc_index_fresh}` (from step 1) and `{ccc_skip_index}` as `true` or `false`, and add `--defer-index` only when `{require_tier_satisfied}` (from step 1) is `false`: a run that ends `tier_failure` then builds no index that is due, while a fresh index record still carries forward. Give the call an extended timeout (or run it in the background and wait for it to finish):
42
+ Pass `{ccc_index_fresh}` (from step 1) and `{ccc_skip_index}` as `true` or `false`, and add `--defer-index` only when `{require_tier_satisfied}` (from step 1) is `false`: a run that ends `tier_failure` then builds no index that is due, while a fresh index record still carries forward. Run the call 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 goes on as the paragraph after the command says:
43
43
 
44
44
  ```bash
45
45
  uv run {mergeCccExclusionsHelper} \
@@ -10,5 +10,5 @@ The flags, gate, outputs, headless contract and failure modes of `skf-setup`, fo
10
10
  | **Flags** | `--headless` / `-H` (skip prompts, auto-resolve gates to defaults); `--quiet` (an alias of `--headless`; its step 3 orphan default is recorded as `quiet-default`); `--require-tier=<Quick\|Forge\|Forge+\|Deep>` or `--require-tier <tier>` (halt with failure if calculated tier does not satisfy the requirement; any other value halts the run `blocked`, see Failure modes); `--orphan-action=<keep\|remove>` (resolve the orphan-removal gate non-interactively, even outside `--headless`; any other value halts at activation); `--ccc-skip-index` (skip building the CCC index: settings.yml is still prepared and its SKF exclusions kept current, and the envelope's `ccc_index.status` becomes `"skipped"`; the fast re-probe lane for an expert re-running only to refresh the detected tier without paying the full ccc re-index cost) |
11
11
  | **Gates** | One optional: orphaned QMD collection removal (step 3, Deep tier only; default: Keep, or whatever `--orphan-action` set; resolves without prompting under `--headless` or `--quiet`). Only a collection with a forge suffix that the registry does not list and whose QMD path lies inside this project is offered: another project's collections never are |
12
12
  | **Outputs** | `{sidecar_path}/forge-tier.yaml` and `{forge_data_folder}/` (setup never writes `preferences.yaml`: the installer creates it, and setup reads it when it is there); when ccc is available, `.cocoindex_code/settings.yml` (created by `ccc init` when missing; SKF exclusion patterns merged and stale ones removed), a `/.cocoindex_code/` line in `.gitignore` when `ccc init` adds one, and the project ccc index; for the run itself, a scratch folder under `{project-root}/_bmad-output/.skf-run/` holding the helpers' outputs, which step 4 deletes (a halt that names a phase leaves it in place) |
13
- | **Headless** | All gates auto-resolve with their default action under `--headless` or its alias `--quiet`, under `headless_mode: true` in `{sidecar_path}/preferences.yaml` (known once activation has loaded it, so the halts before that emit an envelope only under a flag or in a pipeline), and in a forger pipeline. Under `--headless`, `--quiet` or `headless_mode: true`, a standalone run's final message, and so everything `claude -p` prints, is exactly one line: the single-line `SKF_SETUP_RESULT_JSON: {…}` envelope from step 4 (displayed once the health check has finished, or at the tier-miss halt), or a blocked envelope from a halt, or the bare halt reason when no envelope helper can run (see Failure modes). Setup displays no FORGE STATUS banner or health-check output, and no halt diagnostic beyond that one line; the health check still runs and saves any findings to the local improvement queue. Inside a forger pipeline (`{pipeline_mode}` true), setup displays the same line and returns control to the forger. Branch on the envelope's top-level `status` field: `success`, `tier_failure` (require-tier miss), or `blocked` (any pre-report halt, whose `error.phase` names the cause). `skf-emit-result-envelope.py` derives `status` from the run's staged outputs, so pipelines never compose it from `require_tier_satisfied` + `error`. Schema: `shared/scripts/schemas/skf-setup-result-envelope.v1.json`. |
13
+ | **Headless** | All gates auto-resolve with their default action under `--headless` or its alias `--quiet`, under `headless_mode: true` in `{sidecar_path}/preferences.yaml` (known once activation has loaded it, so the halts before that emit an envelope only under a flag or in a pipeline), and in a forger pipeline. Under `--headless`, `--quiet` or `headless_mode: true`, a standalone run's final message, and so everything `claude -p` prints, is exactly one line: the single-line `SKF_SETUP_RESULT_JSON: {…}` envelope from step 4 (displayed once the health check has finished, or at the tier-miss halt), or a blocked envelope from a halt, or the bare halt reason when no envelope helper can run (see Failure modes). Setup displays no FORGE STATUS banner or health-check output, and no halt diagnostic beyond that one line; the health check still runs and saves any findings to the local improvement queue. Inside a forger pipeline (`{pipeline_mode}` true), setup displays the same line and returns control to the forger. Branch on the envelope's top-level `status` field: `success`, `tier_failure` (require-tier miss), or `blocked` (any pre-report halt, whose `error.phase` names the cause). `skf-emit-result-envelope.py` derives `status` from the run's staged outputs, so pipelines never compose it from `require_tier_satisfied` + `error`. Schema: `shared/scripts/schemas/skf-setup-result-envelope.v1.json`. 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, as when no helper can build one (Failure modes), and a caller treats it as a failure. |
14
14
  | **Failure modes** | `--require-tier` not satisfied → status `tier_failure`, the envelope sets `"require_tier_satisfied": false`, and the workflow halts before step 5 (interactive runs also print a "REQUIRED TIER NOT MET" block). Such a run fails fast: it builds no ccc index that is due (`ccc_index.status` `"skipped"`, while a fresh index record still carries forward) and runs no registry hygiene, so it removes no QMD collection, even under `--orphan-action=remove`; it still writes `forge-tier.yaml` with the detected tier. A `--require-tier` value that is not exactly one of the four tiers is no miss: the detector rejects it, and the run ends `blocked` at `step 1:detect-tools` with a reason that names the valid tiers (with a did-you-mean hint for a wrong-case tier). A write failure (forge-tier.yaml or the forge data folder could not be written) halts step 2 with a `blocked` envelope whose `error.phase` (`step 2:write-tools` or `step 2:forge-data-dir`) and `error` name the path and reason. The other halts (uv missing, config missing, malformed or naming no usable `sidecar_path`, an `--orphan-action` other than `keep` or `remove`, a run folder that cannot be created, a helper that no probe path resolves, a failed tool detection) end the same way under `--headless` or `--quiet`: `error.phase` names the halt and `error.reason` carries the diagnostic. The exception is a run where the envelope helper itself cannot run (SKF's scripts are not installed in the project, as in a directory that is not an SKF project, or an On Activation halt on a machine where none of `uv`, `python3`, `python` and `py -3` can run it): its one line is the bare halt reason with no envelope, and a pipeline treats a missing envelope as a failure. |
@@ -29,6 +29,7 @@ These rules apply to every step in this workflow:
29
29
 
30
30
  - Zero hallucination — every finding must trace to actual code with file:line citations
31
31
  - Only load one step file at a time — never preload future steps
32
+ - 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.
32
33
  - Update `stepsCompleted` in output file frontmatter before loading next step
33
34
  - Always communicate in `{communication_language}`
34
35
  - No stage waits for an answer in headless mode: the one question, the skill name, halts `input-missing` there