Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,34 @@
"./skills/bmad-story-automator",
"./skills/bmad-story-automator-review"
]
},
{
"name": "baut",
"source": "./skills",
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In .claude-plugin/marketplace.json line 41, the new baut plugin entry doesn’t include a skills list (unlike bmad-automator) and uses source: "./skills", which may not match the custom-source resolver expectations (docs/plans mention plugin-level skills entries). This could cause legacy source discovery to still fail or to pull an unexpected set of skills.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

"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"
]
}
]
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions docs/installation-and-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<manifest>.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
Expand All @@ -60,12 +69,13 @@ The BMAD Method commands above install through `bmad-method` for the requested `
flowchart TD
A["Run install.sh <project>"] --> 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
Expand Down
23 changes: 23 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
107 changes: 107 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []) {
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In install.sh line 125, walk() collects all .yml/.yaml files under _bmad/, but the PR description/docs describe targeting _bmad/**/manifest.y*ml. If any other YAML under _bmad/ contains modules: plus a - name: baut line, it will also be backed up and rewritten unexpectedly.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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;
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In install.sh line 143, shouldEndBlock() treats blank/comment-only lines as never ending the baut block, so comments/whitespace separating module entries can be removed along with the baut entry. If preserving surrounding YAML formatting/comments is important (as the docs suggest), this behavior may be surprising.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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*$/);
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In install.sh line 166, bautMatch only checks that a modules: key was seen somewhere earlier (moduleIndent !== null), but doesn’t verify the matched - name: baut line is actually inside the modules: list (indentation-wise). This can remove a name: baut block from a later, unrelated YAML list if it appears in the same file after modules:.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
130 changes: 130 additions & 0 deletions scripts/compat-test.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading