diff --git a/.claude/commands.json b/.claude/commands.json index 796e726a..5c912d67 100644 --- a/.claude/commands.json +++ b/.claude/commands.json @@ -160,12 +160,9 @@ "agents": { "orchestrator": "agents/orchestrator.md", "reviewer": "agents/reviewer.md", - "builder": "agents/builder.md", "deployer": "agents/deployer.md", "tester": "agents/qa.md", "architect": "agents/architect.md", - "analyst": "agents/analyst.md", - "developer": "agents/developer.md", "implementer": "agents/implementer.md", "spec-reviewer": "agents/spec-reviewer.md", "security": "agents/security.md", diff --git a/.codex/INSTALL.md b/.codex/INSTALL.md index 47f93331..f587d0e1 100644 --- a/.codex/INSTALL.md +++ b/.codex/INSTALL.md @@ -16,7 +16,7 @@ Project skills source of truth lives in `prompts/skills/` (this repo). Tool fold ```bash sdp init --auto ``` -3. Use `@build 00-XXX-YY` or `sdp plan`, `sdp apply`, `sdp log trace` per [CLAUDE.md](../CLAUDE.md). +3. Use `@build 00-XXX-YY` or `sdp plan`, `sdp apply`, `sdp log trace` per [AGENTS.md](../AGENTS.md). If you want the CLI only, use: @@ -32,7 +32,7 @@ curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | s ├── agents/ # Project-level agent symlink └── skills/ ├── README.md - └── sdp/ # Project-level skills sourced from prompts/skills + └── {skill}/ # Per-skill symlinks to prompts/skills/{skill} ~/.codex/ └── skills/ # User-level skills (persistent) diff --git a/.codex/skills/README.md b/.codex/skills/README.md index 05174fa7..ba9ae8ee 100644 --- a/.codex/skills/README.md +++ b/.codex/skills/README.md @@ -1,9 +1,10 @@ # Project-level skills (Codex) -SDP project skills are defined in `prompts/skills/` (source of truth). This folder contains symlinks for Codex compatibility. +SDP project skills are defined in `prompts/skills/` (source of truth). This folder contains per-skill symlinks for Codex compatibility. +- **@init** — Initialize SDP in a project. See `prompts/skills/init/SKILL.md`. - **@build** — Execute workstream (TDD, guard). See `prompts/skills/build/SKILL.md`. - **@design** — Plan workstreams. See `prompts/skills/design/SKILL.md`. - **@review** — Multi-agent quality review. See `prompts/skills/review/SKILL.md`. -Full list: `prompts/skills/` (build, design, feature, guard, oneshot, review, tdd, etc.). +Full list: `prompts/skills/` (init, build, design, feature, guard, oneshot, review, tdd, etc.). diff --git a/.codex/skills/beads b/.codex/skills/beads new file mode 120000 index 00000000..a196feda --- /dev/null +++ b/.codex/skills/beads @@ -0,0 +1 @@ +../../prompts/skills/beads/ \ No newline at end of file diff --git a/.codex/skills/bugfix b/.codex/skills/bugfix new file mode 120000 index 00000000..3002d33e --- /dev/null +++ b/.codex/skills/bugfix @@ -0,0 +1 @@ +../../prompts/skills/bugfix/ \ No newline at end of file diff --git a/.codex/skills/build b/.codex/skills/build new file mode 120000 index 00000000..22618a5d --- /dev/null +++ b/.codex/skills/build @@ -0,0 +1 @@ +../../prompts/skills/build/ \ No newline at end of file diff --git a/.codex/skills/ci-triage b/.codex/skills/ci-triage new file mode 120000 index 00000000..a3bf96d1 --- /dev/null +++ b/.codex/skills/ci-triage @@ -0,0 +1 @@ +../../prompts/skills/ci-triage/ \ No newline at end of file diff --git a/.codex/skills/debug b/.codex/skills/debug new file mode 120000 index 00000000..1b892410 --- /dev/null +++ b/.codex/skills/debug @@ -0,0 +1 @@ +../../prompts/skills/debug/ \ No newline at end of file diff --git a/.codex/skills/deploy b/.codex/skills/deploy new file mode 120000 index 00000000..1da719ec --- /dev/null +++ b/.codex/skills/deploy @@ -0,0 +1 @@ +../../prompts/skills/deploy/ \ No newline at end of file diff --git a/.codex/skills/design b/.codex/skills/design new file mode 120000 index 00000000..8dee642c --- /dev/null +++ b/.codex/skills/design @@ -0,0 +1 @@ +../../prompts/skills/design/ \ No newline at end of file diff --git a/.codex/skills/discovery b/.codex/skills/discovery new file mode 120000 index 00000000..f5cfec45 --- /dev/null +++ b/.codex/skills/discovery @@ -0,0 +1 @@ +../../prompts/skills/discovery/ \ No newline at end of file diff --git a/.codex/skills/feature b/.codex/skills/feature new file mode 120000 index 00000000..7ac9de75 --- /dev/null +++ b/.codex/skills/feature @@ -0,0 +1 @@ +../../prompts/skills/feature/ \ No newline at end of file diff --git a/.codex/skills/go-modern b/.codex/skills/go-modern new file mode 120000 index 00000000..526885d6 --- /dev/null +++ b/.codex/skills/go-modern @@ -0,0 +1 @@ +../../prompts/skills/go-modern/ \ No newline at end of file diff --git a/.codex/skills/guard b/.codex/skills/guard new file mode 120000 index 00000000..494aff01 --- /dev/null +++ b/.codex/skills/guard @@ -0,0 +1 @@ +../../prompts/skills/guard/ \ No newline at end of file diff --git a/.codex/skills/hotfix b/.codex/skills/hotfix new file mode 120000 index 00000000..64f4ff3a --- /dev/null +++ b/.codex/skills/hotfix @@ -0,0 +1 @@ +../../prompts/skills/hotfix/ \ No newline at end of file diff --git a/.codex/skills/idea b/.codex/skills/idea new file mode 120000 index 00000000..ee5417e7 --- /dev/null +++ b/.codex/skills/idea @@ -0,0 +1 @@ +../../prompts/skills/idea/ \ No newline at end of file diff --git a/.codex/skills/init b/.codex/skills/init new file mode 120000 index 00000000..a1c8f955 --- /dev/null +++ b/.codex/skills/init @@ -0,0 +1 @@ +../../prompts/skills/init/ \ No newline at end of file diff --git a/.codex/skills/issue b/.codex/skills/issue new file mode 120000 index 00000000..00dfc508 --- /dev/null +++ b/.codex/skills/issue @@ -0,0 +1 @@ +../../prompts/skills/issue/ \ No newline at end of file diff --git a/.codex/skills/oneshot b/.codex/skills/oneshot new file mode 120000 index 00000000..c9a489dc --- /dev/null +++ b/.codex/skills/oneshot @@ -0,0 +1 @@ +../../prompts/skills/oneshot/ \ No newline at end of file diff --git a/.codex/skills/protocol-consistency b/.codex/skills/protocol-consistency new file mode 120000 index 00000000..4e6a9c61 --- /dev/null +++ b/.codex/skills/protocol-consistency @@ -0,0 +1 @@ +../../prompts/skills/protocol-consistency/ \ No newline at end of file diff --git a/.codex/skills/prototype b/.codex/skills/prototype new file mode 120000 index 00000000..115a610d --- /dev/null +++ b/.codex/skills/prototype @@ -0,0 +1 @@ +../../prompts/skills/prototype/ \ No newline at end of file diff --git a/.codex/skills/reality b/.codex/skills/reality new file mode 120000 index 00000000..2ea0a11b --- /dev/null +++ b/.codex/skills/reality @@ -0,0 +1 @@ +../../prompts/skills/reality/ \ No newline at end of file diff --git a/.codex/skills/reality-check b/.codex/skills/reality-check new file mode 120000 index 00000000..82b4d59e --- /dev/null +++ b/.codex/skills/reality-check @@ -0,0 +1 @@ +../../prompts/skills/reality-check/ \ No newline at end of file diff --git a/.codex/skills/review b/.codex/skills/review new file mode 120000 index 00000000..9c711f5a --- /dev/null +++ b/.codex/skills/review @@ -0,0 +1 @@ +../../prompts/skills/review/ \ No newline at end of file diff --git a/.codex/skills/sdp b/.codex/skills/sdp deleted file mode 120000 index c4bba781..00000000 --- a/.codex/skills/sdp +++ /dev/null @@ -1 +0,0 @@ -../../prompts/skills \ No newline at end of file diff --git a/.codex/skills/strataudit b/.codex/skills/strataudit new file mode 120000 index 00000000..6b77a7cd --- /dev/null +++ b/.codex/skills/strataudit @@ -0,0 +1 @@ +../../prompts/skills/strataudit/ \ No newline at end of file diff --git a/.codex/skills/tdd b/.codex/skills/tdd new file mode 120000 index 00000000..b17d66b4 --- /dev/null +++ b/.codex/skills/tdd @@ -0,0 +1 @@ +../../prompts/skills/tdd/ \ No newline at end of file diff --git a/.codex/skills/think b/.codex/skills/think new file mode 120000 index 00000000..625bebe1 --- /dev/null +++ b/.codex/skills/think @@ -0,0 +1 @@ +../../prompts/skills/think/ \ No newline at end of file diff --git a/.codex/skills/ux b/.codex/skills/ux new file mode 120000 index 00000000..9746377d --- /dev/null +++ b/.codex/skills/ux @@ -0,0 +1 @@ +../../prompts/skills/ux/ \ No newline at end of file diff --git a/.codex/skills/verify-workstream b/.codex/skills/verify-workstream new file mode 120000 index 00000000..38d467f0 --- /dev/null +++ b/.codex/skills/verify-workstream @@ -0,0 +1 @@ +../../prompts/skills/verify-workstream/ \ No newline at end of file diff --git a/.codex/skills/vision b/.codex/skills/vision new file mode 120000 index 00000000..af95a3f9 --- /dev/null +++ b/.codex/skills/vision @@ -0,0 +1 @@ +../../prompts/skills/vision/ \ No newline at end of file diff --git a/.cursor/README.md b/.cursor/README.md index b499275e..1f550bc2 100644 --- a/.cursor/README.md +++ b/.cursor/README.md @@ -22,6 +22,6 @@ Use `@` prefix to invoke skills: ## See Also -- [CLAUDE.md](../CLAUDE.md) - Full protocol +- [AGENTS.md](../AGENTS.md) - Agent instructions and quality gates - [prompts/skills/](../prompts/skills/) - Canonical skill definitions - [.claude/skills/](../.claude/skills/) - Claude compatibility symlink diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json index de37937e..40dd3005 100644 --- a/.cursor/worktrees.json +++ b/.cursor/worktrees.json @@ -1,11 +1,9 @@ { "setup_commands_unix": [ - "cd sdp-plugin && go mod download 2>/dev/null || echo 'Go modules not configured'", - "echo '✅ Worktree ready (Go project)'" + "echo 'Worktree ready (SDP project)'" ], "setup_commands_windows": [ - "cd sdp-plugin && go mod download 2>nul || echo Go modules not configured", - "echo Worktree ready (Go project)" + "echo Worktree ready (SDP project)" ], - "description": "Template for SDP project worktree setup. Go-first project." + "description": "Template for SDP project worktree setup." } diff --git a/.github/workflows/reference-check.yml b/.github/workflows/reference-check.yml new file mode 100644 index 00000000..23687175 --- /dev/null +++ b/.github/workflows/reference-check.yml @@ -0,0 +1,22 @@ +name: Reference Integrity Check + +on: + pull_request: + branches: [main, dev] + push: + branches: [main, dev] + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-references: + name: Check References + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run reference integrity check + run: sh scripts/check-references.sh diff --git a/AGENTS.md b/AGENTS.md index 4ef4ab00..cce1a087 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,13 +14,15 @@ bd sync # Sync with git ## Quality Gates -Before pushing code changes: - -```bash -go build ./... # must succeed -go test ./... # must pass -go vet ./... # no issues -``` +Before pushing code changes, run the appropriate gates for your project's language: + +| Language | Build | Test | Lint | +|----------|-------|------|------| +| Go | `go build ./...` | `go test ./...` | `go vet ./...` | +| Python | `pip install .` | `pytest` | `ruff check .` | +| Node.js | `npm run build` | `npm test` | `npm run lint` | +| Rust | `cargo build` | `cargo test` | `cargo clippy` | +| Java | `mvn compile` | `mvn test` | `mvn checkstyle:check` | For Go changes, follow the canonical `@go-modern` skill in `prompts/skills/go-modern/SKILL.md` and prefer modern stdlib idioms when they preserve behavior. diff --git a/CLAUDE.md b/CLAUDE.md index 0f20cf3a..9bf6933e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,8 @@ AI-native dev with workstreams, gates, TDD. ## Decision Tree ``` -New? → @vision → @reality → @feature +New? → @init → @vision → @reality → @feature +Demo? → sdp demo No → State? → @reality --quick WS? → @oneshot No → @feature "X" @@ -24,7 +25,10 @@ No → @feature "X" ## Try ```bash -go install github.com/fall-out-bug/sdp/sdp-plugin/cmd/sdp@latest +sdp demo # Guided walkthrough +# or install manually: +# go install github.com/fall-out-bug/sdp/sdp-plugin/cmd/sdp@latest +@init @feature "X" @build 00-001-01 ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0aaeec9f..b8613616 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,7 +79,7 @@ sh scripts/test-install-project.sh ## What to Edit - Edit `prompts/` for prompt or agent behavior. -- Do not hand-edit `.claude/`, `.cursor/`, `.opencode/`, or `.codex/skills/sdp` as source files. +- Do not hand-edit `.claude/`, `.cursor/`, `.opencode/`, or `.codex/skills/` as source files. - Edit `sdp-plugin/` for CLI behavior. - Update docs when public behavior changes. diff --git a/docs/ADOPTION.md b/docs/ADOPTION.md index 921986cc..8b28040b 100644 --- a/docs/ADOPTION.md +++ b/docs/ADOPTION.md @@ -222,7 +222,7 @@ sh sdp/scripts/uninstall.sh **What is removed:** -- Symlinks in `.claude/skills`, `.claude/agents`, `.cursor/skills`, `.cursor/agents`, `.opencode/skills`, `.opencode/agents`, `.codex/skills/sdp`, `.codex/agents` +- Symlinks in `.claude/skills`, `.claude/agents`, `.cursor/skills`, `.cursor/agents`, `.opencode/skills`, `.opencode/agents`, `.codex/skills/*` (legacy `.codex/skills/sdp` also cleaned), `.codex/agents` - SDP-installed files: `.claude/commands.json`, `.codex/INSTALL.md`, `.codex/skills/README.md` - Git hooks pointing to SDP (`pre-commit`, `pre-push`) - SDP entries from `.gitignore` diff --git a/install.sh b/install.sh index eadac3d3..c95e9450 100755 --- a/install.sh +++ b/install.sh @@ -33,11 +33,29 @@ for arg in "$@"; do esac done +detect_ide() { + if [ "$SDP_IDE" != "auto" ] && [ -n "$SDP_IDE" ]; then + echo "$SDP_IDE" + return + fi + # Auto-detect from existing config files + if [ -f ".cursorrules" ] || [ -d ".cursor" ]; then echo "cursor" + elif [ -d ".codex" ]; then echo "codex" + elif [ -d ".claude" ]; then echo "claude" + elif [ -d ".opencode" ]; then echo "opencode" + elif [ -d ".zed" ] || [ -f ".zed/settings.json" ]; then echo "zed" + elif [ -d ".warp" ]; then echo "warp" + else echo "auto" + fi +} + run_remote_script() { name="$1" shift url="https://raw.githubusercontent.com/${SDP_REPO}/${SDP_REF}/scripts/${name}" - curl -fsSL "$url" | SDP_REPO="$SDP_REPO" SDP_REF="$SDP_REF" SDP_IDE="${SDP_IDE:-auto}" sh -s -- "$@" + DETECTED_IDE=$(detect_ide) + echo "Detected IDE: ${DETECTED_IDE}" + curl -fsSL "$url" | SDP_REPO="$SDP_REPO" SDP_REF="$SDP_REF" SDP_IDE="${SDP_IDE:-$DETECTED_IDE}" sh -s -- "$@" } if [ "$BINARY_ONLY" = "1" ]; then diff --git a/prompts/README.md b/prompts/README.md index 10ba2fdc..227f335e 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -14,7 +14,7 @@ Compatibility adapters are provided as symlinks: - `.cursor/agents` -> `../prompts/agents` - `.opencode/skills` -> `../prompts/skills` - `.opencode/agents` -> `../prompts/agents` -- `.codex/skills/sdp` -> `../../prompts/skills` +- `.codex/skills` -> per-skill individual symlinks (e.g. `.codex/skills/build` -> `../../prompts/skills/build`) - `.codex/agents` -> `../prompts/agents` Edit only `prompts/*` to avoid prompt drift across tools. diff --git a/prompts/skills/feature/SKILL.md b/prompts/skills/feature/SKILL.md index 6c5d4461..17a025da 100644 --- a/prompts/skills/feature/SKILL.md +++ b/prompts/skills/feature/SKILL.md @@ -192,6 +192,18 @@ Proceed? [y/n] > **Note:** `--dry-run` and `--yes` are orthogonal to skill mode flags (`--default`, `--quick`, `--auto`). They can be combined with any mode (e.g. `@feature "X" --quick --dry-run`). +## Completion + +When all workstreams are created and verified, output: + +``` +@feature complete. Feature {ID}: {count} workstreams created. + Aggregate: 00-{FFF}-00 + Leaves: 00-{FFF}-01 .. 00-{FFF}-{NN} + +Next: @build 00-{FFF}-01 or @oneshot F{XX} +``` + ## See Also @discovery — Product discovery gate | @idea — Requirements | @ux — UX research | @design — Workstream planning | @build — Execute leaf workstream | @oneshot — Execute all ready leaf workstreams diff --git a/prompts/skills/init/SKILL.md b/prompts/skills/init/SKILL.md new file mode 100644 index 00000000..9173c109 --- /dev/null +++ b/prompts/skills/init/SKILL.md @@ -0,0 +1,129 @@ +--- +name: init +description: Initialize SDP in a new or existing project +version: 1.0.0 +changes: + - Initial release: project detection, config scaffolding, harness setup +--- + +# @init + +Initialize SDP (Spec-Driven Protocol) in the current project directory. + +## Workflow + +When user invokes `@init` or `sdp init`: + +### Step 1: Project Detection + +Auto-detect project characteristics: + +```bash +# Detect language +if [ -f "go.mod" ]; then LANG="go" +elif [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then LANG="python" +elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then LANG="java" +elif [ -f "package.json" ]; then LANG="nodejs" +elif [ -f "Cargo.toml" ]; then LANG="rust" +else LANG="unknown" +fi + +# Detect framework (language-specific) +# Detect existing SDP artifacts +``` + +### Step 2: Ask Configuration Questions + +1. **Project name** — default: directory name +2. **Primary language** — default: detected +3. **AI harness(es)** — which AI tools the team uses (claude, codex, cursor, opencode, zed, warp, other). Default: detect from existing config files. +4. **Issue tracker** — beads (default), github-issues, linear, none + +### Step 3: Scaffold SDP Structure + +Create the following (skip existing): + +``` +docs/ + roadmap/ROADMAP.md # Feature roadmap + workstreams/ # Workstream tracking + drafts/ # Discovery drafts +.sdp/ # SDP internal state + checkpoints/ +AGENTS.md # Agent instructions (harness-neutral) +``` + +### Step 4: Configure Harnesses + +For each selected harness, create appropriate config: + +- **Claude Code** — `.claude/settings.json` (hooks), `.claude/commands/` (slash commands) +- **Codex** — `.codex/` with symlinks to `prompts/skills/` +- **Cursor** — `.cursor/` with `.cursorrules` and skill symlinks +- **OpenCode** — `.opencode/opencode.json` with agent cards + +All harness configs point to the same canonical source: `prompts/skills/` and `prompts/agents/`. + +### Step 5: Configure Quality Gates + +Based on detected language, set up appropriate quality gate commands: + +| Language | Build | Test | Lint | +|----------|-------|------|------| +| Go | `go build ./...` | `go test ./...` | `go vet ./...` | +| Python | `pip install .` | `pytest` | `ruff check .` | +| Node.js | `npm run build` | `npm test` | `npm run lint` | +| Rust | `cargo build` | `cargo test` | `cargo clippy` | +| Java | `mvn compile` | `mvn test` | `mvn checkstyle:check` | + +Write the detected gates into `AGENTS.md`. + +### Step 6: Initialize Issue Tracker + +If beads selected: +```bash +bd init +``` + +### Step 7: Verify + +- All configured harness symlinks resolve +- AGENTS.md exists with quality gates +- Issue tracker is functional (if selected) +- `sdp health` passes + +## Flags + +| Flag | Description | +|------|-------------| +| `--auto` | Skip all questions, accept defaults from detection | +| `--lang ` | Force specific language | +| `--harness ` | Comma-separated list of harnesses to configure | + +## When to Use + +- New project starting with SDP +- Existing project adopting SDP +- Adding a new AI harness to existing SDP project +- After cloning an SDP project (verify/setup) + +## Output + +``` +SDP initialized: {project_name} +Language: {detected} +Harnesses: {configured list} +Quality gates: {build}, {test}, {lint} +Issue tracker: {selected} + +Next steps: + @vision "your product idea" # Start from scratch + @reality --quick # Analyze existing codebase + @feature "add X" # Plan a feature +``` + +## See Also + +- @vision -- Strategic planning +- @reality -- Codebase analysis +- @feature -- Feature planning diff --git a/prompts/skills/vision/SKILL.md b/prompts/skills/vision/SKILL.md index 53abda73..a09b7c27 100644 --- a/prompts/skills/vision/SKILL.md +++ b/prompts/skills/vision/SKILL.md @@ -57,6 +57,20 @@ Initial setup, quarterly review, major pivot, new market. PRODUCT_VISION.md, docs/prd/PRD.md, docs/roadmap/ROADMAP.md, docs/drafts/feature-*.md +## Completion + +When all artifacts are generated, output: + +``` +@vision complete. Artifacts created: + PRODUCT_VISION.md + docs/prd/PRD.md + docs/roadmap/ROADMAP.md + docs/drafts/feature-*.md + +Next: @reality --quick or @feature "description" +``` + ## See Also - @idea — Feature-level requirements diff --git a/scripts/check-references.sh b/scripts/check-references.sh new file mode 100755 index 00000000..aa9348f5 --- /dev/null +++ b/scripts/check-references.sh @@ -0,0 +1,316 @@ +#!/bin/sh +# check-references.sh — Reference Integrity Gate for SDP +# +# Validates that all skill/command/agent references across the codebase +# resolve to actual files. Exit 1 on any broken reference. +# +# Checks: +# 1. Skills mentioned in CLAUDE.md exist in prompts/skills/ +# 2. Commands in .claude/commands.json map to existing skill files +# 3. Patterns in .claude/commands.json map to existing pattern files +# 4. Agents in .claude/commands.json map to existing agent files +# 5. Harness READMEs (.cursor/README.md, .codex/INSTALL.md, +# .opencode/README.md) reference existing skills +# 6. All symlinks resolve correctly +# +# Requirements: +# - GNU grep (for grep -oE extended regex). Ubuntu-latest CI ships GNU grep. +# - POSIX sh, find, sed, readlink. +# +# Usage: +# ./scripts/check-references.sh # from sdp/ root +# ./scripts/check-references.sh /path # explicit root + +# --- Resolve SDP root --- +if [ -n "$1" ]; then + SDP_ROOT="$1" +else + SDP_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +fi + +if [ ! -d "$SDP_ROOT" ]; then + echo "ERROR: SDP root does not exist: $SDP_ROOT" >&2 + exit 1 +fi + +if [ ! -d "$SDP_ROOT/prompts/skills" ]; then + echo "ERROR: $SDP_ROOT does not appear to be an SDP repo (missing prompts/skills/)" >&2 + exit 1 +fi + +ERRORS=0 +WARNINGS=0 + +# Temp file for symlink collection — cleaned up on EXIT/INT/TERM +_SYMLINKS_TMP="" +_cleanup() { + [ -n "$_SYMLINKS_TMP" ] && rm -f "$_SYMLINKS_TMP" 2>/dev/null +} +trap _cleanup EXIT INT TERM + +# --- Helpers --- +log_error() { + printf "ERROR: %s\n" "$1" >&2 + ERRORS=$((ERRORS + 1)) +} + +log_warn() { + printf "WARN: %s\n" "$1" >&2 + WARNINGS=$((WARNINGS + 1)) +} + +log_ok() { + printf " ok: %s\n" "$1" +} + +skill_file_exists() { + _name="$1" + [ -f "${SDP_ROOT}/prompts/skills/${_name}/SKILL.md" ] +} + +# --- Preamble --- +printf "%s\n" "=== SDP Reference Integrity Check ===" +printf "Root: %s\n\n" "$SDP_ROOT" + +# ============================================================ +# 1. Skills mentioned in CLAUDE.md +# ============================================================ +printf "%s\n" "--- Checking CLAUDE.md skill references ---" + +CLAUDE_MD="${SDP_ROOT}/CLAUDE.md" +if [ ! -f "$CLAUDE_MD" ]; then + log_warn "CLAUDE.md not found at ${CLAUDE_MD}" +else + # Extract @command names from the "Commands:" line + # Format: **Commands:** @vision @reality @feature @oneshot @build @review @deploy + COMMANDS_LINE=$(grep -E '^\*\*Commands:\*\*' "$CLAUDE_MD" 2>/dev/null || true) + + if [ -n "$COMMANDS_LINE" ]; then + # Parse @xxx tokens + for token in $COMMANDS_LINE; do + case "$token" in + @*) + skill_name="${token#@}" + # Strip trailing punctuation + skill_name=$(printf '%s' "$skill_name" | sed 's/[^a-zA-Z0-9_-]//g') + if [ -z "$skill_name" ]; then + continue + fi + if skill_file_exists "$skill_name"; then + log_ok "CLAUDE.md @${skill_name} -> prompts/skills/${skill_name}/SKILL.md" + else + log_error "CLAUDE.md references @${skill_name} but prompts/skills/${skill_name}/SKILL.md not found" + fi + ;; + esac + done + else + log_warn "No 'Commands:' line found in CLAUDE.md" + fi +fi + +# ============================================================ +# 2. Commands in .claude/commands.json -> skill files +# ============================================================ +printf "\n%s\n" "--- Checking .claude/commands.json command references ---" + +COMMANDS_JSON="${SDP_ROOT}/.claude/commands.json" +if [ ! -f "$COMMANDS_JSON" ]; then + log_warn ".claude/commands.json not found" +else + # NOTE: JSON parsing uses grep+sed for zero-dependency POSIX compat. + # Requires commands.json to stay pretty-printed (one value per line). + # If JSON is ever minified, add jq or python3 as a CI dependency. + # Extract "file" values from commands section + # Using grep+sed for POSIX compatibility (no jq dependency) + # Pattern: "file": "skills/xxx.md" + file_refs=$(grep '"file"' "$COMMANDS_JSON" | sed 's/.*"file"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + + for ref in $file_refs; do + case "$ref" in + skills/*) + skill_name=$(printf '%s' "$ref" | sed 's|skills/||; s|\.md$||') + if skill_file_exists "$skill_name"; then + log_ok "commands.json ${ref} -> prompts/skills/${skill_name}/SKILL.md" + else + log_error "commands.json references ${ref} but prompts/skills/${skill_name}/SKILL.md not found" + fi + ;; + *) + log_warn "commands.json: unexpected file reference format: ${ref}" + ;; + esac + done +fi + +# ============================================================ +# 3. Patterns in .claude/commands.json -> pattern files +# ============================================================ +printf "\n%s\n" "--- Checking .claude/commands.json pattern references ---" + +if [ -f "$COMMANDS_JSON" ]; then + # Extract pattern value lines: "key": "patterns/xxx.md" + pattern_refs=$(grep '"patterns/' "$COMMANDS_JSON" | sed 's/.*"\([^"]*patterns\/[^"]*\)".*/\1/') + + for ref in $pattern_refs; do + case "$ref" in + patterns/*) + pattern_path="${SDP_ROOT}/.claude/${ref}" + if [ -f "$pattern_path" ]; then + log_ok "commands.json ${ref} -> .claude/${ref}" + else + log_error "commands.json references ${ref} but .claude/${ref} not found" + fi + ;; + *) + log_warn "commands.json: unexpected pattern reference format: ${ref}" + ;; + esac + done +fi + +# ============================================================ +# 4. Agents in .claude/commands.json -> agent files +# ============================================================ +printf "\n%s\n" "--- Checking .claude/commands.json agent references ---" + +if [ -f "$COMMANDS_JSON" ]; then + # Extract agent value lines: "key": "agents/xxx.md" + agent_refs=$(grep '"agents/' "$COMMANDS_JSON" | sed 's/.*"\([^"]*agents\/[^"]*\)".*/\1/') + + for ref in $agent_refs; do + case "$ref" in + agents/*) + agent_path="${SDP_ROOT}/prompts/${ref}" + if [ -f "$agent_path" ]; then + log_ok "commands.json ${ref} -> prompts/${ref}" + else + log_error "commands.json references ${ref} but prompts/${ref} not found" + fi + ;; + *) + log_warn "commands.json: unexpected agent reference format: ${ref}" + ;; + esac + done +fi + +# ============================================================ +# 5. Harness READMEs reference existing skills +# ============================================================ +printf "\n%s\n" "--- Checking harness README skill references ---" + +# Known skill names — discovered dynamically from prompts/skills/*/SKILL.md +KNOWN_SKILLS="" +for _sk_dir in "${SDP_ROOT}"/prompts/skills/*/; do + [ -f "${_sk_dir}SKILL.md" ] && KNOWN_SKILLS="${KNOWN_SKILLS} $(basename "${_sk_dir}")" +done +KNOWN_SKILLS="${KNOWN_SKILLS# }" + +is_known_skill() { + _s="$1" + for k in $KNOWN_SKILLS; do + [ "$_s" = "$k" ] && return 0 + done + return 1 +} + +check_harness_readme() { + _file="$1" + _label="$2" + + if [ ! -f "$_file" ]; then + log_warn "${_label} not found at ${_file}" + return + fi + + # Extract @xxx references from the file. + # Match @skill in any context: start-of-line, after space, after [, after (, after `, after * + for token in $(grep -oE '(^|[][ ()`*])@[a-zA-Z0-9_-]+' "$_file" 2>/dev/null | sed 's/^[^@]*//' || true); do + skill_name="${token#@}" + if skill_file_exists "$skill_name"; then + log_ok "${_label} @${skill_name} -> prompts/skills/${skill_name}/SKILL.md" + else + log_error "${_label} references @${skill_name} but prompts/skills/${skill_name}/SKILL.md not found" + fi + done +} + +check_harness_readme "${SDP_ROOT}/.cursor/README.md" ".cursor/README.md" +check_harness_readme "${SDP_ROOT}/.codex/INSTALL.md" ".codex/INSTALL.md" +check_harness_readme "${SDP_ROOT}/.opencode/README.md" ".opencode/README.md" + +# Also check .codex/skills/README.md if it exists +check_harness_readme "${SDP_ROOT}/.codex/skills/README.md" ".codex/skills/README.md" + +# ============================================================ +# 6. All symlinks resolve correctly +# ============================================================ +printf "\n%s\n" "--- Checking symlink integrity ---" + +# Use a temp file to collect symlinks (avoids subshell variable scope issues) +_SYMLINKS_TMP="${TMPDIR:-/tmp}/sdp-check-refs-$$" +find "${SDP_ROOT}" -type l ! -path '*/.git/*' 2>/dev/null > "$_SYMLINKS_TMP" || true + +while read -r link; do + [ -z "$link" ] && continue + if [ ! -e "$link" ]; then + target=$(readlink "$link") + log_error "Broken symlink: ${link##${SDP_ROOT}/} -> ${target}" + else + target=$(readlink "$link") + log_ok "symlink: ${link##${SDP_ROOT}/} -> ${target}" + fi +done < "$_SYMLINKS_TMP" + +# ============================================================ +# 7. llm_subagents in commands.json — logical names, NOT file refs +# ============================================================ +printf "\n%s\n" "--- Checking .claude/commands.json llm_subagents references ---" + +if [ -f "$COMMANDS_JSON" ]; then + # llm_subagents are logical role names used at runtime by the LLM + # (e.g. "analyst", "product-manager", "quality-reviewer", "documentation"). + # They do NOT map to files in prompts/agents/ and are resolved by the + # orchestrator at spawn time. This is intentional — do not validate them + # as file paths. + llm_names=$(grep -oE '"llm_subagents"[[:space:]]*:[[:space:]]*\[[^]]*\]' "$COMMANDS_JSON" | grep -oE '"[a-z-]+"' | tr -d '"' | sort -u) + for name in $llm_names; do + printf " info: llm_subagent '%s' — logical name, not a file reference (intentional)\n" "$name" + done +fi + +# ============================================================ +# 8. Harness symlink directories resolve to prompts/skills +# ============================================================ +printf "\n%s\n" "--- Checking harness skill/agent symlinks ---" + +for harness in .cursor .codex .opencode .claude; do + for sub in skills agents; do + link_path="${SDP_ROOT}/${harness}/${sub}" + if [ -L "$link_path" ]; then + target=$(readlink "$link_path") + resolved="${SDP_ROOT}/${harness}/${target}" + if [ -d "$resolved" ]; then + log_ok "${harness}/${sub} -> ${target} (resolves)" + else + log_error "${harness}/${sub} -> ${target} (DOES NOT RESOLVE)" + fi + fi + done +done + +# ============================================================ +# Summary +# ============================================================ +printf "\n%s\n" "=== Summary ===" +printf "Errors: %d\n" "$ERRORS" +printf "Warnings: %d\n" "$WARNINGS" + +if [ "$ERRORS" -gt 0 ]; then + printf "\nFAIL: %d broken reference(s) found.\n" "$ERRORS" + exit 1 +fi + +printf "\nPASS: All references are intact.\n" +exit 0 diff --git a/scripts/install-project.sh b/scripts/install-project.sh index 35c89fbf..a144dd45 100755 --- a/scripts/install-project.sh +++ b/scripts/install-project.sh @@ -595,7 +595,19 @@ setup_opencode() { setup_codex() { safe_mkdir ../.codex/skills - sync_link "../../$SDP_DIR/prompts/skills" "../.codex/skills/sdp" + if [ -L ../.codex/skills/sdp ]; then + if [ "$SDP_PREVIEW" = "1" ]; then + SDP_PREVIEW_CHANGES="$SDP_PREVIEW_CHANGES + REMOVE legacy symlink: ../.codex/skills/sdp" + else + rm -f ../.codex/skills/sdp + fi + fi + # Individual skill symlinks (language-agnostic, per-skill granularity) + for _skill_dir in "$SDP_DIR"/prompts/skills/*/; do + _skill_name="$(basename "$_skill_dir")" + [ -f "${_skill_dir}SKILL.md" ] && sync_link "../../$SDP_DIR/prompts/skills/$_skill_name" "../.codex/skills/$_skill_name" + done sync_link "../$SDP_DIR/prompts/agents" "../.codex/agents" sync_file .codex/INSTALL.md ../.codex/INSTALL.md sync_file .codex/skills/README.md ../.codex/skills/README.md @@ -647,7 +659,7 @@ if [ -f ../.gitignore ]; then echo ".cursor/agents" >> ../.gitignore echo ".opencode/skills" >> ../.gitignore echo ".opencode/agents" >> ../.gitignore - echo ".codex/skills/sdp" >> ../.gitignore + echo ".codex/skills" >> ../.gitignore echo ".codex/agents" >> ../.gitignore echo ".prompts" >> ../.gitignore echo "# <<< SDP_END <<<" >> ../.gitignore diff --git a/scripts/test-install-project.sh b/scripts/test-install-project.sh index 47aff9d5..60be7f15 100644 --- a/scripts/test-install-project.sh +++ b/scripts/test-install-project.sh @@ -130,18 +130,20 @@ run_install "$CODEX_PROJECT_DIR" "$TMP_DIR/codex-install.log" env SDP_IDE=codex test -d "$CODEX_PROJECT_DIR/sdp/.git" test -f "$CODEX_PROJECT_DIR/.codex/INSTALL.md" test -f "$CODEX_PROJECT_DIR/.codex/skills/README.md" -test -L "$CODEX_PROJECT_DIR/.codex/skills/sdp" +test -L "$CODEX_PROJECT_DIR/.codex/skills/build" test -L "$CODEX_PROJECT_DIR/.codex/agents" test ! -e "$CODEX_PROJECT_DIR/.claude" -assert_contains ".codex/skills/sdp" "$CODEX_PROJECT_DIR/.gitignore" +assert_contains ".codex/skills" "$CODEX_PROJECT_DIR/.gitignore" assert_contains "Configured integrations:" "$TMP_DIR/codex-install.log" assert_contains "Codex (.codex/)" "$TMP_DIR/codex-install.log" printf '\n\n' >> "$ADMIN_DIR/prompts/skills/build/SKILL.md" git -C "$ADMIN_DIR" commit -am "test: update codex skill source" >/dev/null git -C "$ADMIN_DIR" push origin HEAD:refs/heads/main >/dev/null +ln -sfn ../../sdp/prompts/skills "$CODEX_PROJECT_DIR/.codex/skills/sdp" run_install "$CODEX_PROJECT_DIR" "$TMP_DIR/codex-update.log" env SDP_IDE=codex -assert_contains "codex update marker" "$CODEX_PROJECT_DIR/.codex/skills/sdp/build/SKILL.md" +test ! -e "$CODEX_PROJECT_DIR/.codex/skills/sdp" +assert_contains "codex update marker" "$CODEX_PROJECT_DIR/.codex/skills/build/SKILL.md" # Auto-detect fallback should explain that all integrations were installed. mkdir -p "$NO_IDE_BIN_DIR" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index d8876f8a..8386d7f8 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -169,6 +169,17 @@ else plan_remove_symlink .opencode/skills plan_remove_symlink .opencode/agents plan_remove_symlink .codex/skills/sdp + for link in .codex/skills/*; do + [ -e "$link" ] || continue + case "$link" in + .codex/skills/README.md|.codex/skills/sdp) + continue + ;; + esac + if [ -L "$link" ]; then + plan_remove_symlink "$link" + fi + done plan_remove_symlink .codex/agents plan_remove_file .claude/commands.json plan_remove_file .codex/INSTALL.md @@ -304,6 +315,18 @@ else echo " Removed: $link" fi done + for link in .codex/skills/*; do + [ -e "$link" ] || continue + case "$link" in + .codex/skills/README.md|.codex/skills/sdp) + continue + ;; + esac + if [ -L "$link" ]; then + rm -f "$link" + echo " Removed: $link" + fi + done for file in .claude/commands.json .codex/INSTALL.md .codex/skills/README.md; do if [ -f "$file" ]; then @@ -348,7 +371,7 @@ if [ -f .gitignore ] && grep -q "^# SDP" .gitignore; then for entry in "$SDP_DIR/.git" ".claude/skills" ".claude/agents" \ ".cursor/skills" ".cursor/agents" \ ".opencode/skills" ".opencode/agents" \ - ".codex/skills/sdp" ".codex/agents" ".prompts"; do + ".codex/skills" ".codex/skills/sdp" ".codex/agents" ".prompts"; do sed -i.bak "\|^${entry}\$|d" .gitignore 2>/dev/null || \ sed -i '' "\|^${entry}\$|d" .gitignore 2>/dev/null || true rm -f .gitignore.bak diff --git a/sdp-plugin/cmd/sdp/skill_paths.go b/sdp-plugin/cmd/sdp/skill_paths.go index cbba6582..ca849416 100644 --- a/sdp-plugin/cmd/sdp/skill_paths.go +++ b/sdp-plugin/cmd/sdp/skill_paths.go @@ -6,13 +6,32 @@ var defaultSkillsDirCandidates = []string{ ".claude/skills", ".cursor/skills", ".opencode/skills", + ".codex/skills", ".codex/skills/sdp", } +// hasSkillFiles checks whether a directory contains at least one +// subdirectory with a SKILL.md file (the per-skill layout). +func hasSkillFiles(dir string) bool { + entries, err := os.ReadDir(dir) + if err != nil { + return false + } + for _, e := range entries { + if !e.IsDir() { + continue + } + if _, err := os.Stat(dir + "/" + e.Name() + "/SKILL.md"); err == nil { + return true + } + } + return false +} + func resolveDefaultSkillsDir() string { for _, candidate := range defaultSkillsDirCandidates { info, err := os.Stat(candidate) - if err == nil && info.IsDir() { + if err == nil && info.IsDir() && hasSkillFiles(candidate) { return candidate } } diff --git a/sdp-plugin/cmd/sdp/skill_test.go b/sdp-plugin/cmd/sdp/skill_test.go index 5c0a1ecb..9a22318c 100644 --- a/sdp-plugin/cmd/sdp/skill_test.go +++ b/sdp-plugin/cmd/sdp/skill_test.go @@ -21,38 +21,67 @@ func TestResolveDefaultSkillsDir(t *testing.T) { { name: "detects cursor skills", setup: func(t *testing.T) { - if err := os.MkdirAll(".cursor/skills", 0o755); err != nil { + if err := os.MkdirAll(".cursor/skills/build", 0o755); err != nil { t.Fatalf("mkdir .cursor/skills: %v", err) } + if err := os.WriteFile(".cursor/skills/build/SKILL.md", []byte("# build"), 0o644); err != nil { + t.Fatalf("write SKILL.md: %v", err) + } }, expected: ".cursor/skills", }, { name: "detects opencode skills", setup: func(t *testing.T) { - if err := os.MkdirAll(".opencode/skills", 0o755); err != nil { + if err := os.MkdirAll(".opencode/skills/build", 0o755); err != nil { t.Fatalf("mkdir .opencode/skills: %v", err) } + if err := os.WriteFile(".opencode/skills/build/SKILL.md", []byte("# build"), 0o644); err != nil { + t.Fatalf("write SKILL.md: %v", err) + } }, expected: ".opencode/skills", }, { - name: "detects codex skills", + name: "detects codex skills (new per-skill layout)", + setup: func(t *testing.T) { + if err := os.MkdirAll(".codex/skills/build", 0o755); err != nil { + t.Fatalf("mkdir .codex/skills: %v", err) + } + if err := os.WriteFile(".codex/skills/build/SKILL.md", []byte("# build"), 0o644); err != nil { + t.Fatalf("write SKILL.md: %v", err) + } + }, + expected: ".codex/skills", + }, + { + name: "falls back to .codex/skills/sdp for old layout", setup: func(t *testing.T) { - if err := os.MkdirAll(".codex/skills/sdp", 0o755); err != nil { + // Old layout: .codex/skills/sdp//SKILL.md + // .codex/skills/ exists but has no SKILL.md subdirs (only sdp/) + if err := os.MkdirAll(".codex/skills/sdp/build", 0o755); err != nil { t.Fatalf("mkdir .codex/skills/sdp: %v", err) } + if err := os.WriteFile(".codex/skills/sdp/build/SKILL.md", []byte("# build"), 0o644); err != nil { + t.Fatalf("write SKILL.md: %v", err) + } }, expected: ".codex/skills/sdp", }, { name: "uses stable priority when multiple exist", setup: func(t *testing.T) { - if err := os.MkdirAll(".claude/skills", 0o755); err != nil { + if err := os.MkdirAll(".claude/skills/build", 0o755); err != nil { t.Fatalf("mkdir .claude/skills: %v", err) } - if err := os.MkdirAll(".codex/skills/sdp", 0o755); err != nil { - t.Fatalf("mkdir .codex/skills/sdp: %v", err) + if err := os.WriteFile(".claude/skills/build/SKILL.md", []byte("# build"), 0o644); err != nil { + t.Fatalf("write SKILL.md: %v", err) + } + if err := os.MkdirAll(".codex/skills/build", 0o755); err != nil { + t.Fatalf("mkdir .codex/skills: %v", err) + } + if err := os.WriteFile(".codex/skills/build/SKILL.md", []byte("# build"), 0o644); err != nil { + t.Fatalf("write SKILL.md: %v", err) } }, expected: ".claude/skills",