bmad-module-skill-forge 3.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/architecture.md +2 -2
- package/docs/concepts.md +4 -4
- package/docs/examples.md +33 -26
- package/docs/getting-started.md +3 -3
- package/docs/how-it-works.md +2 -2
- package/docs/index.md +1 -1
- package/docs/skill-model.md +41 -39
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +46 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +5 -5
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +12 -4
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +1437 -178
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
- package/src/shared/scripts/skf-extract-public-api.py +1673 -189
- package/src/shared/scripts/skf-extraction-inventory.py +584 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-parse-gaps.py +135 -26
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-resolve-customization.py +197 -0
- package/src/shared/scripts/skf-shard-body.py +239 -41
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +1439 -79
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +2 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +3 -3
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +4 -11
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-campaign/SKILL.md +2 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +7 -7
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +23 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +10 -11
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +21 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +2 -2
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-export-skill/SKILL.md +2 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -2
- package/src/skf-refine-architecture/SKILL.md +6 -8
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-setup/SKILL.md +2 -2
- package/src/skf-test-skill/SKILL.md +2 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +5 -4
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +69 -111
- package/src/skf-test-skill/references/init.md +5 -6
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +11 -9
- package/src/skf-test-skill/references/scoring-rules.md +8 -6
- package/src/skf-test-skill/references/source-access-protocol.md +15 -18
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +580 -80
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +646 -107
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +450 -23
- package/src/skf-test-skill/scripts/score-signatures.py +356 -61
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +2 -2
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +37 -22
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +17 -12
- package/src/skf-update-skill/references/re-extract.md +4 -2
- package/src/skf-update-skill/references/report.md +7 -4
- package/src/skf-update-skill/references/write.md +77 -36
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
- package/src/skf-verify-stack/SKILL.md +2 -2
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
Source reading via gh_bridge — infer exports from file structure and content.
|
|
6
6
|
|
|
7
7
|
### Strategy
|
|
8
|
-
1.
|
|
9
|
-
2.
|
|
10
|
-
3.
|
|
11
|
-
4.
|
|
12
|
-
5. Infer types from JSDoc, docstrings, type annotations in source
|
|
8
|
+
1. Files, entry points: step 3 §2's `files_in_scope_list`, `files_without_recipes_list`, `entry_points.files`
|
|
9
|
+
2. Read each file that defines public API, locally or with `gh_bridge.read_file(owner, repo, path)`
|
|
10
|
+
3. Extract: exported function names, parameter lists, return types (from signatures)
|
|
11
|
+
4. Infer types from JSDoc, docstrings, type annotations in source
|
|
13
12
|
|
|
14
13
|
### Confidence
|
|
15
14
|
- Every export is read by eye, so every entry is T1-low (source reading without structural verification): a `[SRC:...]` citation, `extraction_method: source-read` and `ast_node_type: null`
|
|
@@ -111,14 +110,14 @@ When AST tools are available (Forge, Forge+ and Deep tiers) and the step that ru
|
|
|
111
110
|
- **Head cap:** each recipe keeps at most 200 matches, or at Forge+ and Deep 500 for `scope.type: "full-library"` and 300 for `scope.type: "component-library"`, in file and line order. `truncated` is true, and so is that recipe's `truncated` in `recipes[]`, when a recipe matched more: the matches past the cap are missing, so warn about them.
|
|
112
111
|
- **Each export** is one name in one file, and gives `export_name`, `source_file`, `source_line` (`$NAME`'s line), `signature_line` (the source line holding it), `citation` (`[AST:{file}:L{line}]`), `ast_recipe`, `ast_node_type` (the `kind` its recipe declares), `export_type`, `from` (a re-export's module), `signature` (the declaration on one line), `params`, `return_type`, `confidence: T1` and `extraction_method: ast-grep`. Record them as they are.
|
|
113
112
|
- **Signatures:** a function's `params` lists each parameter (its name, type, default and whether it is optional), and is null for any other export and for a declaration the runner does not parse; `return_type` is null when the declaration states none.
|
|
114
|
-
- **Entry points:** `entry_point_diff` diffs the names the
|
|
115
|
-
- **Read by eye (T1-low)** only what it leaves: each file in `file_issues` (a syntax error where a recipe can miss an export, or a file that is not UTF-8, unreadable or missing), the files `files_without_recipes` counts that hold public API (by the Quick tier Strategy), the forms Known Limitation #11 below lists, and the `extraction_gaps` names.
|
|
113
|
+
- **Entry points:** `entry_point_diff` diffs the names the entry points export with the recipe matches (`public`, `other_definitions`, `internal`, `extraction_gaps`, `outside_scope`; a `namespace` item's `members` stand in for it), `counts` gives `exports_public_api`, `exports_internal` and `effective_denominator`, and `arms` the monorepo, specific-modules and multi-subpath `exports` flags.
|
|
114
|
+
- **Read by eye (T1-low)** only what it leaves: each file in `file_issues` (a syntax error where a recipe can miss an export, or a file that is not UTF-8, unreadable or missing), the files `files_without_recipes` counts that hold public API (by the Quick tier Strategy), the forms Known Limitation #11 below lists, the `unread` files of a rerun, and the `extraction_gaps` names.
|
|
116
115
|
|
|
117
|
-
Act on its `status`, not its exit code: `ok`, every file in scope was read; `incomplete`, an ast-grep run failed or timed out: keep its exports and
|
|
116
|
+
Act on its `status`, not its exit code: `ok`, every file in scope was read; `incomplete`, an ast-grep run failed or timed out: run the step's command once more, and when it is still incomplete keep its exports and read by eye each file an `errors[]` item lists in `unread`; `no-ast-grep`, no ast-grep it can run, so no recipe ran.
|
|
118
117
|
|
|
119
118
|
### When the Runner Cannot Run
|
|
120
119
|
|
|
121
|
-
There is no runner result when the step gives no runner command, no runner path resolves, or no JSON is at the `-o` path after the call (`uv` missing or failing, an input error, which prints one line on stderr, a crash, or a shell timeout that stopped it). Then, and when its `status` is `no-ast-grep`, run the recipes as **Running the Recipes Without the Runner** below says, on the filtered file list: read this file on from that heading (a run the runner serves stops there), and first load `extraction-patterns-by-hand.md`, beside this file, completely: it holds each recipe's notes and the limitations of patterns run by hand. When no ast-grep can run them either (no `ast-grep` CLI and no ast-grep MCP tool), extraction degrades to source reading: see `tier-degradation-rules.md` "AST Tool Unavailable". When the runner's `scope.languages` is empty, whatever its `status`, no recipe reads the brief's language (the runner warns `no recipe reads {language} files` and reads no file): extract by the Quick tier Strategy (source reading, T1-low).
|
|
120
|
+
There is no runner result when the step gives no runner command, no runner path resolves, or no JSON is at the `-o` path after the call (`uv` missing or failing, an input error, which prints one line on stderr, a crash, or a shell timeout that stopped it). Then, and when its `status` is `no-ast-grep`, run the recipes as **Running the Recipes Without the Runner** below says, on the filtered file list: read this file on from that heading (a run the runner serves stops there), and first load `extraction-patterns-by-hand.md`, beside this file, completely: it holds each recipe's notes and the limitations of patterns run by hand. When no JSON is there and a stderr line holds `error:` (the runner's input error or crash, or `uv` failing; a shell timeout prints none), warn "**Recipe runner failed:** {that line}. Extraction ran without it." and keep the warning with the step's other warnings. When no ast-grep can run them either (no `ast-grep` CLI and no ast-grep MCP tool), extraction degrades to source reading: see `tier-degradation-rules.md` "AST Tool Unavailable". When the runner's `scope.languages` is empty, whatever its `status`, no recipe reads the brief's language (the runner warns `no recipe reads {language} files` and reads no file): extract by the Quick tier Strategy (source reading, T1-low).
|
|
122
121
|
|
|
123
122
|
**"Files in scope"** are the files the brief's include and exclude globs keep, filtered by the language's extensions, never the repository's whole file count: use the filtered count from step 3 section 2 as the decision tree input.
|
|
124
123
|
|
|
@@ -136,9 +135,9 @@ Read the forms #11 lists by eye whether or not the runner ran:
|
|
|
136
135
|
11. **Forms the recipes deliberately do not cover (ast-grep 0.45.3):** read these by eye at T1-low when the source uses them, or follow them with the Re-Export Tracing protocol in `extraction-patterns-tracing.md` where noted.
|
|
137
136
|
- **Every JS/TS `export_statement` recipe:** `export` followed by a line break before the declaration (tree-sitter reads the bare `export` as a statement of its own, so there is no `export_statement` to match; formatters never write it); an export nested in a `namespace`, `declare module` or `declare global` block, a `.d.ts` file's `declare module "pkg" { ... }` included, since it is a member, not a module export.
|
|
138
137
|
- **`js-exported-functions`, `react-component-functions`:** an anonymous `export default function () {}` (#3); a function exported through a list (`export { f }` and `export { f as g }`, which `js-local-exports` finds at the list's line), `export default f;` or `export = f`; a function bound to a `const` (the constants and arrow-function recipes find it); a wrapped default such as `export default memo(function Name() {})` or `forwardRef(...)`, whose value is a call, not a declaration; class, object-literal and interface methods; the implementation of an overloaded function (its signatures match instead). The React recipes also skip generator functions (an overload signature of one still matches, since a signature does not show it) and class components and keep a PascalCase function that is not a component (such as a Next.js `GET` handler).
|
|
139
|
-
- **`js-exported-constants`, `js-exported-arrow-functions`, `react-component-arrow-functions`, `react-wrapped-components`:** every declarator after the first one a recipe accepts in `export const a = 1, b = () => 2` (one match per statement); destructuring (`export const { x } = obj
|
|
138
|
+
- **`js-exported-constants`, `js-exported-arrow-functions`, `react-component-arrow-functions`, `react-wrapped-components`:** every declarator after the first one a recipe accepts in `export const a = 1, b = () => 2` (one match per statement); destructuring (`export const { x } = obj`: the runner lists in `extraction_gaps` the public names of a pattern that opens the statement; read a later or unreadable pattern by eye, as quick mode asks); `let`, `var` and `export declare const`. The arrow-function recipes also skip an arrow wrapped in parentheses, `as`, `satisfies` or a call, and a `function` expression; the constants recipe reports them, and `react-wrapped-components` reports a PascalCase `memo`, `forwardRef` or `lazy` call. `react-wrapped-components` skips any other wrapper (`observer(...)`, `styled.div`, `connect(...)(Component)`), a wrapped call cast with `as` or `satisfies`, and `export default memo(...)`, whose exposed name is `default`.
|
|
140
139
|
- **`js-exported-classes`, `react-props-interfaces`:** `export default class` and `export default interface` (their exposed name is `default`); `export declare class`, and `export declare interface` (`ts-exported-types` finds it); a class expression bound to a `const` (the constants recipe reports it); a props type written as a type alias (`export type XProps = {...}`, which `ts-exported-types` finds); a type exported through a list (`export type { T }`, which `js-local-exports` or `js-reexports` finds).
|
|
141
|
-
- **`ts-exported-types`:** `export default interface` (its exposed name is `default`);
|
|
140
|
+
- **`ts-exported-types`:** `export default interface` (its exposed name is `default`); a type exported through a list (`export type { T }`, `export { type T }`), which `js-local-exports` finds, or `js-reexports` when the list has a `from`.
|
|
142
141
|
- **`js-reexports`, `js-namespace-reexports`, `js-local-exports`:** a bare `export * from '...'`, which exposes no name of its own; an `export { a } from '...' with { type: 'json' }` statement, or the legacy `export { a } from '...' assert { type: 'json' }`, which parse as ERROR nodes; `export default x;` and `export = x`; a list inside a `namespace`, `declare module` or `declare global` block, a member rather than a module export; in a `.js` file, `default` as a list item's name or alias (`export { default } from '...'`, `export { x as default }`), a keyword token there that no `$NAME` captures (TypeScript parses it as a name). Follow these with the Re-Export Tracing protocol.
|
|
143
142
|
- **`rust-public-functions`:** a `pub fn` in an `impl` block (#8); trait methods, which carry no `pub`; `pub fn` imports in an `extern "C" { }` block; functions a macro generates (`macro_rules!` and `cfg_if!` bodies are token trees, which ast-grep does not parse). One file cannot show reachability across files: a `pub fn` at the top of `foo.rs` matches whether or not `mod foo;` is `pub`, so check `pub use` re-exports with the Re-Export Tracing protocol. `#[cfg]`-gated and `#[doc(hidden)]` functions still match, and a raw identifier keeps its prefix (`r#match`).
|
|
144
143
|
- **`go-exported-functions`:** methods; an exported function-typed variable (`var F = func() {}`). `Test*`, `Benchmark*` and `Example*` functions in `_test.go` files, and the exported functions of `package main` or an `internal/` package, match but are not importable API: exclude them through `scope.exclude`.
|
|
@@ -151,7 +151,7 @@ python3 {atomicWriteHelper} flip-link \
|
|
|
151
151
|
--target {version}
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
The helper
|
|
154
|
+
The helper exits 2, with a `{"status": "error", "message": …}` line on stderr, when `{skill_group}/active` already exists as a real directory or file rather than a symlink, or when it cannot take the flip lock on `{skill_group}/active.skf-lock`. In that case **HARD HALT** (exit code 5, `active-link-blocked`, phase `generate-artifacts`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --result-dir "{forge_version}" --target stderr < "{run_dir}/halt.json"`), with `"skill_package"`, `"outputs"` and `{skill_group}/active` as `"path"`: "Refusing to flip `{skill_group}/active`: {the `message` of the helper's stderr line, else its stderr}. Investigate manually; expected a symlink pointing at a version directory, with no other flip running."
|
|
155
155
|
|
|
156
156
|
Do not `rm` + `ln -s` the active link by hand. The bare-rm pattern has two failure modes: (1) a concurrent reader sees a missing `active` mid-flip, and (2) a bug or typo that replaces `{skill_group}/active` with a plain directory turns the next manual `rm -rf {skill_group}/active` into data loss. The helper encapsulates both guards.
|
|
157
157
|
|
|
@@ -7,6 +7,14 @@ nextStepFile: 'health-check.md'
|
|
|
7
7
|
atomicWriteProbeOrder:
|
|
8
8
|
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
9
9
|
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
10
|
+
# The result-contract emitter (Rules).
|
|
11
|
+
emitEnvelopeProbeOrder:
|
|
12
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py'
|
|
13
|
+
- '{project-root}/src/shared/scripts/skf-emit-result-envelope.py'
|
|
14
|
+
# The customization helper §5's post-completion hook runs.
|
|
15
|
+
resolveCustomizationProbeOrder:
|
|
16
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py'
|
|
17
|
+
- '{project-root}/src/shared/scripts/skf-resolve-customization.py'
|
|
10
18
|
---
|
|
11
19
|
|
|
12
20
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -19,7 +27,9 @@ To display the final compilation summary (skill name, version, source, export co
|
|
|
19
27
|
|
|
20
28
|
## Rules
|
|
21
29
|
|
|
22
|
-
- Write only the brief's result files (through the emitter, §5) and, outside `--batch`, the removal of the brief's run folder. Change no skill artifact, except in §5's fallback when `{
|
|
30
|
+
- Write only the brief's result files (through the emitter, §5) and, outside `--batch`, the removal of the brief's run folder. Change no skill artifact, except in §5's fallback when no path in `{emitEnvelopeProbeOrder}` exists: `validation_status` in `{skill_package}/metadata.json` and one warning appended to `{forge_version}/evidence-report.md`
|
|
31
|
+
- §5 emits through `{emitEnvelopeHelper}`, resolved from `{emitEnvelopeProbeOrder}` when it is not bound.
|
|
32
|
+
- §5's post-completion hook runs `{resolveCustomizationHelper}`, resolved from `{resolveCustomizationProbeOrder}` when it is not bound.
|
|
23
33
|
- Deliver structured report with confidence breakdown
|
|
24
34
|
- The user-facing report is not the end of the run: §5 and §6 follow it in order, and §6 chains to the health check via `{nextStepFile}`, or under `--batch` back to `references/batch-mode.md`, which runs it once the last brief is recorded
|
|
25
35
|
|
|
@@ -91,7 +101,7 @@ To use this skill immediately, add the context snippet to your CLAUDE.md:
|
|
|
91
101
|
|
|
92
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}`.
|
|
93
103
|
|
|
94
|
-
**Write the result contract through the emitter.** `{emitEnvelopeHelper}`
|
|
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}`:
|
|
95
105
|
|
|
96
106
|
```bash
|
|
97
107
|
grep -c . "{run_dir}/headless-decisions.jsonl" 2>/dev/null; grep -c '"gate":"zero-exports"' "{run_dir}/headless-decisions.jsonl" 2>/dev/null
|
|
@@ -108,15 +118,21 @@ uv run {emitEnvelopeHelper} emit --workflow skf-create-skill --run-dir "{run_dir
|
|
|
108
118
|
|
|
109
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.
|
|
110
120
|
|
|
111
|
-
**If `{
|
|
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.
|
|
112
122
|
|
|
113
|
-
**Post-completion hook.** When `{onCompleteCommand}` is
|
|
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
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow.on_complete
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
When `{onCompleteCommand}` is non-empty and the emitter wrote this brief's result contract (its `emit` exited 0), run it as a shell command from `{project-root}`, for this brief:
|
|
114
130
|
|
|
115
131
|
```bash
|
|
116
132
|
{onCompleteCommand} --result-path={forge_version}/create-skill-result-latest.json
|
|
117
133
|
```
|
|
118
134
|
|
|
119
|
-
The hook runs after the contract, so a git-add, registry registration or notifier sees a complete package. A hook failure never fails the brief, since the skill is written and its result contract is final: display "on_complete failed for `{name}`: {its exit code and first stderr line}" and go on. When the emitter wrote no result contract for this brief (no `{
|
|
135
|
+
The hook runs after the contract, so a git-add, registry registration or notifier sees a complete package. A hook failure never fails the brief, since the skill is written and its result contract is final: display "on_complete failed for `{name}`: {its exit code and first stderr line}" and go on. When the emitter wrote no result contract for this brief (no path in `{emitEnvelopeProbeOrder}` exists, or `emit` failed twice), `--result-path` would name a missing file or an earlier compile's: skip the hook and display "on_complete skipped for `{name}`: no result contract was written ({the reason})". When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
|
|
120
136
|
|
|
121
137
|
**Remove the run folder.** The brief finished, so outside `--batch` delete `{run_dir}` (`rm -rf "{run_dir}"`); under `--batch`, batch-mode.md §3 reads the staged `result-context.json` there and removes the folder itself. A halted brief keeps its folder, with the decisions it recorded and its staged payloads.
|
|
122
138
|
|
|
@@ -36,7 +36,7 @@ Outcomes, from `tag_resolution.status`:
|
|
|
36
36
|
|
|
37
37
|
### Implicit Tag Resolution (when only brief.version is set)
|
|
38
38
|
|
|
39
|
-
When `brief.target_version` is absent but `brief.version` is present, `brief.version` is an **implicit** target version. This matches `brief-skill`'s behavior, which
|
|
39
|
+
When `brief.target_version` is absent but `brief.version` is present, `brief.version` is an **implicit** target version. This matches `brief-skill`'s behavior, which fills `brief.version` from the source's manifest or version files or, when they hold none, its latest release tag, so a tag matching `brief.version` is the common case, and silently reading HEAD would produce a skill labeled with `brief.version` but built from an unrelated default-branch commit.
|
|
40
40
|
|
|
41
41
|
With `--implicit`, resolve tries only two forms, in this order:
|
|
42
42
|
|
|
@@ -53,7 +53,7 @@ Outcomes, from `tag_resolution.status`:
|
|
|
53
53
|
|
|
54
54
|
**Do not halt on zero matches.** Implicit resolution never blocks compilation: `brief.version` is an auto-populated hint, and some repositories simply do not tag releases. The warning is sufficient notice; the evidence report in step 8 will surface the HEAD fallback for reviewers.
|
|
55
55
|
|
|
56
|
-
**Interaction with Version Reconciliation (below):** When implicit tag resolution matches a tag, the tree's
|
|
56
|
+
**Interaction with Version Reconciliation (below):** When implicit tag resolution matches a tag, the tree's version file should carry `brief.version`, unless it holds a release-time placeholder, which the extractor reports as no version, so the Version Reconciliation section's source-vs-brief mismatch warning will not fire. When implicit resolution falls back to HEAD, Version Reconciliation runs normally against the default branch's version file and may produce its own mismatch warning.
|
|
57
57
|
|
|
58
58
|
### Several Matching Tags
|
|
59
59
|
|
|
@@ -114,23 +114,18 @@ Also store `source_ref` in context (from tag resolution above, or `HEAD` if no t
|
|
|
114
114
|
|
|
115
115
|
**If `source_type: "docs-only"`:** skip this section — no source files exist to reconcile.
|
|
116
116
|
|
|
117
|
-
After the source path is accessible (local path from step 1, or the private tree from above),
|
|
117
|
+
After the source path is accessible (local path from step 1, or the private tree from above), read the source's version with `{extractPublicApiHelper}` (resolve it ← first existing path in `{extractPublicApiProbeOrder}`), never from a version file by eye: it reports a release-time placeholder such as `0.0.0-development`, and a value that is not a string, as no version. The version file is, in the source root, `pyproject.toml`, else `setup.py`, else `setup.cfg` for Python and `Cargo.toml` for Rust; for JavaScript or TypeScript, the `package.json` whose `name` matches `brief.name`, else one with a `bin` field, else the root one. Any other language, or a source that holds none of them, keeps `brief.version`: skip the rest of this section. From `{project-root}`, run:
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
2. Package with a `bin` field (CLI entry point — represents the published version)
|
|
123
|
-
3. Root workspace `package.json` version (if present)
|
|
124
|
-
4. Fall back to `brief.version` if no version found. For monorepos using workspace protocols (pnpm, yarn, npm workspaces), the root `package.json` often has no `version` field — this is expected, not an error.
|
|
125
|
-
- Rust: `Cargo.toml` (`[package] version`)
|
|
126
|
-
- Go: `go.mod` (module version if tagged)
|
|
119
|
+
```bash
|
|
120
|
+
uv run {extractPublicApiHelper} --mode quick --language "{brief.language}" --source-root "{source_root}" --manifest-file "<version file>" --resolve-version
|
|
121
|
+
```
|
|
127
122
|
|
|
128
|
-
**
|
|
123
|
+
**A remote source with no local tree** (Quick tier, or one §2b could not read into a tree): first stage the version file with `{githubFetchHelper}` (resolve it ← the first existing path of `{project-root}/_bmad/skf/shared/scripts/skf-github-fetch.py` and `{project-root}/src/shared/scripts/skf-github-fetch.py`), then run the call above with `--source-root "{run_dir}/files"`:
|
|
129
124
|
|
|
130
|
-
|
|
125
|
+
```bash
|
|
126
|
+
gh api "repos/{owner}/{repo}/git/trees/{source_ref}?recursive=1" --jq '.tree[] | select(.type == "blob") | .path' | uv run {githubFetchHelper} --repo "{owner}/{repo}" --ref "{source_ref}" --tree-file - --dest "{run_dir}/files" "<version file>"
|
|
127
|
+
```
|
|
131
128
|
|
|
132
|
-
|
|
129
|
+
**The source version** is the JSON's `version` when its `version_source` is `manifest` or starts with `file:`. Keep `brief.version`, with no mismatch warning, when `version` is null or `version_source` starts with `tag:` (no file of the source states that version), and when a call exits non-zero, prints no JSON or resolves no path. Record the JSON's `warnings` in context for the evidence report (step 8).
|
|
133
130
|
|
|
134
|
-
**If
|
|
135
|
-
|
|
136
|
-
**If source is remote and accessed via Quick tier (gh_bridge, no local files):** attempt to read the version file via `gh_bridge.read_file(owner, repo, "{version_file}")` — resolved as `gh api repos/{owner}/{repo}/contents/{version_file}` or direct file read if local (see `knowledge/tool-resolution.md`) — for the primary version file of the detected language. If the read fails, keep `brief.version`.
|
|
131
|
+
**If the source version differs from `brief.version`:** ⚠️ Warn the user: "Brief version ({brief.version}) differs from source version ({source_version}). Using source version ({source_version})." Update the working version in context to the source version, and record the mismatch in context for the evidence report (step 8).
|
|
@@ -207,7 +207,7 @@ SKF_T3
|
|
|
207
207
|
**Zero-content check.** Run `uv run {extractionInventoryHelper} summary --inventory "{extraction_inventory}"` from `{project-root}` and read its `counts`:
|
|
208
208
|
|
|
209
209
|
- **`source_type: "docs-only"`, no URL or subpage fetched in this run, and `counts.items` is 0:** there is nothing to compile, and a docs-only skill has no other source. Stage and promote nothing: **HARD HALT** (exit code 3, `docs-unreachable`, phase `fetch-docs`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "**No documentation could be fetched for `{skill-name}`, so there is nothing to compile.** Failed: {each `doc_urls` URL with its reason}. Check the network and that this environment has a web fetch tool, or fix `doc_urls` in the brief, then re-run create-skill."
|
|
210
|
-
- **`source_type: "source"`, `counts.exports` is 0 and every `doc_urls` fetch failed:** step 3 §6's zero-export check let this brief through because it declares `doc_urls`. Apply that check here: show its warning and offer **[C] Continue anyway** or **[R] Refine the brief**: stop here. **GATE [default: C]**: under `{headless_mode}`, log and record its `zero-exports` decision as step 3 §6 says, and continue. On [R], **HARD HALT** (exit code 6, `halted-for-brief-refinement`, phase `fetch-docs`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "**Halting create-skill:** no public export was extracted and no `doc_urls` page could be fetched. Fix `scope.include`, `target_version` or `doc_urls` in the brief (or re-run `skf-brief-skill`), then re-run `skf-create-skill`."
|
|
210
|
+
- **`source_type: "source"`, `counts.exports` is 0 (or equals `counts.not_public`) and every `doc_urls` fetch failed:** step 3 §6's zero-export check let this brief through because it declares `doc_urls`. Apply that check here: show its warning and offer **[C] Continue anyway** or **[R] Refine the brief**: stop here. **GATE [default: C]**: under `{headless_mode}`, log and record its `zero-exports` decision as step 3 §6 says, and continue. On [R], **HARD HALT** (exit code 6, `halted-for-brief-refinement`, phase `fetch-docs`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "**Halting create-skill:** no public export was extracted (or, for a `public-api` brief, every export found is off the public surface: the `Off the public surface` warning names them) and no `doc_urls` page could be fetched. Fix `scope.include`, `target_version` or `doc_urls` in the brief (or re-run `skf-brief-skill`), then re-run `skf-create-skill`."
|
|
211
211
|
- **Otherwise:** continue. When no page of this run was saved (every fetch failed, or no fetch tool), skip §5b and §5c, which have nothing to index or remove, and go on to §6: `{docs_staging}` stays as it was.
|
|
212
212
|
|
|
213
213
|
### 5b. Index into QMD (Deep Tier Only)
|
|
@@ -80,10 +80,10 @@ The sink, `{run_dir}/warnings.jsonl`, is the list: step 7 §8b lists it in `evid
|
|
|
80
80
|
3. **Preferences fallback.** Otherwise, read `headless_mode` from `{sidecar_path}/preferences.yaml` (`true` or `false`).
|
|
81
81
|
4. **Default:** `false`.
|
|
82
82
|
|
|
83
|
-
3. **Resolve workflow customization.**
|
|
83
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
uv run {
|
|
86
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-stack-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
|
|
@@ -114,7 +114,7 @@ Take the first-line format, `|IMPORTANT:` second-line, and token-estimate checks
|
|
|
114
114
|
|
|
115
115
|
Then verify the two stack-specific rows the generic validator does not cover:
|
|
116
116
|
- [ ] Stack and integrations lines present
|
|
117
|
-
- [ ] Token estimate lands near the ~80-120 design target from step 7 §5 (the validator flags only its wider <40 / >
|
|
117
|
+
- [ ] Token estimate lands near the ~80-120 design target from step 7 §5 (the validator flags only its wider <40 / >300 bounds; an ~80-150 snippet with an overflow-strategy `workflow_warning` is expected, not a defect)
|
|
118
118
|
|
|
119
119
|
Record format violations as **WARNING** findings.
|
|
120
120
|
|
|
@@ -59,10 +59,10 @@ Headless callers and pipelines: the inputs, flags, gate map, exit codes and `SKF
|
|
|
59
59
|
|
|
60
60
|
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
|
|
61
61
|
|
|
62
|
-
3. **Resolve workflow customization.**
|
|
62
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
uv run {
|
|
65
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
|
|
@@ -12,6 +12,10 @@ skillInventoryProbeOrder:
|
|
|
12
12
|
dropRosterHelper: 'scripts/drop-roster.py'
|
|
13
13
|
# {dirSizesHelper}: the §4 `disk_freed` label.
|
|
14
14
|
dirSizesHelper: 'scripts/dir-sizes.py'
|
|
15
|
+
# {atomicWriteHelper}: the §4 manual fixes that point `active` at a version.
|
|
16
|
+
atomicWriteProbeOrder:
|
|
17
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
18
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
15
19
|
---
|
|
16
20
|
|
|
17
21
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -144,7 +148,7 @@ Report: "**Rebuilt managed sections in:** {list of updated files}. {if any faile
|
|
|
144
148
|
--skill-group {skills_output_folder}/{target_skill} \
|
|
145
149
|
--version {new_active_version}
|
|
146
150
|
```
|
|
147
|
-
Record a `mismatch`/`missing-target` exit (code 2), or a missing helper (no probe candidate), as above with the manual fix `
|
|
151
|
+
In each manual fix below, `{atomicWriteHelper}` is the first existing path in `{atomicWriteProbeOrder}`, or its first entry when neither exists (the fix then works once SKF is re-installed); the same command works in Git Bash, PowerShell and WSL2. Record a `mismatch`/`missing-target` exit (code 2), or a missing helper (no probe candidate), as above with the manual fix `uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target "{new_active_version}"`, and continue. Record a `busy` exit (code 2, another run was moving the link) the same way, with the manual fix "once the other run has finished, if `active` still names the dropped version, run `uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target "{new_active_version}"`". Record any other non-zero exit, or no JSON on stdout (exit 1, or an exit 2 that prints no JSON: the helper could not run, could not move the link, or failed after moving it, and the last line it prints on stderr says why), the same way, with the manual fix "if `active` still names the dropped version, run `uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target "{new_active_version}"`" followed by that line in parentheses, or by `(exit <code>)` when the helper prints nothing on stderr.
|
|
148
152
|
- **No non-deprecated versions remain** (`resolve.counts.non_deprecated` is `0`, reachable only when dropping the sole surviving version, permitted in step 1 because no other non-deprecated versions existed): remove the now-dangling `active` symlink with a single atomic unlink of the link itself: `rm {skills_output_folder}/{target_skill}/active` (unlink removes only the symlink, never its target). When it fails, record it as above with the manual fix `rm {skills_output_folder}/{target_skill}/active`.
|
|
149
153
|
|
|
150
154
|
3. Format the size of the deleted folders through the sizing helper; do not add or round in-prompt:
|
|
@@ -10,3 +10,5 @@ nextStepFile: 'shared/health-check.md'
|
|
|
10
10
|
# Step 4: Workflow Health Check
|
|
11
11
|
|
|
12
12
|
This is the terminal step of drop-skill. Load `{nextStepFile}`, read it fully, then execute it — do nothing else here (no user-facing reports, file writes, or result contracts; those were step 3). After the shared health check completes, the workflow is done.
|
|
13
|
+
|
|
14
|
+
In a headless run report.md bound `{result_envelope_line}` to the `SKF_DROP_SKILL_RESULT_JSON` line without displaying it: the shared health check displays that line verbatim as the run's last line wherever it stops, so a `claude -p` caller reads the envelope as the final message.
|
|
@@ -30,7 +30,7 @@ Every hard HALT exits with a stable code; the step files name the exact `halt_re
|
|
|
30
30
|
|
|
31
31
|
The single-line JSON contract every headless run of skf-drop-skill prints. The shared emitter, `shared/scripts/skf-emit-result-envelope.py`, builds each line from the payload the emitting step hands it and checks it against `shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json`, so no stage types an envelope and a HALT prints the right shape whatever else is still in context.
|
|
32
32
|
|
|
33
|
-
When `{headless_mode}` is true, step 3 prints this envelope on **stdout**
|
|
33
|
+
When `{headless_mode}` is true, step 3 prints this envelope on **stdout** and the shared health check (step 4) displays it as the run's last line, a `--dry-run` prints it on **stdout** at the step 1 confirmation gate and displays it last there, and every HARD HALT prints it on **stderr** with `status: "error"`:
|
|
34
34
|
|
|
35
35
|
```
|
|
36
36
|
SKF_DROP_SKILL_RESULT_JSON: {"status":"success|error|dry-run","skill":"…|null","drop_mode":"…|null","versions_affected":[],"files_deleted":[],"would_delete":[],"forge_left_in_place":null,"manifest_updated":false,"result_path":null,"exit_code":0,"halt_reason":null,"error":null}
|
|
@@ -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,
|
|
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.
|
|
125
125
|
|
|
126
126
|
### Post-drop hook (optional)
|
|
127
127
|
|
|
@@ -131,7 +131,7 @@ If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 fr
|
|
|
131
131
|
{onCompleteCommand} --result-path={result_json_path}
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
When it exits non-zero, display "on_complete hook failed (exit {code}): {its first stderr line}" as a line of its own
|
|
134
|
+
When it exits non-zero, display "on_complete hook failed (exit {code}): {its first stderr line}" as a line of its own, and never fail the workflow on a hook error: the drop has already completed and may be irreversible. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
|
|
135
135
|
|
|
136
136
|
Whether or not a hook ran, then delete the run folder, whose payload the emitter has read: `rm -rf "{run_dir}"`.
|
|
137
137
|
|
|
@@ -8,6 +8,10 @@ skillInventoryProbeOrder:
|
|
|
8
8
|
- '{project-root}/src/shared/scripts/skf-skill-inventory.py'
|
|
9
9
|
# {dirSizesHelper}: the §9b size of the folders in scope.
|
|
10
10
|
dirSizesHelper: 'scripts/dir-sizes.py'
|
|
11
|
+
# {atomicWriteHelper}: the §7 recovery that points `active` at the version to keep.
|
|
12
|
+
atomicWriteProbeOrder:
|
|
13
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
14
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -167,11 +171,11 @@ Set `target_versions = "all"` and `is_skill_level = true`.
|
|
|
167
171
|
"**Cannot drop the active version `{version}`.**
|
|
168
172
|
Other non-deprecated versions of `{target_skill}` still exist. To proceed, either:
|
|
169
173
|
|
|
170
|
-
**(a)** Make the version you keep active first: point the `active` link at it (`
|
|
174
|
+
**(a)** Make the version you keep active first: point the `active` link at it (`uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target <version to keep>`), run `[EX] Export Skill` for `{target_skill}`, which exports that version and archives `{version}`, then return here to drop `{version}`, OR
|
|
171
175
|
|
|
172
176
|
**(b)** Use the `[A] All versions` option to drop every version of `{target_skill}` at once."
|
|
173
177
|
|
|
174
|
-
In (a), `<version to keep>` is the first `{version_rows}` entry other than `{version}` whose `in_manifest` and `on_disk` are true and whose `status` is not `"deprecated"` (the rows are newest first); with no such entry, show `<version to keep>` as written.
|
|
178
|
+
In (a), `<version to keep>` is the first `{version_rows}` entry other than `{version}` whose `in_manifest` and `on_disk` are true and whose `status` is not `"deprecated"` (the rows are newest first); with no such entry, show `<version to keep>` as written. `{atomicWriteHelper}` is the first existing path in `{atomicWriteProbeOrder}`, or its first entry when neither exists.
|
|
175
179
|
|
|
176
180
|
HALT (exit code 5, `halt_reason: "active-version-guard-refused"`, phase `select:active-version-guard`), with `skill: "{target_skill}"` and `versions_affected: ["{version}"]` in the §1 halt envelope. Do not proceed.
|
|
177
181
|
|
|
@@ -305,7 +309,7 @@ When `{headless_mode}` is true, stage the preview as `{run_dir}/result-context.j
|
|
|
305
309
|
uv run {emitEnvelopeHelper} emit --workflow skf-drop-skill --run-dir "{run_dir}" < "{run_dir}/result-context.json"
|
|
306
310
|
```
|
|
307
311
|
|
|
308
|
-
|
|
312
|
+
A dry run passes no `--result-dir`, so it writes no result file. Then, in either mode, delete the run folder (`rm -rf "{run_dir}"`) and HALT (exit code 0). In a headless run, display the line the emitter printed verbatim (the `message` of its stderr JSON when it exits non-zero) after the delete and before the HALT, with nothing after it, so a `claude -p` caller reads it as the final message. The manifest, filesystem, and context files are untouched.
|
|
309
313
|
|
|
310
314
|
**Otherwise (not a dry-run), GATE [default: Y]:** If `{headless_mode}`: auto-proceed with [Y], record `confirm_source = "headless-auto"`, stage `{run_dir}/decision.json` as `{"gate": "select.confirm", "default_action": "Y", "taken_action": "Y", "reason": "headless auto-confirm"}` and run the §8 `record` command, and log: "headless: auto-confirmed drop of {target_skill}"
|
|
311
315
|
|
|
@@ -58,10 +58,10 @@ Headless callers and pipelines: the inputs, flags, gates, outputs and exit codes
|
|
|
58
58
|
|
|
59
59
|
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
|
|
60
60
|
|
|
61
|
-
3. **Resolve workflow customization.**
|
|
61
|
+
3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
uv run {
|
|
64
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-export-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
|
|
@@ -34,9 +34,9 @@ The root path in context-snippet.md on disk uses the **`skill_root` of the first
|
|
|
34
34
|
- **Line 1:** Skill name + version + platform-aware root path — version signals training data staleness
|
|
35
35
|
- **Line 2 (IMPORTANT):** Retrieval instruction — always present
|
|
36
36
|
- **Line 3 (quick-start):** Anchor pointer to Quick Start section
|
|
37
|
-
- **Line 4 (api):**
|
|
37
|
+
- **Line 4 (api):** The ranked exports (see the api line rule below), with `()` for functions
|
|
38
38
|
- **Line 5 (key-types):** Anchor pointer + inline summary of most important type/enum values
|
|
39
|
-
- **Line 6 (gotchas):**
|
|
39
|
+
- **Line 6 (gotchas):** At most 3 pitfalls, separated by `; `, derived from the current sources (see the gotchas line rule below)
|
|
40
40
|
|
|
41
41
|
## Stack Skill Snippet Template
|
|
42
42
|
|
|
@@ -50,12 +50,12 @@ The root path in context-snippet.md on disk uses the **`skill_root` of the first
|
|
|
50
50
|
|
|
51
51
|
## Rules
|
|
52
52
|
|
|
53
|
-
- **api line**:
|
|
53
|
+
- **api line**: Ranked, so the line stays the same from export to export: the prior snippet's `|api:` names in order, then the names of SKILL.md's `## Key API Summary` rows in row order (a component library's `## Component Catalog` rows, a reference app's `## Pattern Surface` rows), then metadata.json `exports` in order. A row's name is matched without backticks, a module qualifier or generic parameters, and a cell naming several is split on `/`. Each name once, only when `exports` lists it, and never one the current SKILL.md marks deprecated or removed (a deprecation, migration or breaking-changes bullet gives it as the old name, or its Key API Summary row's purpose says deprecated or removed). The first 10 for Deep tier, 5 for all other tiers; above the 300-token ceiling, names drop from the end. Append `()` to a name whose provenance `export_type` is a function, or else when the prior `|api:` line wrote it with `()`. Comma-separated.
|
|
54
54
|
- **key-types line**: Anchor to `#key-types` section + inline summary (~10 words) of the most important type values
|
|
55
|
-
- **gotchas line**: Derived from: T2-future
|
|
55
|
+
- **gotchas line**: At most 3 pitfalls, separated by `; ` (the separator step 3 splits a prior line on). Derived from the current sources: a T2-future annotation in the evidence report (breaking changes, async requirements, version-specific behavior); the current SKILL.md, through a bullet of a section whose heading names gotchas, deprecations, migration, breaking changes or import corrections, a `## Key API Summary` row whose purpose says deprecated or removed, and an async requirement its Quick Start shows; and a prior pitfall either of them still states, kept in its prior wording. The prior pitfalls still stated come first, in their prior order, then the new ones in source order. `[CARRIED]` marks only a prior pitfall no current source backs, which takes a free slot for one export (one-cycle expiry). If no gotchas are available, omit the line.
|
|
56
56
|
- **stack line**: Component versions from metadata.json `components` for stack skills
|
|
57
57
|
- **integrations line**: Co-import patterns from metadata.json `integrations` for stack skills
|
|
58
58
|
- If fewer exports than the limit, list all available
|
|
59
59
|
- If no exports data available, omit the api line
|
|
60
|
-
- Section anchors must be verified against actual SKILL.md headings during generation.
|
|
60
|
+
- Section anchors must be verified against actual SKILL.md headings during generation. Keep `SKILL.md#{slug}` when SKILL.md has the heading. Only when SKILL.md lacks it and the skill is split-body (`references/` exists and the `## Full` headings are absent or stubs; not a stack skill's structural references), point the anchor at the `references/{file}.md#{slug}` that holds it. Omit the anchor line when neither SKILL.md nor `references/*.md` holds the heading
|
|
61
61
|
- Skill path is relative to project root and uses the IDE's `skill_root` prefix (see **Skill Root Path** above, and the IDE → Context File Mapping table in `managed-section-format.md`)
|
|
@@ -18,7 +18,7 @@ To generate or update context-snippet.md for the skill in the Vercel-aligned ind
|
|
|
18
18
|
|
|
19
19
|
- Focus only on generating the context-snippet.md content — T1-now only, no T2 annotations
|
|
20
20
|
- If `passive_context: false` was detected in step 1, skip this step entirely
|
|
21
|
-
- **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), iterate sections 2 to 5 per skill. Each skill has its own prior
|
|
21
|
+
- **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), iterate sections 2 to 5 per skill. Each skill has its own prior snippet state (§2.5: its `prior_gotchas` carry-forward and its `prior_api`): do not share state across skills. §2.7 resolves `{skill_root}`, the snippet's root only, and §2.8 creates the stage folder once for the run (neither depends on the skill). See step 1 §1c.
|
|
22
22
|
- Every HALT names its exit code, `halt_reason` and phase; in headless mode it first emits its envelope as `references/result-envelope.md` states
|
|
23
23
|
|
|
24
24
|
## MANDATORY SEQUENCE
|
|
@@ -43,11 +43,12 @@ Before generating new snippet content, check for a prior snippet:
|
|
|
43
43
|
|
|
44
44
|
1. Read `{resolved_skill_package}/context-snippet.md` if it exists (resolved in step 1 — see `knowledge/version-paths.md`)
|
|
45
45
|
2. If it exists, extract the `|gotchas:` line (if any). Trim leading whitespace and the `|gotchas:` prefix, then capture the remaining content as `prior_gotchas_content`.
|
|
46
|
-
3. **
|
|
46
|
+
3. **Split it into pitfalls and detect the carry-forward marker:** split `prior_gotchas_content` on the pitfall separator {snippetFormatData} gives (`; `), and keep the pitfalls, in order, as `prior_gotchas`. A pitfall that starts with the token `[CARRIED]` (whitespace-insensitive) is **already carried**: strip the marker and record that. A line an older export marked once at its start (`[CARRIED] a, b`) reads as one pitfall, already carried.
|
|
47
47
|
4. **Distinguish empty from absent:** If the `|gotchas:` line exists but has no non-whitespace content after the prefix, treat it as **absent** — set `prior_gotchas = null`. Only a non-empty value counts as a prior gotchas line worth carrying forward.
|
|
48
|
-
5.
|
|
48
|
+
5. **Capture the prior api names:** if the prior snippet has an `|api:` line, split its value on commas, drop each name's trailing `()` and surrounding spaces, and capture the names, in order, as `prior_api` (no `()`, no spaces), noting each name the line wrote with `()`. With no `|api:` line, `prior_api` is empty.
|
|
49
|
+
6. If no prior snippet exists at all, set `prior_gotchas = null` and `prior_api` to an empty list.
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
§3a gives the gotchas line's free slots to the prior pitfalls no current source states, and §3b ranks the `prior_api` names first. The `[CARRIED]` marker provides a **hard one-cycle expiry**: a pitfall already carried once is dropped on the next export rather than preserved indefinitely.
|
|
51
52
|
|
|
52
53
|
### 2.7. Resolve Skill Root Path
|
|
53
54
|
|
|
@@ -71,28 +72,34 @@ python3 -c "import tempfile; print(tempfile.mkdtemp(prefix='skf-export-'))"
|
|
|
71
72
|
|
|
72
73
|
#### 3a. Gotchas (Both Skill Types)
|
|
73
74
|
|
|
74
|
-
Derive
|
|
75
|
+
Derive the gotchas (breaking changes, deprecations, async requirements and version-specific behavior) from the current sources. A gotcha is **derived** when a current source states it:
|
|
76
|
+
|
|
77
|
+
1. **The evidence report:** a T2-future annotation in the evidence report at `{forge_evidence_report}`, when step 1 §2 bound that path (when it is null, the skill has no evidence report to read).
|
|
78
|
+
2. **The current SKILL.md** (`{resolved_skill_package}/SKILL.md`, the published, test-verified artifact): a bullet of a section whose heading names gotchas, deprecations, migration, breaking changes or import corrections (such as `## Migration & Deprecation Warnings`), a `## Key API Summary` row whose purpose says deprecated or removed, and an async requirement its Quick Start shows (an API it calls with `await`).
|
|
79
|
+
3. **A prior pitfall still stated:** a pitfall of `prior_gotchas` (§2.5) that source 1 or 2 still states, kept in its prior wording without the `[CARRIED]` marker.
|
|
80
|
+
|
|
81
|
+
A gotcha the current SKILL.md states is therefore derived even when the evidence report records no T2-future annotation. `[CARRIED]` applies only to a prior pitfall no current source backs.
|
|
75
82
|
|
|
76
83
|
**Detect first-export state before applying carry-forward logic.** The `[CARRIED]` one-cycle expiry is meaningful only on a *re-export*. On a first export, the prior `context-snippet.md` was written by the workflow that built or updated the skill (create-skill, create-stack-skill, quick-skill or update-skill) inside the same forge cycle: those gotchas are freshly derived, not "left over from a previous export." Treating them as carry-forward primes them for premature expiry on the second export.
|
|
77
84
|
|
|
78
85
|
Read the skill's manifest entry: `python3 {manifestOpsHelper} {skills_output_folder} get {skill-name}`. When it returns `not_found`, or an `entry` none of whose `versions` records a `last_exported`, this is a first export: set `is_first_export = true`. Otherwise `is_first_export = false`. The helper returns the entry in the v2 shape whatever is on disk, so a v1 manifest reads the same here as in step 4.
|
|
79
86
|
|
|
80
|
-
Resolve the gotchas line by this decision tree:
|
|
87
|
+
Resolve the gotchas line by this decision tree. The line holds at most 3 pitfalls, joined by the separator {snippetFormatData} gives (`|gotchas: {pitfall-1}; {pitfall-2}; {pitfall-3}`). A prior pitfall no current source states is **unbacked**.
|
|
81
88
|
|
|
82
|
-
- **If
|
|
83
|
-
- **If
|
|
84
|
-
- **If
|
|
85
|
-
- **If
|
|
86
|
-
- **If
|
|
89
|
+
- **If gotchas are derived:** Write them first, with no marker: the prior pitfalls a current source still states, in their prior order, then the new ones, in source order (source 1, then source 2 in SKILL.md order), the first 3 of them. Each slot left free goes to an unbacked prior pitfall, in prior order, by the next three branches; when no gotcha is derived, all 3 slots are free.
|
|
90
|
+
- **If an unbacked prior pitfall gets a free slot AND `is_first_export == true`:** Treat it as **freshly derived** by the workflow that wrote it, and write it **without** the `[CARRIED]` marker. (The marker only applies to re-exports.) No warning needed; this is the normal first-export shape.
|
|
91
|
+
- **If an unbacked prior pitfall gets a free slot AND `is_first_export == false` AND it is not already carried:** First carry-forward cycle on a re-export: write it as `[CARRIED] {pitfall}`, beside any derived ones, so the next export can detect that expiry has been reached. Emit warning: "**Gotchas preserved from prior export (one-cycle carry-forward):** {the pitfalls}. They will be DROPPED on the next export unless the current SKILL.md or evidence report states them. Review now if they are still applicable."
|
|
92
|
+
- **If an unbacked prior pitfall is already carried (expiry reached; re-export only, as the first-export branch above takes precedence) or gets no free slot:** Drop it. Emit warning: "**Stale gotchas dropped:** {the pitfalls} no longer appear in the current SKILL.md or evidence report (each was already carried forward once, or the line's 3 slots went to current gotchas). If one is still relevant, re-forge the skill with update-skill so its SKILL.md states it, or add it to the evidence report's T2-future section, then re-run export."
|
|
93
|
+
- **If no pitfall is written:** Omit the gotchas line.
|
|
87
94
|
|
|
88
95
|
#### 3b. Single Skills (`skill_type: "single"`)
|
|
89
96
|
|
|
90
97
|
1. Read metadata.json for `version`, `exports` array
|
|
91
|
-
2.
|
|
98
|
+
2. **Rank the api names**, so the `|api:` line stays the same from export to export while the surface does: first the `prior_api` names (the prior snippet's `|api:` line, §2.5), in order; then the names of the rows of SKILL.md's `## Key API Summary` table, in row order (for a component library, the Key Components of its `## Component Catalog` rows; for a reference app, the surfaces of its `## Pattern Surface` rows); then the metadata.json `exports`, in order. Match a row's name to `exports` without backticks, `()`, a module qualifier (`cognee.add` is `add`) or generic parameters (`Meta<T = Args>` is `Meta`), and split a cell that names several on `/` (a Key Components cell on commas). Take each name once, only when metadata.json `exports` lists it, and never one the current SKILL.md marks deprecated or removed: a bullet of a section whose heading names deprecations, migration or breaking changes gives it as the deprecated, removed or renamed name (not as its replacement), or its `## Key API Summary` row's purpose says deprecated or removed. Keep the first 10 for the Deep tier, 5 for the other tiers. Append `()` to a name whose `export_type` in the provenance map at `{forge_provenance_map}` is a function; when that map is null or gives no kind for the name, keep the `()` the prior `|api:` line gave it.
|
|
92
99
|
3. Read SKILL.md to extract: heading slugs for `#quick-start` and `#key-types`, inline summary of key types (~10 words)
|
|
93
|
-
4. **Anchor verification (split-body awareness):** For each section anchor (`#quick-start`, `#key-types`)
|
|
100
|
+
4. **Anchor verification (split-body awareness):** For each section anchor (`#quick-start`, `#key-types`): when SKILL.md has the heading, keep `SKILL.md#{slug}`. Only when SKILL.md lacks the heading and the skill is split-body (a `references/` directory exists and the `## Full` headings in SKILL.md are absent or stubs; not a stack skill's structural references), point the anchor at the `references/{file}.md#{slug}` that holds the heading. When neither holds it, omit that anchor line from the snippet.
|
|
94
101
|
|
|
95
|
-
Emit the single-skill template from {snippetFormatData} (loaded in §2), filling `api` from the
|
|
102
|
+
Emit the single-skill template from {snippetFormatData} (loaded in §2), filling `api` from the names ranked above (all of them when fewer than the limit; omit the line when there are none), `key-types` from the inline summary extracted above, and `gotchas` per the §3a decision tree (omit the line when it resolves to none).
|
|
96
103
|
|
|
97
104
|
#### 3c. Stack Skills (`skill_type: "stack"`)
|
|
98
105
|
|
|
@@ -112,7 +119,7 @@ Bind `{count}` ← the `tokens` value of its `context-snippet.md` row (`len(text
|
|
|
112
119
|
|
|
113
120
|
- Target: ~80-120 tokens per skill (aspirational for Quick/Forge tiers)
|
|
114
121
|
- Hard ceiling: 300 tokens (Deep tier may legitimately exceed 120 when gotchas carry load-bearing breaking-change notices)
|
|
115
|
-
- If `{count}` is above 300,
|
|
122
|
+
- If `{count}` is above 300, drop api names from the end of the §3b ranking, one at a time, writing the draft again and measuring it again after each, until `{count}` is 300 or below; only when the api line is gone and `{count}` is still above 300, shorten the key-types summary the same way. **Do NOT drop gotchas to fit**: gotchas exist precisely to deliver the "do not rely on training data" signal and are the last thing to cut
|
|
116
123
|
|
|
117
124
|
When the helper exits non-zero, delete the `{export_stage_dir}` folder and HALT (exit code 4, `halt_reason: "context-rebuild-failed"`, phase `generate-snippet §4`): "SKF cannot measure the snippet: {the helper's error}. Re-install SKF and re-run the export."
|
|
118
125
|
|
|
@@ -99,7 +99,7 @@ The inventory helper chooses the version to export and binds its paths by the Re
|
|
|
99
99
|
uv run {skillInventoryHelper} resolve "{skills_output_folder}" --skill {skill-name} --forge-data-folder "{forge_data_folder}"
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
Bind from its `resolve` object `{resolved_version}` ← `chosen_version`, `{resolved_skill_package}` ← `skill_package`, `{forge_version}` ← `forge_version` (the version's forge folder)
|
|
102
|
+
Bind from its `resolve` object `{resolved_version}` ← `chosen_version`, `{resolved_skill_package}` ← `skill_package`, `{forge_version}` ← `forge_version` (the version's forge folder), `{forge_evidence_report}` ← `paths.evidence_report.path` (that folder's `evidence-report.md`, else the flat copy an older skill keeps, else null), which step 3 derives the gotchas from, and `{forge_provenance_map}` ← `paths.provenance_map.path` (that folder's `provenance-map.json`, else the flat copy, else null), whose `export_type` gives step 3 the api line's `()`. Log each `resolve.errors` entry, and `resolve.manifest_error` when it is set, as an Info note, then act on `reason`:
|
|
103
103
|
|
|
104
104
|
1. `manifest-and-link`, `manifest` or `link`: the export manifest or the `active` link names the version, and its package is on disk. Continue.
|
|
105
105
|
2. `manifest-lags-link` (the Manifest-lag guard): the `active` link names a version the manifest has not caught up with, as when a create or update run flipped `active` after the last export (the SS→TS→EX order). Continue with the link's version, and emit the helper's `detail` as an Info note, followed by "Exporting v{resolved_version}: the manifest's active_version advances to it on this export." Step 4 publishes the version in `{resolved_skill_package}/metadata.json`, so the manifest catches up here.
|