diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index aedd665..7a91c24 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -35,6 +35,34 @@ "./skills/bmad-story-automator", "./skills/bmad-story-automator-review" ] + }, + { + "name": "baut", + "source": "./skills", + "description": "Legacy compatibility alias for pre-rename BMAD Automator installs.", + "version": "1.15.0", + "author": { + "name": "bma-d", + "email": "support@bmadcode.com" + }, + "homepage": "https://github.com/bmad-code-org/bmad-automator#readme", + "repository": "https://github.com/bmad-code-org/bmad-automator", + "license": "MIT", + "keywords": [ + "bmad", + "story-automation", + "claude-code", + "skills", + "workflow", + "legacy" + ], + "category": "Workflow Automation", + "tags": [ + "bmad", + "automation", + "story-workflow", + "legacy" + ] } ] } diff --git a/README.md b/README.md index 495742f..44eada4 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ Codex preview branch, only for testing unpublished follow-up fixes: npx bmad-method install --custom-source https://github.com/bmad-code-org/bmad-automator@next/codex-runtime-support --tools codex --yes ``` +Legacy compatibility: releases before `v1.15.0` used BMAD module code `baut`. The package still advertises a `baut` custom-source alias so cached/custom installs can update instead of failing source discovery. The standalone `npx bmad-story-automator` installer also removes stale `name: baut` entries from `_bmad/**/manifest.y*ml`, writes a `.bak`, and leaves `config.toml` / `config.user.toml` untouched before installing skills. + Current caveat: the official registry sets `automator` to `default_channel: next`, so unqualified `--modules automator` and `--next automator` resolve to `main` HEAD. After this stable release lands on `main`, those commands include Codex support, but use `--all-stable` or `--pin` when you need reproducible stable behavior. For custom-source branch testing, verify the custom-source cache HEAD and installed runtime files instead of trusting installer exit status, summary text, or manifest channel fields alone. ## Expectations diff --git a/docs/installation-and-layout.md b/docs/installation-and-layout.md index 083f88d..d93820f 100644 --- a/docs/installation-and-layout.md +++ b/docs/installation-and-layout.md @@ -52,6 +52,15 @@ npx bmad-method install --modules automator --all-stable --tools claude-code --y If custom-source discovery asks which plugin to install after reading the branch, choose `bmad-automator`. For custom-source branch testing, confirm the custom-source cache HEAD and installed runtime files; installer metadata can still report the registry `next` ref when the custom source uses official module code `automator`. +## Legacy `baut` Compatibility + +Automator releases before `v1.15.0` used BMAD module code `baut`. Current releases use `automator`. + +This repo keeps two compatibility paths: + +- `.claude-plugin/marketplace.json` includes a `baut` alias that points at the same story-automator skills for custom-source/cache based installs. +- `npx bmad-story-automator` scans `_bmad/` for YAML manifests containing a `modules:` list and removes stale `- name: baut` entries. It first writes `.bak`, keeps the rest of the YAML intact, and does not touch `_bmad/config.toml` or `_bmad/config.user.toml`. + The BMAD Method commands above install through `bmad-method` for the requested `--tools` target. The sections below describe the standalone `npx bmad-story-automator` installer and its layout behavior. ## Installer Flow @@ -60,12 +69,13 @@ The BMAD Method commands above install through `bmad-method` for the requested ` flowchart TD A["Run install.sh "] --> B["Verify target is a BMAD project"] B --> C["Verify root skills exist in this repo"] - C --> D["Verify required sibling skills exist in target project"] - D --> E["Resolve optional QA skill if present"] - E --> F["Backup current installs and legacy story-automator paths"] - F --> G["Copy skills into each qualifying skill root"] - G --> H["Remove obsolete legacy command shims"] - H --> I["Print installed paths and verified sibling entrypoints"] + C --> D["Remove stale baut manifest entry if present"] + D --> E["Verify required sibling skills exist in target project"] + E --> F["Resolve optional QA skill if present"] + F --> G["Backup current installs and legacy story-automator paths"] + G --> H["Copy skills into each qualifying skill root"] + H --> I["Remove obsolete legacy command shims"] + I --> J["Print installed paths and verified sibling entrypoints"] ``` ## Target Paths diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8d01640..67d8643 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -112,6 +112,29 @@ module code `automator`, BMAD-METHOD 6.6.0 can still record official registry `next`/`main` metadata in `_bmad/_config/manifest.yaml`; do not treat that manifest field as proof that the branch content failed to install. +## Stale `baut` Manifest Entry + +Symptom: + +```text +Installation failed: Source for module 'baut' is not available. +It will be retained but cannot be updated without its source files. +``` + +Cause: older Automator installs recorded module code `baut`; current BMAD registry installs Automator as `automator`. + +Fix from this package: + +```bash +npx bmad-story-automator /absolute/path/to/your-bmad-project +``` + +That installer backs up the matching manifest as `.bak`, removes only the stale `- name: baut` module entry, and leaves `_bmad/config.toml` plus `_bmad/config.user.toml` unchanged. Then rerun the BMAD Method install with current code: + +```bash +npx bmad-method@next install --action update --modules automator --tools codex --yes +``` + ## Sprint Status Drift If the state doc and `sprint-status.yaml` disagree: diff --git a/install.sh b/install.sh index d8045cf..8a93cec 100755 --- a/install.sh +++ b/install.sh @@ -113,6 +113,112 @@ cleanup_obsolete_command_shims() { done } +migrate_legacy_baut_manifest() { + node - "$TARGET_BMAD" <<'NODE' +const fs = require("node:fs"); +const path = require("node:path"); + +const bmadDir = process.argv[2]; +const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); + +function isManifestFile(file) { + return /^manifest\.ya?ml$/i.test(path.basename(file)); +} + +function walk(dir, files = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full, files); + } else if (entry.isFile() && isManifestFile(full)) { + files.push(full); + } + } + return files; +} + +function nextBackupPath(file) { + const backup = `${file}.bak`; + return fs.existsSync(backup) ? `${backup}-${timestamp}` : backup; +} + +function shouldEndBlock(line, itemIndent, moduleIndent) { + if (/^\s*$/.test(line) || /^\s*#/.test(line)) return false; + const indent = line.match(/^\s*/)[0].length; + return indent <= moduleIndent || (indent === itemIndent && line.trimStart().startsWith("- ")); +} + +function removeBautBlocks(content) { + const lines = content.split("\n"); + const output = []; + let changed = false; + let moduleIndent = null; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const modulesMatch = line.match(/^(\s*)modules:\s*$/); + if (modulesMatch) { + moduleIndent = modulesMatch[1].length; + output.push(line); + continue; + } + + if (moduleIndent !== null && !/^\s*$/.test(line) && !/^\s*#/.test(line)) { + const indent = line.match(/^\s*/)[0].length; + if (indent <= moduleIndent) { + moduleIndent = null; + } + } + + const bautMatch = moduleIndent === null ? null : line.match(/^(\s*)-\s+name:\s+['"]?baut['"]?\s*$/); + if (!bautMatch) { + output.push(line); + continue; + } + + const itemIndent = bautMatch[1].length; + if (itemIndent <= moduleIndent) { + output.push(line); + continue; + } + + changed = true; + index += 1; + const trailingTrivia = []; + while (index < lines.length && !shouldEndBlock(lines[index], itemIndent, moduleIndent)) { + if (/^\s*$/.test(lines[index]) || /^\s*#/.test(lines[index])) { + trailingTrivia.push(lines[index]); + } else { + trailingTrivia.length = 0; + } + index += 1; + } + output.push(...trailingTrivia); + index -= 1; + } + + return changed ? output.join("\n").replace(/\n*$/, "\n") : null; +} + +for (const file of walk(bmadDir)) { + const content = fs.readFileSync(file, "utf8"); + if (!content.includes("modules:") || !/^\s*-\s+name:\s+['"]?baut['"]?\s*$/m.test(content)) { + continue; + } + + const migrated = removeBautBlocks(content); + if (migrated === null) continue; + + const backup = nextBackupPath(file); + fs.copyFileSync(file, backup); + fs.writeFileSync(file, migrated, "utf8"); + const relFile = path.relative(path.dirname(bmadDir), file); + const relBackup = path.relative(path.dirname(bmadDir), backup); + console.log(`Migrated legacy baut manifest entry: ${relFile} (backup: ${relBackup})`); +} +NODE +} + resolve_workflow_path() { local candidate for candidate in "$@"; do @@ -274,6 +380,7 @@ STORY_REVIEW_SOURCE="$SKILL_SOURCE_ROOT/bmad-story-automator-review" [ -f "$STORY_SOURCE/pyproject.toml" ] || err "Missing runtime pyproject: $STORY_SOURCE/pyproject.toml" [ -f "$STORY_REVIEW_SOURCE/SKILL.md" ] || err "Missing review SKILL.md: $STORY_REVIEW_SOURCE/SKILL.md" +migrate_legacy_baut_manifest collect_target_skills_roots if [ "${#TARGET_SKILLS_RELS[@]}" -eq 0 ]; then diff --git a/package.json b/package.json index a8f7d15..dc203fc 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "scripts": { "pack:dry-run": "npm pack --dry-run", "test:python": "PYTHONPATH=skills/bmad-story-automator/src python3 -m unittest discover -s tests", + "test:compat": "bash scripts/compat-test.sh", "test:smoke": "bash scripts/smoke-test.sh", - "verify": "npm run test:python && npm run pack:dry-run && npm run test:smoke" + "verify": "npm run test:python && npm run pack:dry-run && npm run test:smoke && npm run test:compat" }, "engines": { "node": ">=18" diff --git a/scripts/compat-test.sh b/scripts/compat-test.sh new file mode 100755 index 0000000..2ee96ba --- /dev/null +++ b/scripts/compat-test.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/bmad-story-automator-compat.XXXXXX")" +PACK_TARBALL="" + +cleanup() { + if [ -n "$PACK_TARBALL" ] && [ -f "$PACK_TARBALL" ]; then + rm -f "$PACK_TARBALL" + fi + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +assert_file() { + local path="$1" + [ -f "$path" ] || { + echo "Missing file: $path" >&2 + exit 1 + } +} + +assert_contains() { + local needle="$1" + local path="$2" + grep -Fq "$needle" "$path" || { + echo "Missing content in $path: $needle" >&2 + exit 1 + } +} + +assert_not_contains() { + local needle="$1" + local path="$2" + if grep -Fq "$needle" "$path"; then + echo "Unexpected content in $path: $needle" >&2 + exit 1 + fi +} + +make_skill() { + local root="$1" + local name="$2" + mkdir -p "$root/.claude/skills/$name" + printf -- '---\nname: %s\n---\n\nFollow ./workflow.md.\n' "$name" >"$root/.claude/skills/$name/SKILL.md" + printf '# %s\n' "$name" >"$root/.claude/skills/$name/workflow.md" +} + +make_project() { + local root="$1" + local manifest="$root/_bmad/_config/manifest.yaml" + mkdir -p "$root/_bmad/_config" "$root/.claude/commands" + make_skill "$root" bmad-create-story + make_skill "$root" bmad-dev-story + make_skill "$root" bmad-retrospective + make_skill "$root" bmad-qa-generate-e2e-tests + cat >"$manifest" <<'EOF' +installation: + version: 6.6.0 + installDate: 2026-05-17T00:00:00.000Z + lastUpdated: 2026-05-17T00:00:00.000Z +modules: + - name: core + version: 6.6.0 + source: built-in + - name: baut + version: v1.14.2 + installDate: 2026-05-17T00:00:00.000Z + lastUpdated: 2026-05-17T00:00:00.000Z + source: external + npmPackage: bmad-story-automator + repoUrl: https://github.com/bmad-code-org/bmad-automator + channel: stable + sha: 593f338532ea730b5c1a2dd86681e87b5b4f04dd + + # Keep separator comment attached to the next module. + - name: bmm + version: 6.6.0 + source: built-in +ides: + - claude-code +metadata: + examples: + - name: baut + note: keep unrelated baut list entry +EOF + cat >"$root/_bmad/other.yaml" <<'EOF' +modules: + - name: baut + note: keep non-manifest yaml untouched +EOF + printf 'team config untouched\n' >"$root/_bmad/config.toml" + printf 'user config untouched\n' >"$root/_bmad/config.user.toml" +} + +pack_fixture_tarball() { + PACK_TARBALL="$(cd "$ROOT_DIR" && npm pack --silent)" + PACK_TARBALL="$ROOT_DIR/$PACK_TARBALL" + assert_file "$PACK_TARBALL" +} + +run_legacy_baut_manifest_migration_case() { + local root="$TMP_DIR/legacy-baut-manifest-migration" + local manifest="$root/_bmad/_config/manifest.yaml" + local install_log="$root/install.log" + + make_project "$root" + npx --yes --package "file:$PACK_TARBALL" bmad-story-automator "$root" >"$install_log" 2>&1 + + assert_file "$manifest.bak" + assert_contains "name: baut" "$manifest.bak" + assert_contains "name: core" "$manifest" + assert_contains "name: bmm" "$manifest" + assert_contains "Keep separator comment attached to the next module." "$manifest" + assert_contains "note: keep unrelated baut list entry" "$manifest" + assert_not_contains "npmPackage: bmad-story-automator" "$manifest" + assert_contains "note: keep non-manifest yaml untouched" "$root/_bmad/other.yaml" + assert_contains "team config untouched" "$root/_bmad/config.toml" + assert_contains "user config untouched" "$root/_bmad/config.user.toml" + assert_contains "Migrated legacy baut manifest entry: _bmad/_config/manifest.yaml" "$install_log" + assert_file "$root/.claude/skills/bmad-story-automator/SKILL.md" + assert_file "$root/.claude/skills/bmad-story-automator-review/SKILL.md" +} + +pack_fixture_tarball +run_legacy_baut_manifest_migration_case + +echo "compat ok"