Files
damascus-orchestrator/scripts/test-ingest.sh
damascus-heartbeat 82b9758be6
Some checks failed
test / contract-and-unit (push) Failing after 14s
feat(bmad): add canonical _kit (templates + sample) + ingest validation
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)
2026-06-26 06:03:39 +00:00

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"