skf-rename-skill
Rename a skill across all its versions — transactional copy-verify-delete with platform context rebuild. Use when the user requests to "rename a skill."
Install
npx skills add https://github.com/armelhbobdad/bmad-module-skill-forge/tree/main/src/skf-rename-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
Rename Skill
Overview
Renames a skill across all its versions with transactional safety — copy to the new name, verify all references updated, delete the old name only after verification succeeds. Rebuilds platform context files to reference the new name. The agentskills.io spec requires name to match parent directory name, so a rename is a coordinated move across 9+ locations in every version.
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 skill-index rebuild rules when re-keying context files post-rename. Rename-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 precision surgeon who operates on the entire skill group atomically.
Workflow Rules
These rules apply to every step in this workflow:
- Never delete the old skill directories until the new name has been fully materialized and verified
- Never proceed past a verification failure — roll back (delete new directories) and halt
- Never allow a rename to collide with an existing skill name
- 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 — except the atomicity and commit-gate safety helpers that execute.md §0 resolves: a missing one there is a HARD HALT (exit 4), never an LLM fall-through, because hand-driven writes/scans would silently regress the transactional guarantees that keep a failed rename recoverable
- 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 & Validate | references/select.md | No (confirm) |
| 2 | Execute Rename | references/execute.md | No (confirm) |
| 3 | Report | references/report.md | Yes |
| 4 | Workflow Health Check | references/health-check.md | Yes |
Invocation Contract
| Aspect | Detail |
|---|---|
| Inputs | old_name [required], new_name [required] |
| Flags | --headless / -H (auto-resolve all gates); --dry-run (run selection + validation + display the §8 confirmation block, then exit with status="dry-run" — no copy, no manifest re-key, no delete). Useful for verifying the rename plan before the irreversible §8 (delete old) section. |
| Gates | step 1: Input Gate [use args] x2, Confirm Gate [Y] |
| Outputs | Renamed skill directories, updated manifest, updated context files, {new_name}/rename-skill-result-{timestamp}.json and {new_name}/rename-skill-result-latest.json |
| Concurrency | A PID-file lock at {forge_data_folder}/{old_name}/.skf-rename.lock serializes concurrent runs against the same old_name; a live-PID collision HALTs with halt_reason: "halted-for-concurrent-run" (exit 5). See select.md §4b for the acquire / stale-clear / release mechanism. |
| Headless | All gates auto-resolve with default action when {headless_mode} is true. The §6 source-authority warning HALTs by default in headless when source_authority="official"; set force_source_authority_in_headless = "true" in customize.toml to auto-acknowledge and proceed (the override is recorded in headless_decisions[]). |
| Exit codes | Stable per-failure-class codes — see references/exit-codes.md |
Result Contract (Headless)
When {headless_mode} is true, step 3 emits a single-line JSON envelope on stdout before chaining to step 4, and every HARD HALT emits the same envelope shape on stderr with status: "error":
SKF_RENAME_SKILL_RESULT_JSON: {"status":"success|error|dry-run","old_name":"…|null","new_name":"…|null","versions_renamed":[],"manifest_rekeyed":false,"context_files_updated":[],"exit_code":0,"halt_reason":null,"headless_decisions":[]}
status is "success" on the terminal happy path, "dry-run" when --dry-run was set and the workflow exited before §9 stores decisions, "error" on any HALT. halt_reason is one of: null (success), "input-missing", "input-invalid", "manifest-corrupt", "nothing-to-rename", "name-collision", "source-authority-blocked", "halted-for-concurrent-run", "copy-failed", "verify-failed", "manifest-write-failed", "write-failed", "user-cancelled". (§7 context-file rebuild is best-effort and never halts, so it has no halt_reason.) exit_code matches references/exit-codes.md. headless_decisions is the audit trail of confirmation gates auto-resolved under {headless_mode} — each entry {gate, default_action, taken_action, reason} (the §6 source-authority override and the §8 auto-confirm); it is [] in interactive runs and whenever no gate was auto-resolved before the envelope was emitted.
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 four scalars, if the merged value is empty or absent, the bundled default applies:
{forceSourceAuthorityInHeadless}←workflow.force_source_authority_in_headless(empty or non-"true"= HALT in headless on"official"source-authority){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 string (no-op — step 3 skips the post-completion hook invocation)
Stash all four as workflow-context variables. Stage files reference them directly — no conditional at the usage site.
Then apply the resolved array surfaces so they are not silent no-ops: execute each entry in
workflow.activation_steps_prependin order now; treat every entry inworkflow.persistent_factsas standing context for the whole run (entries prefixedfile:are paths or globs whose contents load as facts — the bundled default loads anyproject-context.mdso rename-policy and public-name-stability guardrails stay in mind); and after activation completes, execute each entry inworkflow.activation_steps_appendin order.Pre-flight write probe. Verify both
{skills_output_folder}and{forge_data_folder}are writable. A read-only mount, full disk, or permissions-denied path otherwise only surfaces inside step 2's transactional copy — by then the user has already gone through name validation and confirmation:for dir in "{skills_output_folder}" "{forge_data_folder}"; do mkdir -p "$dir" && \ printf 'probe' > "$dir/.skf-write-probe" && \ rm "$dir/.skf-write-probe" doneOn any non-zero exit: HALT (exit code 4,
halt_reason: "write-failed"). In headless mode, emit the error envelope per Result Contract (Headless) withold_name: nullandnew_name: null(neither is resolved yet at activation time).Load, read the full file, and then execute
references/select.mdto begin the workflow.
Files (bmad-module-skill-forge)
-
references
-
execute.md 26.5 KB
--- nextStepFile: 'report.md' versionPathsKnowledge: 'knowledge/version-paths.md' managedSectionLogic: 'skf-export-skill/assets/managed-section-format.md' # Resolve `{atomicWriteHelper}` by probing `{atomicWriteProbeOrder}` in # order (installed SKF module path first, src/ dev-checkout fallback); # first existing path wins. §3 + §6 use it for crash-safe file rewrites # (stage to .skf-tmp, fsync, rename) — letting the LLM write directly # risks half-written artifacts on process kill or disk-full mid-write. # HALT if neither candidate exists. atomicWriteProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py' - '{project-root}/src/shared/scripts/skf-atomic-write.py' # Resolve `{updateActiveSymlinkHelper}` similarly. §4 uses it to # atomically repair the `active` symlink with `flock` and the Windows # junction fallback — `rm + ln -s` would race against concurrent readers # and silently break on Windows. updateActiveSymlinkProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-update-active-symlink.py' - '{project-root}/src/shared/scripts/skf-update-active-symlink.py' # Resolve `{manifestOpsHelper}` similarly. §6 uses the `rename` action # for atomic re-key (preserves `active_version`, `versions` map, all # fields, then writes via temp + rename). manifestOpsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-manifest-ops.py' - '{project-root}/src/shared/scripts/skf-manifest-ops.py' # Resolve `{rebuildManagedSectionsHelper}` similarly. §7 uses the # `replace` action for the surgical between-marker rewrite. rebuildManagedSectionsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-rebuild-managed-sections.py' - '{project-root}/src/shared/scripts/skf-rebuild-managed-sections.py' # Resolve `{rewriteSkillNameHelper}` similarly. §3 uses it for the four # field-scoped in-file rename transforms (SKILL.md frontmatter `name`, # metadata.json `name`, provenance-map.json `skill_name`, context-snippet # header + `root:` paths) — each a JSON round-trip or anchored-region edit # plus the atomic write in one call, so the LLM never hand-edits JSON (which # risks key reorder/drop) or eyeballs "only within the frontmatter" (which # mis-fires when {old_name} is a substring, e.g. rename -> renamer). rewriteSkillNameProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-rewrite-skill-name.py' - '{project-root}/src/shared/scripts/skf-rewrite-skill-name.py' # Resolve `{verifyNoTraceHelper}` similarly. §5 uses it for the commit-point # no-trace gate: a fixed-file-set, name-token scan over affected_versions with # the SKILL.md frontmatter/body region split (frontmatter = hard failure, body # = advisory warning) and the directory-listing check, returned as JSON. verifyNoTraceProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-verify-no-trace.py' - '{project-root}/src/shared/scripts/skf-verify-no-trace.py' --- <!-- Config: communicate in {communication_language}. --> # Step 2: Execute Rename (Transactional) ## STEP GOAL: Execute the rename decisions recorded in step 1 as a transaction. Copy the old `{skill_group}` and `{forge_group}` to the new name, rename inner directories, rewrite every in-file reference, verify no trace of the old name remains inside the new location, update the export manifest, rebuild platform context files, and only then delete the old directories. Any failure before the final delete rolls back by removing the new directories — the old skill remains intact. ## Rules - Execute sections strictly in order — each section depends on the previous one - Do not re-prompt the user — decisions were made in step 1 - Do not delete anything from old directories before section 8 - Do not proceed past a verification failure in section 5 - Report each section's outcome as it completes **Headless error envelope (self-contained).** Every HALT below that says *"emit the error envelope"* means: write this single line to **stderr**, mirroring SKILL.md's Result Contract (Headless) so this stage stays parseable even if SKILL.md is out of context on a `nextStepFile` chain — ``` SKF_RENAME_SKILL_RESULT_JSON: {"status":"error","old_name":"{old_name}","new_name":"{new_name}","versions_renamed":[],"manifest_rekeyed":false,"context_files_updated":[],"exit_code":<code>,"halt_reason":"<reason>","headless_decisions":{headless_decisions}} ``` Set `exit_code` and `halt_reason` to the values named at that HALT site (see `references/exit-codes.md`); `old_name`/`new_name` are both resolved by step 1 before this stage runs. `{headless_decisions}` is the audit trail carried from step 1 (the §6 source-authority override if it fired; `[]` otherwise) — emit it verbatim so a halt in this stage still preserves the decision trail. ## MANDATORY SEQUENCE **Transactional boundary.** After section 1 (copy), the old skill is untouched; a failure in any of sections 2-7 deletes the two new directories (`{new_skill_group}`, `{new_forge_group}`), reports, and halts with the old skill intact. Section 8 (delete old) is the only irreversible point. ### 0. Re-read Version-Paths Knowledge + Resolve Helpers Read `{versionPathsKnowledge}` again and confirm the templates (`{skill_package}`, `{skill_group}`, `{forge_version}`, `{forge_group}`) and the Rename section. Also read `{managedSectionLogic}` for the managed-section format template and the skill index rebuild rules that will be reused in section 7. **Resolve helpers** in parallel — these are independent file-existence checks that batch into one tool-call message: - `{atomicWriteHelper}` ← first existing path in `{atomicWriteProbeOrder}` (used in §6 for crash-safe manifest restore) - `{rewriteSkillNameHelper}` ← first existing path in `{rewriteSkillNameProbeOrder}` (used in §3 for the field-scoped in-file rename transforms + atomic write) - `{updateActiveSymlinkHelper}` ← first existing path in `{updateActiveSymlinkProbeOrder}` (used in §4 for atomic symlink repair) - `{verifyNoTraceHelper}` ← first existing path in `{verifyNoTraceProbeOrder}` (used in §5 for the deterministic no-trace commit gate) - `{manifestOpsHelper}` ← first existing path in `{manifestOpsProbeOrder}` (used in §6 for the manifest re-key) - `{rebuildManagedSectionsHelper}` ← first existing path in `{rebuildManagedSectionsProbeOrder}` (used in §7 for between-marker swap) If any helper has no existing candidate, release the lock and HALT (exit code 4, `halt_reason: "write-failed"`) — the rename's safety guarantees depend on these helpers, and a fall-through to LLM-driven writes/scans would silently regress atomicity (the write helpers) or the deterministic transform and commit-gate checks (`{rewriteSkillNameHelper}`, `{verifyNoTraceHelper}`). **Lock release contract:** every halt path in this step ends with `rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"` before exiting. The terminal health-check (step 4) is the success-path release. ### 1. Copy skill_group and forge_group **Precondition:** Both `{new_skill_group}` and `{new_forge_group}` must NOT exist (step 1 validated this in the collision check, but verify again before copying). 1. If `{new_skill_group}` or `{new_forge_group}` exists on disk: release the lock (`rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"`) and halt with "**Collision detected at execution time.** `{new_skill_group}` or `{new_forge_group}` now exists on disk — it did not exist during step 1 selection. Aborting before any files are touched." HALT (exit code 4, `halt_reason: "copy-failed"`). In headless, emit the error envelope. 2. Copy `{old_skill_group}` to `{new_skill_group}` recursively: - Preserve file permissions, timestamps, and symlinks - Equivalent to `cp -a {old_skill_group} {new_skill_group}` (preserves symlinks) or `cp -r` followed by explicit symlink re-creation in section 4 - If the copy fails: release the lock (`rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"`) and halt with "**Copy failed:** `{old_skill_group}` → `{new_skill_group}`: {error}. No files were modified. Old skill is intact." HALT (exit code 4, `halt_reason: "copy-failed"`). In headless, emit the error envelope. 3. Copy `{old_forge_group}` to `{new_forge_group}` the same way: - If the copy fails: **rollback** by deleting `{new_skill_group}` (just created in step 2), release the lock (`rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"`), then halt with "**Copy failed:** `{old_forge_group}` → `{new_forge_group}`: {error}. Rolled back new skill_group. Old skill is intact." HALT (exit code 4, `halt_reason: "copy-failed"`). In headless, emit the error envelope. **Rollback procedure for this section:** `rm -rf {new_skill_group}` and `rm -rf {new_forge_group}` (whichever exist). Old skill is untouched. Report: "**Copied** `{old_skill_group}` → `{new_skill_group}` and `{old_forge_group}` → `{new_forge_group}`." ### 2. Rename Inner Version Directories For each version `v` in `affected_versions`: 1. Resolve the old inner directory: `{new_skill_group}/{v}/{old_name}/` 2. Resolve the new inner directory: `{new_skill_group}/{v}/{new_name}/` 3. Rename the directory (move within the same parent): `mv {new_skill_group}/{v}/{old_name} {new_skill_group}/{v}/{new_name}` 4. If the old inner directory does not exist (orphaned version with no skill package), skip with a warning recorded in `section2_warnings` **Rollback on any rename failure:** - `rm -rf {new_skill_group}` and `rm -rf {new_forge_group}` - Release the lock: `rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"` - Halt with: "**Inner directory rename failed** at `{v}/{old_name}`: {error}. Rolled back both new directories. Old skill is intact." HALT (exit code 4, `halt_reason: "write-failed"`). In headless, emit the error envelope. Report: "**Renamed {count} inner directories** to `{new_name}/`." ### 3. Update File Contents Inside the New Location For each version `v` in `affected_versions`, operate on the files inside `{new_skill_group}/{v}/{new_name}/` (the freshly renamed inner directory) and `{new_forge_group}/{v}/`. **Transform semantics (apply to 3a / 3b / 3c / 3d):** `{rewriteSkillNameHelper}` performs each field-scoped substitution AND the crash-safe write (stage to `<target>.skf-tmp`, fsync, atomic rename) in one call — do NOT compute file content in the prompt. Each `--kind` edits exactly one field/region and leaves everything else byte-for-byte intact, so there is no key reorder/drop from hand-editing JSON and no wrong-region substitution when `{old_name}` is a substring (e.g. `rename` → `renamer`). Invoke it once per file: ```bash python3 {rewriteSkillNameHelper} "{target_path}" \ --kind {skill-frontmatter|metadata-json|context-snippet|provenance-json} \ --old-name {old_name} --new-name {new_name} ``` Read the JSON result (`changed`, `wrote`, and per-kind fields). Exit 0 = processed (written only if the content changed). A non-zero exit — a missing structural region (no frontmatter delimiters), invalid JSON, or a write failure — is a **file update failure**: trigger the rollback below. Check file existence first: if the target file does not exist, skip the invocation and record it in `section3_warnings` per the per-item notes (a missing file is not a failure). **3a. SKILL.md frontmatter** — `--kind skill-frontmatter` on `{new_skill_group}/{v}/{new_name}/SKILL.md`. Replaces the top-level `name:` value inside the frontmatter block only (anchored on `^name:`, so a nested `name:` or a longer key like `renamed:` is untouched); body text is preserved verbatim, so a legitimate mention of `{old_name}` below the closing `---` survives. If the file is missing, record it in `section3_warnings` and continue. **3b. metadata.json** — `--kind metadata-json` on `{new_skill_group}/{v}/{new_name}/metadata.json`. Sets `name` = `{new_name}` via a JSON round-trip (key order preserved). If the file is missing, record it in `section3_warnings` and continue. **3c. context-snippet.md** — `--kind context-snippet` on `{new_skill_group}/{v}/{new_name}/context-snippet.md`. Rewrites the display header `[{old_name} v...]` → `[{new_name} v...]` (version suffix preserved) and every `root:` path: it parses `root: {prefix}{old_name}/`, keeps the prefix verbatim, and swaps the trailing `{old_name}/` segment for `{new_name}/` — handling any IDE prefix (`.claude/skills/`, `.windsurf/skills/`, `.github/skills/`, the draft `skills/` prefix) generically, and flattening the legacy `root: skills/{old_name}/active/{old_name}/` form to `root: skills/{new_name}/`. If the file is missing, record it in `section3_warnings` and continue. **3d. provenance-map.json** — `--kind provenance-json` on `{new_forge_group}/{v}/provenance-map.json`. Sets `skill_name` = `{new_name}` via a JSON round-trip. If the file is missing (some versions may not have a provenance map), record it in `section3_warnings` and continue. **Rollback on any update failure (not just a missing file):** - `rm -rf {new_skill_group}` and `rm -rf {new_forge_group}` - Release the lock: `rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"` - Halt with: "**File update failed** at `{path}`: {error}. Rolled back both new directories. Old skill is intact." HALT (exit code 4, `halt_reason: "write-failed"`). In headless, emit the error envelope. Report: "**Updated file contents** across {affected_versions_count} version(s): SKILL.md, metadata.json, context-snippet.md, provenance-map.json." ### 4. Fix the `active` Symlink in the New Location Recreate or repair the `active` symlink in `{new_skill_group}` via `{updateActiveSymlinkHelper}` — the helper holds an `flock` on `{new_skill_group}/active.lock`, surfaces a clear error on Windows non-dev-mode (no silent fallback), and uses the `ln -sfn tmp && mv -Tf tmp link` pattern to make the flip atomic against concurrent readers. 1. Inspect `{old_skill_group}/active` to determine the target version (the value the symlink points to — typically just the version string, not an absolute path). If `{old_skill_group}/active` does not exist, skip this section — there is no symlink to repair. 2. Invoke: ```bash python3 {updateActiveSymlinkHelper} flip-link \ --link {new_skill_group}/active \ --target {target_version} ``` 3. The helper handles all four cases (missing, present-and-correct, present-and-stale, present-and-invalid) uniformly via atomic replace. **Rollback on helper non-zero exit:** - `rm -rf {new_skill_group}` and `rm -rf {new_forge_group}` - Release the lock: `rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"` - Halt with: "**Failed to repair `active` symlink** in `{new_skill_group}`: {captured stderr}. Rolled back both new directories. Old skill is intact." HALT (exit code 4, `halt_reason: "write-failed"`). In headless, emit the error envelope. ### 5. Verify — No Trace of `{old_name}` Inside the New Location This is the commit-point check. If any structural reference to `{old_name}` remains, the rename is not safe to commit. `{verifyNoTraceHelper}` performs the whole scan deterministically — a fixed-file-set, name-token scan across every `affected_versions` entry, the SKILL.md frontmatter/body region split, and the directory-listing check — and returns the verdict as JSON. Invoke it once: ```bash python3 {verifyNoTraceHelper} "{new_skill_group}" \ --forge-group "{new_forge_group}" \ --old-name {old_name} --new-name {new_name} \ --versions {comma-separated affected_versions} ``` Per version `v`, the helper scans `SKILL.md` (frontmatter matches → `hard_matches`; matches in the body below the closing `---` → `body_warnings`), `metadata.json`, `context-snippet.md`, and `provenance-map.json` (any match → `hard_matches`), plus the `{new_skill_group}/{v}/` listing (an `{old_name}/` directory present, or the `{new_name}/` directory missing → `dir_violations`). It matches `{old_name}` only as a complete skill-name token — bounded by non-name characters — so a correctly-renamed `{new_name}` that contains `{old_name}` as a substring (e.g. `rename` → `rename-skill`) is never a false leftover, and the frontmatter=hard / body=warning split is applied by region with no meaning-interpretation. A missing file is recorded under `skipped`, not treated as a match. Exit 0 = clean; exit 1 = at least one hard match or dir violation. Read the JSON and decide: - **If `clean` is `true` (empty `hard_matches` AND empty `dir_violations`):** the rename is safe to commit. Set `verification_warnings` = the returned `body_warnings` (informational SKILL.md body mentions of `{old_name}` that are retained). Proceed. - **If `clean` is `false`:** this is a hard failure — - `rm -rf {new_skill_group}` and `rm -rf {new_forge_group}` - Release the lock: `rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"` - Halt with: "**Verification failed.** `{old_name}` still appears in: {the files from `hard_matches` plus any `dir_violations`}. Rolled back both new directories. Old skill is intact." HALT (exit code 5, `halt_reason: "verify-failed"`). In headless, emit the error envelope. Report: "**Verified** — no structural references to `{old_name}` remain inside the new location across {affected_versions_count} version(s). {if verification_warnings is non-empty: 'Informational body-text mentions retained in SKILL.md: {list}.'}" ### 6. Update Export Manifest **If `manifest_exists = false` (step 1 recorded no manifest on disk):** Skip this section entirely. Set `manifest_updated = false` and `manifest_backup = null`. There is no manifest to re-key — the skill was never exported. Section 7 will find no platform context files to rebuild either (no manifest means no prior export, so no `<!-- SKF:BEGIN -->` markers exist), and any platform file that happens to be present will be left alone by the section 2 marker check. Report: "**Manifest update skipped** — no `.export-manifest.json` on disk. The rename is a pure on-disk operation." **If `manifest_exists = true`:** 1. **Hold a deep copy in memory** as `manifest_backup` — required for rollback in this section and section 7 on failure. Read `{skills_output_folder}/.export-manifest.json` once and stash the parsed object. 2. **Re-key via the helper.** If the manifest contains `exports.{old_name}`, invoke: ```bash python3 {manifestOpsHelper} {skills_output_folder} rename {old_name} {new_name} ``` The helper preserves `active_version`, `versions` map, and all fields, then writes the manifest atomically via temp + rename. If the manifest does NOT contain `exports.{old_name}` (the skill was on disk but never exported), skip the invocation — the manifest has nothing to change. **Rollback on helper non-zero exit:** - Restore the manifest from `manifest_backup` via `{atomicWriteHelper} write {skills_output_folder}/.export-manifest.json` (re-pipe the JSON-serialized backup) - `rm -rf {new_skill_group}` and `rm -rf {new_forge_group}` - Release the lock: `rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock"` - Halt with: "**Manifest update failed:** {captured stderr}. Restored manifest from backup and rolled back new directories. Old skill is intact." HALT (exit code 4, `halt_reason: "manifest-write-failed"`). In headless, emit the error envelope. Set context flag `manifest_updated = true`. Report: "**Manifest updated** — re-keyed `exports.{old_name}` → `exports.{new_name}`." ### 7. Rebuild Context Files After §6 re-keys the manifest from `{old_name}` to `{new_name}`, every IDE's context file (`CLAUDE.md`, `.cursorrules`, `AGENTS.md`, etc.) still carries the old name in its managed-section snippet rows. This section rewrites each one in-place via the surgical between-marker swap so the on-disk managed sections reflect the new name. It runs only on the success path — the §4–§6 rollback jumps never reach here. **7a. Resolve `target_context_files`.** Load the `ides` list from `config.yaml`. The installer writes IDE identifiers — map each to a context file and skill root via the "IDE → Context File 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 in the table, default to `{unknownIdeDefaultContextFile}` / `{unknownIdeDefaultSkillRoot}` 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}`. **7b. Per-file loop.** For each entry in `target_context_files`: 1. **Resolve the target file** at `{context_file}` (absolute path). 2. **Read the current file:** - If it does not exist, skip (nothing to rebuild — export-skill re-creates it on its next run). - If it exists but has no `<!-- SKF:BEGIN -->` marker, skip (no managed section to rewrite). - If it has `<!-- SKF:BEGIN -->` but no matching `<!-- SKF:END -->`, record the error against that file and continue to the next entry — do not halt the whole rename on one malformed context file. 3. **Build the exported skill set (version-aware, deprecated-excluded)** using the same logic as `skf-export-skill/references/update-context.md` §4b (skill set) and §4c (snippet resolution): - Read the manifest's `exports` object (already updated in §6, so `{new_name}` is present and `{old_name}` is absent). - For each skill, resolve its `active_version`; if `versions.{active_version}.status == "deprecated"`, skip that skill. - For each remaining `{skill-name, active_version}` pair, read `{skills_output_folder}/{skill-name}/{active_version}/{skill-name}/context-snippet.md`; if missing, fall back to the `active` symlink path; if still missing, skip with a warning. 4. **Rewrite root paths** using the generic algorithm from `skf-export-skill/references/update-context.md` §4d: parse each snippet's `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`. 5. **Sort and count.** Sort skills alphabetically by name; count totals (skills, stack skills). 6. **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 --> ``` 7. **Surgical replacement — atomic, deterministic.** Invoke `{rebuildManagedSectionsHelper}` (resolved in §0) for the between-marker swap: ```bash python3 {rebuildManagedSectionsHelper} {context_file} replace --content "{new_managed_section_text}" ``` The helper handles marker location, the 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 — treat any non-zero exit as a per-file failure. 8. **On per-file failure**, record the error against that context file and continue to the next entry. Do not halt the rename on a recoverable per-context-file error — the manifest and filesystem are already consistent, so context files can be re-rebuilt later via `[EX] Export Skill`. **7c. After the loop**, record: - `context_files_updated` — list of files successfully rewritten - `context_files_failed` — list of any that failed Report: `**Rebuilt managed sections in:** {list of updated files}. {if any failed: 'Failed: {list} — re-run [EX] Export Skill to retry.'}` Then proceed to §8. **Note:** §7 failures do not trigger a rollback. Platform context files are derived artifacts; the manifest and on-disk skill directories are the canonical state. ### 8. Delete Old Directories (Point of No Return) This is the only section after which rollback is impossible. Precondition: §1–7 have fully materialized and verified the new name (both new directories renamed, no `{old_name}` references remaining, manifest re-keyed, context files rebuilt best-effort) — only now is deleting the old name safe. Execute the deletes: 1. Verify `{old_skill_group}` is inside `{skills_output_folder}` (defense in depth) 2. `rm -rf {old_skill_group}` — delete the old skill_group recursively 3. Verify deletion succeeded (the path no longer exists) 4. Verify `{old_forge_group}` is inside `{forge_data_folder}` (defense in depth) 5. `rm -rf {old_forge_group}` — delete the old forge_group recursively 6. Verify deletion succeeded **On deletion error:** - Record the error in `deletion_errors` against the specific path - Continue attempting the other path — partial cleanup is still better than none - Do NOT attempt any rollback — the new name is already committed and the old name's remnants can be removed manually Report: "**Deleted old directories:** `{old_skill_group}` and `{old_forge_group}`. {if deletion_errors is non-empty: 'Errors: {list} — remove manually with `rm -rf {path}`.'}" ### 9. Store Results in Context Store the following for step 3: - `old_name` — the previous skill name - `new_name` — the new skill name - `affected_versions` — list of versions that were renamed - `affected_versions_count` — integer count - `files_updated_per_version` — structured summary (SKILL.md, metadata.json, context-snippet.md, provenance-map.json — each with ×count) - `manifest_rekeyed` — boolean (true if section 6 succeeded) - `context_files_updated` — list of successfully rebuilt files - `context_files_failed` — list of files that failed to rebuild (empty if none) - `section2_warnings` — list of orphaned version warnings (empty if none) - `section3_warnings` — list of missing file warnings (empty if none) - `verification_warnings` — list of informational SKILL.md body mentions of `{old_name}` retained (empty if none) - `deletion_errors` — list of post-commit deletion errors (empty if none) - `headless_decisions` — the audit trail of confirmation gates auto-resolved under `{headless_mode}`, carried forward from step 1 unchanged (empty in interactive runs). Step 3 surfaces it in the result envelope and the per-run result JSON. ### 10. Load Next Step Load, read the full file, and then execute `{nextStepFile}`. -
exit-codes.md 2 KB
# Exit Codes — skf-rename-skill Every HARD HALT in the rename workflow exits with a stable code so headless automators can branch on the failure class without grepping message text: | Code | Meaning | Raised by | | ---- | -------------------- | -------------------------------------------------------------------------------------------- | | 0 | success | step 4 (terminal) | | 2 | input-missing / input-invalid | step 1 §4/§5 (headless missing `old_name`/`new_name` arg) → `input-missing`; new name fails kebab-case / length / same-as-old → `input-invalid` | | 3 | resolution-failure | step 1 §2 (manifest is malformed JSON); step 1 §3 (no skills found anywhere) | | 4 | write-failure | On-Activation §4 pre-flight write probe (skills_output_folder / forge_data_folder unwritable); step 2 §1 (copy → `copy-failed`); step 2 §2 (inner-dir rename) / §3 (file content update) / §4 (symlink repair) → `write-failed`; step 2 §6 (manifest write → `manifest-write-failed`). step 2 §7 (context-file rewrite) is **best-effort and never halts** — per-file failures are recorded in `context_files_failed` and the rename still succeeds (manifest + disk are canonical; re-run `[EX] Export Skill` to retry). | | 5 | state-conflict | step 1 §5 (name-collision check fails: target name already in use); step 1 §6 (source-authority="official" headless without `force_source_authority_in_headless`); concurrency lock collision (`halted-for-concurrent-run`); step 2 §5 (no-trace verification failed — `{old_name}` still present inside the new location → `verify-failed`; note this rollback is code 5, unlike the copy/write rollbacks at code 4) | | 6 | user-cancelled | step 1 §6 (source-authority warning [N]); step 1 §8 confirmation gate `[N]`; any prompt that accepted `cancel`/`exit`/`:q` | -
health-check.md 1.2 KB
--- # 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 ## STEP GOAL: Chain to the shared workflow self-improvement health check at `{nextStepFile}`. This is the terminal step of rename-skill — after the shared health check completes, the workflow is fully done. ## Rules - No user-facing reports, file writes, or result contracts in this step — those belong in step 3 - Delegate directly to `{nextStepFile}` with no additional commentary - Do not attempt any other action between loading this step and executing `{nextStepFile}` ## MANDATORY SEQUENCE 1. **Release the rename lock.** Delete `{forge_data_folder}/{old_name}/.skf-rename.lock` if it still exists (the lock is acquired in select.md §4b and released here as the terminal cleanup). A no-op when `old_name` was never resolved or the lock was already removed by an earlier halt path. ```bash rm -f "{forge_data_folder}/{old_name}/.skf-rename.lock" ``` 2. Load `{nextStepFile}`, read it fully, then execute it. -
report.md 5.3 KB
--- nextStepFile: 'health-check.md' --- <!-- Config: communicate in {communication_language}. Render the report block in {document_output_language}. --> # Step 3: Report Rename Results ## STEP GOAL: Present a clear, final summary of what the rename workflow changed — old and new names, versions renamed, file-level update counts, manifest re-key, platform context rebuild, and any residual warnings or deletion errors — 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 rename - Do not hide verification warnings, context file rebuild failures, or deletion errors - Present next-steps guidance so the user knows which downstream workflows to run - Chains to the local health-check step via `{nextStepFile}` after completion (see section 2) ## MANDATORY SEQUENCE ### 1. Render the Report Display the following block, filling in values from context: ``` **Rename complete.** From: {old_name} To: {new_name} Versions renamed: {affected_versions_count} ({comma-separated affected_versions}) References updated: - SKILL.md frontmatter (×{affected_versions_count}) - metadata.json (×{affected_versions_count}) - context-snippet.md (×{affected_versions_count}) - provenance-map.json (×{affected_versions_count}) Manifest updated: {if manifest_rekeyed: "exports.{new_name} (re-keyed from exports.{old_name})" else: "(no manifest entry existed for {old_name})"} 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} → Re-run `[EX] Export Skill` to retry the managed section rebuild for these files. {if section2_warnings is non-empty:} Warnings (inner directory rename): {list each warning from section2_warnings} {if section3_warnings is non-empty:} Warnings (missing files during content update): {list each warning from section3_warnings} {if verification_warnings is non-empty:} Informational: the old name still appears in SKILL.md body text (prose only, non-structural) in: {list each path from verification_warnings} → These are typically historical notes or changelog entries. Review and edit manually if you want them updated. {if deletion_errors is non-empty:} **Post-commit deletion errors:** {list each error} → The new name is fully committed. Remove the remnants manually with `rm -rf {path}`. {if headless_decisions is non-empty:} Headless auto-decisions: {for each entry: "{gate}: took {taken_action} (default {default_action}) — {reason}"} --- **Next steps:** - Run `@Ferris EX` if you want to re-verify the managed sections in platform context files - If you had QMD collections or external tooling registered under `{old_name}`, re-run `@Ferris SF` (or your registration command) to re-index under `{new_name}` - If this skill was published to agentskills.io under `{old_name}`, the registry version is unchanged — this rename is a LOCAL operation only ``` ### Result Contract Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{skills_output_folder}/{new_name}/rename-skill-result-{timestamp}.json` (reuse the activation-stored `{timestamp}`, resolution to seconds) and a copy at `{skills_output_folder}/{new_name}/rename-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include all updated file paths (SKILL.md, metadata.json, context-snippet.md, provenance-map.json) in `outputs`; include `old_name`, `new_name`, `versions_renamed`, and `headless_decisions` (the auto-resolved gate audit trail carried from step 1 — `[]` in interactive runs) in `summary`. When `{headless_mode}` is true, also emit the single-line envelope on **stdout** before chaining to step 4 (matches the SKILL.md "Result Contract (Headless)" shape): ``` SKF_RENAME_SKILL_RESULT_JSON: {"status":"success","old_name":"{old_name}","new_name":"{new_name}","versions_renamed":{affected_versions},"manifest_rekeyed":{manifest_rekeyed},"context_files_updated":{context_files_updated},"exit_code":0,"halt_reason":null,"headless_decisions":{headless_decisions}} ``` Substitute `{affected_versions}`, `{context_files_updated}`, and `{headless_decisions}` as JSON arrays; `manifest_rekeyed` is the boolean from step 2's context. **Post-completion hook (optional).** If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 from `workflow.on_complete`), invoke it after the result contract is finalized, passing the per-run result path: ```bash {onCompleteCommand} --result-path={skills_output_folder}/{new_name}/rename-skill-result-{timestamp}.json ``` Log success/failure but never fail the workflow on a hook error — the rename is already committed. The hook runs last so an audit-log emit, registry re-index, or notifier sees the completed rename. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely. ### 2. Chain to Health Check ONLY WHEN the rename 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. -
select.md 21.1 KB
--- nextStepFile: 'execute.md' versionPathsKnowledge: 'knowledge/version-paths.md' # Resolve `{manifestOpsHelper}` by probing `{manifestOpsProbeOrder}` in order # (installed SKF module path first, src/ dev-checkout fallback); first existing # path wins. §7 uses its `affected-versions` action to enumerate the versions a # rename must touch — the union of manifest version keys and on-disk version # dirs, deduped and semver-sorted (numeric, so 0.10.0 precedes 0.9.0). Unlike # execute.md's write helpers this one is not atomicity-critical: if neither path # resolves, §7 falls back to computing the union in the prompt. manifestOpsProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-manifest-ops.py' - '{project-root}/src/shared/scripts/skf-manifest-ops.py' # Resolve `{skillInventoryHelper}` by probing `{skillInventoryProbeOrder}` # (installed SKF module path first, src/ dev-checkout fallback). §3 uses it to # enumerate the rename candidates: the union of manifest `exports` and on-disk # skill directories, each with its version list and active version — computing # that union/count in the prompt has one correct answer per input. §3 falls # back to an in-prompt scan if neither path resolves. skillInventoryProbeOrder: - '{project-root}/_bmad/skf/shared/scripts/skf-skill-inventory.py' - '{project-root}/src/shared/scripts/skf-skill-inventory.py' # `{renameNameValidator}` is this skill's own deterministic new-name gate. §5 # uses it for the format / length / identity / collision checks (one correct # answer per (name, filesystem, manifest) state) and the interrupted-rename # recovery fingerprint; §5 falls back to the same checks in-prompt if Python is # unavailable. renameNameValidator: 'scripts/skf-validate-rename-name.py' --- <!-- Config: communicate in {communication_language}. --> # Step 1: Select Rename Target ## STEP GOAL: Identify the skill the user wants to rename, validate the new name against the agentskills.io spec (kebab-case, length, uniqueness), warn about source authority implications, enumerate every version that will be touched, and obtain explicit user confirmation before any filesystem operation is scheduled. Every selection decision is stored in context so step 2 can execute the rename transactionally. ## Rules - Focus only on selection, validation, and confirmation — do not modify manifest, copy, or delete files - Do not proceed without explicit user confirmation at the final gate - Do not accept a new name that fails validation (kebab-case, length, uniqueness) - Present the list of affected versions clearly so the user understands the scope **Headless error envelope (self-contained).** Where a halt below says *"emit the error envelope per SKILL.md 'Result Contract (Headless)'"*, write this single line to **stderr** (restated here so selection halts stay parseable even if SKILL.md is out of context) — ``` SKF_RENAME_SKILL_RESULT_JSON: {"status":"error","old_name":"…|null","new_name":"…|null","versions_renamed":[],"manifest_rekeyed":false,"context_files_updated":[],"exit_code":<code>,"halt_reason":"<reason>","headless_decisions":[]} ``` Use the `old_name`/`new_name`, `exit_code`, and `halt_reason` named at each halt site (see `references/exit-codes.md`) — `old_name`/`new_name` are `null` until resolved in §4/§5. `headless_decisions` is `[]` at every selection-stage halt: the only decision this step records (the §6 source-authority override) means *proceed*, so no recorded decision is ever paired with a halt here. ## 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) - The Rename section under "Skill Management Operations" — the complete list of 9 locations that must be updated coherently You will use these templates and rules to build directory paths, enumerate affected versions, and plan the transactional rename in step 2. ### 2. Read Export Manifest Load `{skills_output_folder}/.export-manifest.json` if it exists. **If the file is missing or empty:** Treat as an empty manifest — proceed to section 3 and rely entirely on the on-disk directory scan. Drafted or never-exported skills can still be renamed. Store `manifest_exists = false` for later use in step 2 (section 6 will not attempt to update a manifest that does not exist). **If the file exists but contains no `exports` entries:** Same handling — proceed to section 3 with the directory scan. Store `manifest_exists = true` so step 2 still touches the (empty) manifest on write. **If the file exists with entries:** Parse JSON and verify `schema_version` is `"2"`. If the manifest is v1 (no `schema_version` field), note this but continue — treat every entry as having a single active version derived from its current state. Store `manifest_exists = true`. **Hard halt condition:** If the file exists but is malformed (not valid JSON), halt with: "**Export manifest is corrupt** at `{skills_output_folder}/.export-manifest.json` — fix or remove the file before renaming." HALT (exit code 3, `halt_reason: "manifest-corrupt"`). In headless mode, emit the error envelope per SKILL.md "Result Contract (Headless)" with `old_name: null`, `new_name: null`. ### 3. List Available Skills Enumerate every skill available for rename deterministically. Resolve `{skillInventoryHelper}` ← first existing path in `{skillInventoryProbeOrder}` and run: ```bash python3 {skillInventoryHelper} {skills_output_folder} ``` Read the JSON. Each entry in `skills[]` carries `name`, its `versions` array, and `active_version` (the helper unions the manifest `exports` with the on-disk directories and dedupes, so manifest-tracked and orphaned skills both appear). A skill whose `name` is absent from `manifest.exports` is a draft/orphan the rename workflow can still handle — annotate it "(not in manifest)". **If `{skillInventoryHelper}` has no existing candidate** (neither probe path resolves — e.g. Python/`uv` unavailable): build the list in the prompt instead — read `exports` from the manifest (if `manifest_exists`), scan `{skills_output_folder}/` for top-level directories, union the two, and read each one's `active_version` and version count. Directories absent from `exports` are the "(not in manifest)" entries. **If the list is empty** (no manifest entries AND no on-disk skill directories): halt with "**Rename Skill — nothing to rename.** No skills found in `{skills_output_folder}/`. Run `[CS] Create Skill` first." HALT (exit code 3, `halt_reason: "nothing-to-rename"`). In headless mode, emit the error envelope with `old_name: null`, `new_name: null`. Display the list, one line per skill as `{name} ({n} versions, active: {active_version})` (append "(not in manifest)" for orphans): ``` **Rename Skill — select target** Available skills: 1. cognee (3 versions, active: 0.6.0) 2. express (1 version, active: 4.18.0) 3. legacy-helper (not in manifest) ``` ### 4. Ask Which Skill "**Which skill would you like to rename?** 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 old 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 old_name argument." In headless, emit the error envelope. - If the user enters `cancel`, `exit`, `[X]`, `q`, or `:q`: Display "Cancelled — no changes were made." and HALT (exit code 6, `halt_reason: "user-cancelled"`). - **If the user's input does not match any listed skill:** Re-display the list and ask again. Store the selection as `old_name`. ### 4b. Concurrency Guard Two concurrent rename runs against the same `old_name` would corrupt state mid-copy: one would `rm -rf` the other's freshly-staged new directories, or both would race on the manifest re-key. The lock below catches the common accidental-double-invoke case. It is a **best-effort PID-file guard**, not a held flock — the LLM-driven workflow spans many turn boundaries and no single bash invocation can hold flock across them. **Mirror this exactly so the guard works the same way every run:** ```bash LOCK={forge_data_folder}/{old_name}/.skf-rename.lock mkdir -p "$(dirname "$LOCK")" if [ -f "$LOCK" ]; then HELD_PID=$(head -n1 "$LOCK" 2>/dev/null | awk '{print $1}') if [ -n "$HELD_PID" ] && kill -0 "$HELD_PID" 2>/dev/null; then echo "skf-rename-skill: another rename is in progress (pid=$HELD_PID, started $(awk 'NR==2' "$LOCK" 2>/dev/null))" exit 1 fi echo "skf-rename-skill: clearing stale lock from pid=$HELD_PID" fi printf '%s\n%s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$LOCK" ``` **Halt protocol on live-PID collision:** - Display: `"**Another rename is in progress.** The skill {old_name} is locked by pid={HELD_PID} (started {timestamp from line 2 of the lock file}). Wait for that run to finish, or — if you know that pid is no longer running — delete {LOCK} manually and re-run."` - HALT (exit code 5, `halt_reason: "halted-for-concurrent-run"`). In `{headless_mode}`, emit the error envelope per SKILL.md "Result Contract (Headless)" with `old_name: "{old_name}"`, `new_name: null`. **No `headless_decisions[]` entry** — this is a hard halt before any gate fires. **Release contract:** - This run owns the lock only after the `printf … > "$LOCK"` line above runs — i.e. once §4b has cleared the live-PID check and written this run's PID. The live-PID collision halt directly above is the **sole exception**: it exits *before* that line, so the lock belongs to the other live run and this run must **never** delete it (clearing a live lock it just honored would let a third invocation run concurrently). - The terminal health-check step (step 4) deletes the lock as its final action on the success path. - **Every halt from §5 onward — after this run has acquired the lock — must delete it before exiting** (`rm -f "$LOCK"` per halt site): the §5 input/format/collision halts, the §6 source-authority and cancel halts, the §8 cancel and dry-run exits, and execute.md's copy/verify/manifest/write rollbacks. Otherwise the next attempt would see a stale lock from this run. ### 5. Ask for New Name "**What is the new name for this skill?** The new name must be kebab-case: lowercase alphanumeric with hyphens, 1-64 characters, matching the regex `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` (starts and ends with a lowercase letter or digit; a single letter or digit is valid). Or type `cancel` / `exit` / `:q` to abort." Wait for user input. Trim whitespace. **GATE [default: use args]** — If `{headless_mode}` and new_name was provided as argument: use it and auto-proceed through validation. If not provided, release the lock and HALT (exit code 2, `halt_reason: "input-missing"`): "headless mode requires new_name argument." In headless, emit the error envelope. - If the user enters `cancel`, `exit`, `[X]`, `q`, or `:q`: release the lock (`rm -f {forge_data_folder}/{old_name}/.skf-rename.lock`), display "Cancelled — no changes were made.", and HALT (exit code 6, `halt_reason: "user-cancelled"`). **Validate the candidate deterministically.** Run `{renameNameValidator}` (a per-skill helper, always shipped with the skill) — it applies format, length, identity, and collision in that order and returns the verdict as JSON: ```bash python3 {renameNameValidator} \ --old-name {old_name} --new-name {candidate} \ --skills-output-folder {skills_output_folder} \ --forge-data-folder {forge_data_folder} ``` The checks: **format** = the kebab regex `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` (the module's canonical rule, same as `skf-validate-output.py` / `skf-validate-brief-inputs.py`, so a digit-leading name like `3d-tools` renames cleanly); **length** = 1-64 characters (agentskills.io spec); **identity** = differs from `{old_name}`; **collision** = the name is not a manifest `exports` key, a `{skills_output_folder}` directory, or a `{forge_data_folder}` directory. Read `valid`, `first_failure`, `checks`, and `interrupted_rename`. If `valid` is true, store the input as `new_name` and proceed to §6. Otherwise branch on `first_failure` — interactive: display the message and re-ask; headless: release the lock (`rm -f {forge_data_folder}/{old_name}/.skf-rename.lock`), HALT with the mapped code, and emit the error envelope: - **`format`** → "**Invalid name format.** The new name must be lowercase alphanumeric with hyphens, starting and ending with a lowercase letter or digit. Try again." (headless HALT: exit code 2, `halt_reason: "input-invalid"`) - **`length`** → "**Invalid name length.** The new name must be 1-64 characters. Try again." (headless HALT: exit code 2, `halt_reason: "input-invalid"`) - **`identity`** → "**The new name is identical to the current name.** Nothing to rename. Try again or abort the workflow." (headless HALT: exit code 2, `halt_reason: "input-invalid"`) - **`collision`** → "**Name collision.** `{new-name}` already exists at: {the `path` of each entry in `checks.collision.locations`}. Pick a different name." When `interrupted_rename` is true, append: "This may be a stranded partial rename from an earlier interrupted run — `{new_name}` was staged but `{old_name}` was never removed. Confirm the `{new_name}` directories are not a skill you want to keep, then clean them up (`rm -rf {skills_output_folder}/{new_name} {forge_data_folder}/{new_name}`) and re-run this rename." — this gives headless pipelines a named recovery path instead of a dead-end collision halt. (headless HALT: exit code 5, `halt_reason: "name-collision"`) **If `{renameNameValidator}` cannot run** (Python/`uv` unavailable): apply the same four checks in the same order in the prompt — the kebab regex, the 1-64 length bound, inequality with `{old_name}`, and the three-source collision lookup (plus the interrupted-rename fingerprint above) — using the identical messages and halt mapping. ### 6. Source Authority Check Resolve `{skill_package}` for the active version using the manifest: `{skills_output_folder}/{old_name}/{active_version}/{old_name}/metadata.json` Read the `source_authority` field (if present). **If `source_authority == "official"`:** Display the warning: ``` ⚠️ **source_authority: "official"** This skill has `source_authority: "official"`. Renaming locally will diverge from any published skill at agentskills.io under this name. Consumers fetching from the registry will still get the original name. Rename is a LOCAL operation only — it does not rename anything at the registry. ``` Ask: "**Continue anyway?** [Y/N] (or `cancel` / `exit` / `:q` to abort)" Wait for response. - **If `N`** (or `cancel` / `exit` / `[X]` / `:q`) → release the lock (`rm -f {forge_data_folder}/{old_name}/.skf-rename.lock`), display "**Cancelled.** No changes were made.", HALT (exit code 6, `halt_reason: "user-cancelled"`). - **If `Y`** → proceed. Set `source_authority_override = true`. **Headless behavior:** If `{headless_mode}` is true AND `{forceSourceAuthorityInHeadless}` is `"true"`, auto-proceed and record `{gate: "source-authority", default_action: "halt", taken_action: "proceed", reason: "force_source_authority_in_headless override"}` in `headless_decisions[]`. Otherwise, release the lock and HALT (exit code 5, `halt_reason: "source-authority-blocked"`) and emit the error envelope — the safe default protects against silent registry-divergence on `published`-tagged skills. **If `source_authority` is absent, or any value other than `"official"`:** skip the warning and proceed. ### 7. Enumerate Affected Versions Resolve `{manifestOpsHelper}` ← first existing path in `{manifestOpsProbeOrder}` (installed SKF module path first, `src/` dev-checkout fallback). Enumerate every version the rename must touch deterministically via its `affected-versions` action: ```bash python3 {manifestOpsHelper} {skills_output_folder} affected-versions {old_name} ``` Read the JSON result. Store `affected_versions` = `result.affected_versions` and `affected_versions_count` = `result.count`. The helper unions the manifest's `exports.{old_name}.versions` keys with the on-disk version directories under `{skills_output_folder}/{old_name}/` (every entry that is not the `active` symlink), so it handles both manifest-tracked and orphaned on-disk versions, deduplicates, and applies a **numeric** semver-descending sort (so `0.10.0` correctly precedes `0.9.0`, which a lexical sort gets wrong). An incomplete union would risk leaving a version internally un-renamed in the new copy — a leftover that step 2 §5 only catches for the versions it was told about. **If `{manifestOpsHelper}` has no existing candidate** (neither probe path resolves — e.g. Python/`uv` unavailable): compute `affected_versions` in the prompt instead — read every key under `exports.{old_name}.versions` in the manifest, list every directory under `{skills_output_folder}/{old_name}/` that is not `active`, union the two sets, and sort descending (newest first, comparing version components numerically). Store the list as `affected_versions` and its length as `affected_versions_count`. Also resolve the four outer paths using the templates from `{versionPathsKnowledge}`: - `old_skill_group` = `{skills_output_folder}/{old_name}/` - `new_skill_group` = `{skills_output_folder}/{new_name}/` - `old_forge_group` = `{forge_data_folder}/{old_name}/` - `new_forge_group` = `{forge_data_folder}/{new_name}/` ### 8. Confirmation Gate Display the full operation summary: ``` **About to rename:** From: {old_name} To: {new_name} Versions affected: {affected_versions_count} ({comma-separated affected_versions}) Directories that will be copied then removed: {old_skill_group} → {new_skill_group} {old_forge_group} → {new_forge_group} Inside each version, the inner `{old_name}/` directory will be renamed to `{new_name}/`, and the following files will be updated: - SKILL.md (frontmatter `name` field) - metadata.json (`name` field) - context-snippet.md (display name and root paths) - provenance-map.json (`skill_name` field, under {old_forge_group}) Manifest `exports.{old_name}` will be re-keyed to `exports.{new_name}`. Platform context files (CLAUDE.md, .cursorrules, AGENTS.md) will be rebuilt so the managed section references `{new_name}` instead of `{old_name}`. Operation is **transactional** — the new name will be fully materialized and verified before the old name is removed. If any step fails before the final delete, the new directories are removed and the old skill remains intact. Proceed? [Y/N] ``` **GATE [default: Y]** — If `{headless_mode}`: auto-proceed with [Y] and append `{gate: "confirm-rename", default_action: "proceed", taken_action: "proceed", reason: "headless auto-confirm"}` to `headless_decisions[]` (log line: "headless: auto-confirmed rename {old_name} → {new_name}"). Skip this append when `--dry-run` is set — a dry run does not execute the rename, so there is no confirmation to record. Wait for explicit user response. **If `--dry-run` was passed**: skip the Y/N prompt entirely. Release the lock (`rm -f {forge_data_folder}/{old_name}/.skf-rename.lock`), display "**[DRY RUN] No changes were made — preview above shows what would be renamed.**", and emit the success envelope per SKILL.md "Result Contract (Headless)" with `status: "dry-run"`, the resolved `old_name`, `new_name`, `versions_renamed: {affected_versions}`, and `headless_decisions: {headless_decisions}` (carries the §6 source-authority override entry if it fired, else `[]`), then HALT (exit code 0). No copy, no manifest re-key, no delete. - **If `Y`** → proceed to section 9 - **If `N`** (or `cancel` / `exit` / `[X]` / `:q`) → release the lock (`rm -f {forge_data_folder}/{old_name}/.skf-rename.lock`), display "**Cancelled.** No changes were made.", HALT (exit code 6, `halt_reason: "user-cancelled"`). In headless mode, emit the error envelope per SKILL.md "Result Contract (Headless)" with the resolved `old_name` and `new_name`. - **Any other input** → re-display the confirmation and ask again ### 9. Store Decisions in Context Store the following decisions in workflow context for step 2: - `old_name` — the current skill name - `new_name` — the validated new name - `affected_versions` — list of version strings for every version that must be updated - `affected_versions_count` — integer count of the above - `old_skill_group` — absolute path `{skills_output_folder}/{old_name}/` - `new_skill_group` — absolute path `{skills_output_folder}/{new_name}/` - `old_forge_group` — absolute path `{forge_data_folder}/{old_name}/` - `new_forge_group` — absolute path `{forge_data_folder}/{new_name}/` - `source_authority_override` — boolean (true if the user acknowledged the `"official"` warning, false/absent otherwise) - `headless_decisions` — the audit trail of confirmation gates auto-resolved under `{headless_mode}` (the §6 source-authority override and the §8 auto-confirm entries appended above). Initialize to `[]`; in interactive runs it stays `[]`. Step 2 carries it forward and step 3 surfaces it in the result envelope. ### 10. Load Next Step Load, read the full file, and then execute `{nextStepFile}`.
-
-
scripts
-
skf-validate-rename-name.py 5.8 KB
#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # /// """SKF Validate Rename Name — deterministic new-name gate for skf-rename-skill. select.md §5 accepts a proposed new skill name and must decide four things, each with one correct answer for a given (name, filesystem, manifest) state: format — matches the module's canonical kebab-case rule ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ (same regex as skf-validate-output.py / skf-validate-brief-inputs.py) length — 1..64 characters (agentskills.io spec) identity — differs from the current name (nothing to rename otherwise) collision — does NOT already exist as a manifest `exports` key, a top-level directory under the skills output folder, or a top-level directory under the forge data folder Doing this in the prompt is a set-membership computation across three sources plus a regex match — deterministic, so it lives here and the prompt keeps only the interactive re-ask / halt decision. Checks run in the select.md §5 order and `first_failure` names the first failing one so the caller maps it to the right halt (`input-invalid`/exit 2 for format/length/identity, `name-collision`/exit 5 for collision). `interrupted_rename` fires the select.md §5 recovery fingerprint: the new name collides only on disk (not in the manifest) AND both old-name directories still exist — the signature of a rename interrupted between copy and delete-old, which the caller surfaces as a named cleanup path instead of a dead-end collision. Output (stdout, always JSON): {status, valid, old_name, new_name, first_failure, checks{...}, interrupted_rename, regex} Exit codes: 0 valid (all checks pass) 2 invalid (at least one check failed; see first_failure / checks) 1 operation error (bad args) CLI example: python3 skf-validate-rename-name.py --old-name rename --new-name rename-skill \ --skills-output-folder /out --forge-data-folder /forge """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path NAME_REGEX = r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" MIN_LEN = 1 MAX_LEN = 64 def load_exports(manifest_path: Path) -> tuple[set[str], str | None]: """Return (export skill-name keys, error). Missing manifest -> empty set.""" if not manifest_path.is_file(): return set(), None try: data = json.loads(manifest_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError) as e: # select.md §2 halts before §5 on a malformed manifest, so treat an # unreadable file here as "no exports" rather than crashing the gate. return set(), f"{type(e).__name__}: {e}" exports = data.get("exports", {}) if isinstance(data, dict) else {} return set(exports.keys()) if isinstance(exports, dict) else set(), None def validate_name( old_name: str, new_name: str, skills_output_folder: str, forge_data_folder: str, manifest_path: str | None = None, ) -> dict: skills_dir = Path(skills_output_folder) forge_dir = Path(forge_data_folder) manifest = Path(manifest_path) if manifest_path else skills_dir / ".export-manifest.json" export_keys, manifest_error = load_exports(manifest) fmt_ok = re.match(NAME_REGEX, new_name) is not None length = len(new_name) len_ok = MIN_LEN <= length <= MAX_LEN identity_ok = new_name != old_name locations: list[dict] = [] if new_name in export_keys: locations.append({"kind": "manifest.exports", "path": str(manifest)}) if (skills_dir / new_name).is_dir(): locations.append({"kind": "skills_output_folder", "path": str(skills_dir / new_name)}) if (forge_dir / new_name).is_dir(): locations.append({"kind": "forge_data_folder", "path": str(forge_dir / new_name)}) collision_ok = len(locations) == 0 # Recovery fingerprint: collides on disk only (not manifest) and both # old-name directories still exist -> stranded partial rename, not a genuine # clash. collides_on_disk_only = ( not collision_ok and all(loc["kind"] != "manifest.exports" for loc in locations) ) interrupted_rename = bool( collides_on_disk_only and (skills_dir / old_name).is_dir() and (forge_dir / old_name).is_dir() ) checks = { "format": {"ok": fmt_ok}, "length": {"ok": len_ok, "length": length}, "identity": {"ok": identity_ok}, "collision": {"ok": collision_ok, "locations": locations}, } first_failure = None for name in ("format", "length", "identity", "collision"): if not checks[name]["ok"]: first_failure = name break result = { "status": "ok", "valid": first_failure is None, "old_name": old_name, "new_name": new_name, "first_failure": first_failure, "checks": checks, "interrupted_rename": interrupted_rename, "regex": NAME_REGEX, } if manifest_error: result["manifest_error"] = manifest_error return result def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Validate a proposed rename target name.") parser.add_argument("--old-name", required=True) parser.add_argument("--new-name", required=True) parser.add_argument("--skills-output-folder", required=True) parser.add_argument("--forge-data-folder", required=True) parser.add_argument("--manifest", default=None) args = parser.parse_args(argv) result = validate_name( args.old_name, args.new_name, args.skills_output_folder, args.forge_data_folder, args.manifest, ) print(json.dumps(result, indent=2)) return 0 if result["valid"] else 2 if __name__ == "__main__": sys.exit(main())
-
-
customize.toml 2.5 KB
# DO NOT EDIT -- overwritten on every update. # # Workflow customization surface for skf-rename-skill. # Team overrides: _bmad/custom/skf-rename-skill.toml (under {project-root}) # Personal overrides: _bmad/custom/skf-rename-skill.user.toml (under {project-root}) [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 rename 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 # (rename-policy reminders, public-name-stability rules, audit guardrails). # Overrides append. # # Each entry is either: # - a literal sentence, e.g. "Renames of skills tagged `published` require approval." # - a file reference prefixed with `file:`, e.g. # "file:{project-root}/docs/rename-policy.md" (globs supported; file # contents are loaded and treated as facts). persistent_facts = [ "file:{project-root}/**/project-context.md", ] # Optional post-completion hook. When non-empty, invoked with # `--result-path=<path>` in step 3 after the rename result JSON is # finalized (before the terminal health-check step). Failures are logged # but never fail the workflow — the rename is already committed. Use for # an audit-log emit, skill-registry re-index, or a post-rename notifier. on_complete = "" # --- Optional safety + default scalars --- # # Auto-acknowledge the source-authority="official" warning in headless mode. # Empty string = HALT in headless when source_authority is "official" # (the safe default — local renames diverge from any registry-published # name). Set to "true" to silently proceed and record the override in the # headless decision trail. force_source_authority_in_headless = "" # Fallback IDE → context-file mapping for unknown IDEs in `config.yaml.ides`. # Empty string = use bundled defaults (AGENTS.md / .agents/skills/). # Used by execute.md §7 when rebuilding context files. unknown_ide_default_context_file = "" unknown_ide_default_skill_root = "" -
SKILL.md 9.8 KB
--- name: skf-rename-skill description: Rename a skill across all its versions — transactional copy-verify-delete with platform context rebuild. Use when the user requests to "rename a skill." --- # Rename Skill ## Overview Renames a skill across all its versions with transactional safety — copy to the new name, verify all references updated, delete the old name only after verification succeeds. Rebuilds platform context files to reference the new name. The agentskills.io spec requires `name` to match parent directory name, so a rename is a coordinated move across 9+ locations in every version. ## 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 skill-index rebuild rules when re-keying context files post-rename. Rename-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 precision surgeon who operates on the entire skill group atomically. ## Workflow Rules These rules apply to every step in this workflow: - Never delete the old skill directories until the new name has been fully materialized and verified - Never proceed past a verification failure — roll back (delete new directories) and halt - Never allow a rename to collide with an existing skill name - 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 — **except** the atomicity and commit-gate safety helpers that execute.md §0 resolves: a missing one there is a HARD HALT (exit 4), never an LLM fall-through, because hand-driven writes/scans would silently regress the transactional guarantees that keep a failed rename recoverable - 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 & Validate | references/select.md | No (confirm) | | 2 | Execute Rename | references/execute.md | No (confirm) | | 3 | Report | references/report.md | Yes | | 4 | Workflow Health Check | references/health-check.md | Yes | ## Invocation Contract | Aspect | Detail | |--------|--------| | **Inputs** | old_name [required], new_name [required] | | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--dry-run` (run selection + validation + display the §8 confirmation block, then exit with `status="dry-run"` — no copy, no manifest re-key, no delete). Useful for verifying the rename plan before the irreversible §8 (delete old) section. | | **Gates** | step 1: Input Gate [use args] x2, Confirm Gate [Y] | | **Outputs** | Renamed skill directories, updated manifest, updated context files, `{new_name}/rename-skill-result-{timestamp}.json` and `{new_name}/rename-skill-result-latest.json` | | **Concurrency** | A PID-file lock at `{forge_data_folder}/{old_name}/.skf-rename.lock` serializes concurrent runs against the same `old_name`; a live-PID collision HALTs with `halt_reason: "halted-for-concurrent-run"` (exit 5). See select.md §4b for the acquire / stale-clear / release mechanism. | | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. The §6 source-authority warning HALTs by default in headless when `source_authority="official"`; set `force_source_authority_in_headless = "true"` in `customize.toml` to auto-acknowledge and proceed (the override is recorded in `headless_decisions[]`). | | **Exit codes** | Stable per-failure-class codes — see `references/exit-codes.md` | ## Result Contract (Headless) When `{headless_mode}` is true, step 3 emits a single-line JSON envelope on **stdout** before chaining to step 4, and every HARD HALT emits the same envelope shape on **stderr** with `status: "error"`: ``` SKF_RENAME_SKILL_RESULT_JSON: {"status":"success|error|dry-run","old_name":"…|null","new_name":"…|null","versions_renamed":[],"manifest_rekeyed":false,"context_files_updated":[],"exit_code":0,"halt_reason":null,"headless_decisions":[]} ``` `status` is `"success"` on the terminal happy path, `"dry-run"` when `--dry-run` was set and the workflow exited before §9 stores decisions, `"error"` on any HALT. `halt_reason` is one of: `null` (success), `"input-missing"`, `"input-invalid"`, `"manifest-corrupt"`, `"nothing-to-rename"`, `"name-collision"`, `"source-authority-blocked"`, `"halted-for-concurrent-run"`, `"copy-failed"`, `"verify-failed"`, `"manifest-write-failed"`, `"write-failed"`, `"user-cancelled"`. (§7 context-file rebuild is best-effort and never halts, so it has no `halt_reason`.) `exit_code` matches `references/exit-codes.md`. `headless_decisions` is the audit trail of confirmation gates auto-resolved under `{headless_mode}` — each entry `{gate, default_action, taken_action, reason}` (the §6 source-authority override and the §8 auto-confirm); it is `[]` in interactive runs and whenever no gate was auto-resolved before the envelope was emitted. ## 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 four scalars, if the merged value is empty or absent, the bundled default applies: - `{forceSourceAuthorityInHeadless}` ← `workflow.force_source_authority_in_headless` (empty or non-`"true"` = HALT in headless on `"official"` source-authority) - `{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 string (no-op — step 3 skips the post-completion hook invocation) Stash all four as workflow-context variables. Stage files reference them directly — no conditional at the usage site. Then apply the resolved array surfaces so they are not silent no-ops: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts — the bundled default loads any `project-context.md` so rename-policy and public-name-stability guardrails stay in mind); and after activation completes, execute each entry in `workflow.activation_steps_append` in order. 4. **Pre-flight write probe.** Verify both `{skills_output_folder}` and `{forge_data_folder}` are writable. A read-only mount, full disk, or permissions-denied path otherwise only surfaces inside step 2's transactional copy — by then the user has already gone through name validation and confirmation: ```bash for dir in "{skills_output_folder}" "{forge_data_folder}"; do mkdir -p "$dir" && \ printf 'probe' > "$dir/.skf-write-probe" && \ rm "$dir/.skf-write-probe" done ``` 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 `old_name: null` and `new_name: null` (neither is resolved yet at activation time). 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.