Some checks failed
test / contract-and-unit (push) Failing after 14s
BMAD-onboarding kit for the Damascus orchestrator:
- docs/adding-a-new-project.md — full onboarding guide covering layout,
required story section headers, common pitfalls (with the four classes
of bug that have cost real cycles here: Path.rglob doesn't follow
symlinks, architecture.md must be at planning-artifacts/architecture.md
exactly, missing section headers burn 3 retries each, etc.)
- bmad/_kit/ — read-only reference material (templates + sample)
- templates/{prd,architecture,epics,story}.md
- sample/hello-bmad/_bmad-output/ — one fully-formed worked example
(2-story FastAPI project, valid end-to-end)
- README.md — kit-level contract
- scripts/test-ingest.sh — pre-flight validation that catches the four
bug classes before any DB write. Verified against the live orchestrator
container: passes on the sample, fails (correctly) on a hand-broken tree
with both missing-section AND symlink bugs in one run.
- docker-compose.yml — replace /home/kaykayyali/_bmad bind (which
doesn't exist on this server) with ./bmad/_kit. Kit now ships with
the repo.
- .gitignore — re-include bmad/_kit/ so it travels with the repo while
keeping the existing 'bmad/ is ephemeral mount content' contract.
Verified end-to-end: 'damascus ingest --project hello-bmad' succeeded
on the live orchestrator, _find_bmad_story resolved both stories.
The 'architecture.md is ingested as a work item' quirk is documented in
docs/adding-a-new-project.md §'Common pitfalls' with a one-liner fix.
Refs: t_5aa80e4b (parallel dashboard work — committed separately)
260 lines
9.8 KiB
Bash
Executable File
260 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-ingest.sh — Validate a BMAD project's _bmad-output/ tree BEFORE
|
|
# running the real `damascus ingest`. Catches the four classes of bug
|
|
# that have cost real cycles on this orchestrator:
|
|
#
|
|
# 1. Missing required section headers in story files
|
|
# (orchestrator's spec-refiner returns `spec_wrong` and burns
|
|
# 3 retries per story)
|
|
# 2. Symlinks in the tree that Path.rglob won't follow
|
|
# (Python 3.12 default — orchestrator's find_bmad_story uses rglob)
|
|
# 3. architecture.md missing from planning-artifacts/architecture.md
|
|
# (spec-refiner hardcodes this path)
|
|
# 4. Story files in implementation-artifacts/ not mirrored to
|
|
# planning-artifacts/stories/ (orchestrator only ingests from
|
|
# planning-artifacts/)
|
|
#
|
|
# Usage:
|
|
# ./scripts/test-ingest.sh /root/<project>/_bmad-output <project-name>
|
|
#
|
|
# --check-only run only the local tree validation; don't contact
|
|
# the orchestrator container
|
|
#
|
|
# Exit codes:
|
|
# 0 tree is valid and ready to ingest
|
|
# 1 validation failure (printed to stderr)
|
|
# 2 orchestrator container unreachable (only when not --check-only)
|
|
#
|
|
# This script does NOT write to the DB. It only validates shape.
|
|
|
|
set -euo pipefail
|
|
|
|
BMAD_ROOT="${1:-}"
|
|
PROJECT_NAME="${2:-}"
|
|
|
|
if [ -z "$BMAD_ROOT" ] || [ -z "$PROJECT_NAME" ]; then
|
|
echo "usage: $0 <path-to-_bmad-output> <project-name> [--check-only]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
CHECK_ONLY=false
|
|
if [ "${3:-}" = "--check-only" ]; then
|
|
CHECK_ONLY=true
|
|
fi
|
|
|
|
# Resolve to absolute path
|
|
BMAD_ROOT=$(cd "$BMAD_ROOT" 2>/dev/null && pwd || { echo "ERROR: $BMAD_ROOT is not a directory" >&2; exit 1; })
|
|
|
|
echo "=== test-ingest.sh ==="
|
|
echo "BMAD root: $BMAD_ROOT"
|
|
echo "Project: $PROJECT_NAME"
|
|
echo "Mode: $([ "$CHECK_ONLY" = true ] && echo 'check-only (no orchestrator contact)' || echo 'full (will contact orchestrator)')"
|
|
echo ""
|
|
|
|
# ── Check 1: required layout ──────────────────────────────────────────
|
|
echo "── Check 1: required layout ──"
|
|
|
|
FAILED_CHECKS=0
|
|
|
|
REQUIRED_PATHS=(
|
|
"$BMAD_ROOT/planning-artifacts"
|
|
"$BMAD_ROOT/planning-artifacts/architecture.md"
|
|
)
|
|
|
|
for p in "${REQUIRED_PATHS[@]}"; do
|
|
if [ ! -e "$p" ]; then
|
|
echo " ✗ MISSING: $p" >&2
|
|
echo " The orchestrator hardcodes this path. Without it, the spec-refiner runs blind." >&2
|
|
FAILED_CHECKS=$((FAILED_CHECKS + 1))
|
|
else
|
|
echo " ✓ $p"
|
|
fi
|
|
done
|
|
|
|
# Stories must be under planning-artifacts/ OR mirrored there from implementation-artifacts/
|
|
STORIES_DIR="$BMAD_ROOT/planning-artifacts/stories"
|
|
if [ ! -d "$STORIES_DIR" ]; then
|
|
echo " ✗ MISSING: $STORIES_DIR" >&2
|
|
echo " Per-story briefs must be at planning-artifacts/stories/ for the orchestrator to ingest them." >&2
|
|
FAILED_CHECKS=$((FAILED_CHECKS + 1))
|
|
else
|
|
echo " ✓ $STORIES_DIR"
|
|
|
|
STORY_COUNT=$(find "$STORIES_DIR" -maxdepth 1 -name '*.md' -type f | wc -l | tr -d ' ')
|
|
if [ "$STORY_COUNT" -eq 0 ]; then
|
|
echo " ✗ No story files found in $STORIES_DIR" >&2
|
|
FAILED_CHECKS=$((FAILED_CHECKS + 1))
|
|
else
|
|
echo " ✓ Found $STORY_COUNT story file(s)"
|
|
fi
|
|
|
|
# Check if there's also an implementation-artifacts/ that needs to be in sync
|
|
IMPL_STORIES="$BMAD_ROOT/../implementation-artifacts/stories"
|
|
if [ -d "$IMPL_STORIES" ] && [ ! -L "$STORIES_DIR" ]; then
|
|
IMPL_COUNT=$(find "$IMPL_STORIES" -maxdepth 1 -name '*.md' -type f | wc -l | tr -d ' ')
|
|
if [ "$IMPL_COUNT" -ne "$STORY_COUNT" ]; then
|
|
echo " ⚠ WARNING: implementation-artifacts/stories/ has $IMPL_COUNT files, planning-artifacts/stories/ has $STORY_COUNT." >&2
|
|
echo " If you use the standard BMAD layout, copy or bind-mount the stories into planning-artifacts/stories/." >&2
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ── Check 2: no symlinks that rglob won't follow ──────────────────────
|
|
echo ""
|
|
echo "── Check 2: symlink audit (Path.rglob won't follow these in Python 3.12) ──"
|
|
|
|
SYM_COUNT=0
|
|
SYM_FILES=()
|
|
while IFS= read -r -d '' link; do
|
|
SYM_COUNT=$((SYM_COUNT + 1))
|
|
SYM_FILES+=("$link")
|
|
done < <(find "$BMAD_ROOT" -type l -print0 2>/dev/null || true)
|
|
|
|
if [ "$SYM_COUNT" -gt 0 ]; then
|
|
for link in "${SYM_FILES[@]}"; do
|
|
echo " ✗ SYMLINK: $link → $(readlink "$link")" >&2
|
|
done
|
|
echo " Replace with a real copy or a bind mount (see docs/adding-a-new-project.md)." >&2
|
|
FAILED_CHECKS=$((FAILED_CHECKS + 1))
|
|
else
|
|
echo " ✓ No symlinks in the tree"
|
|
fi
|
|
|
|
# ── Check 3: required story section headers ───────────────────────────
|
|
echo ""
|
|
echo "── Check 3: required section headers in every story ──"
|
|
|
|
REQUIRED_SECTIONS=(
|
|
"## Goal"
|
|
"## Acceptance Criteria"
|
|
"## TDD Plan"
|
|
"## File Scope"
|
|
"## Test Command"
|
|
"## Ambiguities"
|
|
)
|
|
|
|
BAD_COUNT=0
|
|
while IFS= read -r story; do
|
|
story_basename=$(basename "$story")
|
|
missing=()
|
|
for section in "${REQUIRED_SECTIONS[@]}"; do
|
|
if ! grep -qF "$section" "$story"; then
|
|
missing+=("$section")
|
|
fi
|
|
done
|
|
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
BAD_COUNT=$((BAD_COUNT + 1))
|
|
echo " ✗ $story_basename — missing sections: ${missing[*]}" >&2
|
|
else
|
|
echo " ✓ $story_basename"
|
|
fi
|
|
done < <(find "$STORIES_DIR" -maxdepth 1 -name '*.md' -type f)
|
|
|
|
if [ "$BAD_COUNT" -gt 0 ]; then
|
|
echo "" >&2
|
|
echo " $BAD_COUNT story file(s) have missing sections." >&2
|
|
echo " The orchestrator's spec-refiner returns 'spec_wrong' for each one and burns 3 retries." >&2
|
|
echo " Fix: copy from bmad/_kit/templates/story.md and re-run." >&2
|
|
FAILED_CHECKS=$((FAILED_CHECKS + 1))
|
|
fi
|
|
|
|
# ── Check 4: every story has a non-empty Test Command ────────────────
|
|
echo ""
|
|
echo "── Check 4: Test Command has a real shell command ──"
|
|
|
|
EMPTY_CMD_COUNT=0
|
|
while IFS= read -r story; do
|
|
story_basename=$(basename "$story")
|
|
# Extract everything between "## Test Command" and the next ## heading
|
|
cmd=$(awk '/^## Test Command/{flag=1; next} /^## /{flag=0} flag' "$story" | sed '/^```/d; /^$/d' | head -5)
|
|
if [ -z "$(echo "$cmd" | tr -d '[:space:]')" ]; then
|
|
EMPTY_CMD_COUNT=$((EMPTY_CMD_COUNT + 1))
|
|
echo " ✗ $story_basename — Test Command is empty" >&2
|
|
fi
|
|
done < <(find "$STORIES_DIR" -maxdepth 1 -name '*.md' -type f)
|
|
|
|
if [ "$EMPTY_CMD_COUNT" -gt 0 ]; then
|
|
echo "" >&2
|
|
echo " $EMPTY_CMD_COUNT story file(s) have empty Test Commands." >&2
|
|
echo " The orchestrator will run 'echo no test command' which always passes — your story ships unverified." >&2
|
|
FAILED_CHECKS=$((FAILED_CHECKS + 1))
|
|
else
|
|
echo " ✓ All Test Commands populated"
|
|
fi
|
|
|
|
# ── Optional Check 5: live orchestrator dry-run ───────────────────────
|
|
if [ "$CHECK_ONLY" = false ] && [ "$FAILED_CHECKS" -eq 0 ]; then
|
|
echo ""
|
|
echo "── Check 5: live orchestrator dry-run ──"
|
|
|
|
# Check the orchestrator container is reachable
|
|
if ! docker exec damascus-orchestrator-orchestrator-1 true 2>/dev/null; then
|
|
echo " ✗ Orchestrator container not reachable" >&2
|
|
echo " Either bring it up ('docker compose up -d orchestrator') or re-run with --check-only" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Verify the bind mount is in place inside the container
|
|
CONTAINER_PATH="/opt/damascus/bmad/$PROJECT_NAME/_bmad-output"
|
|
if ! docker exec damascus-orchestrator-orchestrator-1 test -d "$CONTAINER_PATH" 2>/dev/null; then
|
|
echo " ✗ $CONTAINER_PATH not visible inside orchestrator container" >&2
|
|
echo " Add a bind mount to docker-compose.yml:" >&2
|
|
echo " - $BMAD_ROOT:$CONTAINER_PATH:ro" >&2
|
|
echo " Then 'docker compose up -d --force-recreate --no-deps orchestrator'" >&2
|
|
exit 1
|
|
fi
|
|
echo " ✓ Bind mount visible inside container at $CONTAINER_PATH"
|
|
|
|
# Run the actual dry-run ingest
|
|
echo ""
|
|
echo " Running: damascus ingest --project $PROJECT_NAME --dry-run"
|
|
if ! docker exec damascus-orchestrator-orchestrator-1 \
|
|
damascus ingest --project "$PROJECT_NAME" --dry-run 2>&1; then
|
|
echo " ✗ Dry-run ingest failed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo " Now verifying _find_bmad_story can locate each story (the real bottleneck):"
|
|
CANNOT_FIND=0
|
|
while IFS= read -r story; do
|
|
story_basename=$(basename "$story" .md)
|
|
# The orchestrator's match is: story_id in f.stem
|
|
# story_id comes from Path(f).stem during ingest (the filename without .md)
|
|
if ! docker exec damascus-orchestrator-orchestrator-1 \
|
|
python3 -c "
|
|
from pathlib import Path
|
|
import sys
|
|
p = Path('$CONTAINER_PATH')
|
|
sid = '$story_basename'
|
|
found = any(sid in f.stem for f in p.rglob('*.md'))
|
|
sys.exit(0 if found else 1)
|
|
" 2>/dev/null; then
|
|
CANNOT_FIND=$((CANNOT_FIND + 1))
|
|
echo " ✗ $story_basename — _find_bmad_story won't find this!" >&2
|
|
else
|
|
echo " ✓ $story_basename"
|
|
fi
|
|
done < <(find "$STORIES_DIR" -maxdepth 1 -name '*.md' -type f)
|
|
|
|
if [ "$CANNOT_FIND" -gt 0 ]; then
|
|
echo "" >&2
|
|
echo " $CANNOT_FIND story file(s) cannot be located by the spec-refiner." >&2
|
|
echo " This is the symlink-or-missing-section bug. Check:" >&2
|
|
echo " - Are there symlinks in the tree? Path.rglob won't follow them." >&2
|
|
echo " - Are the story files actually under planning-artifacts/stories/?" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$FAILED_CHECKS" -gt 0 ]; then
|
|
echo "=== $FAILED_CHECKS check(s) FAILED — fix the issues above and re-run ===" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== All checks passed ==="
|
|
echo ""
|
|
echo "Next step: docker exec damascus-orchestrator-orchestrator-1 \\"
|
|
echo " damascus ingest --project $PROJECT_NAME" |