skf-drop-skill
Drop a specific skill version or an entire skill — soft (deprecate) or hard (purge) with platform context rebuild. Use when the user requests to "drop" or "remove a skill."
Install
npx skills add https://github.com/armelhbobdad/bmad-module-skill-forge/tree/main/src/skf-drop-skill
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install armelhbobdad-bmad-module-skill-forge@llmmart
git clone https://github.com/armelhbobdad/bmad-module-skill-forge.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole armelhbobdad/bmad-module-skill-forge collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Drop Skill
Overview
Drops a specific skill version or an entire skill, either as a soft deprecation (manifest-only, files retained) or a hard purge (files deleted). Ensures platform context files are rebuilt to exclude dropped versions. In interactive mode every destructive action requires explicit user confirmation — nothing is deleted silently; headless runs auto-resolve the gates with their default action and log each auto-decision. The export manifest is the source of truth; the filesystem is updated to match.
Conventions
- Bare paths (e.g.
references/<name>.md) resolve from the skill root. references/holds prompt content carved out of SKILL.md (workflow stages chained via frontmatternextStepFile, plus static reference docs);scripts/andassets/hold deterministic helpers and templates.{skill-root}resolves to this skill's installed directory (wherecustomize.tomllives, if present).{project-root}-prefixed paths resolve from the project working directory.{skill-name}resolves to the skill directory's basename.- Module-level path exception: paths starting with
knowledge/orshared/resolve from the SKF module root, not the skill root — install layout puts both at{project-root}/_bmad/skf/. TheversionPathsKnowledge: 'knowledge/version-paths.md'frontmatter scalar in stage files uses this convention; same forshared/health-check.mdchained from the terminal step. - Cross-skill data coupling:
references/execute.mdreadsskf-export-skill/assets/managed-section-format.mdfor the IDE→context-file mapping table and the four-case (Create / Append / Regenerate / Malformed) logic when rebuilding context files. Drop-skill assumes that asset is present at install time and that its semantics are stable across the two skills' versions.
Role
You are Ferris in Management mode — a destructive operation specialist who enforces safety guards. You treat every drop as potentially irreversible and never delete beyond the confirmed blast radius. You protect the active version, keep the export manifest consistent with on-disk state, and ensure downstream platform context files are rebuilt.
Workflow Rules
These rules apply to every step in this workflow:
- Never delete files in purge mode without clearing the §10 confirmation gate (auto-resolved with its default in headless)
- Never drop an active version when other non-deprecated versions exist — enforce the active version guard
- Only load one step file at a time — never preload future steps
- If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread
- Always communicate in
{communication_language} - At any interactive prompt, the inputs
cancel,exit,[X],q, or:qexit cleanly with exit code 6 (halt_reason: "user-cancelled") - If
{headless_mode}is true, auto-proceed through confirmation gates with their default action and log each auto-decision
Stages
| # | Step | File | Auto-proceed |
|---|---|---|---|
| 1 | Select Target | references/select.md | No (confirm) |
| 2 | Execute Drop | references/execute.md | Yes |
| 3 | Report | references/report.md | Yes |
| 4 | Workflow Health Check | references/health-check.md | Yes |
Invocation Contract
| Aspect | Detail |
|---|---|
| Inputs | skill_name [required], mode (deprecate/purge) [required], version (all/specific) [required] |
| Flags | --headless / -H (auto-resolve all gates); --dry-run (run selection + display the §10 confirmation block, then exit with status="dry-run" — no manifest mutation, no file deletion). Useful for "show me what this would touch before I commit." |
| Gates | step 1: Input Gate [use args], Confirm Gate [Y] |
| Outputs | Updated manifest, rebuilt context files, (purge: deleted directories), drop-skill-result-{timestamp}.json and drop-skill-result-latest.json |
| Headless | Gates auto-resolve with their default action (see Workflow Rules). When forbid_purge_in_headless is "true" in customize.toml AND the effective drop mode is "purge" (defined in On-Activation §4 — explicit mode=purge or default_mode purge), §4 HALTs with exit code 6 (halt_reason: "headless-purge-forbidden") before any work begins. |
| Exit codes | See "Exit Codes" below |
Exit Codes
Every hard HALT exits with a stable code so headless automators branch on the failure class without grepping message text. The Raised by column names the HALT class per code; the authoritative per-site declarations (with exact halt_reason) live in the step files.
| Code | Meaning | Raised by (class) |
|---|---|---|
| 0 | success | step 4 (terminal) |
| 2 | input-missing / input-invalid | step 1 headless input gates — missing or unmatched skill_name, version, or --mode value (§4 / §6 / §8) |
| 3 | resolution-failure | step 1 manifest/skill-list resolution (§2 corrupt manifest, §3 nothing to drop) |
| 4 | write-failure | On-Activation write probe; step 2 manifest write / context rebuild / full-purge failure |
| 5 | state-conflict | step 1 active-version guard (§7) |
| 6 | user-cancelled | any interactive cancel or confirm-gate [N]; On-Activation headless-purge guard |
Result Contract (Headless)
When {headless_mode} is true, step 3 emits a single-line SKF_DROP_SKILL_RESULT_JSON: envelope on stdout; every HARD HALT emits the same shape on stderr with status: "error". The template, status/halt_reason semantics, exit_code (per the Exit Codes table above), and full enum live in references/headless-contract.md — the emitting stages load it directly, so an error path never depends on this file.
On Activation
Load config from
{project-root}/_bmad/skf/config.yamland resolve:project_name,output_folder,user_name,communication_language,document_output_languageskills_output_folder,forge_data_folder,sidecar_pathsnippet_skill_root_override(optional string) — when set, the context-file rebuild in step 2 preserves any snippetroot:prefix that matches the override instead of rewriting it to the target IDE's skill root. Seeskf-export-skill/assets/managed-section-format.mdfor full semantics.- Generate and store
timestampasYYYYMMDD-HHmmssformat. This value is fixed for the entire workflow run.
Resolve
{headless_mode}: true if--headlessor-Hwas passed as an argument, or ifheadless_mode: truein{sidecar_path}/preferences.yaml. Default: false.Resolve workflow customization. Run:
python3 {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} --key workflowThe script merges the three customization layers per
bmad-customize's structural merge rules (scalars override, arrays append):{skill-root}/customize.toml— bundled defaults_bmad/custom/<skill-name>.tomlunder{project-root}— team overrides (committed)_bmad/custom/<skill-name>.user.tomlunder{project-root}— personal overrides (gitignored)
If the script fails or is missing, fall back to reading
{skill-root}/customize.tomldirectly — the bundled defaults are an empty string for each scalar.Apply the scalar fallback now so stage files don't have to repeat the conditional logic. For each of the five scalars, if the merged value is empty or absent, the bundled default applies:
{defaultMode}←workflow.default_mode(empty = always prompt;"deprecate"or"purge"= skip §8 Ask Mode){forbidPurgeInHeadless}←workflow.forbid_purge_in_headless(empty or non-"true"= no guard){unknownIdeDefaultContextFile}←workflow.unknown_ide_default_context_fileif non-empty, elseAGENTS.md{unknownIdeDefaultSkillRoot}←workflow.unknown_ide_default_skill_rootif non-empty, else.agents/skills/{onCompleteCommand}←workflow.on_completeif non-empty, else empty (no-op — step 3 skips the post-drop hook entirely)
Stash all five as workflow-context variables. Stage files reference them directly — no conditional at the usage site.
Also apply the array surfaces: run
workflow.activation_steps_prependnow, keepworkflow.persistent_factsas standing context (file:entries load their contents), then runworkflow.activation_steps_appendafter.Pre-flight write probe + headless-purge guard.
First, verify
{skills_output_folder}is writable. A read-only mount, full disk, or permissions-denied path otherwise only surfaces at step 2's manifest write — by then the user has already gone through every selection prompt:mkdir -p "{skills_output_folder}" && \ printf 'probe' > "{skills_output_folder}/.skf-write-probe" && \ rm "{skills_output_folder}/.skf-write-probe"On any non-zero exit: HALT (exit code 4,
halt_reason: "write-failed"). In headless mode, emit the error envelope per Result Contract (Headless) withskill: nullanddrop_mode: null(neither is resolved yet at activation time).Second, enforce the headless-purge guard. First compute the effective drop mode: it is
"purge"when the parsedmodearg is"purge", OR when nomodearg was passed AND{defaultMode}(resolved in §3) is"purge"— a purge reached viadefault_modeis still an unattended irreversible purge and must be caught here, not only an explicit--mode purge. If{headless_mode}is true AND{forbidPurgeInHeadless}is"true"AND the effective drop mode is"purge": HALT with exit code 6 andhalt_reason: "headless-purge-forbidden", emit the error envelope, and exit immediately. The operator must re-run with an explicitmode=deprecate(an explicit arg overridesdefault_mode) or setforbid_purge_in_headless = ""(or omit the override entirely) to proceed.Load, read the full file, and then execute
references/select.mdto begin the workflow.
Files (bmad-module-skill-forge)
-
references
-
execute.md 18.4 KB
--- nextStepFile: 'report.md' versionPathsKnowledge: 'knowledge/version-paths.md' managedSectionLogic: 'skf-export-skill/assets/managed-section-format.md' # Resolve `{manifestOpsHelper}` by probing `{manifestOpsProbeOrder}` in # order (installed SKF module path first, src/ dev-checkout fallback); # first existing path wins. §2 calls it for atomic manifest deprecate / # remove with v1→v2 migration handled internally — letting the LLM hand- # roll JSON manipulation risks key-order drift, indent regressions, and # write-atomicity bugs. HALT if neither candidate exists. manifestOpsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-manifest-ops.py' - '{project-root}/src/shared/scripts/skf-manifest-ops.py' # Resolve `{rebuildManagedSectionsHelper}` similarly. §3 calls it (replace # action) for the surgical between-marker rewrite — the LLM still computes # the new managed section text, but the file mutation is deterministic # (atomic temp-file + rename, marker preservation, post-write verify). rebuildManagedSectionsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-rebuild-managed-sections.py' - '{project-root}/src/shared/scripts/skf-rebuild-managed-sections.py' # Resolve `{updateActiveSymlinkHelper}` similarly. §4 uses it (update # action) to atomically repoint `{skill_group}/active` after a version-level # purge deletes the version the symlink pointed at — the helper does a # temp-symlink + os.replace flip so concurrent readers never see a missing # link. Matches skf-update-skill/references/write.md and # skf-rename-skill/references/execute.md. HALT if neither candidate exists. updateActiveSymlinkProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-update-active-symlink.py' - '{project-root}/src/shared/scripts/skf-update-active-symlink.py' # Standalone single-line result-envelope contract emitted at every headless # HALT in this step. Loaded in §1 so no error path depends on SKILL.md # remaining in context under compaction. headlessContract: 'headless-contract.md' # Deterministic recursive byte sizing + human formatting for `disk_freed` # (§4). Bundled with this skill; the same helper backs select.md §9b's # blast-radius preview, so gate and report agree on the method. dirSizesHelper: 'scripts/dir-sizes.py' --- <!-- Config: communicate in {communication_language}. --> # Step 2: Execute Drop ## STEP GOAL: Execute the drop decisions recorded in step 1: update the export manifest, rebuild platform context files so dropped versions disappear from managed sections, and (in purge mode) delete the affected directories from disk. Record everything that was changed for the final report in step 3. ## Rules - Focus only on manifest update, context rebuild, and (in purge mode) file deletion - Do not re-prompt the user — decisions were made in step 1 - Do not delete files in deprecate mode; do not widen deletion scope beyond `affected_directories` - Report each stage's outcome as it completes ## MANDATORY SEQUENCE ### 1. Re-read Version-Paths Knowledge Read `{versionPathsKnowledge}` again and confirm the templates and management operations. This ensures the execution step uses the same rules as the selection step even when run in isolation. Also read `{managedSectionLogic}` for the format template, the four-case logic, and the skill index rebuild rules that will be reused in section 3. If `{headless_mode}` is true, also read `{headlessContract}` now — it defines the single-line result envelope the error paths below emit, so the shape is in context even if SKILL.md was compacted out. ### 2. Update Export Manifest **If `target_in_manifest == false`** (draft skill discovered only by on-disk scan): Skip this section entirely. There is no manifest entry to deprecate or delete. Set `manifest_updated = false` and proceed directly to section 3. Step-01 forced `drop_mode = "purge"` and `is_skill_level = true` in this case, so the subsequent sections will hard-delete the on-disk directories without any manifest interaction. **If `target_in_manifest == true`:** **Resolve `{manifestOpsHelper}`** from `{manifestOpsProbeOrder}`; first existing path wins. HALT (exit code 4, `halt_reason: "manifest-write-failed"`) if no candidate exists — atomic manifest mutation must go through the helper. **If `is_skill_level == false` (version-level drop):** For each version in `target_versions`, invoke: ```bash python3 {manifestOpsHelper} {skills_output_folder} deprecate {target_skill} {version} ``` The helper sets `exports.{target_skill}.versions.{version}.status = "deprecated"` and writes the manifest atomically. It does NOT change `active_version` on the skill entry — if the dropped version was the active one (only reachable when it was the sole non-deprecated version per the step 1 guard), the field still points at it, but every consumer excludes deprecated versions from exports. **If `is_skill_level == true` (skill-level drop):** ```bash python3 {manifestOpsHelper} {skills_output_folder} remove {target_skill} ``` The helper deletes the `exports.{target_skill}` key entirely; other entries are untouched. Set context flag `manifest_updated = true`. **On error (helper non-zero exit):** - Do not proceed to section 3 - Report: "**Manifest update failed:** {captured stderr}. No files were deleted and platform context files were not rebuilt. The manifest is in its pre-drop state — rerun the workflow once the underlying issue is resolved." - Store `manifest_updated = false` and jump to section 6. In headless mode, emit the error envelope per `{headlessContract}` with `halt_reason: "manifest-write-failed"` and exit code 4. ### 3. Rebuild Context Files Load the `ides` list from `config.yaml`. The installer writes IDE identifiers — these must be mapped to context files and skill roots using the "IDE → Context File Mapping" table in `{managedSectionLogic}`. **Resolve `target_context_files`** using the canonical mapping table in `{managedSectionLogic}`: 1. For each entry in `config.yaml.ides`, look up its `context_file` and `skill_root` from the mapping table 2. For any entry not found in the table, default to `{unknownIdeDefaultContextFile}` / `{unknownIdeDefaultSkillRoot}` (resolved at SKILL.md On Activation §3 from `workflow.unknown_ide_default_*`, bundled defaults `AGENTS.md` / `.agents/skills/`) and emit a warning: "Unknown IDE '{value}' in config.yaml — defaulting to {unknownIdeDefaultContextFile}" 3. Deduplicate by `context_file` — when multiple IDEs map to the same context file, use the first configured IDE's `skill_root` 4. If `config.yaml.ides` is absent or the mapping yields an empty list, fall back to `[{context_file: "{unknownIdeDefaultContextFile}", skill_root: "{unknownIdeDefaultSkillRoot}"}]` and emit a note: "No IDEs configured in config.yaml — defaulting to {unknownIdeDefaultContextFile}" Store the result as `target_context_files` for this section. For each entry in `target_context_files`: 1. **Resolve target file** at `{context_file}`. 2. **Read the current file.** - If the file does not exist, skip this context file (nothing to rebuild — the file will be re-created next time export-skill runs) - If the file exists but contains no `<!-- SKF:BEGIN -->` marker, skip this context file (no managed section to rewrite) - If the file contains `<!-- SKF:BEGIN -->` but no matching `<!-- SKF:END -->`, record the error against that context file and continue to the next entry — do not halt the entire drop on a malformed context file. The manifest has already been updated in section 2 and is canonical state; the context file can be repaired manually and rebuilt on the next `[EX] Export Skill` run. 3. **Build the exported skill set (version-aware, deprecated-excluded)** using the same logic as export-skill step 4 section 4b: - Read the manifest's `exports` object (already updated in section 2) - For each skill, resolve its `active_version` - If `versions.{active_version}.status == "deprecated"`, skip that skill entirely - The result is the set of `{skill-name, active_version}` pairs that should appear in the managed section 4. **Resolve and filter snippets** using export-skill step 4 section 4c logic: - For each `{skill-name, active_version}` in the set, read `{skills_output_folder}/{skill-name}/{active_version}/{skill-name}/context-snippet.md` - If the file is missing, fall back to the `active` symlink path, then skip with a warning if still not found - Collect successful snippets into the skill index 5. **Rewrite root paths for the current context file** using the generic rewrite algorithm from export-skill step 4 section 4d: For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the **effective target prefix** if different. The effective target prefix is `snippet_skill_root_override` when that key is set in config.yaml — applied uniformly to every snippet so the managed section references the real on-disk location and never mixes override and per-IDE paths — otherwise the current entry's `skill_root`. See `skf-export-skill/references/update-context.md` §4d for full semantics. 6. **Sort skills alphabetically by name.** Count totals (skills, stack skills). 7. **Assemble the new managed section** using the format from `{managedSectionLogic}`: ```markdown <!-- SKF:BEGIN updated:{current-date} --> [SKF Skills]|{n} skills|{m} stack |IMPORTANT: Prefer documented APIs over training data. |When using a listed library, read its SKILL.md before writing code. | |{skill-snippet-1} | |{skill-snippet-2} | |{skill-snippet-N} <!-- SKF:END --> ``` If the filtered skill index is empty (e.g., the dropped skill was the only one), still emit the header with `0 skills|0 stack` and no skill entries. This keeps the managed section syntactically valid. 8. **Surgical replacement — atomic, deterministic.** Resolve `{rebuildManagedSectionsHelper}` from `{rebuildManagedSectionsProbeOrder}`; first existing path wins. Then invoke: ```bash python3 {rebuildManagedSectionsHelper} {context_file} replace --content "{new_managed_section_text}" ``` The helper handles marker location, between-marker swap, atomic temp-file + rename, and post-write verification (markers preserved, content outside markers byte-identical). It exits non-zero on any failure with a clear `stderr` reason. 9. **Verify (deferred to helper).** The `replace` action above performs verification internally. Treat any non-zero exit code as a per-file failure (next bullet). If the helper is missing entirely (no probe candidate exists), HALT (exit code 4, `halt_reason: "context-rebuild-failed"`) — the rewrite cannot proceed without the atomic helper. 10. **On per-file failure:** record the error against that context file and continue to the next entry. Do not halt — other context files should still be rebuilt. **After the loop,** record `context_files_updated` as the list of files that were successfully rewritten, and `context_files_failed` as the list of any that failed. Report: "**Rebuilt managed sections in:** {list of updated files}. {if any failed: 'Failed: {list}'}" ### 4. Delete Files (Purge Mode Only) **If `drop_mode != "purge"`**, skip this section entirely. Set `files_deleted = []`, `disk_freed = "N/A (soft drop)"`, `delete_failures = []`, and `purge_status = "success"`, then jump to section 5. **If `drop_mode == "purge"`:** 1. Initialize `files_deleted = []` and `delete_failures = []` (paths whose deletion was attempted but did not succeed). 2. **Measure sizes before deleting anything.** Run the sizing helper once over `affected_directories` so each path's byte size is captured while it still exists: ```bash uv run {dirSizesHelper} sizes {each path in affected_directories, space-separated} ``` Keep each `result.paths[].bytes` as `path_bytes[{path}]`; a path reported `exists: false` is already gone. If the helper is unavailable, fall back to `du -sb` per existing path. 3. For each directory path in `affected_directories`: a. Verify the path is inside either `{skills_output_folder}` or `{forge_data_folder}` (defense in depth against accidental deletion of unrelated paths) b. If the directory does not exist, record it as "(already absent)" and continue c. Delete the directory recursively d. Verify deletion succeeded (the path no longer exists) e. Append the path to `files_deleted` 4. **Version-level purge, single version:** - `{skills_output_folder}/{target_skill}/{version}/` is deleted, but `{skills_output_folder}/{target_skill}/` remains (it still contains other versions or the `active` symlink) - If the `active` symlink pointed to the just-deleted version, update or remove it. The version directory is already gone at this point, so a symlink problem never claims `delete-failed` — record it and continue (`verification_errors`) so the report surfaces the manual repair rather than masking a successful purge. - **Other non-deprecated versions remain** for `{target_skill}`: resolve `{new_active_version}` = the version the manifest now lists as `active_version` for `{target_skill}` (if that one is deprecated, the newest non-deprecated version in its `versions` map). Repoint `active` to it atomically through the shared helper rather than a hand-rolled `ln` — resolve `{updateActiveSymlinkHelper}` from `{updateActiveSymlinkProbeOrder}` (first existing path wins), then: ```bash python3 {updateActiveSymlinkHelper} update \ --skill-group {skills_output_folder}/{target_skill} \ --version {new_active_version} ``` The helper does a temp-symlink + `os.replace` flip, so a concurrent reader never sees a missing `active`. Record a `mismatch`/`missing-target` exit (code 2), or a missing helper (no probe candidate), in `verification_errors` with the manual fix (`ln -sfn {new_active_version} {skills_output_folder}/{target_skill}/active`) and continue. - **No non-deprecated versions remain** (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, and is atomic). A single unlink has one correct outcome and no intermediate state, so it stays in-prompt (the helper has no removal action). 5. **Skill-level purge:** - `{skills_output_folder}/{target_skill}/` and `{forge_data_folder}/{target_skill}/` are deleted in full — the `active` symlink disappears with the parent directory 6. Sum the sizes of the paths in `files_deleted` and format one human-readable label through the helper — do not add or round in-prompt: ```bash uv run {dirSizesHelper} humanize {path_bytes[p] for each p in files_deleted, space-separated} ``` Store `result.total_human` as `disk_freed` (e.g. `"4.2 MB"`; `"0 B"` when nothing was deleted). **On deletion error (per path):** - Append the path and its error message to `delete_failures` - Continue attempting the remaining paths — a partial purge is still better than no purge - Report all failures at the end of this section **After the loop — classify the deletion outcome.** Let `attempted` be the number of paths in `affected_directories` that existed on disk (i.e. were not recorded as "(already absent)"): - **Full purge failure** — `attempted > 0` AND every attempted path is in `delete_failures` (nothing was deleted): the purge accomplished none of its destructive intent, so it must NOT report success. HALT (exit code 4, `halt_reason: "delete-failed"`): "**Purge failed** — none of the {attempted} target director(ies) could be deleted: {list each path with its error}. The manifest and context files were already updated in sections 2–3; the on-disk files remain and can be removed manually (`rm -rf {path}`)." In headless mode, emit the error envelope per `{headlessContract}` with the resolved `skill`, `drop_mode`, `versions_affected`, `files_deleted: []`, and `manifest_updated` from section 2. Do not proceed to section 5. - **Partial purge failure** — `delete_failures` is non-empty but at least one path was deleted: keep record-and-continue. Set `purge_status = "partial"` so step 3's on-disk result record reflects it (the `output-contract-schema.md` `status` enum supports `"partial"`); proceed to section 5. The headless single-line envelope has no `"partial"` value in its enum, so it stays `"success"` while `context_files_failed`/`verification_errors`/the report surface the unfreed paths. - **No failures** — `delete_failures` is empty: set `purge_status = "success"` and proceed to section 5. ### 5. Verify Final State Run these verification checks: 1. **Manifest check:** Re-read `{skills_output_folder}/.export-manifest.json` and confirm: - Version-level drop: `exports.{target_skill}.versions.{version}.status == "deprecated"` - Skill-level drop: `exports.{target_skill}` is absent 2. **Context files check:** For each file in `context_files_updated`, spot-check that the dropped skill/version is no longer referenced between the markers. 3. **Purge check (purge mode only):** For each path in `files_deleted`, confirm it no longer exists on disk. If any verification fails, record the specific failure in `verification_errors` but do not halt — proceed to step 3 so the report can surface what succeeded and what needs manual attention. ### 6. Store Results in Context Store the following for step 3: - `files_deleted` — list of directory paths actually deleted (purge mode) or `[]` (soft drop) - `disk_freed` — human-readable size (purge mode) or `"N/A (soft drop)"` - `delete_failures` — list of `{path, error}` for paths whose deletion was attempted but failed (empty if none; a *full* purge failure already HALTed in section 4 and never reaches this step) - `purge_status` — `"success"`, `"partial"` (some paths failed to delete), or `"success"` for soft drops; step 3 maps this to the on-disk result record's `status` field - `manifest_updated` — boolean (true if section 2 succeeded) - `context_files_updated` — list of successfully rebuilt files - `context_files_failed` — list of files that failed to rebuild (empty if none) - `verification_errors` — list of verification failures (empty if none) ### 7. Load Next Step The report in `{nextStepFile}` renders from the results stored in §6, so chain to it only after every execution stage above has been attempted and its outcome stored. Load, read the full file, and then execute it. -
headless-contract.md 1.6 KB
# Headless Result Envelope The single-line JSON contract every headless run of skf-drop-skill emits. It stands alone: the stages that emit it load this file directly, so an error path never depends on any other file staying in context under compaction. When `{headless_mode}` is true, step 3 emits this envelope on **stdout** before chaining to step 4; every HARD HALT emits the same shape on **stderr** with `status: "error"`: ``` SKF_DROP_SKILL_RESULT_JSON: {"status":"success|error|dry-run","skill":"…|null","drop_mode":"…|null","versions_affected":[],"files_deleted":[],"manifest_updated":false,"exit_code":0,"halt_reason":null} ``` Field rules: - `status` — `"success"` on the terminal happy path, `"dry-run"` when `--dry-run` was set and the workflow exited at the confirmation gate before any mutation, `"error"` on any HALT. - `halt_reason` — `null` on success/dry-run; otherwise one of `"input-missing"`, `"input-invalid"`, `"manifest-corrupt"`, `"nothing-to-drop"`, `"active-version-guard-refused"`, `"headless-purge-forbidden"`, `"manifest-write-failed"`, `"context-rebuild-failed"`, `"delete-failed"`, `"write-failed"`, `"user-cancelled"`. - `exit_code` — the stable numeric code the emitting HALT specifies (`0` on success/dry-run). - `skill` / `drop_mode` / `versions_affected` / `files_deleted` / `manifest_updated` — the discriminating values the emit site supplies; any key not yet resolved at that point takes the default shown in the template (`null`, `[]`, or `false`). Each emit site supplies its own `exit_code`, `halt_reason`, and whichever discriminating fields are known at that point; all other keys default as shown. -
health-check.md 596 B
--- # Note: `shared/health-check.md` resolves relative to the SKF module root # ({project-root}/_bmad/skf/ when installed, {project-root}/src/ during # development), NOT relative to this step file. nextStepFile: 'shared/health-check.md' --- <!-- Config: communicate in {communication_language}. --> # Step 4: Workflow Health Check 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. -
report.md 6.9 KB
--- nextStepFile: 'health-check.md' # §1 resolves `{manifestOpsHelper}` from this order (installed SKF module path # first, src/ dev-checkout fallback) to read the target skill's remaining # versions — `get` for the versions+status map and `affected-versions` for the # numeric semver-descending order — instead of re-parsing the manifest by hand. # A read, not an atomicity-critical write: if neither path resolves, §1 reads # the manifest in-prompt. manifestOpsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-manifest-ops.py' - '{project-root}/src/shared/scripts/skf-manifest-ops.py' --- <!-- Config: communicate in {communication_language}. Render the report block in {document_output_language}. --> # Step 3: Report Drop Results ## STEP GOAL: Present a clear, final summary of what the drop workflow changed — manifest state, platform context files, deleted directories, disk freed, and remaining versions — so the user can verify the outcome and know whether any manual follow-up is required. ## Rules - Focus only on reporting results stored in context by step 2 — do not re-execute any part of the drop - Do not hide verification errors or failed context file rebuilds - Chains to the local health-check step via `{nextStepFile}` after completion (see §3) ## MANDATORY SEQUENCE ### 1. Determine Remaining Versions **If `is_skill_level == true`:** Set `remaining_versions_display = "(skill fully removed)"`. **If `is_skill_level == false`:** **Resolve `{manifestOpsHelper}`** ← first existing path in `{manifestOpsProbeOrder}`, then read the target skill's remaining versions through it rather than re-parsing the manifest by hand: ```bash python3 {manifestOpsHelper} {skills_output_folder} get {target_skill} python3 {manifestOpsHelper} {skills_output_folder} affected-versions {target_skill} ``` `get` returns `result.entry` (its `active_version` and its `versions` map with each version's `status`); `affected-versions` returns `result.affected_versions` sorted numerically-descending (so `0.10.0` precedes `0.9.0`). Build the display in that order, annotating each version with its `status` and marking `active_version` with a trailing `*`: ``` - 0.6.0 (active) * - 0.5.0 (archived) - 0.1.0 (deprecated) ``` **If neither `{manifestOpsProbeOrder}` candidate resolves:** read `exports.{target_skill}.versions` from `{skills_output_folder}/.export-manifest.json` in-prompt, list each remaining version with its `status` (active marked `*`), ordering newest-first by comparing version components numerically. ### 2. Render the Report Display the following block, filling in values from context: ``` **Drop operation complete.** Operation: {Deprecate | Purge} Skill: {target_skill} Version(s): {comma-separated target_versions or "ALL"} Changes: - Manifest updated: {yes | no} - Context files rebuilt: {list from context_files_updated, or "(none)"} {if context_files_failed is non-empty:} - Context files FAILED: {list from context_files_failed} {if drop_mode == "purge":} - Files deleted: {list from files_deleted, or "(none — nothing on disk)"} - Disk space freed: {disk_freed} Remaining versions for {target_skill}: {remaining_versions_display} {if drop_mode == "deprecate":} **Note:** Files remain on disk. This operation is reversible by manually editing `{skills_output_folder}/.export-manifest.json` and changing the version's `status` field back to `"active"` or `"archived"`, then re-running `[EX] Export Skill` to restore the managed section entry. {if verification_errors is non-empty:} **Verification warnings:** {list each verification error} These require manual review — see the error-handling guidance in step 2. ``` ### Result Contract Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{skills_output_folder}/drop-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include all purged file paths in `outputs`; include `target_skill`, `drop_mode`, `versions_affected`, and a `headless_provenance` object in `summary`. `headless_provenance` persists the §8/§10 decision trail from step 1 so an unattended run's auto-decisions survive in the durable record, not only in the transient log — a consumer can tell an operator-confirmed drop from a headless auto-confirmed one without re-deriving it: ```json "headless_provenance": {"headless": {headless_mode}, "mode_source": "{mode_source}", "confirm": "{confirm_source}"} ``` where `{headless_mode}` is the resolved boolean, `{mode_source}` is the step-1 §8 value (`"--mode argument"` / `"customize.toml.workflow.default_mode"` / `"interactive-prompt"` / `"draft-skill-forced-purge"`), and `{confirm_source}` is the step-1 §10 value (`"headless-auto"` / `"user-explicit"`). Set the record's `status` from step 2's `purge_status`: `"partial"` when some (but not all) purge-mode directories failed to delete — surface the failing paths from `delete_failures` in `summary.delete_failures` — otherwise `"success"`. A *full* purge failure never reaches this step: step 2 §4 HALTs with `halt_reason: "delete-failed"` and the error-envelope path below handles it. When `{headless_mode}` is true, also emit the single-line envelope on **stdout** before chaining to step 4 (the full shape and field rules are in `references/headless-contract.md`): ``` SKF_DROP_SKILL_RESULT_JSON: {"status":"success","skill":"{target_skill}","drop_mode":"{drop_mode}","versions_affected":{target_versions},"files_deleted":{files_deleted},"manifest_updated":{manifest_updated},"exit_code":0,"halt_reason":null} ``` Substitute `{target_versions}` as a JSON array (e.g. `["0.5.0"]`) or the literal string `"all"`; substitute `{files_deleted}` as a JSON array of absolute paths (`[]` in soft-drop mode); `manifest_updated` is the boolean from step 2's context. ### Post-drop hook (optional) If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 from `workflow.on_complete`), invoke it once the result contract above is finalized: ```bash {onCompleteCommand} --result-path={result_json_path} ``` where `{result_json_path}` is the per-run record written above (`{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json`). Log success or failure to `workflow_warnings[]` — 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. ### 3. Chain to Health Check ONLY WHEN the report has been rendered and the result contract saved will you then load, read the full file, and execute `{nextStepFile}`. The health-check step is the true terminal step — do not stop here even though the report reads as final, and do not re-run any earlier step after it completes (a fresh drop means re-invoking the workflow from the top). -
select.md 21.8 KB
--- nextStepFile: 'execute.md' versionPathsKnowledge: 'knowledge/version-paths.md' # Read-side inventory helpers (reads, not atomicity-critical). §2 uses # `{manifestOpsHelper} read` (manifest parse + v1→v2 migration + corrupt-JSON # detection); §3 uses `{skillInventoryHelper}` (on-disk scan + exports∪on-disk # diff for orphan detection) and `{manifestOpsHelper} affected-versions <skill>` # (numeric semver-descending order, so 0.10.0 precedes 0.9.0 — LLM-unreliable). # Probe each in order (installed SKF path first, src/ fallback); first hit wins. # If neither candidate resolves, the section computes the result in-prompt. manifestOpsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-manifest-ops.py' - '{project-root}/src/shared/scripts/skf-manifest-ops.py' skillInventoryProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-skill-inventory.py' - '{project-root}/src/shared/scripts/skf-skill-inventory.py' # Standalone single-line result-envelope contract emitted at every headless # HALT in this step. Loaded in §1 so no error path depends on SKILL.md # remaining in context under compaction. headlessContract: 'headless-contract.md' # Deterministic recursive byte sizing + human formatting for the §9b # blast-radius line. Bundled with this skill (no probe order needed); # execute.md §4 reuses the same helper for the canonical `disk_freed`. dirSizesHelper: 'scripts/dir-sizes.py' --- <!-- Config: communicate in {communication_language}. --> # Step 1: Select Drop Target ## STEP GOAL: Identify exactly what the user wants to drop — which skill, which version(s), and whether the drop is a soft deprecation (manifest-only) or a hard purge (files deleted). Enforce the active version guard, gather the list of affected directories, and obtain explicit user confirmation before any write or delete operation is scheduled. ## Rules - Focus only on selection, validation, and confirmation — do not modify the manifest or delete files - Do not proceed without explicit user confirmation at the final gate - Do not drop an active version when other non-deprecated versions exist - Present selections clearly so the user can verify scope, mode, and blast radius - **Interactive cancel (every gate below):** at any prompt, `cancel` / `exit` / `[X]` / `q` / `:q` → display "Cancelled — no changes were made." and HALT (exit code 6, `halt_reason: "user-cancelled"`). Stated once here; the §10 commit gate adds its own tip + headless envelope on top of this. ## MANDATORY SEQUENCE ### 1. Load Knowledge Read `{versionPathsKnowledge}` completely and extract: - Path templates: `{skill_package}`, `{skill_group}`, `{forge_version}`, `{forge_group}` - Export manifest v2 schema (`schema_version`, `exports`, `active_version`, `versions` map, `status` field values) - Skill management operations (Drop section — soft vs hard, active version guard, skill-level drop) You will use these templates and rules to build directory paths and enforce safety guards in the following sections. If `{headless_mode}` is true, also read `{headlessContract}` now — it defines the single-line result envelope every HALT below emits, so the shape is in context even if SKILL.md was compacted out. ### 2. Read Export Manifest **Resolve `{manifestOpsHelper}`** ← first existing path in `{manifestOpsProbeOrder}`. Parse the manifest through it rather than hand-rolling JSON — the helper migrates v1→v2, normalizes `platforms`→`ides`, and reports a parse error deterministically: ```bash python3 {manifestOpsHelper} {skills_output_folder} read ``` Read the JSON result: - **`status == "error"`** — the manifest file exists but is malformed (the helper returns `{"status":"error","error":"Manifest JSON parse error: ..."}`): halt with "**Export manifest is corrupt** at `{skills_output_folder}/.export-manifest.json` — fix or remove the file before dropping." HALT (exit code 3, `halt_reason: "manifest-corrupt"`). In headless mode, emit the error envelope per `{headlessContract}` with `skill: null`, `drop_mode: null`, `versions_affected: []`. - **`status == "ok"`** — use `result.manifest` (already migrated to v2) as `manifest` for the rest of this step. Set `manifest_exists = true` when `manifest.exports` has at least one entry, else `false` (a missing manifest reads back as an empty `exports` object, so it correctly yields `false`). When `manifest_exists = false`, section 3's on-disk scan is authoritative: draft skills (created by `[CS]`/`[QS]`/`[SS]` but never exported) can still be hard-dropped in purge mode, and section 8 restricts the options to purge only — soft-deprecate is meaningless without a manifest entry to record it against. **If neither `{manifestOpsProbeOrder}` candidate resolves:** read the manifest file in-prompt — a missing/empty file is `manifest_exists = false`; a file with `exports` entries is `true` (no `schema_version` field means v1 — treat each entry as a single active version); invalid JSON takes the corrupt-manifest HALT above. ### 3. List Available Skills Build and display a summary of every skill available to drop: every manifest-tracked skill, plus every on-disk skill not in the manifest (draft/orphaned, eligible for purge only). **Manifest-tracked skills** come from the `manifest` resolved in section 2 — for each key in `manifest.exports`, its `active_version` and its `versions` map (with each version's `status`) are already parsed. Order each skill's versions newest-first through the helper rather than by eye: ```bash python3 {manifestOpsHelper} {skills_output_folder} affected-versions {skill-name} ``` `result.affected_versions` is that skill's versions deduped and sorted in the helper's numeric-descending order (see the frontmatter note). Annotate each with its `status` from `manifest.exports.{skill-name}.versions.{version}.status` and mark `active_version` with a trailing `*`. **On-disk (not-in-manifest) skills** come from the inventory helper. **Resolve `{skillInventoryHelper}`** ← first existing path in `{skillInventoryProbeOrder}`; it scans `{skills_output_folder}/` and computes the exports∪on-disk merge for you — do not re-scan the directory in the prompt: ```bash python3 {skillInventoryHelper} {skills_output_folder} ``` Any `result.skills[].name` that is **not** a key in `manifest.exports` is a draft or orphaned skill — record it as "(not in manifest — purge only)". When the manifest is empty, every on-disk skill lands here. The inventory lists only skills with an on-disk directory, so a manifest entry whose files were already removed still appears above via `manifest.exports`. **If the combined roster is empty** (no `manifest.exports` entries AND `result.skills[]` is empty): halt with "**Drop Skill — nothing to drop.** No skills found in `{skills_output_folder}/` and no entries in `.export-manifest.json`. Run `[CS] Create Skill` first." HALT (exit code 3, `halt_reason: "nothing-to-drop"`). In headless mode, emit the error envelope per `{headlessContract}` with `skill: null`, `drop_mode: null`, `versions_affected: []`. Display the combined list (versions newest-first): ``` **Drop Skill — select target** Available skills: 1. cognee - 0.6.0 (active) * - 0.5.0 (archived) - 0.1.0 (deprecated) 2. express - 4.18.0 (active) * 3. legacy-helper (not in manifest — purge only) ``` **If neither helper resolves** (Python/helper unavailable): fall back to the in-prompt computation — list each `manifest.exports` skill (versions with `status`, active marked `*`, ordered newest-first by comparing version components numerically), then scan `{skills_output_folder}/` for top-level directories absent from `manifest.exports` and record them as "(not in manifest — purge only)". If the combined list is empty, take the "nothing to drop" HALT above. ### 4. Ask Which Skill "**Which skill would you like to drop?** Enter the skill name or its number from the list above, or `cancel` / `exit` / `:q` to abort." Wait for user input. Accept either the numeric index or the skill name (exact match). **GATE [default: use args]** — If `{headless_mode}` and skill name was provided as argument: select that skill and auto-proceed. If not provided, HALT (exit code 2, `halt_reason: "input-missing"`): "headless mode requires skill name argument." In headless mode, emit the error envelope per `{headlessContract}` with `skill: null`, `drop_mode: null`. - **If the user's input does not match any listed skill:** - **Interactive:** Re-display the list and ask again. - **Headless (`{headless_mode}` is true):** the supplied `skill_name` argument resolves to no skill in the combined list — there is no further input to re-prompt for. HALT (exit code 2, `halt_reason: "input-invalid"`): "headless mode: skill argument `{supplied value}` does not match any listed skill." Emit the error envelope per `{headlessContract}` with `skill: null`, `drop_mode: null`, `versions_affected: []`. Store the selection as `target_skill`. Also store `target_in_manifest = true` if the selected skill has an entry in the manifest, `false` otherwise — subsequent sections use this flag to restrict the available drop options. ### 5. Display Version Details **If `target_in_manifest = true`**, display every version with its full metadata from the manifest: ``` **{target_skill} — versions:** | Version | Status | Last Exported | Platforms | |---------|------------|---------------|------------------------| | 0.1.0 | deprecated | 2026-01-15 | claude | | 0.5.0 | archived | 2026-03-15 | claude | | 0.6.0 | active * | 2026-04-04 | claude, copilot | ``` **If `target_in_manifest = false`** (draft skill discovered only by on-disk scan), display the on-disk version directories instead and note the constraint: ``` **{target_skill} — on-disk versions (not in manifest):** {list version subdirectories found under {skills_output_folder}/{target_skill}/, or "(flat layout)" if no version nesting is present} **Note:** This skill has no manifest entry, so soft-deprecate is not available. Only a skill-level hard purge can be performed — the drop will delete the entire on-disk skill group and forge group. ``` ### 6. Ask Scope **If `target_in_manifest = false`:** Skip this prompt — draft skills can only be dropped as a whole. Set `target_versions = "all"` and `is_skill_level = true`, then proceed to section 7. **If `target_in_manifest = true`:** "**Drop which version(s)?** - **[N]** Specific version — soft deprecate or hard purge a single version - **[A]** All versions — drops the entire skill (skill-level operation) - **[X]** Cancel and exit (or type `cancel` / `exit` / `:q`)" Wait for user selection. **If [N] Specific version:** "**Which version?** Enter the version string (e.g. `0.5.0`)." Wait for user input. Validate that the version exists in the manifest's `versions` map for `target_skill`. - **If it does not match (interactive):** repeat the prompt. - **If it does not match (headless — `{headless_mode}` is true):** the supplied `version` argument is unparseable or absent from the `versions` map — there is no further input to re-prompt for. HALT (exit code 2, `halt_reason: "input-invalid"`): "headless mode: version argument `{supplied value}` does not exist in `{target_skill}`'s versions." Emit the error envelope per `{headlessContract}` with `skill: "{target_skill}"`, `drop_mode: null`, `versions_affected: []`. Set `target_versions = [<selected version>]` and `is_skill_level = false`. **If [A] All versions:** Set `target_versions = "all"` and `is_skill_level = true`. ### 7. Active Version Guard **Does not apply when `target_in_manifest = false`:** A draft skill has no manifest-recorded active version, so the guard is a no-op. Proceed to section 8. **Applies only when `target_in_manifest = true` AND `is_skill_level = false` (specific version selected):** 1. Read the selected version's `status` field from the manifest 2. If `status != "active"` → skip this guard, the version is safe to drop 3. If `status == "active"`: a. Count the number of OTHER versions in the `versions` map with `status != "deprecated"` (i.e., `active`, `archived`, or `draft`) b. If that count is `> 0` → REFUSE the drop: "**Cannot drop the active version `{version}`.** Other non-deprecated versions of `{target_skill}` still exist. To proceed, either: **(a)** Switch the active version to another version first by re-running `[EX] Export Skill` with a different version selected, then return here to drop `{version}`, OR **(b)** Use the `[A] All versions` option to drop every version of `{target_skill}` at once." HALT (exit code 5, `halt_reason: "active-version-guard-refused"`). In headless mode, emit the error envelope per `{headlessContract}` with `skill: "{target_skill}"`, `drop_mode: null`, `versions_affected: ["{version}"]`. Do not proceed. c. If the count is `0` → the active version is the ONLY version; allow the drop to continue (it is functionally equivalent to a skill-level drop on a single-version skill) ### 8. Ask Mode **If `target_in_manifest = false`:** Skip this prompt — soft-deprecate is meaningless without a manifest entry to mark. Force `drop_mode = "purge"` (record `mode_source = "draft-skill-forced-purge"`) and inform the user: "**Mode forced to purge** — `{target_skill}` has no manifest entry, so there is nothing to deprecate. The skill's on-disk directories will be deleted." **If `target_in_manifest = true`:** **If a `mode` argument was supplied at invocation:** an explicit `mode` arg is the per-run override and takes precedence over `{defaultMode}`. If it is `"deprecate"` or `"purge"`, set `drop_mode` from it and record the decision source `mode_source = "--mode argument"`. If a `mode` arg was supplied but is not one of `deprecate` / `purge`, HALT (exit code 2, `halt_reason: "input-invalid"`): "invalid `--mode` value `{supplied}` — expected `deprecate` or `purge`." In headless mode, emit the error envelope per `{headlessContract}` with `skill: "{target_skill}"`, `drop_mode: null`. **Else if `{defaultMode}` is non-empty (`"deprecate"` or `"purge"`)**: skip the prompt, set `drop_mode = "{defaultMode}"`, and record the decision source `mode_source = "customize.toml.workflow.default_mode"` for the headless decision trail. **Otherwise (interactive):** If `{headless_mode}` is true at this point (no `mode` arg and no `{defaultMode}`), there is no input to prompt for — HALT (exit code 2, `halt_reason: "input-missing"`): "headless mode requires `--mode deprecate|purge` or `default_mode` in customize.toml to set the drop mode." Emit the error envelope per `{headlessContract}` with `skill: "{target_skill}"`, `drop_mode: null`. Otherwise, prompt the user: "**How should this be dropped?** - **[D]** Deprecate (soft) — Mark the version as `deprecated` in the manifest. Files remain on disk. Export-skill will exclude it from all platform context files. Reversible by editing the manifest. - **[P]** Purge (hard) — Deprecate AND delete files from disk (`{skill_package}` and `{forge_version}`, or full `{skill_group}` and `{forge_group}` for a skill-level drop). **Irreversible.** - **[X]** Cancel and exit (or type `cancel` / `exit` / `:q`)" Wait for user selection. Set `drop_mode` to `"deprecate"` (on D) or `"purge"` (on P), and record `mode_source = "interactive-prompt"`. ### 9. Compute Affected Directories Using the templates from `{versionPathsKnowledge}`, resolve the list of directories that would be affected: **If `is_skill_level = false` (version-level drop):** - `{skill_package}` = `{skills_output_folder}/{target_skill}/{version}/{target_skill}/` - The enclosing version directory = `{skills_output_folder}/{target_skill}/{version}/` - `{forge_version}` = `{forge_data_folder}/{target_skill}/{version}/` **If `is_skill_level = true` (skill-level drop):** - `{skill_group}` = `{skills_output_folder}/{target_skill}/` - `{forge_group}` = `{forge_data_folder}/{target_skill}/` Store the list as `affected_directories`. If `drop_mode == "deprecate"`, record the list but present it as "retained" in the confirmation output — no deletion will occur. #### 9b. Compute Blast-Radius Metrics (for §10 summary) Compute three scalars to put in front of the path list at §10, so the user sees the scale of an irreversible drop before scanning individual paths: 1. **`versions_count`** — the number of skill versions in scope: - Version-level drop: `len(target_versions)` (typically `1`) - Skill-level drop: count of non-deprecated versions in `exports.{target_skill}.versions` (the deprecated ones are already absent from the active managed sections) 2. **`bytes_total`** — the on-disk size of `affected_directories`. Delegate the recursive sum and the human label to the sizing helper rather than adding file sizes in-prompt: ```bash uv run {dirSizesHelper} sizes {each path in affected_directories, space-separated} ``` Read `total_human` (e.g. `"4.2 MB"`) as `bytes_total` and `total_bytes` as `bytes_total_raw`; non-existent paths report `exists: false` and drop out of the total. If the helper is unavailable, fall back to `du -sb` per path — the display is best-effort. execute.md §4 re-runs the same helper on the paths it actually deletes for the canonical `disk_freed`, so the two share one method and differ only if files change between this gate and execution. 3. **`context_files_count`** — the number of distinct context files the §3 rebuild loop will rewrite: - Read `config.yaml.ides` - For each entry, look up its `context_file` via the canonical mapping table in `skf-export-skill/assets/managed-section-format.md` (use the `{unknownIdeDefaultContextFile}` fallback for unknown IDEs) - Deduplicate by `context_file` - Count the result. If `config.yaml.ides` is absent or empty, default to `1` (the single `{unknownIdeDefaultContextFile}` fallback) Store as `blast_radius = {versions_count, bytes_total, bytes_total_raw, context_files_count}` for §10's summary line. ### 10. Confirmation Gate Display the full operation summary with the blast-radius summary line ahead of the path list so the user sees the scale before scanning paths: ``` **About to drop:** Skill: {target_skill} Version: {version or "ALL versions"} Mode: {Deprecate (soft) | Purge (hard)} Scope: {versions_count} version(s), ~{bytes_total} on disk, will rebuild {context_files_count} context file(s) Files: {for each path in affected_directories, list one per line} {or "(retained on disk — soft drop)" if drop_mode == "deprecate"} {if drop_mode == "purge":} ⚠️ This operation cannot be undone. Files will be permanently deleted. {else:} Files remain on disk. Reversible by manually editing the manifest. Proceed? [Y/N] ``` The `Scope:` line is the §9b-computed `blast_radius` rendered as one line. In `deprecate` mode the `~{bytes_total} on disk` reads as "size that will remain on disk (soft drop — files retained)"; the user is still served by knowing it. In `purge` mode it reads as "approximate disk that will be freed". The wording stays the same — the surrounding `Mode:` field disambiguates intent. **Resolve `--dry-run` first — it takes precedence over the headless auto-confirm.** `--dry-run` and `--headless` can be combined (dry-run is the automated-preview path), so a dry-run run must always short-circuit to the preview below and never mutate, even when `{headless_mode}` is true. **If `--dry-run` was passed**: skip the Y/N prompt entirely — do not evaluate the headless auto-confirm gate. Display the `[DRY RUN]` line with the resolved selection, so the user can re-run interactively with the same values when ready to commit: ``` **[DRY RUN] No changes were made — preview above shows what would be dropped.** Resolved selection: Skill: {target_skill} Version: {target_versions[0] if is_skill_level == false else "all"} Mode: {Deprecate (soft) | Purge (hard)} ``` Then emit the success envelope per `{headlessContract}` with `status: "dry-run"`, the resolved `skill`, `drop_mode`, and `versions_affected`, then HALT (exit code 0). The manifest, filesystem, and context files are untouched. **Otherwise (not a dry-run) — GATE [default: Y]:** If `{headless_mode}`: auto-proceed with [Y], record `confirm_source = "headless-auto"`, log: "headless: auto-confirmed drop of {target_skill}" Wait for explicit user response. - **If `Y`** → record `confirm_source = "user-explicit"` and proceed to section 11 - **If `N`** (or `cancel` / `exit` / `[X]` / `:q`) → "**Cancelled.** No changes were made. (Tip: invoke with `--dry-run` next time to preview the operation without reaching the commit prompt.)" HALT (exit code 6, `halt_reason: "user-cancelled"`). In headless mode, emit the error envelope per `{headlessContract}` with the resolved `skill`, `drop_mode`, and `versions_affected`. - **Any other input** → re-display the confirmation and ask again ### 11. Store Decisions in Context Store the following decisions in workflow context for step 2: - `target_skill` — the skill name - `target_in_manifest` — boolean (true if the skill has a manifest entry, false if it was discovered only by on-disk scan) - `target_versions` — list of version strings (`[<version>]`) or the literal string `"all"` - `drop_mode` — `"deprecate"` or `"purge"` (always `"purge"` when `target_in_manifest = false`) - `is_skill_level` — boolean (true if all versions; always true when `target_in_manifest = false`) - `affected_directories` — list of absolute directory paths that step 2 will delete in purge mode (or retain in deprecate mode) - `mode_source` — where `drop_mode` was decided, set inline at §8 (one of the four sources named there) - `confirm_source` — how the §10 gate was cleared, set inline at §10 (`"headless-auto"` or `"user-explicit"`) ### 12. Load Next Step `{nextStepFile}` performs the destructive mutation, so reach it only after the §10 gate returned `Y` and §11 stored the decisions — chaining any earlier would drop without the user's explicit consent (the sequence above enforces this ordering). Load, read the full file, and then execute it.
-
-
scripts
-
dir-sizes.py 3.9 KB
#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # /// """Deterministic directory-size measurement for skf-drop-skill. The drop workflow twice needs an exact recursive byte total plus a stable human label ("4.2 MB" / "812 KB"): select.md §9b renders the blast-radius line ahead of the confirmation gate, and execute.md §4 reports the canonical `disk_freed`. Summing file sizes and rounding a unit in the prompt has one correct answer per input, so it belongs here — identical input yields identical output, and the gate preview and the post-purge report agree on the method (they can still differ only because they read at different times). Two operations: sizes <path>... recursive byte size of each path (best-effort: a non-existent path reports exists=false, bytes=0 and is excluded from the total). Symlinks are measured by the size of the link itself, never followed. humanize <bytes>... sum a set of already-measured byte counts and format one human label. execute.md §4 feeds it the sizes of the paths that were actually deleted. Output is a single JSON object on stdout. Exit codes: 0 ok; 2 usage error (missing/unknown op, or a non-integer byte count for `humanize`). """ from __future__ import annotations import json import os import sys # 1024-based, matching the `du -h` feel the step files reference. Labelled # KB/MB for readability; the raw integer total travels alongside for any # consumer that needs exact arithmetic. _UNITS = ["B", "KB", "MB", "GB", "TB", "PB"] def humanize_bytes(n: int) -> str: size = float(n) for unit in _UNITS: if size < 1024 or unit == _UNITS[-1]: if unit == "B": return f"{int(size)} {unit}" return f"{size:.1f} {unit}" size /= 1024 return f"{int(n)} B" # unreachable; keeps type checkers happy def dir_bytes(path: str) -> int: """Recursive byte size of a file, directory, or symlink (link not followed).""" if os.path.islink(path): return os.lstat(path).st_size if os.path.isfile(path): return os.path.getsize(path) total = 0 for root, _dirs, files in os.walk(path): for name in files: fp = os.path.join(root, name) try: total += os.lstat(fp).st_size except OSError: pass return total def _op_sizes(paths: list[str]) -> dict: entries = [] total = 0 for p in paths: exists = os.path.exists(p) or os.path.islink(p) b = dir_bytes(p) if exists else 0 if exists: total += b entries.append({"path": p, "exists": exists, "bytes": b, "human": humanize_bytes(b)}) return { "status": "ok", "op": "sizes", "paths": entries, "total_bytes": total, "total_human": humanize_bytes(total), } def _op_humanize(raw: list[str]) -> dict: total = 0 for value in raw: try: total += int(value) except (TypeError, ValueError): print( json.dumps({"status": "error", "error": f"not an integer byte count: {value!r}"}), file=sys.stderr, ) sys.exit(2) return {"status": "ok", "op": "humanize", "total_bytes": total, "total_human": humanize_bytes(total)} def main(argv: list[str]) -> int: if not argv: print(json.dumps({"status": "error", "error": "usage: dir-sizes.py {sizes|humanize} <args>"}), file=sys.stderr) return 2 op, args = argv[0], argv[1:] if op == "sizes": print(json.dumps(_op_sizes(args))) return 0 if op == "humanize": print(json.dumps(_op_humanize(args))) return 0 print(json.dumps({"status": "error", "error": f"unknown op: {op!r}"}), file=sys.stderr) return 2 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))
-
-
customize.toml 2.6 KB
# DO NOT EDIT -- overwritten on every update. # # Workflow customization surface for skf-drop-skill. # Team overrides: {project-root}/_bmad/custom/skf-drop-skill.toml # Personal overrides: {project-root}/_bmad/custom/skf-drop-skill.user.toml [workflow] # --- Configurable below. Overrides merge per BMad structural rules: --- # scalars: override wins • arrays (persistent_facts, activation_steps_*): append # arrays-of-tables with `code`/`id`: replace matching items, append new ones. # Steps to run before the standard activation (uv probe, config load). # Overrides append. Use for org-wide pre-flight checks (audit-log emit, # ticket-reference enforcement) that must precede any destructive work. activation_steps_prepend = [] # Steps to run after activation but before the first stage executes. # Overrides append. Use for context loads or banner customization that # should run once activation completes successfully. activation_steps_append = [] # Persistent facts the workflow keeps in mind for the whole run # (drop-policy reminders, retention guardrails, audit-trail rules). # Overrides append. # # Each entry is either: # - a literal sentence, e.g. "Purges of skills tagged `compliance` require ticket reference." # - a file reference prefixed with `file:`, e.g. # "file:{project-root}/docs/drop-policy.md" (globs supported; file # contents are loaded and treated as facts). persistent_facts = [ "file:{project-root}/**/project-context.md", ] # Optional post-drop hook. When non-empty, invoked with # `--result-path=<path>` after the drop-skill result JSON is finalized # (references/report.md). Failures are logged to workflow_warnings[] but # never fail the workflow — useful for audit-log emit, a notifier, or a # ticket-tracker integration after an irreversible purge. on_complete = "" # --- Optional safety + default scalars --- # # Default drop mode. Empty string = always prompt the user at §8 Ask Mode. # Set to "deprecate" or "purge" to skip the prompt — the headless # auto-decision log records the override source. default_mode = "" # Hard-block hard-purge in headless mode. When set to "true", a headless # invocation with `mode=purge` HALTs at activation with exit code 6 # (`halt_reason: "headless-purge-forbidden"`) instead of silently deleting # files. Operators must either re-run with `mode=deprecate` or unset this # flag for that run. Empty string = no guard (default). forbid_purge_in_headless = "" # Fallback IDE → context-file mapping for unknown IDEs in `config.yaml.ides`. # Empty string = use bundled defaults (AGENTS.md / .agents/skills/). unknown_ide_default_context_file = "" unknown_ide_default_skill_root = "" -
SKILL.md 10.1 KB
--- name: skf-drop-skill description: Drop a specific skill version or an entire skill — soft (deprecate) or hard (purge) with platform context rebuild. Use when the user requests to "drop" or "remove a skill." --- # Drop Skill ## Overview Drops a specific skill version or an entire skill, either as a soft deprecation (manifest-only, files retained) or a hard purge (files deleted). Ensures platform context files are rebuilt to exclude dropped versions. In interactive mode every destructive action requires explicit user confirmation — nothing is deleted silently; headless runs auto-resolve the gates with their default action and log each auto-decision. The export manifest is the source of truth; the filesystem is updated to match. ## Conventions - Bare paths (e.g. `references/<name>.md`) resolve from the skill root. - `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates. - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present). - `{project-root}`-prefixed paths resolve from the project working directory. - `{skill-name}` resolves to the skill directory's basename. - **Module-level path exception:** paths starting with `knowledge/` or `shared/` resolve from the SKF module root, not the skill root — install layout puts both at `{project-root}/_bmad/skf/`. The `versionPathsKnowledge: 'knowledge/version-paths.md'` frontmatter scalar in stage files uses this convention; same for `shared/health-check.md` chained from the terminal step. - **Cross-skill data coupling:** `references/execute.md` reads `skf-export-skill/assets/managed-section-format.md` for the IDE→context-file mapping table and the four-case (Create / Append / Regenerate / Malformed) logic when rebuilding context files. Drop-skill assumes that asset is present at install time and that its semantics are stable across the two skills' versions. ## Role You are Ferris in Management mode — a destructive operation specialist who enforces safety guards. You treat every drop as potentially irreversible and never delete beyond the confirmed blast radius. You protect the active version, keep the export manifest consistent with on-disk state, and ensure downstream platform context files are rebuilt. ## Workflow Rules These rules apply to every step in this workflow: - Never delete files in purge mode without clearing the §10 confirmation gate (auto-resolved with its default in headless) - Never drop an active version when other non-deprecated versions exist — enforce the active version guard - Only load one step file at a time — never preload future steps - If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread - Always communicate in `{communication_language}` - At any interactive prompt, the inputs `cancel`, `exit`, `[X]`, `q`, or `:q` exit cleanly with exit code 6 (`halt_reason: "user-cancelled"`) - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision ## Stages | # | Step | File | Auto-proceed | |---|------|------|--------------| | 1 | Select Target | references/select.md | No (confirm) | | 2 | Execute Drop | references/execute.md | Yes | | 3 | Report | references/report.md | Yes | | 4 | Workflow Health Check | references/health-check.md | Yes | ## Invocation Contract | Aspect | Detail | |--------|--------| | **Inputs** | skill_name [required], mode (deprecate/purge) [required], version (all/specific) [required] | | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--dry-run` (run selection + display the §10 confirmation block, then exit with `status="dry-run"` — no manifest mutation, no file deletion). Useful for "show me what this would touch before I commit." | | **Gates** | step 1: Input Gate [use args], Confirm Gate [Y] | | **Outputs** | Updated manifest, rebuilt context files, (purge: deleted directories), `drop-skill-result-{timestamp}.json` and `drop-skill-result-latest.json` | | **Headless** | Gates auto-resolve with their default action (see Workflow Rules). When `forbid_purge_in_headless` is `"true"` in `customize.toml` AND the effective drop mode is `"purge"` (defined in On-Activation §4 — explicit `mode=purge` or `default_mode` purge), §4 HALTs with exit code 6 (`halt_reason: "headless-purge-forbidden"`) before any work begins. | | **Exit codes** | See "Exit Codes" below | ## Exit Codes Every hard HALT exits with a stable code so headless automators branch on the failure class without grepping message text. The `Raised by` column names the HALT class per code; the authoritative per-site declarations (with exact `halt_reason`) live in the step files. | Code | Meaning | Raised by (class) | | ---- | -------------------- | ----------------- | | 0 | success | step 4 (terminal) | | 2 | input-missing / input-invalid | step 1 headless input gates — missing or unmatched `skill_name`, `version`, or `--mode` value (§4 / §6 / §8) | | 3 | resolution-failure | step 1 manifest/skill-list resolution (§2 corrupt manifest, §3 nothing to drop) | | 4 | write-failure | On-Activation write probe; step 2 manifest write / context rebuild / full-purge failure | | 5 | state-conflict | step 1 active-version guard (§7) | | 6 | user-cancelled | any interactive cancel or confirm-gate `[N]`; On-Activation headless-purge guard | ## Result Contract (Headless) When `{headless_mode}` is true, step 3 emits a single-line `SKF_DROP_SKILL_RESULT_JSON:` envelope on **stdout**; every HARD HALT emits the same shape on **stderr** with `status: "error"`. The template, `status`/`halt_reason` semantics, `exit_code` (per the Exit Codes table above), and full enum live in `references/headless-contract.md` — the emitting stages load it directly, so an error path never depends on this file. ## On Activation 1. Load config from `{project-root}/_bmad/skf/config.yaml` and resolve: - `project_name`, `output_folder`, `user_name`, `communication_language`, `document_output_language` - `skills_output_folder`, `forge_data_folder`, `sidecar_path` - `snippet_skill_root_override` (optional string) — when set, the context-file rebuild in step 2 preserves any snippet `root:` prefix that matches the override instead of rewriting it to the target IDE's skill root. See `skf-export-skill/assets/managed-section-format.md` for full semantics. - Generate and store `timestamp` as `YYYYMMDD-HHmmss` format. This value is fixed for the entire workflow run. 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. 3. **Resolve workflow customization.** Run: ```bash python3 {project-root}/_bmad/scripts/resolve_customization.py \ --skill {skill-root} --key workflow ``` The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append): - `{skill-root}/customize.toml` — bundled defaults - `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed) - `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored) If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly — the bundled defaults are an empty string for each scalar. Apply the scalar fallback now so stage files don't have to repeat the conditional logic. For each of the five scalars, if the merged value is empty or absent, the bundled default applies: - `{defaultMode}` ← `workflow.default_mode` (empty = always prompt; `"deprecate"` or `"purge"` = skip §8 Ask Mode) - `{forbidPurgeInHeadless}` ← `workflow.forbid_purge_in_headless` (empty or non-`"true"` = no guard) - `{unknownIdeDefaultContextFile}` ← `workflow.unknown_ide_default_context_file` if non-empty, else `AGENTS.md` - `{unknownIdeDefaultSkillRoot}` ← `workflow.unknown_ide_default_skill_root` if non-empty, else `.agents/skills/` - `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty (no-op — step 3 skips the post-drop hook entirely) Stash all five as workflow-context variables. Stage files reference them directly — no conditional at the usage site. Also apply the array surfaces: run `workflow.activation_steps_prepend` now, keep `workflow.persistent_facts` as standing context (`file:` entries load their contents), then run `workflow.activation_steps_append` after. 4. **Pre-flight write probe + headless-purge guard.** First, verify `{skills_output_folder}` is writable. A read-only mount, full disk, or permissions-denied path otherwise only surfaces at step 2's manifest write — by then the user has already gone through every selection prompt: ```bash mkdir -p "{skills_output_folder}" && \ printf 'probe' > "{skills_output_folder}/.skf-write-probe" && \ rm "{skills_output_folder}/.skf-write-probe" ``` On any non-zero exit: HALT (exit code 4, `halt_reason: "write-failed"`). In headless mode, emit the error envelope per **Result Contract (Headless)** with `skill: null` and `drop_mode: null` (neither is resolved yet at activation time). Second, enforce the headless-purge guard. First compute the **effective drop mode**: it is `"purge"` when the parsed `mode` arg is `"purge"`, OR when no `mode` arg was passed AND `{defaultMode}` (resolved in §3) is `"purge"` — a purge reached via `default_mode` is still an unattended irreversible purge and must be caught here, not only an explicit `--mode purge`. If `{headless_mode}` is true AND `{forbidPurgeInHeadless}` is `"true"` AND the effective drop mode is `"purge"`: HALT with exit code 6 and `halt_reason: "headless-purge-forbidden"`, emit the error envelope, and exit immediately. The operator must re-run with an explicit `mode=deprecate` (an explicit arg overrides `default_mode`) or set `forbid_purge_in_headless = ""` (or omit the override entirely) to proceed. 5. Load, read the full file, and then execute `references/select.md` to begin the workflow.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.