From 353fda58970cd343257c245e56c67868e7d74d9f Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 16:45:01 +0300 Subject: [PATCH 01/14] Clarify onboarding paths and IDE support --- README.md | 12 ++++++++++-- docs/QUICKSTART.md | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d26cf6f4..79cc748e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Protocol + evidence layer for AI agent workflows.** -SDP gives your AI agents a structured process (Discovery → Delivery → Evidence) and produces proof of what they actually did. Works with Claude Code, Cursor, OpenCode, or anything that can read markdown. +SDP gives your AI agents a structured process (Discovery → Delivery → Evidence) and produces proof of what they actually did. Today the smoothest setup path is for `Claude Code`, `Cursor`, and `OpenCode` / `Windsurf`. `Codex` compatibility exists, but the setup path is still more manual. > [Manifesto](docs/MANIFESTO.md) — what exists, what's coming, why evidence matters. @@ -19,14 +19,21 @@ curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | s git submodule add https://github.com/fall-out-bug/sdp.git sdp ``` -Skills load from `sdp/.claude/skills/` (Claude) or `sdp/.cursor/skills/` (Cursor). +Installer auto-detects `Claude Code`, `Cursor`, and `OpenCode` / `Windsurf`. + +`Codex` users should use the manual setup note in [`.codex/INSTALL.md`](.codex/INSTALL.md). + +Skills load from `sdp/.claude/skills/` (Claude), `sdp/.cursor/skills/` (Cursor), or `sdp/.opencode/skills/` (OpenCode). If you embed SDP as a submodule inside another repo, use the public GitHub URL above as the source of truth. Do not point `.gitmodules` at a local sibling path such as `../sdp`, or teammates and CI will drift onto commits nobody else can fetch. +SDP installs prompts, hooks, and optional CLI helpers. You still bring your own model access and provider keys through your IDE or agent runtime. + **First run:** ```bash sdp init --auto +sdp doctor @feature "Your feature" @oneshot @review @@ -82,6 +89,7 @@ sdp init --auto | File | Content | |------|---------| | [QUICKSTART.md](docs/QUICKSTART.md) | 5-minute getting started | +| [.codex/INSTALL.md](.codex/INSTALL.md) | Manual Codex setup | | [MANIFESTO.md](docs/MANIFESTO.md) | Vision, evidence, what exists | | [ROADMAP.md](docs/ROADMAP.md) | Where SDP is going | | [PROTOCOL.md](docs/PROTOCOL.md) | Full specification | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 49780f08..19aba58b 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -2,6 +2,15 @@ Get from zero to your first feature in 5 minutes. +Use this doc when your goal is to adopt SDP in your own repo, not to work on `sdp_lab`. + +## 0. Choose Your Starting Point + +- **Greenfield:** new repo or empty service. Install SDP, run `sdp init --auto`, then start the feature flow. +- **Brownfield:** existing codebase. Install SDP, prefer `sdp init --guided` so you can inspect defaults, then run `sdp doctor` before trusting the flow. + +SDP installs prompts, hooks, and optional CLI helpers. You still configure your model provider and API keys in your IDE or agent runtime. + ## 1. Install **Full project** (prompts + hooks + optional CLI): default install @@ -25,6 +34,14 @@ git submodule add https://github.com/fall-out-bug/sdp.git sdp Use the GitHub URL as the canonical submodule source. A local relative URL like `../sdp` is only a private convenience clone and will break reproducibility for other machines and CI. +Auto-setup today is first-class for: + +- `Claude Code` +- `Cursor` +- `OpenCode` / `Windsurf` + +`Codex` compatibility exists, but setup is still manual today. Use [../.codex/INSTALL.md](../.codex/INSTALL.md). + Skills load from `sdp/.claude/skills/`, `sdp/.cursor/skills/`, or `sdp/.opencode/`. ## 2. Initialize @@ -39,6 +56,12 @@ Creates `.sdp/config.yml`, guard rules, and IDE integration. *If you get "unknown flag: --auto", upgrade the CLI: `curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only`* +Then verify the environment: + +```bash +sdp doctor +``` + ## 3. Create a Feature **Discovery (planning):** From bec585d229b77f238d0aada0750830008dd1d874 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 17:21:40 +0300 Subject: [PATCH 02/14] Harden installer update paths --- .github/workflows/go-ci.yml | 6 ++ scripts/install-project.sh | 129 +++++++++++++++++++++----------- scripts/test-install-project.sh | 107 ++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 43 deletions(-) create mode 100644 scripts/test-install-project.sh diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index a32f2cb9..ad7f2679 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -90,6 +90,12 @@ jobs: set -euo pipefail ../scripts/test-root-installer.sh + - name: Project installer regression check + shell: bash + run: | + set -euo pipefail + ../scripts/test-install-project.sh + - name: Check for contracts id: check_contracts run: | diff --git a/scripts/install-project.sh b/scripts/install-project.sh index 8769da96..e1f3dae2 100755 --- a/scripts/install-project.sh +++ b/scripts/install-project.sh @@ -4,7 +4,7 @@ # Installs prompts/hooks config into the current project. # This script does NOT require installing the SDP CLI binary. -set -e +set -eu SDP_DIR="${SDP_DIR:-sdp}" SDP_IDE="${SDP_IDE:-auto}" @@ -54,10 +54,79 @@ detect_auto_ide() { echo "$detected" } +sync_file() { + src="$1" + dest="$2" + + if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "$dest" ]; then + return + fi + + mkdir -p "$(dirname "$dest")" + cp "$src" "$dest" +} + +sync_tree_files() { + src_dir="$1" + dest_dir="$2" + + if [ ! -d "$src_dir" ]; then + return + fi + + find "$src_dir" -type f | while IFS= read -r src; do + rel="${src#$src_dir/}" + sync_file "$src" "$dest_dir/$rel" + done +} + +sync_link() { + target="$1" + dest="$2" + + if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "$dest" ]; then + return + fi + + mkdir -p "$(dirname "$dest")" + ln -sfn "$target" "$dest" +} + +ensure_managed_checkout() { + if ! git -C "$SDP_DIR" rev-parse --git-dir >/dev/null 2>&1; then + echo "ERROR: $SDP_DIR exists but is not a git checkout." >&2 + echo "Move or remove it, then rerun the installer." >&2 + exit 1 + fi +} + +ensure_clean_checkout() { + if [ -n "$(git -C "$SDP_DIR" status --porcelain)" ]; then + echo "ERROR: $SDP_DIR has local changes." >&2 + echo "Commit, stash, or remove them before rerunning the installer." >&2 + git -C "$SDP_DIR" status --short >&2 || true + exit 1 + fi +} + +update_existing_checkout() { + echo "⚠️ $SDP_DIR already exists. Updating..." + ensure_managed_checkout + ensure_clean_checkout + + if git -C "$SDP_DIR" remote get-url origin >/dev/null 2>&1; then + git -C "$SDP_DIR" remote set-url origin "$REMOTE" + else + git -C "$SDP_DIR" remote add origin "$REMOTE" + fi + + git -C "$SDP_DIR" fetch --depth 1 origin "$SDP_REF" + git -C "$SDP_DIR" checkout -B "$SDP_REF" FETCH_HEAD >/dev/null 2>&1 +} + # Check if already installed if [ -d "$SDP_DIR" ]; then - echo "⚠️ $SDP_DIR already exists. Updating..." - git -C "$SDP_DIR" fetch origin "$SDP_REF" && git -C "$SDP_DIR" checkout "$SDP_REF" 2>/dev/null || git -C "$SDP_DIR" pull origin main + update_existing_checkout else echo "📦 Cloning SDP (ref: $SDP_REF)..." git clone --depth 1 -b "$SDP_REF" "$REMOTE" "$SDP_DIR" 2>/dev/null || git clone --depth 1 "$REMOTE" "$SDP_DIR" @@ -124,56 +193,28 @@ fi setup_claude() { mkdir -p ../.claude - if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "../.claude/skills" ]; then - : - else - ln -sf "../$SDP_DIR/prompts/skills" "../.claude/skills" 2>/dev/null || true - fi - if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "../.claude/agents" ]; then - : - else - ln -sf "../$SDP_DIR/prompts/agents" "../.claude/agents" 2>/dev/null || true - fi - cp -n .claude/commands.json ../.claude/ 2>/dev/null || true - cp -rn .claude/hooks ../.claude/ 2>/dev/null || true - cp -rn .claude/patterns ../.claude/ 2>/dev/null || true - cp -n .claude/settings.json ../.claude/ 2>/dev/null || true + sync_link "../$SDP_DIR/prompts/skills" "../.claude/skills" + sync_link "../$SDP_DIR/prompts/agents" "../.claude/agents" + sync_file .claude/commands.json ../.claude/commands.json + sync_tree_files .claude/hooks ../.claude/hooks + sync_tree_files .claude/patterns ../.claude/patterns + sync_file .claude/settings.json ../.claude/settings.json } setup_cursor() { mkdir -p ../.cursor - if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "../.cursor/skills" ]; then - : - else - ln -sf "../$SDP_DIR/prompts/skills" "../.cursor/skills" 2>/dev/null || true - fi - if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "../.cursor/agents" ]; then - : - else - ln -sf "../$SDP_DIR/prompts/agents" "../.cursor/agents" 2>/dev/null || true - fi + sync_link "../$SDP_DIR/prompts/skills" "../.cursor/skills" + sync_link "../$SDP_DIR/prompts/agents" "../.cursor/agents" mkdir -p ../.cursor/commands - for cmd in .cursor/commands/*.md; do - [ -f "$cmd" ] && cp -n "$cmd" ../.cursor/commands/ 2>/dev/null || true - done + sync_tree_files .cursor/commands ../.cursor/commands } setup_opencode() { mkdir -p ../.opencode - if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "../.opencode/skills" ]; then - : - else - ln -sf "../$SDP_DIR/prompts/skills" "../.opencode/skills" 2>/dev/null || true - fi - if [ "$SDP_PRESERVE_CONFIG" = "1" ] && [ -e "../.opencode/agents" ]; then - : - else - ln -sf "../$SDP_DIR/prompts/agents" "../.opencode/agents" 2>/dev/null || true - fi + sync_link "../$SDP_DIR/prompts/skills" "../.opencode/skills" + sync_link "../$SDP_DIR/prompts/agents" "../.opencode/agents" mkdir -p ../.opencode/commands - for cmd in .opencode/commands/*.md; do - [ -f "$cmd" ] && cp -n "$cmd" ../.opencode/commands/ 2>/dev/null || true - done + sync_tree_files .opencode/commands ../.opencode/commands } for ide in $SDP_IDE_LIST; do @@ -208,6 +249,8 @@ if [ -f ../.gitignore ]; then echo ".claude/agents" >> ../.gitignore echo ".cursor/skills" >> ../.gitignore echo ".cursor/agents" >> ../.gitignore + echo ".opencode/skills" >> ../.gitignore + echo ".opencode/agents" >> ../.gitignore echo ".prompts" >> ../.gitignore echo "✅ Added entries to .gitignore" fi diff --git a/scripts/test-install-project.sh b/scripts/test-install-project.sh new file mode 100644 index 00000000..02a16415 --- /dev/null +++ b/scripts/test-install-project.sh @@ -0,0 +1,107 @@ +#!/bin/sh +# Regression checks for install-project.sh end-to-end behavior. + +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +TMP_DIR=$(mktemp -d) +trap 'chmod -R u+w "$TMP_DIR" 2>/dev/null || true; rm -rf "$TMP_DIR"' EXIT + +SOURCE_BARE="$TMP_DIR/source.git" +ADMIN_DIR="$TMP_DIR/admin" +HOME_DIR="$TMP_DIR/home" +PROJECT_DIR="$TMP_DIR/project" +FULL_PROJECT_DIR="$TMP_DIR/project-full" + +mkdir -p "$HOME_DIR" + +git clone --bare "$ROOT_DIR" "$SOURCE_BARE" >/dev/null +git clone "$SOURCE_BARE" "$ADMIN_DIR" >/dev/null +git -C "$ADMIN_DIR" config user.name "SDP Installer Test" +git -C "$ADMIN_DIR" config user.email "installer-test@example.com" + +run_install() { + project_dir="$1" + log_file="$2" + shift 2 + + mkdir -p "$project_dir" + if [ ! -d "$project_dir/.git" ]; then + git -C "$project_dir" init -q + fi + + ( + cd "$project_dir" + HOME="$HOME_DIR" \ + SDP_REMOTE="$SOURCE_BARE" \ + SDP_IDE=claude \ + "$@" \ + sh "$ROOT_DIR/scripts/install-project.sh" + ) >"$log_file" 2>&1 +} + +hash_file() { + file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + else + shasum -a 256 "$file" | awk '{print $1}' + fi +} + +assert_contains() { + needle="$1" + file="$2" + if ! grep -Fq "$needle" "$file"; then + echo "expected '$needle' in $file" >&2 + exit 1 + fi +} + +update_admin_file() { + file="$1" + from="$2" + to="$3" + message="$4" + + perl -0pi -e "s/\Q$from\E/$to/" "$ADMIN_DIR/$file" + git -C "$ADMIN_DIR" commit -am "$message" >/dev/null + git -C "$ADMIN_DIR" push origin HEAD:main >/dev/null +} + +# Cold start / clean install +run_install "$PROJECT_DIR" "$TMP_DIR/clean-install.log" env +test -d "$PROJECT_DIR/sdp/.git" +test -L "$PROJECT_DIR/.claude/skills" +test -f "$PROJECT_DIR/.claude/commands.json" +assert_contains '"version": "1.1.0"' "$PROJECT_DIR/.claude/commands.json" + +# Clean reinstall / update should refresh vendored checkout and managed files. +update_admin_file ".claude/commands.json" '"version": "1.1.0"' '"version": "9.9.9"' "test: update commands manifest" +run_install "$PROJECT_DIR" "$TMP_DIR/update-install.log" env +assert_contains '"version": "9.9.9"' "$PROJECT_DIR/.claude/commands.json" +assert_contains '"version": "9.9.9"' "$PROJECT_DIR/sdp/.claude/commands.json" + +# Dirty reinstall should fail clearly before git noise if managed checkout changed. +perl -0pi -e 's/"version": "9\.9\.9"/"version": "LOCAL-DIRTY"/' "$PROJECT_DIR/sdp/.claude/commands.json" +update_admin_file ".claude/commands.json" '"version": "9.9.9"' '"version": "10.0.0"' "test: update commands manifest again" +if run_install "$PROJECT_DIR" "$TMP_DIR/dirty-install.log" env; then + echo "dirty installer rerun unexpectedly succeeded" >&2 + exit 1 +fi +assert_contains "ERROR: sdp has local changes." "$TMP_DIR/dirty-install.log" +assert_contains ".claude/commands.json" "$TMP_DIR/dirty-install.log" + +# Full install path should build/update CLI when requested. +run_install "$FULL_PROJECT_DIR" "$TMP_DIR/full-install.log" env SDP_INSTALL_CLI=1 +test -x "$HOME_DIR/.local/bin/sdp" +before_hash=$(hash_file "$HOME_DIR/.local/bin/sdp") +update_admin_file "sdp-plugin/cmd/sdp/main.go" 'var version = "dev"' 'var version = "dev-full-install-update"' "test: update cli source" +run_install "$FULL_PROJECT_DIR" "$TMP_DIR/full-update.log" env SDP_INSTALL_CLI=1 +after_hash=$(hash_file "$HOME_DIR/.local/bin/sdp") +if [ "$before_hash" = "$after_hash" ]; then + echo "full installer did not refresh CLI binary" >&2 + exit 1 +fi + +echo "install-project regression checks passed" From c2743f195c72e4bd91c2dfac7ce4b49fb2b215f3 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 17:23:23 +0300 Subject: [PATCH 03/14] Fix installer regression workflow entrypoint --- .github/workflows/go-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index ad7f2679..518fec12 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -94,7 +94,7 @@ jobs: shell: bash run: | set -euo pipefail - ../scripts/test-install-project.sh + sh ../scripts/test-install-project.sh - name: Check for contracts id: check_contracts From 26dff219521d61d3fed2de0460062cf9521d0c0f Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 17:29:15 +0300 Subject: [PATCH 04/14] Handle detached CI checkouts in installer tests --- scripts/test-install-project.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/test-install-project.sh b/scripts/test-install-project.sh index 02a16415..88d332df 100644 --- a/scripts/test-install-project.sh +++ b/scripts/test-install-project.sh @@ -19,6 +19,8 @@ git clone --bare "$ROOT_DIR" "$SOURCE_BARE" >/dev/null git clone "$SOURCE_BARE" "$ADMIN_DIR" >/dev/null git -C "$ADMIN_DIR" config user.name "SDP Installer Test" git -C "$ADMIN_DIR" config user.email "installer-test@example.com" +git -C "$ADMIN_DIR" checkout -B main >/dev/null +git -C "$ADMIN_DIR" push origin HEAD:refs/heads/main >/dev/null run_install() { project_dir="$1" @@ -66,7 +68,7 @@ update_admin_file() { perl -0pi -e "s/\Q$from\E/$to/" "$ADMIN_DIR/$file" git -C "$ADMIN_DIR" commit -am "$message" >/dev/null - git -C "$ADMIN_DIR" push origin HEAD:main >/dev/null + git -C "$ADMIN_DIR" push origin HEAD:refs/heads/main >/dev/null } # Cold start / clean install From 3b269086d2a47850a66bfd72be1c77498f1d364b Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 20:11:34 +0300 Subject: [PATCH 05/14] Fix init auto first-run scaffolding --- sdp-plugin/cmd/sdp/init.go | 4 ++ sdp-plugin/cmd/sdp/init_test.go | 17 +++++ sdp-plugin/cmd/sdp/main.go | 17 ++++- sdp-plugin/cmd/sdp/main_test.go | 63 +++++++++++++++++ sdp-plugin/internal/sdpinit/headless.go | 4 ++ sdp-plugin/internal/sdpinit/headless_test.go | 16 ++++- sdp-plugin/internal/sdpinit/init.go | 66 +++++++++++++++++- sdp-plugin/internal/sdpinit/init_test.go | 71 ++++++++++++++++++++ 8 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 sdp-plugin/cmd/sdp/main_test.go diff --git a/sdp-plugin/cmd/sdp/init.go b/sdp-plugin/cmd/sdp/init.go index a8b61b10..97548d7b 100644 --- a/sdp-plugin/cmd/sdp/init.go +++ b/sdp-plugin/cmd/sdp/init.go @@ -176,6 +176,10 @@ func runAutoInit(cfg sdpinit.Config, preflight *sdpinit.PreflightResult) error { if cfg.DryRun { fmt.Println("\n[DRY RUN] Would create:") + fmt.Println(" .sdp/") + fmt.Println(" .sdp/config.yml") + fmt.Println(" .sdp/guard-rules.yml") + fmt.Println(" .sdp/log/") fmt.Println(" .claude/") fmt.Println(" .claude/skills/") fmt.Println(" .claude/agents/") diff --git a/sdp-plugin/cmd/sdp/init_test.go b/sdp-plugin/cmd/sdp/init_test.go index fa0819ac..d1a1f461 100644 --- a/sdp-plugin/cmd/sdp/init_test.go +++ b/sdp-plugin/cmd/sdp/init_test.go @@ -109,6 +109,9 @@ func TestInitCmd(t *testing.T) { if _, err := os.Stat(".claude"); os.IsNotExist(err) { t.Error("initCmd() did not create .claude directory") } + if _, err := os.Stat(".sdp/config.yml"); os.IsNotExist(err) { + t.Error("initCmd() did not create .sdp/config.yml") + } } // TestInitCmdWithSkipBeads tests init with skip-beads flag @@ -155,6 +158,9 @@ func TestInitCmdWithSkipBeads(t *testing.T) { if _, err := os.Stat(".claude"); os.IsNotExist(err) { t.Error("initCmd() did not create .claude directory") } + if _, err := os.Stat(".sdp/guard-rules.yml"); os.IsNotExist(err) { + t.Error("initCmd() did not create .sdp/guard-rules.yml") + } } // TestInitCmdWithAuto tests init with --auto flag @@ -188,6 +194,9 @@ func TestInitCmdWithAuto(t *testing.T) { if _, err := os.Stat(".claude"); os.IsNotExist(err) { t.Error("initCmd() did not create .claude directory") } + if _, err := os.Stat(".sdp/config.yml"); os.IsNotExist(err) { + t.Error("initCmd() did not create .sdp/config.yml") + } } // TestInitCmdWithHeadless tests init with --headless flag @@ -368,6 +377,14 @@ func TestInitCmdWithNoEvidence(t *testing.T) { if !strings.Contains(string(content), `"enabled": false`) { t.Error("Settings should have evidence disabled") } + + configContent, err := os.ReadFile(".sdp/config.yml") + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + if !strings.Contains(string(configContent), "enabled: false") { + t.Error("Config should have evidence disabled") + } } // TestInitCmdWithSkills tests init with --skills flag diff --git a/sdp-plugin/cmd/sdp/main.go b/sdp-plugin/cmd/sdp/main.go index 201320fa..0a782f23 100644 --- a/sdp-plugin/cmd/sdp/main.go +++ b/sdp-plugin/cmd/sdp/main.go @@ -14,6 +14,21 @@ var version = "dev" var consentAsked = false // Track if we've asked for consent this session +func shouldAskForTelemetryConsent(cmd *cobra.Command) bool { + if cmd == nil { + return false + } + + for _, flagName := range []string{"auto", "headless"} { + flag := cmd.Flags().Lookup(flagName) + if flag != nil && flag.Value.String() == "true" { + return false + } + } + + return true +} + func main() { var noColor bool @@ -67,7 +82,7 @@ is provided by the Claude Plugin prompts in .claude/.`, ui.NoColor = noColor // Check for first-run consent (only once per session) - if !consentAsked && cmd.Name() != "telemetry" { + if !consentAsked && cmd.Name() != "telemetry" && shouldAskForTelemetryConsent(cmd) { configDir, err := os.UserConfigDir() if err == nil { configPath := filepath.Join(configDir, "sdp", "telemetry.json") diff --git a/sdp-plugin/cmd/sdp/main_test.go b/sdp-plugin/cmd/sdp/main_test.go new file mode 100644 index 00000000..73602b9a --- /dev/null +++ b/sdp-plugin/cmd/sdp/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestShouldAskForTelemetryConsent(t *testing.T) { + tests := []struct { + name string + auto bool + headless bool + wantPrompt bool + }{ + { + name: "interactive command prompts", + wantPrompt: true, + }, + { + name: "auto init skips prompt", + auto: true, + wantPrompt: false, + }, + { + name: "headless init skips prompt", + headless: true, + wantPrompt: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{Use: "init"} + cmd.Flags().Bool("auto", false, "") + cmd.Flags().Bool("headless", false, "") + + if err := cmd.Flags().Set("auto", boolString(tt.auto)); err != nil { + t.Fatalf("set auto flag: %v", err) + } + if err := cmd.Flags().Set("headless", boolString(tt.headless)); err != nil { + t.Fatalf("set headless flag: %v", err) + } + + if got := shouldAskForTelemetryConsent(cmd); got != tt.wantPrompt { + t.Fatalf("shouldAskForTelemetryConsent() = %t, want %t", got, tt.wantPrompt) + } + }) + } +} + +func TestShouldAskForTelemetryConsentNilCommand(t *testing.T) { + if shouldAskForTelemetryConsent(nil) { + t.Fatal("nil command should not request telemetry consent") + } +} + +func boolString(v bool) string { + if v { + return "true" + } + return "false" +} diff --git a/sdp-plugin/internal/sdpinit/headless.go b/sdp-plugin/internal/sdpinit/headless.go index 38216323..e71a2056 100644 --- a/sdp-plugin/internal/sdpinit/headless.go +++ b/sdp-plugin/internal/sdpinit/headless.go @@ -122,6 +122,10 @@ func (h *HeadlessRunner) validate() error { func (h *HeadlessRunner) trackCreatedFiles() { h.output.Created = []string{ + ".sdp/", + ".sdp/config.yml", + ".sdp/guard-rules.yml", + ".sdp/log/", ".claude/", ".claude/skills/", ".claude/agents/", diff --git a/sdp-plugin/internal/sdpinit/headless_test.go b/sdp-plugin/internal/sdpinit/headless_test.go index c53b475c..c4d4456e 100644 --- a/sdp-plugin/internal/sdpinit/headless_test.go +++ b/sdp-plugin/internal/sdpinit/headless_test.go @@ -101,7 +101,14 @@ func TestHeadlessRunner_TrackCreatedFiles(t *testing.T) { } // Check expected files - expectedFiles := []string{".claude/", ".claude/settings.json"} + expectedFiles := []string{ + ".sdp/", + ".sdp/config.yml", + ".sdp/guard-rules.yml", + ".sdp/log/", + ".claude/", + ".claude/settings.json", + } for _, expected := range expectedFiles { if !slices.Contains(runner.output.Created, expected) { t.Errorf("Expected created file: %s", expected) @@ -181,6 +188,9 @@ func TestHeadlessRunner_Run_Actual(t *testing.T) { if _, err := os.Stat(".claude"); os.IsNotExist(err) { t.Error("Run should create .claude directory") } + if _, err := os.Stat(".sdp/config.yml"); os.IsNotExist(err) { + t.Error("Run should create .sdp/config.yml") + } } func TestHeadlessOutput_GetExitCode(t *testing.T) { @@ -227,7 +237,7 @@ func TestHeadlessOutput_OutputJSON(t *testing.T) { output := &HeadlessOutput{ Success: true, ProjectType: "go", - Created: []string{".claude/", ".claude/settings.json"}, + Created: []string{".sdp/config.yml", ".claude/", ".claude/settings.json"}, Config: &ConfigSummary{ Skills: []string{"feature", "build"}, EvidenceEnabled: true, @@ -247,7 +257,7 @@ func TestHeadlessOutput_JSONMarshaling(t *testing.T) { output := &HeadlessOutput{ Success: true, ProjectType: "go", - Created: []string{".claude/"}, + Created: []string{".sdp/", ".claude/"}, Preflight: &PreflightResult{ ProjectType: "go", HasGit: true, diff --git a/sdp-plugin/internal/sdpinit/init.go b/sdp-plugin/internal/sdpinit/init.go index 315fbf05..4141eacd 100644 --- a/sdp-plugin/internal/sdpinit/init.go +++ b/sdp-plugin/internal/sdpinit/init.go @@ -6,6 +6,9 @@ import ( "os" "path/filepath" "strings" + + projectconfig "github.com/fall-out-bug/sdp/internal/config" + "gopkg.in/yaml.v3" ) // Config holds initialization configuration options. @@ -59,13 +62,34 @@ func Run(cfg Config) error { return fmt.Errorf("create settings: %w", err) } + if err := createProjectFiles(cfg); err != nil { + return fmt.Errorf("create project files: %w", err) + } + // In headless mode, don't print text output if !cfg.Headless { - fmt.Println("✓ SDP initialized in .claude/") + fmt.Println("✓ SDP initialized in .claude/ and .sdp/") fmt.Printf(" Project type: %s\n", cfg.ProjectType) fmt.Println("\nNext steps:") - fmt.Println(" 1. Review .claude/settings.json") - fmt.Println(" 2. Start using Claude Code with SDP prompts") + fmt.Println(" 1. Review .sdp/config.yml") + fmt.Println(" 2. Review .claude/settings.json") + fmt.Println(" 3. Start using SDP prompts in your IDE") + } + + return nil +} + +func createProjectFiles(cfg Config) error { + if err := os.MkdirAll(filepath.Join(".sdp", "log"), 0o755); err != nil { + return fmt.Errorf("create .sdp/log: %w", err) + } + + if err := createProjectConfig(cfg); err != nil { + return fmt.Errorf("create config.yml: %w", err) + } + + if err := createGuardRules(cfg); err != nil { + return fmt.Errorf("create guard-rules.yml: %w", err) } return nil @@ -247,6 +271,42 @@ func createSettings(claudeDir string, cfg Config) error { ) } +func createProjectConfig(cfg Config) error { + defaults := MergeDefaults(cfg.ProjectType, &cfg) + projectCfg := projectconfig.DefaultConfig() + projectCfg.Evidence.Enabled = defaults.EvidenceEnabled + projectCfg.Acceptance.Command = defaults.TestCommand + + content, err := yaml.Marshal(projectCfg) + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + + return writeManagedProjectFile(filepath.Join(".sdp", "config.yml"), content, 0o644, cfg.Force) +} + +func createGuardRules(cfg Config) error { + rules := projectconfig.DefaultGuardRules() + content, err := yaml.Marshal(rules) + if err != nil { + return fmt.Errorf("marshal guard rules: %w", err) + } + + return writeManagedProjectFile(filepath.Join(".sdp", "guard-rules.yml"), content, 0o644, cfg.Force) +} + +func writeManagedProjectFile(path string, content []byte, perm os.FileMode, force bool) error { + if !force { + if _, err := os.Stat(path); err == nil { + return nil + } else if !os.IsNotExist(err) { + return err + } + } + + return os.WriteFile(path, content, perm) +} + // formatStringsAsJSON formats a string slice as a JSON array. func formatStringsAsJSON(items []string) string { if len(items) == 0 { diff --git a/sdp-plugin/internal/sdpinit/init_test.go b/sdp-plugin/internal/sdpinit/init_test.go index bdd202a4..8ad04de6 100644 --- a/sdp-plugin/internal/sdpinit/init_test.go +++ b/sdp-plugin/internal/sdpinit/init_test.go @@ -6,6 +6,8 @@ import ( "runtime" "strings" "testing" + + projectconfig "github.com/fall-out-bug/sdp/internal/config" ) func TestRun(t *testing.T) { @@ -79,6 +81,21 @@ func TestRun(t *testing.T) { t.Fatal("settings.json was not created") } + configPath := filepath.Join(".sdp", "config.yml") + if _, err := os.Stat(configPath); os.IsNotExist(err) { + t.Fatal(".sdp/config.yml was not created") + } + + guardRulesPath := filepath.Join(".sdp", "guard-rules.yml") + if _, err := os.Stat(guardRulesPath); os.IsNotExist(err) { + t.Fatal(".sdp/guard-rules.yml was not created") + } + + logDirPath := filepath.Join(".sdp", "log") + if info, err := os.Stat(logDirPath); err != nil || !info.IsDir() { + t.Fatalf(".sdp/log was not created as a directory: %v", err) + } + // Check settings.json content content, err := os.ReadFile(settingsPath) if err != nil { @@ -94,6 +111,28 @@ func TestRun(t *testing.T) { t.Errorf("settings.json missing skills: %s", settingsStr) } + projectCfg, err := projectconfig.Load(tmpDir) + if err != nil { + t.Fatalf("Load project config: %v", err) + } + if projectCfg.Acceptance.Command != "go test ./..." { + t.Errorf("Acceptance.Command = %q, want %q", projectCfg.Acceptance.Command, "go test ./...") + } + if !projectCfg.Evidence.Enabled { + t.Error("project config should enable evidence") + } + + guardRules, err := projectconfig.LoadGuardRules(tmpDir) + if err != nil { + t.Fatalf("Load guard rules: %v", err) + } + if len(guardRules.Rules) == 0 { + t.Fatal("guard rules should include defaults") + } + if guardRules.Rules[0].ID != "max-file-loc" { + t.Errorf("first guard rule = %q, want %q", guardRules.Rules[0].ID, "max-file-loc") + } + // Check file permissions (0600) info, err := os.Stat(settingsPath) if err != nil { @@ -350,3 +389,35 @@ func TestCreateSettings(t *testing.T) { t.Errorf("Wrong permissions: got %o, want 0600", perm) } } + +func TestRun_NoEvidenceCreatesDisabledProjectConfig(t *testing.T) { + tmpDir := t.TempDir() + promptsDir := filepath.Join(tmpDir, "prompts") + if err := os.MkdirAll(filepath.Join(promptsDir, "skills"), 0o755); err != nil { + t.Fatalf("mkdir skills: %v", err) + } + if err := os.WriteFile(filepath.Join(promptsDir, "skills", "test.md"), []byte("# Test"), 0o644); err != nil { + t.Fatalf("write skill: %v", err) + } + + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir: %v", err) + } + + if err := Run(Config{ProjectType: "python", NoEvidence: true}); err != nil { + t.Fatalf("Run() failed: %v", err) + } + + projectCfg, err := projectconfig.Load(tmpDir) + if err != nil { + t.Fatalf("Load project config: %v", err) + } + if projectCfg.Acceptance.Command != "pytest" { + t.Errorf("Acceptance.Command = %q, want %q", projectCfg.Acceptance.Command, "pytest") + } + if projectCfg.Evidence.Enabled { + t.Error("project config should disable evidence") + } +} From 31a87b7ae3d3d64ce67ef8bfd140d1d00dd0ad76 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 21:21:52 +0300 Subject: [PATCH 06/14] Make Codex onboarding first-class --- .codex/INSTALL.md | 24 +++- .codex/skills/sdp | 1 + README.md | 11 +- docs/QUICKSTART.md | 9 +- scripts/install-project.sh | 19 ++- scripts/test-install-project.sh | 19 +++ sdp-plugin/cmd/sdp/doctor.go | 4 +- sdp-plugin/cmd/sdp/doctor_test.go | 12 +- sdp-plugin/cmd/sdp/init.go | 27 ++-- sdp-plugin/cmd/sdp/init_test.go | 46 ++++++ sdp-plugin/cmd/sdp/metrics.go | 6 +- sdp-plugin/cmd/sdp/metrics_classify.go | 1 + sdp-plugin/cmd/sdp/metrics_report.go | 2 + sdp-plugin/internal/doctor/doctor.go | 64 ++++++--- .../internal/doctor/doctor_drift_test.go | 5 +- sdp-plugin/internal/doctor/doctor_test.go | 31 +++- sdp-plugin/internal/sdpinit/headless.go | 12 +- sdp-plugin/internal/sdpinit/headless_test.go | 27 ++++ sdp-plugin/internal/sdpinit/init.go | 134 ++++++++++++++---- sdp-plugin/internal/sdpinit/init_test.go | 36 +++++ sdp-plugin/internal/sdpinit/preflight.go | 35 ++++- sdp-plugin/internal/sdpinit/preflight_test.go | 23 +++ 22 files changed, 441 insertions(+), 107 deletions(-) create mode 120000 .codex/skills/sdp diff --git a/.codex/INSTALL.md b/.codex/INSTALL.md index be98830f..47f93331 100644 --- a/.codex/INSTALL.md +++ b/.codex/INSTALL.md @@ -8,19 +8,31 @@ Project skills source of truth lives in `prompts/skills/` (this repo). Tool fold ## Quick start -1. Install SDP CLI (from repo root): +1. Install SDP into your project with Codex integration: ```bash - cd sdp-plugin && CGO_ENABLED=0 go build -o sdp ./cmd/sdp && mv sdp ../ + SDP_IDE=codex curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh + ``` +2. Run project init: + ```bash + sdp init --auto ``` -2. Ensure `sdp` is on PATH. 3. Use `@build 00-XXX-YY` or `sdp plan`, `sdp apply`, `sdp log trace` per [CLAUDE.md](../CLAUDE.md). +If you want the CLI only, use: + +```bash +curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only +``` + ## Directory layout ``` .codex/ ├── INSTALL.md # This file (read by Codex) -└── skills/ # Project-level symlinks to prompts/skills +├── agents/ # Project-level agent symlink +└── skills/ + ├── README.md + └── sdp/ # Project-level skills sourced from prompts/skills ~/.codex/ └── skills/ # User-level skills (persistent) @@ -29,3 +41,7 @@ Project skills source of truth lives in `prompts/skills/` (this repo). Tool fold ## Beads (optional) If Beads is installed (`bd --version`), use `bd ready`, `bd update`, `bd close` for task tracking. See [AGENTS.md](../AGENTS.md). + +## Updates + +Rerun the same installer command to refresh the vendored `sdp/` checkout and managed Codex links after upstream changes. diff --git a/.codex/skills/sdp b/.codex/skills/sdp new file mode 120000 index 00000000..c4bba781 --- /dev/null +++ b/.codex/skills/sdp @@ -0,0 +1 @@ +../../prompts/skills \ No newline at end of file diff --git a/README.md b/README.md index 79cc748e..117dfc76 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Protocol + evidence layer for AI agent workflows.** -SDP gives your AI agents a structured process (Discovery → Delivery → Evidence) and produces proof of what they actually did. Today the smoothest setup path is for `Claude Code`, `Cursor`, and `OpenCode` / `Windsurf`. `Codex` compatibility exists, but the setup path is still more manual. +SDP gives your AI agents a structured process (Discovery → Delivery → Evidence) and produces proof of what they actually did. The public install flow now supports `Claude Code`, `Cursor`, `OpenCode` / `Windsurf`, and `Codex`. > [Manifesto](docs/MANIFESTO.md) — what exists, what's coming, why evidence matters. @@ -19,11 +19,10 @@ curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | s git submodule add https://github.com/fall-out-bug/sdp.git sdp ``` -Installer auto-detects `Claude Code`, `Cursor`, and `OpenCode` / `Windsurf`. +Installer auto-detects `Claude Code`, `Cursor`, `OpenCode` / `Windsurf`, and `Codex`. +If detection misses your tool, set `SDP_IDE=claude|cursor|opencode|codex` explicitly before running the installer. -`Codex` users should use the manual setup note in [`.codex/INSTALL.md`](.codex/INSTALL.md). - -Skills load from `sdp/.claude/skills/` (Claude), `sdp/.cursor/skills/` (Cursor), or `sdp/.opencode/skills/` (OpenCode). +Skills load from `sdp/.claude/skills/` (Claude), `sdp/.cursor/skills/` (Cursor), `sdp/.opencode/skills/` (OpenCode), or `.codex/skills/sdp/` (Codex). If you embed SDP as a submodule inside another repo, use the public GitHub URL above as the source of truth. Do not point `.gitmodules` at a local sibling path such as `../sdp`, or teammates and CI will drift onto commits nobody else can fetch. @@ -89,7 +88,7 @@ sdp doctor | File | Content | |------|---------| | [QUICKSTART.md](docs/QUICKSTART.md) | 5-minute getting started | -| [.codex/INSTALL.md](.codex/INSTALL.md) | Manual Codex setup | +| [.codex/INSTALL.md](.codex/INSTALL.md) | Codex-specific install notes | | [MANIFESTO.md](docs/MANIFESTO.md) | Vision, evidence, what exists | | [ROADMAP.md](docs/ROADMAP.md) | Where SDP is going | | [PROTOCOL.md](docs/PROTOCOL.md) | Full specification | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 19aba58b..a14c1b10 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -16,7 +16,7 @@ SDP installs prompts, hooks, and optional CLI helpers. You still configure your **Full project** (prompts + hooks + optional CLI): default install ```bash -# Into your project (auto-detects Claude Code, Cursor, OpenCode) +# Into your project (auto-detects Claude Code, Cursor, OpenCode, Codex) curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh ``` @@ -39,10 +39,11 @@ Auto-setup today is first-class for: - `Claude Code` - `Cursor` - `OpenCode` / `Windsurf` +- `Codex` -`Codex` compatibility exists, but setup is still manual today. Use [../.codex/INSTALL.md](../.codex/INSTALL.md). +If auto-detect misses your tool, rerun with `SDP_IDE=claude|cursor|opencode|codex`. -Skills load from `sdp/.claude/skills/`, `sdp/.cursor/skills/`, or `sdp/.opencode/`. +Skills load from `sdp/.claude/skills/`, `sdp/.cursor/skills/`, `sdp/.opencode/`, or `.codex/skills/sdp/`. ## 2. Initialize @@ -52,7 +53,7 @@ sdp init --auto # Safe defaults, non-interactive sdp init --guided # Interactive wizard ``` -Creates `.sdp/config.yml`, guard rules, and IDE integration. +Creates `.sdp/config.yml`, guard rules, and refreshes the IDE integration already present in the project. If no IDE integration exists yet, `sdp init` falls back to `.claude/`. *If you get "unknown flag: --auto", upgrade the CLI: `curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only`* diff --git a/scripts/install-project.sh b/scripts/install-project.sh index e1f3dae2..e810ec4d 100755 --- a/scripts/install-project.sh +++ b/scripts/install-project.sh @@ -44,10 +44,13 @@ detect_auto_ide() { if [ -d "../.opencode" ] || command -v opencode >/dev/null 2>&1 || command -v windsurf >/dev/null 2>&1; then detected="$detected opencode" fi + if [ -d "../.codex" ] || command -v codex >/dev/null 2>&1; then + detected="$detected codex" + fi if [ -z "$detected" ]; then echo "No IDE detected from PATH/project; falling back to all integrations." >&2 - echo "claude cursor opencode" + echo "claude cursor opencode codex" return fi @@ -217,6 +220,14 @@ setup_opencode() { sync_tree_files .opencode/commands ../.opencode/commands } +setup_codex() { + mkdir -p ../.codex/skills + sync_link "../../$SDP_DIR/prompts/skills" "../.codex/skills/sdp" + 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 +} + for ide in $SDP_IDE_LIST; do case "$ide" in claude|claude-code) @@ -228,10 +239,14 @@ for ide in $SDP_IDE_LIST; do opencode|windsurf) setup_opencode ;; + codex) + setup_codex + ;; all) setup_claude setup_cursor setup_opencode + setup_codex ;; *) echo "⚠️ Unknown SDP_IDE value '$ide', skipping" @@ -251,6 +266,8 @@ if [ -f ../.gitignore ]; then echo ".cursor/agents" >> ../.gitignore echo ".opencode/skills" >> ../.gitignore echo ".opencode/agents" >> ../.gitignore + echo ".codex/skills/sdp" >> ../.gitignore + echo ".codex/agents" >> ../.gitignore echo ".prompts" >> ../.gitignore echo "✅ Added entries to .gitignore" fi diff --git a/scripts/test-install-project.sh b/scripts/test-install-project.sh index 88d332df..f726f023 100644 --- a/scripts/test-install-project.sh +++ b/scripts/test-install-project.sh @@ -12,6 +12,7 @@ ADMIN_DIR="$TMP_DIR/admin" HOME_DIR="$TMP_DIR/home" PROJECT_DIR="$TMP_DIR/project" FULL_PROJECT_DIR="$TMP_DIR/project-full" +CODEX_PROJECT_DIR="$TMP_DIR/project-codex" mkdir -p "$HOME_DIR" @@ -106,4 +107,22 @@ if [ "$before_hash" = "$after_hash" ]; then exit 1 fi +# Codex install/update should provision project-level Codex surface and refresh managed links. +mkdir -p "$CODEX_PROJECT_DIR" +: > "$CODEX_PROJECT_DIR/.gitignore" +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/agents" +test ! -e "$CODEX_PROJECT_DIR/.claude" +assert_contains ".codex/skills/sdp" "$CODEX_PROJECT_DIR/.gitignore" + +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 +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" + echo "install-project regression checks passed" diff --git a/sdp-plugin/cmd/sdp/doctor.go b/sdp-plugin/cmd/sdp/doctor.go index 47ccebdb..a555d9d9 100644 --- a/sdp-plugin/cmd/sdp/doctor.go +++ b/sdp-plugin/cmd/sdp/doctor.go @@ -22,9 +22,9 @@ func doctorCmd() *cobra.Command { Verifies: - Git is installed - - Claude Code CLI is available (optional) + - Claude Code CLI is available (optional for Claude users) - Go compiler is available (for building binary) - - .claude/ directory exists and is properly structured + - At least one supported IDE integration exists (.claude, .cursor, .opencode, or .codex) - Documentation-code drift (with --drift flag) Modes: diff --git a/sdp-plugin/cmd/sdp/doctor_test.go b/sdp-plugin/cmd/sdp/doctor_test.go index ac3b9c08..74a65429 100644 --- a/sdp-plugin/cmd/sdp/doctor_test.go +++ b/sdp-plugin/cmd/sdp/doctor_test.go @@ -11,8 +11,10 @@ import ( func TestDoctorCmd(t *testing.T) { // Create .claude directory for doctor checks tmpDir := t.TempDir() - if err := os.MkdirAll(filepath.Join(tmpDir, ".claude", "skills"), 0o755); err != nil { - t.Fatalf("Failed to create .claude dir: %v", err) + for _, dir := range []string{"skills", "agents", "validators"} { + if err := os.MkdirAll(filepath.Join(tmpDir, ".claude", dir), 0o755); err != nil { + t.Fatalf("Failed to create .claude dir: %v", err) + } } originalWd, _ := os.Getwd() @@ -45,8 +47,10 @@ func TestDoctorCmd(t *testing.T) { func TestDoctorCmdWithDriftFlag(t *testing.T) { // Create .claude directory for doctor checks tmpDir := t.TempDir() - if err := os.MkdirAll(filepath.Join(tmpDir, ".claude", "skills"), 0o755); err != nil { - t.Fatalf("Failed to create .claude dir: %v", err) + for _, dir := range []string{"skills", "agents", "validators"} { + if err := os.MkdirAll(filepath.Join(tmpDir, ".claude", dir), 0o755); err != nil { + t.Fatalf("Failed to create .claude dir: %v", err) + } } originalWd, _ := os.Getwd() diff --git a/sdp-plugin/cmd/sdp/init.go b/sdp-plugin/cmd/sdp/init.go index 97548d7b..120ab9b5 100644 --- a/sdp-plugin/cmd/sdp/init.go +++ b/sdp-plugin/cmd/sdp/init.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strings" "github.com/fall-out-bug/sdp/internal/sdpinit" "github.com/spf13/cobra" @@ -27,10 +28,10 @@ func initCmd() *cobra.Command { Short: "Initialize project with SDP prompts", Long: `Initialize current project with SDP prompts and configuration. -Creates .claude/ directory structure: - skills/ - Claude Code skills - agents/ - Multi-agent prompts - validators/ - AI-based quality validators +Creates SDP project scaffold: + .sdp/ - Project config, guard rules, evidence log path + Existing IDE dirs - Refreshes supported integrations already present + .claude/ (fallback) - Created only when no other IDE integration exists yet Modes: Interactive (default): Prompts for configuration options @@ -155,8 +156,8 @@ func runAutoInit(cfg sdpinit.Config, preflight *sdpinit.PreflightResult) error { if preflight.HasSDP { fmt.Println("Info: .sdp/ already exists") } - if !preflight.HasGit { - fmt.Println("Warning: Not a git repository (version control recommended)") + if len(preflight.Integrations) > 0 { + fmt.Printf("Detected IDE integration: %s\n", strings.Join(preflight.Integrations, ", ")) } for _, conflict := range preflight.Conflicts { @@ -164,7 +165,7 @@ func runAutoInit(cfg sdpinit.Config, preflight *sdpinit.PreflightResult) error { } for _, warning := range preflight.Warnings { - fmt.Printf("Note: %s\n", warning) + fmt.Printf("Warning: %s\n", warning) } // Get defaults @@ -176,15 +177,9 @@ func runAutoInit(cfg sdpinit.Config, preflight *sdpinit.PreflightResult) error { if cfg.DryRun { fmt.Println("\n[DRY RUN] Would create:") - fmt.Println(" .sdp/") - fmt.Println(" .sdp/config.yml") - fmt.Println(" .sdp/guard-rules.yml") - fmt.Println(" .sdp/log/") - fmt.Println(" .claude/") - fmt.Println(" .claude/skills/") - fmt.Println(" .claude/agents/") - fmt.Println(" .claude/validators/") - fmt.Println(" .claude/settings.json") + for _, artifact := range sdpinit.PlannedArtifacts() { + fmt.Printf(" %s\n", artifact) + } return nil } diff --git a/sdp-plugin/cmd/sdp/init_test.go b/sdp-plugin/cmd/sdp/init_test.go index d1a1f461..198724f7 100644 --- a/sdp-plugin/cmd/sdp/init_test.go +++ b/sdp-plugin/cmd/sdp/init_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "os" + "path/filepath" "strings" "testing" @@ -295,6 +296,9 @@ func TestInitCmdWithDryRun(t *testing.T) { if _, err := os.Stat(".claude"); !os.IsNotExist(err) { t.Error("Dry-run should not create .claude directory") } + if _, err := os.Stat(".sdp"); !os.IsNotExist(err) { + t.Error("Dry-run should not create .sdp directory") + } } // TestInitCmdWithForce tests init with --force flag @@ -480,3 +484,45 @@ func TestInitCmdFlags(t *testing.T) { } } } + +func TestInitCmdWithExistingCodexKeepsCodexSurface(t *testing.T) { + originalWd, _ := os.Getwd() + tmpDir := t.TempDir() + + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to chdir: %v", err) + } + + if err := os.MkdirAll("prompts/skills", 0o755); err != nil { + t.Fatalf("Failed to create prompts dir: %v", err) + } + if err := os.WriteFile("prompts/skills/test.md", []byte("# Test"), 0o644); err != nil { + t.Fatalf("Failed to create test prompt: %v", err) + } + if err := os.MkdirAll(filepath.Join(".codex", "skills"), 0o755); err != nil { + t.Fatalf("Failed to create .codex/skills: %v", err) + } + if err := os.MkdirAll(filepath.Join(".codex", "agents"), 0o755); err != nil { + t.Fatalf("Failed to create .codex/agents: %v", err) + } + if err := os.WriteFile(filepath.Join(".codex", "INSTALL.md"), []byte("codex"), 0o644); err != nil { + t.Fatalf("Failed to create .codex/INSTALL.md: %v", err) + } + + cmd := initCmd() + if err := cmd.Flags().Set("auto", "true"); err != nil { + t.Fatalf("Failed to set auto flag: %v", err) + } + + if err := cmd.RunE(cmd, []string{}); err != nil { + t.Fatalf("initCmd() failed: %v", err) + } + + if _, err := os.Stat(".claude"); !os.IsNotExist(err) { + t.Error("initCmd() should not create .claude when Codex integration already exists") + } + if _, err := os.Stat(".sdp/config.yml"); os.IsNotExist(err) { + t.Error("initCmd() should create .sdp/config.yml") + } +} diff --git a/sdp-plugin/cmd/sdp/metrics.go b/sdp-plugin/cmd/sdp/metrics.go index ae0235ed..3e6b0dd9 100644 --- a/sdp-plugin/cmd/sdp/metrics.go +++ b/sdp-plugin/cmd/sdp/metrics.go @@ -76,6 +76,8 @@ func metricsCollectCmd() *cobra.Command { outputPath = ".sdp/metrics/latest.json" } + initMetricsDir() + // Evidence log path: .sdp/log/events.jsonl logPath := ".sdp/log/events.jsonl" @@ -116,7 +118,3 @@ func metricsCollectCmd() *cobra.Command { return cmd } - -func init() { - initMetricsDir() -} diff --git a/sdp-plugin/cmd/sdp/metrics_classify.go b/sdp-plugin/cmd/sdp/metrics_classify.go index f1bbbc81..1837da56 100644 --- a/sdp-plugin/cmd/sdp/metrics_classify.go +++ b/sdp-plugin/cmd/sdp/metrics_classify.go @@ -28,6 +28,7 @@ func metricsClassifyCmd() *cobra.Command { # Classify single event manually sdp metrics classify --id=evt-123 --type=type_error --notes="Missing import"`, RunE: func(cmd *cobra.Command, args []string) error { + initMetricsDir() taxonomyPath := ".sdp/metrics/taxonomy.json" taxonomy := metrics.NewTaxonomy(taxonomyPath) diff --git a/sdp-plugin/cmd/sdp/metrics_report.go b/sdp-plugin/cmd/sdp/metrics_report.go index a7b9a440..4de5df12 100644 --- a/sdp-plugin/cmd/sdp/metrics_report.go +++ b/sdp-plugin/cmd/sdp/metrics_report.go @@ -36,6 +36,8 @@ func metricsReportCmd() *cobra.Command { outputPath = ".sdp/metrics/benchmark-{{QUARTER}}.md" } + initMetricsDir() + // Check if metrics exist if _, err := os.Stat(metricsPath); os.IsNotExist(err) { return fmt.Errorf("metrics not found: %s\nRun 'sdp metrics collect' first", metricsPath) diff --git a/sdp-plugin/internal/doctor/doctor.go b/sdp-plugin/internal/doctor/doctor.go index 7f5ab3a6..7c456637 100644 --- a/sdp-plugin/internal/doctor/doctor.go +++ b/sdp-plugin/internal/doctor/doctor.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" ) @@ -31,8 +32,8 @@ func RunWithOptions(opts RunOptions) []CheckResult { // Check 3: Go (for building binary) results = append(results, checkGo()) - // Check 4: .claude/ directory - results = append(results, checkClaudeDir()) + // Check 4: IDE integration + results = append(results, checkIDEIntegration()) // Check 5: File permissions on sensitive data results = append(results, checkFilePermissions()) @@ -138,34 +139,61 @@ func checkGo() CheckResult { } func checkClaudeDir() CheckResult { - if _, err := os.Stat(".claude"); os.IsNotExist(err) { - return CheckResult{ - Name: ".claude/ directory", - Status: "error", - Message: "Not found. Run 'sdp init' to initialize", + return checkIDEIntegration() +} + +func checkIDEIntegration() CheckResult { + checks := []struct { + label string + basePath string + required []string + }{ + {label: "Claude", basePath: ".claude", required: []string{"skills", "agents", "validators"}}, + {label: "Cursor", basePath: ".cursor", required: []string{"skills", "agents"}}, + {label: "OpenCode", basePath: ".opencode", required: []string{"skills", "agents"}}, + {label: "Codex", basePath: ".codex", required: []string{"skills", "agents", "INSTALL.md"}}, + } + + found := []string{} + incomplete := []string{} + + for _, check := range checks { + info, err := os.Stat(check.basePath) + if err != nil || !info.IsDir() { + continue + } + + found = append(found, check.label) + missing := []string{} + for _, required := range check.required { + if _, err := os.Stat(filepath.Join(check.basePath, required)); err != nil { + missing = append(missing, required) + } + } + if len(missing) > 0 { + incomplete = append(incomplete, fmt.Sprintf("%s missing %s", check.label, strings.Join(missing, ", "))) } } - // Check if it has expected structure - dirs := []string{"skills", "agents", "validators"} - missing := []string{} - for _, dir := range dirs { - if _, err := os.Stat(".claude/" + dir); os.IsNotExist(err) { - missing = append(missing, dir) + if len(found) == 0 { + return CheckResult{ + Name: "IDE integration", + Status: "error", + Message: "Not found. Run the SDP installer or set up one of .claude/, .cursor/, .opencode/, or .codex/", } } - if len(missing) > 0 { + if len(incomplete) > 0 { return CheckResult{ - Name: ".claude/ directory", + Name: "IDE integration", Status: "warning", - Message: fmt.Sprintf("Incomplete (missing: %s)", strings.Join(missing, ", ")), + Message: fmt.Sprintf("Found: %s. Incomplete: %s", strings.Join(found, ", "), strings.Join(incomplete, "; ")), } } return CheckResult{ - Name: ".claude/ directory", + Name: "IDE integration", Status: "ok", - Message: "SDP prompts installed", + Message: fmt.Sprintf("Found: %s", strings.Join(found, ", ")), } } diff --git a/sdp-plugin/internal/doctor/doctor_drift_test.go b/sdp-plugin/internal/doctor/doctor_drift_test.go index 3d30272d..0f20b091 100644 --- a/sdp-plugin/internal/doctor/doctor_drift_test.go +++ b/sdp-plugin/internal/doctor/doctor_drift_test.go @@ -435,9 +435,8 @@ func TestCheckClaudeDirPartialSubdirs(t *testing.T) { result := checkClaudeDir() // Should detect partial installation - // Name is ".claude/ directory" not "Claude Directory" - if result.Name != ".claude/ directory" { - t.Errorf("Expected name '.claude/ directory', got '%s'", result.Name) + if result.Name != "IDE integration" { + t.Errorf("Expected name 'IDE integration', got '%s'", result.Name) } // Should be warning since subdirs are missing diff --git a/sdp-plugin/internal/doctor/doctor_test.go b/sdp-plugin/internal/doctor/doctor_test.go index 8ad8964d..fd1f2d00 100644 --- a/sdp-plugin/internal/doctor/doctor_test.go +++ b/sdp-plugin/internal/doctor/doctor_test.go @@ -29,7 +29,7 @@ func TestCheckClaudeDir(t *testing.T) { if result.Status != "ok" { t.Errorf("Expected status ok, got %s", result.Status) } - if !strings.Contains(result.Message, "SDP prompts installed") { + if !strings.Contains(result.Message, "Claude") { t.Errorf("Wrong message: %s", result.Message) } } @@ -54,6 +54,33 @@ func TestCheckClaudeDir_NotFound(t *testing.T) { } } +func TestCheckIDEIntegration_Codex(t *testing.T) { + tmpDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(tmpDir, ".codex", "skills"), 0o755); err != nil { + t.Fatalf("mkdir skills: %v", err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, ".codex", "agents"), 0o755); err != nil { + t.Fatalf("mkdir agents: %v", err) + } + if err := os.WriteFile(filepath.Join(tmpDir, ".codex", "INSTALL.md"), []byte("codex"), 0o644); err != nil { + t.Fatalf("write install: %v", err) + } + + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir: %v", err) + } + + result := checkIDEIntegration() + if result.Status != "ok" { + t.Fatalf("Expected status ok, got %s: %s", result.Status, result.Message) + } + if !strings.Contains(result.Message, "Codex") { + t.Fatalf("Expected Codex in message, got %s", result.Message) + } +} + func TestCheckFilePermissions(t *testing.T) { // This test checks real file permissions in HOME/.sdp // We can't easily mock this, so just verify the function runs @@ -172,7 +199,7 @@ func TestRun(t *testing.T) { t.Error("Expected results slice, got nil") } - // Should have at least 5 checks (Git, Claude Code, Go, .claude/, File Permissions) + // Should have at least 5 checks (Git, Claude Code, Go, IDE integration, File Permissions) expectedMinChecks := 5 if len(results) < expectedMinChecks { t.Errorf("Expected at least %d checks, got %d", expectedMinChecks, len(results)) diff --git a/sdp-plugin/internal/sdpinit/headless.go b/sdp-plugin/internal/sdpinit/headless.go index e71a2056..35137d88 100644 --- a/sdp-plugin/internal/sdpinit/headless.go +++ b/sdp-plugin/internal/sdpinit/headless.go @@ -121,17 +121,7 @@ func (h *HeadlessRunner) validate() error { } func (h *HeadlessRunner) trackCreatedFiles() { - h.output.Created = []string{ - ".sdp/", - ".sdp/config.yml", - ".sdp/guard-rules.yml", - ".sdp/log/", - ".claude/", - ".claude/skills/", - ".claude/agents/", - ".claude/validators/", - ".claude/settings.json", - } + h.output.Created = PlannedArtifacts() } // RunHeadless is a convenience function for headless initialization. diff --git a/sdp-plugin/internal/sdpinit/headless_test.go b/sdp-plugin/internal/sdpinit/headless_test.go index c4d4456e..98dfa392 100644 --- a/sdp-plugin/internal/sdpinit/headless_test.go +++ b/sdp-plugin/internal/sdpinit/headless_test.go @@ -309,6 +309,33 @@ func TestRunHeadless(t *testing.T) { } } +func TestPlannedArtifacts_WithExistingCodexOmitsClaudeFallback(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir: %v", err) + } + if err := os.MkdirAll(".codex/skills", 0o755); err != nil { + t.Fatalf("mkdir .codex/skills: %v", err) + } + if err := os.MkdirAll(".codex/agents", 0o755); err != nil { + t.Fatalf("mkdir .codex/agents: %v", err) + } + if err := os.WriteFile(".codex/INSTALL.md", []byte("codex"), 0o644); err != nil { + t.Fatalf("write install: %v", err) + } + + created := PlannedArtifacts() + if slices.Contains(created, ".claude/") { + t.Fatalf("PlannedArtifacts() should not include .claude when Codex integration already exists: %v", created) + } + if !slices.Contains(created, ".sdp/config.yml") { + t.Fatalf("PlannedArtifacts() should include .sdp/config.yml: %v", created) + } +} + func TestHeadlessRunner_WithConflict(t *testing.T) { // Create temp directory with existing settings tmpDir := t.TempDir() diff --git a/sdp-plugin/internal/sdpinit/init.go b/sdp-plugin/internal/sdpinit/init.go index 4141eacd..da8b3262 100644 --- a/sdp-plugin/internal/sdpinit/init.go +++ b/sdp-plugin/internal/sdpinit/init.go @@ -33,52 +33,106 @@ type Config struct { DryRun bool } +type integrationState struct { + existing []string + manageClaude bool +} + func Run(cfg Config) error { - // Create .claude/ directory - claudeDir := ".claude" - if err := os.MkdirAll(claudeDir, 0o755); err != nil { - return fmt.Errorf("create .claude/: %w", err) + state, err := detectIntegrationState() + if err != nil { + return fmt.Errorf("detect IDE integration state: %w", err) } - // Create subdirectories - dirs := []string{ - "skills", - "agents", - "validators", - } - for _, dir := range dirs { - if err := os.MkdirAll(filepath.Join(claudeDir, dir), 0o755); err != nil { - return fmt.Errorf("create %s: %w", dir, err) + if state.manageClaude { + if err := ensureClaudeIntegration(cfg); err != nil { + return err } } - // Copy prompts from prompts/ directory - if err := copyPrompts(claudeDir); err != nil { - return fmt.Errorf("copy prompts: %w", err) - } - - // Create settings.json - if err := createSettings(claudeDir, cfg); err != nil { - return fmt.Errorf("create settings: %w", err) - } - if err := createProjectFiles(cfg); err != nil { return fmt.Errorf("create project files: %w", err) } // In headless mode, don't print text output if !cfg.Headless { - fmt.Println("✓ SDP initialized in .claude/ and .sdp/") + if state.manageClaude { + fmt.Println("✓ SDP initialized in .claude/ and .sdp/") + } else { + fmt.Println("✓ SDP initialized in .sdp/") + fmt.Printf(" IDE integration: existing %s\n", strings.Join(state.existing, ", ")) + } fmt.Printf(" Project type: %s\n", cfg.ProjectType) fmt.Println("\nNext steps:") fmt.Println(" 1. Review .sdp/config.yml") - fmt.Println(" 2. Review .claude/settings.json") - fmt.Println(" 3. Start using SDP prompts in your IDE") + if state.manageClaude { + fmt.Println(" 2. Review .claude/settings.json") + fmt.Println(" 3. Start using SDP prompts in your IDE") + } else { + fmt.Println(" 2. Start using SDP prompts in your IDE") + } } return nil } +func detectIntegrationState() (integrationState, error) { + existing := detectIDEIntegrations() + return integrationState{ + existing: existing, + manageClaude: shouldManageClaude(existing), + }, nil +} + +func shouldManageClaude(existing []string) bool { + if len(existing) == 0 { + return true + } + + for _, integration := range existing { + if integration == "claude" { + return true + } + } + + return false +} + +func PlannedArtifacts() []string { + state, err := detectIntegrationState() + if err != nil { + return []string{ + ".sdp/", + ".sdp/config.yml", + ".sdp/guard-rules.yml", + ".sdp/log/", + ".claude/", + ".claude/skills/", + ".claude/agents/", + ".claude/validators/", + ".claude/settings.json", + } + } + + artifacts := []string{ + ".sdp/", + ".sdp/config.yml", + ".sdp/guard-rules.yml", + ".sdp/log/", + } + if state.manageClaude { + artifacts = append(artifacts, + ".claude/", + ".claude/skills/", + ".claude/agents/", + ".claude/validators/", + ".claude/settings.json", + ) + } + + return artifacts +} + func createProjectFiles(cfg Config) error { if err := os.MkdirAll(filepath.Join(".sdp", "log"), 0o755); err != nil { return fmt.Errorf("create .sdp/log: %w", err) @@ -95,6 +149,34 @@ func createProjectFiles(cfg Config) error { return nil } +func ensureClaudeIntegration(cfg Config) error { + claudeDir := ".claude" + if err := os.MkdirAll(claudeDir, 0o755); err != nil { + return fmt.Errorf("create .claude/: %w", err) + } + + dirs := []string{ + "skills", + "agents", + "validators", + } + for _, dir := range dirs { + if err := os.MkdirAll(filepath.Join(claudeDir, dir), 0o755); err != nil { + return fmt.Errorf("create %s: %w", dir, err) + } + } + + if err := copyPrompts(claudeDir); err != nil { + return fmt.Errorf("copy prompts: %w", err) + } + + if err := createSettings(claudeDir, cfg); err != nil { + return fmt.Errorf("create settings: %w", err) + } + + return nil +} + func copyPrompts(destDir string) error { promptsDir, err := resolvePromptsDir() if err != nil { diff --git a/sdp-plugin/internal/sdpinit/init_test.go b/sdp-plugin/internal/sdpinit/init_test.go index 8ad04de6..bbdc12f9 100644 --- a/sdp-plugin/internal/sdpinit/init_test.go +++ b/sdp-plugin/internal/sdpinit/init_test.go @@ -421,3 +421,39 @@ func TestRun_NoEvidenceCreatesDisabledProjectConfig(t *testing.T) { t.Error("project config should disable evidence") } } + +func TestRun_WithExistingCodexIntegrationDoesNotCreateClaude(t *testing.T) { + tmpDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(tmpDir, "prompts", "skills"), 0o755); err != nil { + t.Fatalf("mkdir skills: %v", err) + } + if err := os.WriteFile(filepath.Join(tmpDir, "prompts", "skills", "test.md"), []byte("# Test"), 0o644); err != nil { + t.Fatalf("write skill: %v", err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, ".codex", "skills"), 0o755); err != nil { + t.Fatalf("mkdir .codex/skills: %v", err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, ".codex", "agents"), 0o755); err != nil { + t.Fatalf("mkdir .codex/agents: %v", err) + } + if err := os.WriteFile(filepath.Join(tmpDir, ".codex", "INSTALL.md"), []byte("codex"), 0o644); err != nil { + t.Fatalf("write install: %v", err) + } + + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir: %v", err) + } + + if err := Run(Config{ProjectType: "go"}); err != nil { + t.Fatalf("Run() failed: %v", err) + } + + if _, err := os.Stat(".claude"); !os.IsNotExist(err) { + t.Error("Run() should not create .claude when Codex integration already exists") + } + if _, err := os.Stat(filepath.Join(".sdp", "config.yml")); os.IsNotExist(err) { + t.Error("Run() should create .sdp/config.yml") + } +} diff --git a/sdp-plugin/internal/sdpinit/preflight.go b/sdp-plugin/internal/sdpinit/preflight.go index b9d278e0..092dc709 100644 --- a/sdp-plugin/internal/sdpinit/preflight.go +++ b/sdp-plugin/internal/sdpinit/preflight.go @@ -8,12 +8,13 @@ import ( // PreflightResult contains preflight check results type PreflightResult struct { - ProjectType string // "go", "node", "python", "mixed", "unknown" - HasSDP bool // .sdp directory exists - HasClaude bool // .claude directory exists - HasGit bool // .git directory exists - Conflicts []string // Existing files that would be overwritten - Warnings []string // Non-fatal issues + ProjectType string // "go", "node", "python", "mixed", "unknown" + HasSDP bool // .sdp directory exists + HasClaude bool // .claude directory exists + HasGit bool // .git directory exists + Integrations []string // Existing IDE integrations (.claude/.cursor/.opencode/.codex) + Conflicts []string // Existing files that would be overwritten + Warnings []string // Non-fatal issues } // RunPreflight runs all preflight checks @@ -29,6 +30,7 @@ func RunPreflight() *PreflightResult { result.HasSDP = dirExists(".sdp") result.HasClaude = dirExists(".claude") result.HasGit = dirExists(".git") + result.Integrations = detectIDEIntegrations() // Check for conflicts result.Conflicts = checkConflicts() @@ -140,3 +142,24 @@ func checkConflicts() []string { return conflicts } + +func detectIDEIntegrations() []string { + integrations := []string{} + candidates := []struct { + name string + path string + }{ + {name: "claude", path: ".claude"}, + {name: "cursor", path: ".cursor"}, + {name: "opencode", path: ".opencode"}, + {name: "codex", path: ".codex"}, + } + + for _, candidate := range candidates { + if dirExists(candidate.path) { + integrations = append(integrations, candidate.name) + } + } + + return integrations +} diff --git a/sdp-plugin/internal/sdpinit/preflight_test.go b/sdp-plugin/internal/sdpinit/preflight_test.go index fd4c2d6c..0e302f97 100644 --- a/sdp-plugin/internal/sdpinit/preflight_test.go +++ b/sdp-plugin/internal/sdpinit/preflight_test.go @@ -279,3 +279,26 @@ func TestPreflightResult_Warnings(t *testing.T) { t.Error("Should not have git warning when .git exists") } } + +func TestPreflightResult_Integrations(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + if len(RunPreflight().Integrations) != 0 { + t.Fatal("Expected no integrations in empty temp dir") + } + + if err := os.MkdirAll(".codex/skills", 0o755); err != nil { + t.Fatalf("mkdir .codex/skills: %v", err) + } + if err := os.MkdirAll(".codex/agents", 0o755); err != nil { + t.Fatalf("mkdir .codex/agents: %v", err) + } + + result := RunPreflight() + if len(result.Integrations) != 1 || result.Integrations[0] != "codex" { + t.Fatalf("Integrations = %v, want [codex]", result.Integrations) + } +} From c4f65ebf8cd594497650d1b2bff2148b652285e1 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 21:30:27 +0300 Subject: [PATCH 07/14] Polish first-run CLI onboarding --- sdp-plugin/cmd/sdp/doctor.go | 2 +- sdp-plugin/cmd/sdp/main.go | 15 ++++- sdp-plugin/cmd/sdp/main_test.go | 16 ++++- sdp-plugin/internal/doctor/doctor.go | 73 ++++++++++++++++------- sdp-plugin/internal/doctor/doctor_test.go | 38 ++++++++++-- sdp-plugin/internal/ui/completion_zsh.go | 2 +- 6 files changed, 115 insertions(+), 31 deletions(-) diff --git a/sdp-plugin/cmd/sdp/doctor.go b/sdp-plugin/cmd/sdp/doctor.go index a555d9d9..11f8e871 100644 --- a/sdp-plugin/cmd/sdp/doctor.go +++ b/sdp-plugin/cmd/sdp/doctor.go @@ -22,9 +22,9 @@ func doctorCmd() *cobra.Command { Verifies: - Git is installed - - Claude Code CLI is available (optional for Claude users) - Go compiler is available (for building binary) - At least one supported IDE integration exists (.claude, .cursor, .opencode, or .codex) + - Tool-specific optional checks for integrations already present in the project - Documentation-code drift (with --drift flag) Modes: diff --git a/sdp-plugin/cmd/sdp/main.go b/sdp-plugin/cmd/sdp/main.go index 0a782f23..49064420 100644 --- a/sdp-plugin/cmd/sdp/main.go +++ b/sdp-plugin/cmd/sdp/main.go @@ -13,12 +13,24 @@ import ( var version = "dev" var consentAsked = false // Track if we've asked for consent this session +var telemetryConsentSkipCommands = map[string]struct{}{ + "completion": {}, + "demo": {}, + "doctor": {}, + "init": {}, + "next": {}, + "status": {}, +} func shouldAskForTelemetryConsent(cmd *cobra.Command) bool { if cmd == nil { return false } + if _, skip := telemetryConsentSkipCommands[cmd.Name()]; skip { + return false + } + for _, flagName := range []string{"auto", "headless"} { flag := cmd.Flags().Lookup(flagName) if flag != nil && flag.Value.String() == "true" { @@ -49,7 +61,8 @@ func main() { completion Generate shell completion script These commands are optional convenience tools. The core SDP functionality -is provided by the Claude Plugin prompts in .claude/.`, +is provided by the prompts installed into your supported IDE integration +directory (.claude/, .cursor/, .opencode/, or .codex/).`, Example: ` # Initialize SDP in a project sdp init . diff --git a/sdp-plugin/cmd/sdp/main_test.go b/sdp-plugin/cmd/sdp/main_test.go index 73602b9a..2aa06f83 100644 --- a/sdp-plugin/cmd/sdp/main_test.go +++ b/sdp-plugin/cmd/sdp/main_test.go @@ -9,29 +9,43 @@ import ( func TestShouldAskForTelemetryConsent(t *testing.T) { tests := []struct { name string + use string auto bool headless bool wantPrompt bool }{ { name: "interactive command prompts", + use: "plan", wantPrompt: true, }, { name: "auto init skips prompt", + use: "init", auto: true, wantPrompt: false, }, { name: "headless init skips prompt", + use: "init", headless: true, wantPrompt: false, }, + { + name: "doctor skips prompt", + use: "doctor", + wantPrompt: false, + }, + { + name: "status skips prompt", + use: "status", + wantPrompt: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd := &cobra.Command{Use: "init"} + cmd := &cobra.Command{Use: tt.use} cmd.Flags().Bool("auto", false, "") cmd.Flags().Bool("headless", false, "") diff --git a/sdp-plugin/internal/doctor/doctor.go b/sdp-plugin/internal/doctor/doctor.go index 7c456637..84352d67 100644 --- a/sdp-plugin/internal/doctor/doctor.go +++ b/sdp-plugin/internal/doctor/doctor.go @@ -22,18 +22,21 @@ type RunOptions struct { // RunWithOptions runs doctor checks with the given options func RunWithOptions(opts RunOptions) []CheckResult { results := []CheckResult{} + integrations := detectIDEIntegrations() // Check 1: Git results = append(results, checkGit()) - // Check 2: Claude Code - results = append(results, checkClaudeCode()) - - // Check 3: Go (for building binary) + // Check 2: Go (for building binary) results = append(results, checkGo()) - // Check 4: IDE integration - results = append(results, checkIDEIntegration()) + // Check 3: IDE integration + results = append(results, checkIDEIntegration(integrations)) + + // Check 4: Tool-specific optional checks for active integrations + if hasIntegration(integrations, "Claude") { + results = append(results, checkClaudeCode()) + } // Check 5: File permissions on sensitive data results = append(results, checkFilePermissions()) @@ -139,10 +142,42 @@ func checkGo() CheckResult { } func checkClaudeDir() CheckResult { - return checkIDEIntegration() + return checkIDEIntegration(detectIDEIntegrations()) } -func checkIDEIntegration() CheckResult { +func detectIDEIntegrations() []string { + checks := []struct { + label string + basePath string + }{ + {label: "Claude", basePath: ".claude"}, + {label: "Cursor", basePath: ".cursor"}, + {label: "OpenCode", basePath: ".opencode"}, + {label: "Codex", basePath: ".codex"}, + } + + found := []string{} + for _, check := range checks { + info, err := os.Stat(check.basePath) + if err != nil || !info.IsDir() { + continue + } + found = append(found, check.label) + } + + return found +} + +func hasIntegration(integrations []string, target string) bool { + for _, integration := range integrations { + if integration == target { + return true + } + } + return false +} + +func checkIDEIntegration(found []string) CheckResult { checks := []struct { label string basePath string @@ -153,17 +188,21 @@ func checkIDEIntegration() CheckResult { {label: "OpenCode", basePath: ".opencode", required: []string{"skills", "agents"}}, {label: "Codex", basePath: ".codex", required: []string{"skills", "agents", "INSTALL.md"}}, } - - found := []string{} incomplete := []string{} + if len(found) == 0 { + return CheckResult{ + Name: "IDE integration", + Status: "error", + Message: "Not found. Run the SDP installer or set up one of .claude/, .cursor/, .opencode/, or .codex/", + } + } + for _, check := range checks { - info, err := os.Stat(check.basePath) - if err != nil || !info.IsDir() { + if !hasIntegration(found, check.label) { continue } - found = append(found, check.label) missing := []string{} for _, required := range check.required { if _, err := os.Stat(filepath.Join(check.basePath, required)); err != nil { @@ -175,14 +214,6 @@ func checkIDEIntegration() CheckResult { } } - if len(found) == 0 { - return CheckResult{ - Name: "IDE integration", - Status: "error", - Message: "Not found. Run the SDP installer or set up one of .claude/, .cursor/, .opencode/, or .codex/", - } - } - if len(incomplete) > 0 { return CheckResult{ Name: "IDE integration", diff --git a/sdp-plugin/internal/doctor/doctor_test.go b/sdp-plugin/internal/doctor/doctor_test.go index fd1f2d00..f9d9b68f 100644 --- a/sdp-plugin/internal/doctor/doctor_test.go +++ b/sdp-plugin/internal/doctor/doctor_test.go @@ -72,7 +72,7 @@ func TestCheckIDEIntegration_Codex(t *testing.T) { t.Fatalf("chdir: %v", err) } - result := checkIDEIntegration() + result := checkIDEIntegration(detectIDEIntegrations()) if result.Status != "ok" { t.Fatalf("Expected status ok, got %s: %s", result.Status, result.Message) } @@ -81,6 +81,32 @@ func TestCheckIDEIntegration_Codex(t *testing.T) { } } +func TestRunWithOptions_CodexOnlySkipsClaudeCheck(t *testing.T) { + tmpDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(tmpDir, ".codex", "skills"), 0o755); err != nil { + t.Fatalf("mkdir skills: %v", err) + } + if err := os.MkdirAll(filepath.Join(tmpDir, ".codex", "agents"), 0o755); err != nil { + t.Fatalf("mkdir agents: %v", err) + } + if err := os.WriteFile(filepath.Join(tmpDir, ".codex", "INSTALL.md"), []byte("codex"), 0o644); err != nil { + t.Fatalf("write install: %v", err) + } + + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir: %v", err) + } + + results := RunWithOptions(RunOptions{}) + for _, result := range results { + if result.Name == "Claude Code" { + t.Fatalf("RunWithOptions() should skip Claude Code check in Codex-only project: %+v", results) + } + } +} + func TestCheckFilePermissions(t *testing.T) { // This test checks real file permissions in HOME/.sdp // We can't easily mock this, so just verify the function runs @@ -199,7 +225,7 @@ func TestRun(t *testing.T) { t.Error("Expected results slice, got nil") } - // Should have at least 5 checks (Git, Claude Code, Go, IDE integration, File Permissions) + // Should have at least 5 checks (Git, Go, IDE integration, File Permissions, .sdp/config.yml) expectedMinChecks := 5 if len(results) < expectedMinChecks { t.Errorf("Expected at least %d checks, got %d", expectedMinChecks, len(results)) @@ -230,8 +256,8 @@ func TestRunWithOptions(t *testing.T) { t.Error("Expected results slice, got nil") } - // Should have 6 checks (no drift: Git, Claude, Go, .claude/, permissions, .sdp/config.yml) - expectedChecks := 6 + // Without a Claude integration in cwd, doctor skips the Claude-specific optional check. + expectedChecks := 5 if len(results) != expectedChecks { t.Errorf("Expected %d checks without drift, got %d", expectedChecks, len(results)) } @@ -273,8 +299,8 @@ func TestRunWithOptions_WithDrift(t *testing.T) { t.Error("Expected results slice, got nil") } - // Should have 7 checks (6 standard + drift) - expectedChecks := 7 + // Should have 6 checks (5 standard + drift) without a Claude integration in cwd. + expectedChecks := 6 if len(results) != expectedChecks { t.Errorf("Expected %d checks with drift, got %d", expectedChecks, len(results)) } diff --git a/sdp-plugin/internal/ui/completion_zsh.go b/sdp-plugin/internal/ui/completion_zsh.go index 4768152b..8822880c 100644 --- a/sdp-plugin/internal/ui/completion_zsh.go +++ b/sdp-plugin/internal/ui/completion_zsh.go @@ -17,7 +17,7 @@ _sdp() { commands=( 'init:Initialize project with SDP prompts' - 'doctor:Check environment (Git, Claude Code, .claude/)' + 'doctor:Check environment (Git, Go, IDE integration)' 'status:Show current project state' 'next:Get next-step recommendation' 'demo:Run a guided first-success walkthrough' From 0805121064d36cb728559f8146895730de84d469 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 21:40:54 +0300 Subject: [PATCH 08/14] Soften Claude init preflight --- sdp-plugin/cmd/sdp/init.go | 2 +- sdp-plugin/cmd/sdp/init_test.go | 74 +++++++++++++++++++ sdp-plugin/internal/sdpinit/headless_test.go | 49 ++++++++++++ sdp-plugin/internal/sdpinit/preflight.go | 49 +++++++++--- sdp-plugin/internal/sdpinit/preflight_test.go | 73 +++++++++++++++++- 5 files changed, 233 insertions(+), 14 deletions(-) diff --git a/sdp-plugin/cmd/sdp/init.go b/sdp-plugin/cmd/sdp/init.go index 120ab9b5..9d2ee1fd 100644 --- a/sdp-plugin/cmd/sdp/init.go +++ b/sdp-plugin/cmd/sdp/init.go @@ -150,7 +150,7 @@ func runAutoInit(cfg sdpinit.Config, preflight *sdpinit.PreflightResult) error { fmt.Println("=======================") fmt.Printf("Detected project type: %s\n", preflight.ProjectType) - if preflight.HasClaude && !cfg.Force { + if preflight.HasClaude && !preflight.ManagedClaudeConfig && !cfg.Force { fmt.Println("Warning: .claude/ already exists (use --force to overwrite)") } if preflight.HasSDP { diff --git a/sdp-plugin/cmd/sdp/init_test.go b/sdp-plugin/cmd/sdp/init_test.go index 198724f7..ff146703 100644 --- a/sdp-plugin/cmd/sdp/init_test.go +++ b/sdp-plugin/cmd/sdp/init_test.go @@ -341,6 +341,80 @@ func TestInitCmdWithForce(t *testing.T) { } } +func TestInitCmdWithInstallerManagedClaudeDoesNotWarnConflict(t *testing.T) { + originalWd, _ := os.Getwd() + tmpDir := t.TempDir() + + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to chdir: %v", err) + } + + if err := os.MkdirAll(".claude/hooks", 0o755); err != nil { + t.Fatalf("Failed to create hooks dir: %v", err) + } + if err := os.MkdirAll(".claude/patterns", 0o755); err != nil { + t.Fatalf("Failed to create patterns dir: %v", err) + } + if err := os.MkdirAll("sdp/prompts/skills", 0o755); err != nil { + t.Fatalf("Failed to create managed skills dir: %v", err) + } + if err := os.MkdirAll("sdp/prompts/agents", 0o755); err != nil { + t.Fatalf("Failed to create managed agents dir: %v", err) + } + if err := os.WriteFile(".claude/settings.json", []byte(`{"managed":true}`), 0o644); err != nil { + t.Fatalf("Failed to create settings: %v", err) + } + if err := os.WriteFile(".claude/commands.json", []byte(`{}`), 0o644); err != nil { + t.Fatalf("Failed to create commands: %v", err) + } + if err := os.Symlink("../sdp/prompts/skills", ".claude/skills"); err != nil { + t.Fatalf("Failed to create skills symlink: %v", err) + } + if err := os.Symlink("../sdp/prompts/agents", ".claude/agents"); err != nil { + t.Fatalf("Failed to create agents symlink: %v", err) + } + + if err := os.MkdirAll("prompts/skills", 0o755); err != nil { + t.Fatalf("Failed to create prompts dir: %v", err) + } + if err := os.WriteFile("prompts/skills/test.md", []byte("# Test"), 0o644); err != nil { + t.Fatalf("Failed to create test prompt: %v", err) + } + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + cmd := initCmd() + if err := cmd.Flags().Set("auto", "true"); err != nil { + w.Close() + os.Stdout = oldStdout + t.Fatalf("Failed to set auto flag: %v", err) + } + + err := cmd.RunE(cmd, []string{}) + + w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + if _, readErr := buf.ReadFrom(r); readErr != nil { + t.Fatalf("ReadFrom stdout: %v", readErr) + } + output := buf.String() + + if err != nil { + t.Fatalf("initCmd() failed: %v", err) + } + if strings.Contains(output, "already exists") { + t.Fatalf("managed Claude install should not warn about existing .claude: %s", output) + } + if strings.Contains(output, "Conflict: .claude/settings.json") { + t.Fatalf("managed Claude install should not report settings conflict: %s", output) + } +} + // TestInitCmdWithNoEvidence tests init with --no-evidence flag func TestInitCmdWithNoEvidence(t *testing.T) { originalWd, _ := os.Getwd() diff --git a/sdp-plugin/internal/sdpinit/headless_test.go b/sdp-plugin/internal/sdpinit/headless_test.go index 98dfa392..1f56d6d3 100644 --- a/sdp-plugin/internal/sdpinit/headless_test.go +++ b/sdp-plugin/internal/sdpinit/headless_test.go @@ -373,6 +373,55 @@ func TestHeadlessRunner_WithConflict(t *testing.T) { } } +func TestHeadlessRunner_WithManagedClaudeInstallerLayout(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir: %v", err) + } + + if err := os.MkdirAll(".claude/hooks", 0o755); err != nil { + t.Fatalf("mkdir hooks: %v", err) + } + if err := os.MkdirAll(".claude/patterns", 0o755); err != nil { + t.Fatalf("mkdir patterns: %v", err) + } + if err := os.WriteFile(".claude/settings.json", []byte("{}"), 0o644); err != nil { + t.Fatalf("write settings: %v", err) + } + if err := os.WriteFile(".claude/commands.json", []byte("{}"), 0o644); err != nil { + t.Fatalf("write commands: %v", err) + } + if err := os.Symlink("../sdp/prompts/skills", ".claude/skills"); err != nil { + t.Fatalf("symlink skills: %v", err) + } + if err := os.Symlink("../sdp/prompts/agents", ".claude/agents"); err != nil { + t.Fatalf("symlink agents: %v", err) + } + if err := os.MkdirAll("prompts/skills", 0o755); err != nil { + t.Fatalf("mkdir prompts: %v", err) + } + + cfg := Config{ + ProjectType: "go", + DryRun: true, + } + + runner := NewHeadlessRunner(cfg) + output, err := runner.Run() + if err != nil { + t.Fatalf("managed Claude layout should not fail headless init: %v", err) + } + if !output.Success { + t.Fatal("managed Claude layout should succeed") + } + if output.Preflight == nil || !output.Preflight.ManagedClaudeConfig { + t.Fatalf("expected managed Claude preflight metadata, got %+v", output.Preflight) + } +} + func TestHeadlessRunner_ForceWithConflict(t *testing.T) { // Create temp directory with existing settings tmpDir := t.TempDir() diff --git a/sdp-plugin/internal/sdpinit/preflight.go b/sdp-plugin/internal/sdpinit/preflight.go index 092dc709..1e24ab06 100644 --- a/sdp-plugin/internal/sdpinit/preflight.go +++ b/sdp-plugin/internal/sdpinit/preflight.go @@ -8,13 +8,14 @@ import ( // PreflightResult contains preflight check results type PreflightResult struct { - ProjectType string // "go", "node", "python", "mixed", "unknown" - HasSDP bool // .sdp directory exists - HasClaude bool // .claude directory exists - HasGit bool // .git directory exists - Integrations []string // Existing IDE integrations (.claude/.cursor/.opencode/.codex) - Conflicts []string // Existing files that would be overwritten - Warnings []string // Non-fatal issues + ProjectType string // "go", "node", "python", "mixed", "unknown" + HasSDP bool // .sdp directory exists + HasClaude bool // .claude directory exists + ManagedClaudeConfig bool // .claude layout matches installer-managed SDP integration + HasGit bool // .git directory exists + Integrations []string // Existing IDE integrations (.claude/.cursor/.opencode/.codex) + Conflicts []string // Existing files that would be overwritten + Warnings []string // Non-fatal issues } // RunPreflight runs all preflight checks @@ -29,11 +30,12 @@ func RunPreflight() *PreflightResult { // Check for existing SDP structure result.HasSDP = dirExists(".sdp") result.HasClaude = dirExists(".claude") + result.ManagedClaudeConfig = isManagedClaudeConfig() result.HasGit = dirExists(".git") result.Integrations = detectIDEIntegrations() // Check for conflicts - result.Conflicts = checkConflicts() + result.Conflicts = checkConflicts(result.ManagedClaudeConfig) // Add warnings if !result.HasGit { @@ -132,17 +134,42 @@ func dirExists(path string) bool { return info.IsDir() } -func checkConflicts() []string { +func checkConflicts(managedClaude bool) []string { conflicts := []string{} // Check if .claude would overwrite anything - if _, err := os.Stat(".claude/settings.json"); err == nil { - conflicts = append(conflicts, ".claude/settings.json") + if !managedClaude { + if _, err := os.Stat(".claude/settings.json"); err == nil { + conflicts = append(conflicts, ".claude/settings.json") + } } return conflicts } +func isManagedClaudeConfig() bool { + if !dirExists(".claude") { + return false + } + if !isSymlink(".claude/skills") || !isSymlink(".claude/agents") { + return false + } + if _, err := os.Stat(".claude/settings.json"); err != nil { + return false + } + if _, err := os.Stat(".claude/commands.json"); err != nil { + return false + } + if !dirExists(".claude/hooks") { + return false + } + if !dirExists(".claude/patterns") { + return false + } + + return true +} + func detectIDEIntegrations() []string { integrations := []string{} candidates := []struct { diff --git a/sdp-plugin/internal/sdpinit/preflight_test.go b/sdp-plugin/internal/sdpinit/preflight_test.go index 0e302f97..961a049b 100644 --- a/sdp-plugin/internal/sdpinit/preflight_test.go +++ b/sdp-plugin/internal/sdpinit/preflight_test.go @@ -203,7 +203,7 @@ func TestCheckConflicts(t *testing.T) { defer os.Chdir(oldWd) // No conflicts - conflicts := checkConflicts() + conflicts := checkConflicts(false) if len(conflicts) != 0 { t.Errorf("Expected no conflicts, got %v", conflicts) } @@ -212,12 +212,47 @@ func TestCheckConflicts(t *testing.T) { os.MkdirAll(".claude", 0o755) os.WriteFile(".claude/settings.json", []byte{}, 0o644) - conflicts = checkConflicts() + conflicts = checkConflicts(false) if len(conflicts) != 1 { t.Errorf("Expected 1 conflict, got %v", conflicts) } } +func TestCheckConflicts_ManagedClaudeInstallerLayout(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + if err := os.MkdirAll(".claude/hooks", 0o755); err != nil { + t.Fatalf("mkdir hooks: %v", err) + } + if err := os.MkdirAll(".claude/patterns", 0o755); err != nil { + t.Fatalf("mkdir patterns: %v", err) + } + if err := os.WriteFile(".claude/settings.json", []byte("{}"), 0o644); err != nil { + t.Fatalf("write settings: %v", err) + } + if err := os.WriteFile(".claude/commands.json", []byte("{}"), 0o644); err != nil { + t.Fatalf("write commands: %v", err) + } + if err := os.Symlink("../sdp/prompts/skills", ".claude/skills"); err != nil { + t.Fatalf("symlink skills: %v", err) + } + if err := os.Symlink("../sdp/prompts/agents", ".claude/agents"); err != nil { + t.Fatalf("symlink agents: %v", err) + } + + if !isManagedClaudeConfig() { + t.Fatal("expected managed Claude installer layout to be detected") + } + + conflicts := checkConflicts(true) + if len(conflicts) != 0 { + t.Fatalf("expected no conflicts for managed Claude config, got %v", conflicts) + } +} + func TestPreflightResult_HasSDP(t *testing.T) { tmpDir := t.TempDir() oldWd, _ := os.Getwd() @@ -280,6 +315,40 @@ func TestPreflightResult_Warnings(t *testing.T) { } } +func TestRunPreflight_ManagedClaudeConfig(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + if err := os.MkdirAll(".claude/hooks", 0o755); err != nil { + t.Fatalf("mkdir hooks: %v", err) + } + if err := os.MkdirAll(".claude/patterns", 0o755); err != nil { + t.Fatalf("mkdir patterns: %v", err) + } + if err := os.WriteFile(".claude/settings.json", []byte("{}"), 0o644); err != nil { + t.Fatalf("write settings: %v", err) + } + if err := os.WriteFile(".claude/commands.json", []byte("{}"), 0o644); err != nil { + t.Fatalf("write commands: %v", err) + } + if err := os.Symlink("../sdp/prompts/skills", ".claude/skills"); err != nil { + t.Fatalf("symlink skills: %v", err) + } + if err := os.Symlink("../sdp/prompts/agents", ".claude/agents"); err != nil { + t.Fatalf("symlink agents: %v", err) + } + + result := RunPreflight() + if !result.ManagedClaudeConfig { + t.Fatal("expected ManagedClaudeConfig=true") + } + if len(result.Conflicts) != 0 { + t.Fatalf("expected no conflicts, got %v", result.Conflicts) + } +} + func TestPreflightResult_Integrations(t *testing.T) { tmpDir := t.TempDir() oldWd, _ := os.Getwd() From 5a2553352e5335a81f02a83de901bcba1dc68a68 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 22:10:55 +0300 Subject: [PATCH 09/14] Clarify installer success messaging --- scripts/install-project.sh | 97 +++++++++++++++++++++++++++++---- scripts/test-install-project.sh | 32 +++++++++++ 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/scripts/install-project.sh b/scripts/install-project.sh index e810ec4d..a118adbb 100755 --- a/scripts/install-project.sh +++ b/scripts/install-project.sh @@ -14,6 +14,8 @@ SDP_INSTALL_CLI="${SDP_INSTALL_CLI:-0}" SDP_INSTALL_CLI_FROM_SOURCE="${SDP_INSTALL_CLI_FROM_SOURCE:-0}" SDP_PRESERVE_CONFIG="${SDP_PRESERVE_CONFIG:-0}" DEFAULT_REMOTE="https://github.com/fall-out-bug/sdp.git" +SDP_AUTO_FALLBACK_ALL=0 +SDP_CONFIGURED_INTEGRATIONS="" for arg in "$@"; do case "$arg" in @@ -49,12 +51,48 @@ detect_auto_ide() { fi if [ -z "$detected" ]; then - echo "No IDE detected from PATH/project; falling back to all integrations." >&2 + echo "No supported IDE detected from PATH/project; installing all supported integrations." >&2 echo "claude cursor opencode codex" - return + return 10 fi echo "$detected" + return 0 +} + +register_integration() { + label="$1" + path="$2" + entry="$label ($path)" + + case " +$SDP_CONFIGURED_INTEGRATIONS +" in + *" +$entry +"*) + return + ;; + esac + + if [ -n "$SDP_CONFIGURED_INTEGRATIONS" ]; then + SDP_CONFIGURED_INTEGRATIONS="$SDP_CONFIGURED_INTEGRATIONS +$entry" + else + SDP_CONFIGURED_INTEGRATIONS="$entry" + fi +} + +print_configured_integrations() { + if [ -z "$SDP_CONFIGURED_INTEGRATIONS" ]; then + return + fi + + echo "Configured integrations:" + printf '%s\n' "$SDP_CONFIGURED_INTEGRATIONS" | while IFS= read -r entry; do + [ -n "$entry" ] || continue + echo " - $entry" + done } sync_file() { @@ -187,8 +225,16 @@ fi # Setup for selected IDE if [ "$SDP_IDE" = "auto" ]; then - SDP_IDE_LIST=$(detect_auto_ide) - echo "🔗 Setting up for auto-detected IDEs: $SDP_IDE_LIST" + if SDP_IDE_LIST=$(detect_auto_ide); then + echo "🔗 Setting up for auto-detected IDEs: $SDP_IDE_LIST" + else + status=$? + if [ "$status" -ne 10 ]; then + exit "$status" + fi + SDP_AUTO_FALLBACK_ALL=1 + echo "🔗 Setting up for all supported IDEs: $SDP_IDE_LIST" + fi else SDP_IDE_LIST="$SDP_IDE" echo "🔗 Setting up for: $SDP_IDE" @@ -202,6 +248,7 @@ setup_claude() { sync_tree_files .claude/hooks ../.claude/hooks sync_tree_files .claude/patterns ../.claude/patterns sync_file .claude/settings.json ../.claude/settings.json + register_integration "Claude" ".claude/" } setup_cursor() { @@ -210,6 +257,7 @@ setup_cursor() { sync_link "../$SDP_DIR/prompts/agents" "../.cursor/agents" mkdir -p ../.cursor/commands sync_tree_files .cursor/commands ../.cursor/commands + register_integration "Cursor" ".cursor/" } setup_opencode() { @@ -218,6 +266,7 @@ setup_opencode() { sync_link "../$SDP_DIR/prompts/agents" "../.opencode/agents" mkdir -p ../.opencode/commands sync_tree_files .opencode/commands ../.opencode/commands + register_integration "OpenCode" ".opencode/" } setup_codex() { @@ -226,6 +275,7 @@ setup_codex() { 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 + register_integration "Codex" ".codex/" } for ide in $SDP_IDE_LIST; do @@ -283,16 +333,43 @@ fi echo "" echo "✅ SDP project assets installed successfully!" echo "" +print_configured_integrations + +if [ "$SDP_AUTO_FALLBACK_ALL" = "1" ]; then + echo "" + echo "Note: no supported IDE was detected, so SDP installed all supported integrations." + echo "To install only one surface, rerun with SDP_IDE=claude|cursor|opencode|codex." +fi + +cli_path="" +init_cmd="" +update_cmd="curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only" + if [ -x "${HOME}/.local/bin/sdp" ]; then - echo "CLI: ${HOME}/.local/bin/sdp" - echo "Try: ${HOME}/.local/bin/sdp init --auto" - echo " (update CLI: curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only)" + cli_path="${HOME}/.local/bin/sdp" + init_cmd="${HOME}/.local/bin/sdp init --auto" elif command -v sdp >/dev/null 2>&1; then cli_path=$(command -v sdp) + init_cmd="sdp init --auto" +fi + +echo "" +if [ -n "$cli_path" ]; then echo "CLI: ${cli_path}" - echo "Try: sdp init --auto" - echo " (update CLI: curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only)" + echo "" + echo "Next:" + echo " 1. Run ${init_cmd}" + echo " 2. After init, review .sdp/config.yml" + echo " 3. Open this repo in your IDE" + echo "" + echo "Update CLI:" + echo " ${update_cmd}" else echo "CLI not found in PATH. Install binary with:" - echo " curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only" + echo " ${update_cmd}" + echo "" + echo "Next:" + echo " 1. Install the CLI command above" + echo " 2. Run sdp init --auto" + echo " 3. After init, review .sdp/config.yml" fi diff --git a/scripts/test-install-project.sh b/scripts/test-install-project.sh index f726f023..47aff9d5 100644 --- a/scripts/test-install-project.sh +++ b/scripts/test-install-project.sh @@ -13,6 +13,8 @@ HOME_DIR="$TMP_DIR/home" PROJECT_DIR="$TMP_DIR/project" FULL_PROJECT_DIR="$TMP_DIR/project-full" CODEX_PROJECT_DIR="$TMP_DIR/project-codex" +AUTO_PROJECT_DIR="$TMP_DIR/project-auto" +NO_IDE_BIN_DIR="$TMP_DIR/no-ide-bin" mkdir -p "$HOME_DIR" @@ -72,12 +74,26 @@ update_admin_file() { git -C "$ADMIN_DIR" push origin HEAD:refs/heads/main >/dev/null } +link_tool() { + name="$1" + src=$(command -v "$name") + if [ -z "$src" ]; then + echo "required tool '$name' not found in PATH" >&2 + exit 1 + fi + ln -sf "$src" "$NO_IDE_BIN_DIR/$name" +} + # Cold start / clean install run_install "$PROJECT_DIR" "$TMP_DIR/clean-install.log" env test -d "$PROJECT_DIR/sdp/.git" test -L "$PROJECT_DIR/.claude/skills" test -f "$PROJECT_DIR/.claude/commands.json" assert_contains '"version": "1.1.0"' "$PROJECT_DIR/.claude/commands.json" +assert_contains "Configured integrations:" "$TMP_DIR/clean-install.log" +assert_contains "Claude (.claude/)" "$TMP_DIR/clean-install.log" +assert_contains "Next:" "$TMP_DIR/clean-install.log" +assert_contains "After init, review .sdp/config.yml" "$TMP_DIR/clean-install.log" # Clean reinstall / update should refresh vendored checkout and managed files. update_admin_file ".claude/commands.json" '"version": "1.1.0"' '"version": "9.9.9"' "test: update commands manifest" @@ -118,6 +134,8 @@ test -L "$CODEX_PROJECT_DIR/.codex/skills/sdp" test -L "$CODEX_PROJECT_DIR/.codex/agents" test ! -e "$CODEX_PROJECT_DIR/.claude" assert_contains ".codex/skills/sdp" "$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 @@ -125,4 +143,18 @@ git -C "$ADMIN_DIR" push origin HEAD:refs/heads/main >/dev/null 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" +# Auto-detect fallback should explain that all integrations were installed. +mkdir -p "$NO_IDE_BIN_DIR" +for tool in git sh mkdir cp find ln grep chmod dirname; do + link_tool "$tool" +done +run_install "$AUTO_PROJECT_DIR" "$TMP_DIR/auto-install.log" env PATH="$NO_IDE_BIN_DIR" SDP_IDE=auto SDP_INSTALL_CLI=0 +assert_contains "No supported IDE detected from PATH/project; installing all supported integrations." "$TMP_DIR/auto-install.log" +assert_contains "Configured integrations:" "$TMP_DIR/auto-install.log" +assert_contains "Claude (.claude/)" "$TMP_DIR/auto-install.log" +assert_contains "Cursor (.cursor/)" "$TMP_DIR/auto-install.log" +assert_contains "OpenCode (.opencode/)" "$TMP_DIR/auto-install.log" +assert_contains "Codex (.codex/)" "$TMP_DIR/auto-install.log" +assert_contains "To install only one surface, rerun with SDP_IDE=claude|cursor|opencode|codex." "$TMP_DIR/auto-install.log" + echo "install-project regression checks passed" From 5af1ac25d36aa998bf1aa559f8b95191d8383187 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 22:24:18 +0300 Subject: [PATCH 10/14] Make skill command harness-aware --- sdp-plugin/cmd/sdp/skill.go | 8 ++-- sdp-plugin/cmd/sdp/skill_checkall.go | 5 +- sdp-plugin/cmd/sdp/skill_list.go | 2 +- sdp-plugin/cmd/sdp/skill_paths.go | 21 ++++++++ sdp-plugin/cmd/sdp/skill_test.go | 71 ++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 sdp-plugin/cmd/sdp/skill_paths.go diff --git a/sdp-plugin/cmd/sdp/skill.go b/sdp-plugin/cmd/sdp/skill.go index f4638328..27a92479 100644 --- a/sdp-plugin/cmd/sdp/skill.go +++ b/sdp-plugin/cmd/sdp/skill.go @@ -11,22 +11,22 @@ func skillCmd() *cobra.Command { Use: "skill", Short: "Skill management commands", Long: `Skill management operations for validating and listing -Claude Code skills. +project-local SDP skills. Subcommands: validate - Validate a skill file against standards - check-all - Validate all skills in .claude/skills/ + check-all - Validate all skills in the detected project skills directory list - List all available skills show - Show skill file content`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if skillsDir == "" { - skillsDir = ".claude/skills" + skillsDir = resolveDefaultSkillsDir() } return nil }, } - cmd.PersistentFlags().StringVar(&skillsDir, "skills-dir", "", "Skills directory (default: .claude/skills)") + cmd.PersistentFlags().StringVar(&skillsDir, "skills-dir", "", "Skills directory (default: first existing project-local skills dir)") cmd.AddCommand(skillValidate()) cmd.AddCommand(skillCheckAll()) diff --git a/sdp-plugin/cmd/sdp/skill_checkall.go b/sdp-plugin/cmd/sdp/skill_checkall.go index 0dab39ef..dbb321cf 100644 --- a/sdp-plugin/cmd/sdp/skill_checkall.go +++ b/sdp-plugin/cmd/sdp/skill_checkall.go @@ -10,8 +10,8 @@ import ( func skillCheckAll() *cobra.Command { cmd := &cobra.Command{ Use: "check-all", - Short: "Validate all skills in .claude/skills/", - Long: `Validate all skill files in the .claude/skills/ directory + Short: "Validate all skills in the selected skills directory", + Long: `Validate all skill files in the selected project-local skills directory against SDP standards.`, RunE: func(cmd *cobra.Command, args []string) error { skillsDir, _ := cmd.Flags().GetString("skills-dir") //nolint:errcheck // String flag never errors @@ -55,6 +55,5 @@ against SDP standards.`, }, } - cmd.Flags().String("skills-dir", "", "Skills directory") return cmd } diff --git a/sdp-plugin/cmd/sdp/skill_list.go b/sdp-plugin/cmd/sdp/skill_list.go index ec61e326..310733f4 100644 --- a/sdp-plugin/cmd/sdp/skill_list.go +++ b/sdp-plugin/cmd/sdp/skill_list.go @@ -11,7 +11,7 @@ func skillList() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List all available skills", - Long: `List all skill directories found in .claude/skills/`, + Long: `List all skill directories found in the selected project-local skills directory.`, RunE: func(cmd *cobra.Command, args []string) error { skillsDir, _ := cmd.Flags().GetString("skills-dir") //nolint:errcheck // String flag never errors diff --git a/sdp-plugin/cmd/sdp/skill_paths.go b/sdp-plugin/cmd/sdp/skill_paths.go new file mode 100644 index 00000000..cbba6582 --- /dev/null +++ b/sdp-plugin/cmd/sdp/skill_paths.go @@ -0,0 +1,21 @@ +package main + +import "os" + +var defaultSkillsDirCandidates = []string{ + ".claude/skills", + ".cursor/skills", + ".opencode/skills", + ".codex/skills/sdp", +} + +func resolveDefaultSkillsDir() string { + for _, candidate := range defaultSkillsDirCandidates { + info, err := os.Stat(candidate) + if err == nil && info.IsDir() { + return candidate + } + } + + return ".claude/skills" +} diff --git a/sdp-plugin/cmd/sdp/skill_test.go b/sdp-plugin/cmd/sdp/skill_test.go index 4980cdc4..5c0a1ecb 100644 --- a/sdp-plugin/cmd/sdp/skill_test.go +++ b/sdp-plugin/cmd/sdp/skill_test.go @@ -7,6 +7,76 @@ import ( "testing" ) +func TestResolveDefaultSkillsDir(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T) + expected string + }{ + { + name: "fallbacks to claude path when nothing exists", + setup: func(t *testing.T) {}, + expected: ".claude/skills", + }, + { + name: "detects cursor skills", + setup: func(t *testing.T) { + if err := os.MkdirAll(".cursor/skills", 0o755); err != nil { + t.Fatalf("mkdir .cursor/skills: %v", err) + } + }, + expected: ".cursor/skills", + }, + { + name: "detects opencode skills", + setup: func(t *testing.T) { + if err := os.MkdirAll(".opencode/skills", 0o755); err != nil { + t.Fatalf("mkdir .opencode/skills: %v", err) + } + }, + expected: ".opencode/skills", + }, + { + name: "detects codex skills", + setup: func(t *testing.T) { + if err := os.MkdirAll(".codex/skills/sdp", 0o755); err != nil { + t.Fatalf("mkdir .codex/skills/sdp: %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 { + 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) + } + }, + expected: ".claude/skills", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + t.Cleanup(func() { os.Chdir(originalWd) }) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to chdir: %v", err) + } + + tt.setup(t) + + if got := resolveDefaultSkillsDir(); got != tt.expected { + t.Fatalf("resolveDefaultSkillsDir() = %q, want %q", got, tt.expected) + } + }) + } +} + // TestSkillValidateCmd tests the skill validate command func TestSkillValidateCmd(t *testing.T) { // Create temp directory with skill file @@ -168,6 +238,7 @@ func TestSkillCheckAllCmd(t *testing.T) { } cmd := skillCheckAll() + cmd.Flags().String("skills-dir", "", "Skills directory") if err := cmd.Flags().Set("skills-dir", skillsDir); err != nil { t.Fatalf("Failed to set skills-dir flag: %v", err) } From 8026df9f7f3f4e81be3968caedbf3267ccef890f Mon Sep 17 00:00:00 2001 From: Andrei Date: Sat, 4 Apr 2026 22:38:09 +0300 Subject: [PATCH 11/14] Fix hanging quality coverage test --- sdp-plugin/cmd/sdp/quality.go | 18 +++++++++++++----- sdp-plugin/cmd/sdp/quality_test.go | 30 ++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/sdp-plugin/cmd/sdp/quality.go b/sdp-plugin/cmd/sdp/quality.go index aff286af..2dfbd64d 100644 --- a/sdp-plugin/cmd/sdp/quality.go +++ b/sdp-plugin/cmd/sdp/quality.go @@ -6,6 +6,14 @@ import ( "github.com/spf13/cobra" ) +var ( + runQualityCoverageCmd = runQualityCoverage + runQualityComplexityCmd = runQualityComplexity + runQualitySizeCmd = runQualitySize + runQualityTypesCmd = runQualityTypes + runQualityAllCmd = runQualityAll +) + func qualityCmd() *cobra.Command { var strict bool @@ -35,7 +43,7 @@ Strict Mode (--strict): Use: "coverage", Short: "Check test coverage", RunE: func(cmd *cobra.Command, args []string) error { - return runQualityCoverage(strict) + return runQualityCoverageCmd(strict) }, }) @@ -43,7 +51,7 @@ Strict Mode (--strict): Use: "complexity", Short: "Check cyclomatic complexity", RunE: func(cmd *cobra.Command, args []string) error { - return runQualityComplexity(strict) + return runQualityComplexityCmd(strict) }, }) @@ -51,7 +59,7 @@ Strict Mode (--strict): Use: "size", Short: "Check file sizes", RunE: func(cmd *cobra.Command, args []string) error { - return runQualitySize(strict) + return runQualitySizeCmd(strict) }, }) @@ -59,7 +67,7 @@ Strict Mode (--strict): Use: "types", Short: "Check type completeness", RunE: func(cmd *cobra.Command, args []string) error { - return runQualityTypes(strict) + return runQualityTypesCmd(strict) }, }) @@ -71,7 +79,7 @@ Strict Mode (--strict): if ctx == nil { ctx = context.Background() } - return runQualityAll(ctx, strict) + return runQualityAllCmd(ctx, strict) }, }) diff --git a/sdp-plugin/cmd/sdp/quality_test.go b/sdp-plugin/cmd/sdp/quality_test.go index d09222ee..1e8af52c 100644 --- a/sdp-plugin/cmd/sdp/quality_test.go +++ b/sdp-plugin/cmd/sdp/quality_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "testing" "github.com/spf13/cobra" @@ -33,6 +34,20 @@ func TestQualityCmd(t *testing.T) { // TestQualityCoverageCmd tests the quality coverage command func TestQualityCoverageCmd(t *testing.T) { + called := false + gotStrict := true + expectedErr := errors.New("coverage failed") + + originalRunner := runQualityCoverageCmd + runQualityCoverageCmd = func(strict bool) error { + called = true + gotStrict = strict + return expectedErr + } + t.Cleanup(func() { + runQualityCoverageCmd = originalRunner + }) + cmd := qualityCmd() // Find the 'coverage' subcommand @@ -53,13 +68,16 @@ func TestQualityCoverageCmd(t *testing.T) { t.Errorf("quality coverage command has wrong use: %s", coverageCmd.Use) } - // Test that command can be executed (will fail due to real quality issues) + // Test that command wiring executes the configured coverage runner. err := coverageCmd.RunE(coverageCmd, []string{}) - // Expected to fail (coverage < 80%, complexity > 10, etc. in real codebase) - if err == nil { - t.Log("quality coverage command succeeded (codebase quality is good!)") - } else { - t.Log("quality coverage command failed as expected (real quality issues exist)") + if !called { + t.Fatal("quality coverage command did not invoke coverage runner") + } + if gotStrict { + t.Fatal("quality coverage command unexpectedly enabled strict mode") + } + if !errors.Is(err, expectedErr) { + t.Fatalf("quality coverage command returned %v, want %v", err, expectedErr) } } From 3f56a1b2b75d394b264bc2520d42134a359e85de Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 5 Apr 2026 11:21:56 +0300 Subject: [PATCH 12/14] Make doctor command tests deterministic --- sdp-plugin/cmd/sdp/doctor.go | 21 +++++-- sdp-plugin/cmd/sdp/doctor_test.go | 96 ++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/sdp-plugin/cmd/sdp/doctor.go b/sdp-plugin/cmd/sdp/doctor.go index 11f8e871..2b91c8bd 100644 --- a/sdp-plugin/cmd/sdp/doctor.go +++ b/sdp-plugin/cmd/sdp/doctor.go @@ -7,6 +7,15 @@ import ( "github.com/spf13/cobra" ) +var ( + doctorRunWithOptions = doctor.RunWithOptions + doctorRunWithRepair = doctor.RunWithRepair + doctorRunDeepChecks = doctor.RunDeepChecks + doctorHasUnfixable = doctor.HasUnfixableErrors + doctorMigrate = doctor.MigrateConfig + doctorRollback = doctor.RollbackMigration +) + func doctorCmd() *cobra.Command { var driftCheck bool var repair bool @@ -36,7 +45,7 @@ Modes: // Handle rollback first if rollback != "" { fmt.Println("Rolling back config...") - if err := doctor.RollbackMigration(rollback); err != nil { + if err := doctorRollback(rollback); err != nil { return fmt.Errorf("rollback failed: %w", err) } fmt.Printf("✓ Config restored from %s\n", rollback) @@ -46,7 +55,7 @@ Modes: // Handle migration if migrate { fmt.Println("Migrating config...") - m, err := doctor.MigrateConfig(dryRun) + m, err := doctorMigrate(dryRun) if err != nil { return fmt.Errorf("migration failed: %w", err) } @@ -66,7 +75,7 @@ Modes: opts := doctor.RunOptions{ DriftCheck: driftCheck, } - results := doctor.RunWithOptions(opts) + results := doctorRunWithOptions(opts) // Print results fmt.Println("SDP Environment Check") @@ -90,7 +99,7 @@ Modes: if repair { fmt.Println("\nRepair Mode") fmt.Println("===========") - actions := doctor.RunWithRepair() + actions := doctorRunWithRepair() for _, a := range actions { icon := "✓" if a.Status == "failed" || a.Status == "manual" { @@ -102,7 +111,7 @@ Modes: fmt.Printf(" %s\n\n", a.Message) } - if doctor.HasUnfixableErrors(actions) { + if doctorHasUnfixable(actions) { return fmt.Errorf("some issues require manual intervention") } fmt.Println("All repairable issues fixed!") @@ -112,7 +121,7 @@ Modes: if deep { fmt.Println("\nDeep Diagnostics") fmt.Println("================") - deepResults := doctor.RunDeepChecks() + deepResults := doctorRunDeepChecks() for _, r := range deepResults { icon := "✓" if r.Status == "warning" { diff --git a/sdp-plugin/cmd/sdp/doctor_test.go b/sdp-plugin/cmd/sdp/doctor_test.go index 74a65429..03329c31 100644 --- a/sdp-plugin/cmd/sdp/doctor_test.go +++ b/sdp-plugin/cmd/sdp/doctor_test.go @@ -1,77 +1,83 @@ package main import ( + "bytes" "os" "path/filepath" "strings" "testing" + + "github.com/fall-out-bug/sdp/internal/doctor" ) // TestDoctorCmd tests the doctor command func TestDoctorCmd(t *testing.T) { - // Create .claude directory for doctor checks - tmpDir := t.TempDir() - for _, dir := range []string{"skills", "agents", "validators"} { - if err := os.MkdirAll(filepath.Join(tmpDir, ".claude", dir), 0o755); err != nil { - t.Fatalf("Failed to create .claude dir: %v", err) + originalRunWithOptions := doctorRunWithOptions + doctorRunWithOptions = func(opts doctor.RunOptions) []doctor.CheckResult { + if opts.DriftCheck { + t.Fatal("doctor command unexpectedly enabled drift checks") + } + return []doctor.CheckResult{ + {Name: "Git", Status: "ok", Message: "Installed (git version test)"}, + {Name: "IDE integration", Status: "ok", Message: "Found: Codex"}, } } - - originalWd, _ := os.Getwd() - t.Cleanup(func() { os.Chdir(originalWd) }) - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to chdir: %v", err) - } + t.Cleanup(func() { + doctorRunWithOptions = originalRunWithOptions + }) cmd := doctorCmd() - // Test command structure if cmd.Use != "doctor" { t.Errorf("doctorCmd() has wrong use: %s", cmd.Use) } - - // Test flag exists if cmd.Flags().Lookup("drift") == nil { t.Error("doctorCmd() missing --drift flag") } - // Test that command runs without crashing - err := cmd.RunE(cmd, []string{}) - // Should succeed (all required checks should pass with .claude present) + output, err := captureCommandOutput(t, func() error { + return cmd.RunE(cmd, []string{}) + }) if err != nil { t.Errorf("doctorCmd() failed: %v", err) } + for _, snippet := range []string{"SDP Environment Check", "✓ Git", "✓ IDE integration", "All required checks passed!"} { + if !strings.Contains(output, snippet) { + t.Fatalf("doctor output missing %q:\n%s", snippet, output) + } + } } // TestDoctorCmdWithDriftFlag tests the doctor command with drift check enabled func TestDoctorCmdWithDriftFlag(t *testing.T) { - // Create .claude directory for doctor checks - tmpDir := t.TempDir() - for _, dir := range []string{"skills", "agents", "validators"} { - if err := os.MkdirAll(filepath.Join(tmpDir, ".claude", dir), 0o755); err != nil { - t.Fatalf("Failed to create .claude dir: %v", err) + originalRunWithOptions := doctorRunWithOptions + doctorRunWithOptions = func(opts doctor.RunOptions) []doctor.CheckResult { + if !opts.DriftCheck { + t.Fatal("doctor command did not pass drift flag to runner") + } + return []doctor.CheckResult{ + {Name: "Git", Status: "ok", Message: "Installed (git version test)"}, + {Name: "Drift Detection", Status: "ok", Message: "No recent workstreams to check"}, } } - - originalWd, _ := os.Getwd() - t.Cleanup(func() { os.Chdir(originalWd) }) - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to chdir: %v", err) - } + t.Cleanup(func() { + doctorRunWithOptions = originalRunWithOptions + }) cmd := doctorCmd() - - // Set drift flag if err := cmd.Flags().Set("drift", "true"); err != nil { t.Fatalf("Failed to set drift flag: %v", err) } - // Test that command runs without crashing - err := cmd.RunE(cmd, []string{}) - // Should succeed (all required checks should pass with .claude present) + output, err := captureCommandOutput(t, func() error { + return cmd.RunE(cmd, []string{}) + }) if err != nil { t.Errorf("doctorCmd() with drift failed: %v", err) } + if !strings.Contains(output, "✓ Drift Detection") { + t.Fatalf("doctor drift output missing drift result:\n%s", output) + } } func TestDoctorHooksProvenanceSubcommandExists(t *testing.T) { @@ -144,3 +150,27 @@ func TestDoctorHooksProvenanceRunE_MissingHook(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +func captureCommandOutput(t *testing.T, run func() error) (string, error) { + t.Helper() + + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + os.Stdout = w + + runErr := run() + + if err := w.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + os.Stdout = oldStdout + + var buf bytes.Buffer + if _, err := buf.ReadFrom(r); err != nil { + t.Fatalf("read output: %v", err) + } + return buf.String(), runErr +} From 25773b648120d7660c367ef86c08215cea7c37d9 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 5 Apr 2026 12:44:13 +0300 Subject: [PATCH 13/14] Make deploy command tests deterministic --- sdp-plugin/cmd/sdp/deploy.go | 32 +++++++++++++------ sdp-plugin/cmd/sdp/deploy_test.go | 51 ++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/sdp-plugin/cmd/sdp/deploy.go b/sdp-plugin/cmd/sdp/deploy.go index e4779f67..126e510f 100644 --- a/sdp-plugin/cmd/sdp/deploy.go +++ b/sdp-plugin/cmd/sdp/deploy.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os/exec" + "strings" "github.com/fall-out-bug/sdp/internal/evidence" "github.com/spf13/cobra" @@ -10,6 +11,23 @@ import ( const deployWSID = "00-000-00" // repo-level approval +var ( + deployResolveSHA = func() (string, error) { + out, err := exec.Command("git", "rev-parse", "HEAD").Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil + } + deployResolveApprover = func() (string, error) { + out, err := exec.Command("git", "config", "user.name").Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil + } +) + func deployCmd() *cobra.Command { var targetBranch, sha, who string @@ -25,22 +43,16 @@ func deployCmd() *cobra.Command { targetBranch = "main" } if sha == "" { - out, err := exec.Command("git", "rev-parse", "HEAD").Output() + resolvedSHA, err := deployResolveSHA() if err != nil { return fmt.Errorf("git rev-parse HEAD: %w", err) } - sha = string(out) - if len(sha) > 0 && sha[len(sha)-1] == '\n' { - sha = sha[:len(sha)-1] - } + sha = resolvedSHA } if who == "" { - out, err := exec.Command("git", "config", "user.name").Output() + resolvedWho, err := deployResolveApprover() if err == nil { - who = string(out) - } - if len(who) > 0 && who[len(who)-1] == '\n' { - who = who[:len(who)-1] + who = resolvedWho } if who == "" { who = "unknown" diff --git a/sdp-plugin/cmd/sdp/deploy_test.go b/sdp-plugin/cmd/sdp/deploy_test.go index 87f4d64d..92b828a7 100644 --- a/sdp-plugin/cmd/sdp/deploy_test.go +++ b/sdp-plugin/cmd/sdp/deploy_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "os" "path/filepath" "strings" @@ -69,6 +70,19 @@ func TestDeployCmd_ApprovalEvent(t *testing.T) { // TestDeployCmd_DefaultValues tests deploy with default values func TestDeployCmd_DefaultValues(t *testing.T) { evidence.ResetGlobalWriter() + originalResolveSHA := deployResolveSHA + originalResolveApprover := deployResolveApprover + deployResolveSHA = func() (string, error) { + return "deadbeefcafebabe", nil + } + deployResolveApprover = func() (string, error) { + return "Test Runner", nil + } + t.Cleanup(func() { + deployResolveSHA = originalResolveSHA + deployResolveApprover = originalResolveApprover + }) + originalWd, _ := os.Getwd() tmpDir := t.TempDir() @@ -89,10 +103,15 @@ func TestDeployCmd_DefaultValues(t *testing.T) { } cmd := deployCmd() - // No flags set - should use defaults - // This will fail if git is not available, but we test the logic path - _ = cmd.RunE(cmd, []string{}) - // Don't fail test on error since git may not be available in test env + output, err := captureDeployOutput(t, func() error { + return cmd.RunE(cmd, []string{}) + }) + if err != nil { + t.Fatalf("deployCmd() with defaults failed: %v", err) + } + if !strings.Contains(output, "Approval recorded: deadbee -> main (Test Runner)") { + t.Fatalf("deploy output missing resolved defaults:\n%s", output) + } } // TestDeployCmd_EvidenceDisabled tests deploy when evidence is disabled @@ -144,3 +163,27 @@ func containsAll(s string, subs ...string) bool { } return true } + +func captureDeployOutput(t *testing.T, run func() error) (string, error) { + t.Helper() + + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + os.Stdout = w + + runErr := run() + + if err := w.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + os.Stdout = oldStdout + + var buf bytes.Buffer + if _, err := buf.ReadFrom(r); err != nil { + t.Fatalf("read output: %v", err) + } + return buf.String(), runErr +} From 1dd11370217baa0e37f86a4aef944d157f0ac36e Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 5 Apr 2026 13:47:55 +0300 Subject: [PATCH 14/14] docs: align public docs to current runtime truth --- CONTRIBUTING.md | 233 ++++----- DEVELOPMENT.md | 61 +-- README.md | 100 ++-- docs/CLI_REFERENCE.md | 101 ++-- docs/PROTOCOL.md | 658 ++---------------------- docs/QUICKSTART.md | 140 ++--- docs/reference/GLOSSARY.md | 4 +- docs/reference/README.md | 221 +------- docs/reference/build-spec.md | 2 + docs/reference/design-spec.md | 2 + docs/reference/integration-contracts.md | 2 + docs/reference/review-spec.md | 2 + docs/reference/skills.md | 499 ++---------------- prompts/README.md | 1 + 14 files changed, 443 insertions(+), 1583 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3e97fe9..0aaeec9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,189 +1,148 @@ -# Contributing to Spec-Driven Protocol +# Contributing to SDP -Thank you for your interest in contributing! +Thank you for contributing. -**New contributors:** See [DEVELOPMENT.md](DEVELOPMENT.md) for setup instructions. +Start with [DEVELOPMENT.md](DEVELOPMENT.md) for local setup and build commands. ## Ways to Contribute -- **Report bugs** — Open an issue describing the problem -- **Suggest features** — Open an issue with your idea -- **Improve documentation** — Fix typos, add examples, clarify explanations -- **Add skills** — Create new agent skills in `prompts/skills/` -- **Add agents** — Create new agent definitions in `prompts/agents/` -- **Share integrations** — Document how you use SDP with other tools +- Report bugs with a reproducible example +- Improve onboarding and reference docs +- Fix CLI behavior in `sdp-plugin/` +- Improve prompt and agent definitions in `prompts/` +- Add or document integrations for supported harnesses ## Getting Started -1. Fork the repository -2. Clone your fork: - ```bash - git clone https://github.com/YOUR_USERNAME/sdp.git - cd sdp - ``` -3. Create a branch: - ```bash - git checkout -b feature/your-feature-name - ``` - -## Project Structure +1. Fork the repository. +2. Clone your fork. +3. Create a branch for one coherent change. +```bash +git clone https://github.com/YOUR_USERNAME/sdp.git +cd sdp +git checkout -b feature/your-feature-name ``` + +## Repository Layout + +```text sdp/ -├── sdp-plugin/ # Go implementation (CLI + agents) -│ ├── cmd/ # CLI commands -│ └── internal/ # Core logic -├── src/sdp/ # Go source (graph, monitoring, synthesis) -├── tests/ # Go test suite -├── prompts/ -│ ├── skills/ # Canonical AI skill definitions (source of truth) -│ └── agents/ # Canonical multi-agent definitions (source of truth) -├── .claude/ -│ ├── skills -> ../prompts/skills # Compatibility symlink -│ └── agents -> ../prompts/agents # Compatibility symlink -├── .cursor/ # Cursor IDE integration -├── .opencode/ # OpenCode integration -├── docs/ -│ ├── PROTOCOL.md # Core specification -│ ├── reference/ # API and command reference -│ ├── vision/ # Strategic vision documents -│ ├── drafts/ # Feature specifications -│ ├── decisions/ # Architecture Decision Records -│ └── workstreams/ # Backlog and completed WS -├── hooks/ # Git hooks and validators -├── templates/ # Workstream templates -├── PRODUCT_VISION.md # Product vision v3.0 -├── CLAUDE.md # Claude Code integration guide -├── AGENTS.md # Agent instructions -└── go.mod # Go module definition +├── sdp-plugin/ # Go CLI implementation +│ ├── cmd/ # CLI entry points +│ └── internal/ # CLI/runtime packages +├── src/sdp/ # Root-module Go packages +├── prompts/ # Canonical prompt and agent source +│ ├── commands/ +│ ├── skills/ +│ └── agents/ +├── .claude/ # Claude adapter around prompts/ +├── .cursor/ # Cursor adapter around prompts/ +├── .opencode/ # OpenCode adapter around prompts/ +├── .codex/ # Codex adapter around prompts/ +├── docs/ # Onboarding, protocol, and reference docs +├── hooks/ # Validation and git-hook support +├── schema/ # JSON schemas and contracts +└── templates/ # Project and workflow templates ``` -## Go Module Structure (WS-067-10) - -SDP uses two separate Go modules: +## Build and Test -| Module | Location | Module Path | Purpose | -|--------|----------|-------------|---------| -| **Root** | `go.mod` | `github.com/fall-out-bug/sdp` | Core libraries (src/sdp/) | -| **Plugin** | `sdp-plugin/go.mod` | `github.com/fall-out-bug/sdp` | CLI implementation | +SDP currently uses Go `1.26` in both the root module and `sdp-plugin/`. -### Building +Build the CLI: ```bash -# Build CLI (primary development) -cd sdp-plugin && CGO_ENABLED=0 go build -o sdp ./cmd/sdp - -# Build root module (if needed) -go build ./... +cd sdp-plugin +CGO_ENABLED=0 go build -o sdp ./cmd/sdp ``` -### Testing +Run the main CLI test suite: ```bash -# Test CLI module -cd sdp-plugin && go test ./... - -# Test root module +cd sdp-plugin go test ./... ``` -### Why No go.work? - -Both modules share the same module path (`github.com/fall-out-bug/sdp`), which prevents using Go workspaces. See [ADR-001](docs/decisions/ADR-001-dual-module-structure.md) for the consolidation decision. - -## Using SDP for Contributions +Run root-module tests when your change touches root packages: -For larger changes, use the SDP workflow: +```bash +go test ./... +``` -1. **Requirements** — Run `@idea "description"` to create draft -2. **Design** — Run `@design idea-{slug}` to create workstreams -3. **Implement** — Run `@build 00-FFF-SS` for each workstream -4. **Review** — Run `@review F{FF}` to verify quality -5. **Deploy** — Run `@deploy F{FF}` when ready +If you change installer behavior, also run: -## Pull Request Process +```bash +sh scripts/test-install-project.sh +``` -1. **Update documentation** if your change affects usage -2. **Write clear commit messages** (conventional commits) -3. **One feature per PR** -4. **Reference issues** in PR description +## What to Edit -### PR Title Format +- Edit `prompts/` for prompt or agent behavior. +- Do not hand-edit `.claude/`, `.cursor/`, `.opencode/`, or `.codex/skills/sdp` as source files. +- Edit `sdp-plugin/` for CLI behavior. +- Update docs when public behavior changes. -``` -type: brief description +If docs and runtime disagree, fix docs to match shipped behavior. Do not document planned behavior as if it already exists. -Examples: -- docs: add integration example -- feat: add @refactor skill -- fix: correct dependency resolution -``` +## Using SDP While Contributing -## Code Style +Two workflows exist today: -- **Go** — Follow standard Go conventions, `gofmt` -- **Markdown** — Consistent formatting, no trailing whitespace -- **Skills** — Follow `prompts/skills/` SKILL.md format +- **Local Mode:** `sdp init`, `sdp doctor`, `sdp plan`, `sdp apply`, `sdp verify`, `sdp status`, `sdp next` +- **Operator Mode:** prompt surfaces plus Beads-backed queue management -### Go Style +For most contributors, Local Mode is the simplest way to exercise the current product. -Use modern stdlib idioms that are supported by the repo's Go version. +If you use prompt surfaces during development, treat them as harness-specific adapters over the same stage model. Important distinction: -- Prefer `slices.SortFunc` over `sort.Slice` -- Prefer `strings.Cut` over `strings.SplitN(..., 2)` or manual `strings.Index` slicing -- Prefer `strings.CutPrefix` or `strings.CutSuffix` over prefix or suffix checks plus trim -- Prefer `slices.Contains`, `maps.Copy`, and `maps.Clone` over handwritten helper loops -- Prefer `any` over `interface{}` when behavior and public contracts stay the same -- Use `golangci-lint` or `staticcheck` instead of `golint` +- `sdp deploy` records an approval event after merge +- `sdp deploy` does not merge your branch or deploy infrastructure -For agent-driven Go work, load `@go-modern` before making style or cleanup changes. +## Pull Requests -## PR Checklist +Before opening a PR: -Before submitting a PR, ensure: +- run the relevant Go tests for the code you changed +- update user-facing docs when behavior changed +- keep prompt edits in `prompts/` +- keep one feature or fix per PR +- reference the issue or problem statement in the PR description -- [ ] Go version is 1.24 (`go version`) -- [ ] Tests pass (`cd sdp-plugin && go test ./...`) -- [ ] Coverage ≥80% (`go test -cover ./... | grep total`) -- [ ] Guard checks pass (`./sdp guard check --staged`) -- [ ] Prompt edits are in `prompts/` only (not `.claude/` or `sdp-plugin/prompts/`) -- [ ] No `.out`, `bin/`, or `dist/` files staged -- [ ] Run drift check: `./hooks/check-prompt-drift.sh` -- [ ] Update relevant documentation if behavior changed +Suggested PR titles: -## Canonical Prompt Paths +```text +docs: clarify codex onboarding +feat: add init preflight check +fix: correct apply status output +``` -**CRITICAL:** All prompt/agent definitions have a single canonical location. +## Code and Doc Style -| Content | Canonical Path | Symlink | -|---------|---------------|---------| -| Skills | `prompts/skills/` | `.claude/skills` | -| Agents | `prompts/agents/` | `.claude/agents` | +- Go: standard Go formatting with `gofmt` +- Markdown: short sentences, clear headings, concrete examples +- Prompts: keep canonical source in `prompts/` -**Rules:** -1. **Never create duplicate prompt files** in other locations -2. **Always edit canonical files** in `prompts/` -3. **Tool adapters** should reference canonical paths or symlinks -4. **CI validates** no duplicate prompt trees exist +Modern Go guidance used in this repo: -To check for drift: `./hooks/check-prompt-drift.sh` +- Prefer `slices.SortFunc` over `sort.Slice` +- Prefer `strings.Cut` over manual split or index logic +- Prefer `strings.CutPrefix` and `strings.CutSuffix` over trim-after-check +- Prefer `slices.Contains`, `maps.Copy`, and `maps.Clone` over handwritten loops +- Prefer `any` over `interface{}` -## Generated Files +## Prompt Source of Truth -The following directories contain generated artifacts and should not be committed: +All prompt and agent definitions have one canonical source: -| Directory | Description | Why Ignore | -|-----------|-------------|------------| -| `.contracts/` | API contracts generated from code | Derived from source, regenerable | -| `.oneshot/` | Checkpoint files | May contain sensitive state | -| `docs/decisions/` | Local decision logs | Local audit trail only | +| Content | Canonical path | +|---------|----------------| +| Commands | `prompts/commands/` | +| Skills | `prompts/skills/` | +| Agents | `prompts/agents/` | -These are configured in `.gitignore`. If you see them in your working tree, do not commit them. +Tool-specific directories are adapters. Edit `prompts/`, not the adapters. ## License -By contributing, you agree that your contributions will be licensed under the MIT License. - ---- - -**Version:** 0.10.0 +By contributing, you agree that your contributions are licensed under the MIT License. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a14ab4c0..60811d43 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -43,7 +43,7 @@ CGO_ENABLED=0 go build -o sdp ./cmd/sdp go test ./... # Verify installation -./sdp version +./sdp --version ``` Expected time: **5 minutes** @@ -57,15 +57,15 @@ sdp/ ├── sdp-plugin/ # Go CLI implementation │ ├── cmd/sdp/ # CLI entry point │ └── internal/ # Core logic -├── prompts/ # Canonical skill/agent definitions +├── prompts/ # Canonical prompt, skill, and agent definitions │ ├── skills/ # AI skill prompts +│ ├── commands/ # Harness command adapters │ └── agents/ # Multi-agent definitions -├── docs/ # Documentation -│ ├── PROTOCOL.md # Core specification -│ └── workstreams/ # Feature planning -└── .claude/ # Claude Code integration - ├── skills -> ../prompts/skills - └── agents -> ../prompts/agents +├── docs/ # Onboarding, protocol, and reference docs +├── .claude/ # Claude adapter +├── .cursor/ # Cursor adapter +├── .opencode/ # OpenCode adapter +└── .codex/ # Codex adapter ``` --- @@ -180,39 +180,27 @@ Recommended `settings.json`: --- -## Using SDP Skills +## Using SDP During Development -SDP includes AI-powered skills for common workflows. See [CLAUDE.md](CLAUDE.md) for full usage. +SDP currently has two working surfaces: -### Example Workflow +- **Local Mode:** `sdp init`, `sdp doctor`, `sdp plan`, `sdp apply`, `sdp verify`, `sdp status`, `sdp next` +- **Prompt surfaces:** harness-native commands installed through `.claude/`, `.cursor/`, `.opencode/`, or `.codex/` -```bash -# 1. Create a new feature -@feature "Add user authentication" - -# 2. Design workstreams -@design idea-auth - -# 3. Execute workstreams -@build 00-001-01 - -# 4. Review quality -@review F01 +For most contributors, the simplest local check is: -# 5. Deploy -@deploy F01 +```bash +cd sdp-plugin +go run ./cmd/sdp init --help +go run ./cmd/sdp doctor --help +go run ./cmd/sdp plan --help ``` -### Available Skills +If you use prompt surfaces while developing: -| Skill | Purpose | -|-------|---------| -| `@feature` | Plan new feature | -| `@build` | Execute workstream | -| `@review` | Quality review | -| `@deploy` | Merge to production | -| `@debug` | Debug issues | -| `@issue` | Route bugs | +- edit canonical prompt source in `prompts/` +- treat Beads-backed queue flows as advanced, not required +- remember that `sdp deploy` only records approval after merge --- @@ -223,13 +211,10 @@ SDP includes AI-powered skills for common workflows. See [CLAUDE.md](CLAUDE.md) bd ready # Start working on issue -bd update sdp-xxx --status=in_progress +bd update sdp-xxx --status in_progress # Close when done bd close sdp-xxx - -# Sync to remote -bd sync ``` --- diff --git a/README.md b/README.md index 117dfc76..f56aab28 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,92 @@ # SDP: Structured Development Protocol -**Protocol + evidence layer for AI agent workflows.** +**Prompt bundle plus optional local CLI for stage-based AI work with evidence.** -SDP gives your AI agents a structured process (Discovery → Delivery → Evidence) and produces proof of what they actually did. The public install flow now supports `Claude Code`, `Cursor`, `OpenCode` / `Windsurf`, and `Codex`. +SDP installs prompt and agent surfaces into supported IDE integrations and can install the `sdp` CLI for local setup, planning, execution, and inspection. The public install flow supports `Claude Code`, `Cursor`, `OpenCode` / `Windsurf`, and `Codex`. -> [Manifesto](docs/MANIFESTO.md) — what exists, what's coming, why evidence matters. +> [Manifesto](docs/MANIFESTO.md) — why SDP exists and where the project is headed. ## Quick Start ```bash -# Install (prompts, hooks, optional CLI) +# Install prompts, hooks, and optional CLI curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -# Or binary only +# Or install only the CLI binary curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only -# Or submodule +# Or vendor SDP as a submodule git submodule add https://github.com/fall-out-bug/sdp.git sdp ``` -Installer auto-detects `Claude Code`, `Cursor`, `OpenCode` / `Windsurf`, and `Codex`. -If detection misses your tool, set `SDP_IDE=claude|cursor|opencode|codex` explicitly before running the installer. +Installer auto-detects `Claude Code`, `Cursor`, `OpenCode` / `Windsurf`, and `Codex`. If detection misses your tool, rerun with `SDP_IDE=claude|cursor|opencode|codex`. -Skills load from `sdp/.claude/skills/` (Claude), `sdp/.cursor/skills/` (Cursor), `sdp/.opencode/skills/` (OpenCode), or `.codex/skills/sdp/` (Codex). +If you embed SDP as a submodule inside another repo, keep the public GitHub URL in `.gitmodules`. Do not point the submodule at a local sibling path such as `../sdp`. -If you embed SDP as a submodule inside another repo, use the public GitHub URL above as the source of truth. Do not point `.gitmodules` at a local sibling path such as `../sdp`, or teammates and CI will drift onto commits nobody else can fetch. - -SDP installs prompts, hooks, and optional CLI helpers. You still bring your own model access and provider keys through your IDE or agent runtime. - -**First run:** +**Recommended first success:** ```bash sdp init --auto sdp doctor -@feature "Your feature" -@oneshot -@review -@deploy +sdp demo ``` -→ [5-minute guide](docs/QUICKSTART.md) - -## What SDP Does +After that, use the local CLI flow: -1. **Structures agent work** — Intent → Plan → Execute → Verify → Review → Publish. Each phase has a contract. +```bash +sdp plan "Add auth" +sdp apply --dry-run +sdp apply +``` -2. **Produces evidence** — JSON envelope with intent, plan, execution, verification, provenance (hash chain). [Details](docs/MANIFESTO.md#the-evidence-envelope). +→ [5-minute guide](docs/QUICKSTART.md) -3. **Gates PRs** — `sdp-evidence validate` in CI. Incomplete evidence = blocked merge. +## Two Modes -## Core Workflow +| Mode | Use when | What it needs | +|------|----------|---------------| +| Local Mode | You want the fastest first success in one repo | `sdp` CLI plus one supported IDE integration | +| Operator Mode | You already run queue-backed work with workstreams and operators | Prompt surfaces plus `Beads` for the live queue | -| Phase | Command | -|-------|---------| -| Planning | `@vision "AI task manager"` → `@feature "Add auth"` | -| Execution | `@oneshot ` or `@build 00-001-01` | -| Review | `@review ` | -| Deploy | `@deploy ` | -| Debug | `@debug`, `@hotfix`, `@bugfix` | +**Local Mode is the recommended starting point.** It works without Beads and is the current public onboarding path. -**Done = @review APPROVED + @deploy completed.** +**Operator Mode is advanced.** Use it only if you already want a board-backed queue and multi-session execution. Beads is required for that operating model. -## Skills +## Current Workflow Surfaces -| Skill | Purpose | -|-------|---------| -| `@vision` | Strategic planning | -| `@feature` | Feature planning (→ workstreams) | -| `@oneshot` | Autonomous execution | -| `@build` | Single workstream (TDD) | -| `@review` | Multi-agent quality review | -| `@deploy` | Merge to main | -| `@debug` / `@hotfix` / `@bugfix` | Debug flows | +| Stage | Local CLI | Prompt surface | Notes | +|-------|-----------|----------------|-------| +| Bootstrap | `sdp init`, `sdp doctor`, `sdp demo` | Installed into `.claude/`, `.cursor/`, `.opencode/`, or `.codex/` | `sdp init` refreshes existing integrations and falls back to `.claude/` only when none exists yet | +| Plan | `sdp plan` | `/feature`, `/idea`, `/design` | CLI is the clearest first-run path | +| Execute | `sdp apply`, `sdp build` | `/build`, `/oneshot` | `sdp build` executes one workstream; `sdp apply` runs ready workstreams | +| Verify and inspect | `sdp verify`, `sdp status`, `sdp next`, `sdp log show` | `/review` | `status` and `next` are current inspection surfaces | +| Record approval | `sdp deploy` | `/deploy` | `sdp deploy` records an approval event after merge; it does not merge branches or deploy infrastructure | -## Optional +## What SDP Installs -**CLI:** `sdp doctor`, `sdp status`, `sdp next`, `sdp guard activate`, `sdp log show`, `sdp demo` +1. `.sdp/` project config, guard rules, and evidence paths. +2. Prompt and agent adapters for the supported IDE integration already present in your repo. +3. Optional CLI helpers for setup, planning, execution, verification, and diagnostics. -**Beads:** `brew tap beads-dev/tap && brew install beads` — task tracking for multi-session work. +Canonical prompt sources live in `prompts/`. Tool-specific directories such as `.claude/`, `.cursor/`, `.opencode/`, and `.codex/` are adapters around that source tree. -**Platform:** Evidence layer uses `flock` — macOS/Linux only. Windows not supported. +## Optional Components -**Research Lab:** We're exploring multi-persona adversarial review, self-improvement loops, cross-project federation, and telemetry-driven backlog generation in [sdp_lab](https://github.com/fall-out-bug/sdp_lab). Private for now — open an issue if you'd like to play with us. +- **CLI:** `sdp init`, `sdp doctor`, `sdp plan`, `sdp apply`, `sdp status`, `sdp next`, `sdp log`, `sdp demo` +- **Beads:** `brew tap beads-dev/tap && brew install beads` for board-backed, multi-session work +- **Platform note:** some evidence helpers rely on `flock`, so macOS/Linux is the tested path ## Docs | File | Content | |------|---------| -| [QUICKSTART.md](docs/QUICKSTART.md) | 5-minute getting started | +| [QUICKSTART.md](docs/QUICKSTART.md) | Recommended first-success path | +| [CLI_REFERENCE.md](docs/CLI_REFERENCE.md) | Current `sdp` command surfaces | +| [PROTOCOL.md](docs/PROTOCOL.md) | Current protocol overview | +| [reference/README.md](docs/reference/README.md) | Reference index and legacy-doc status | | [.codex/INSTALL.md](.codex/INSTALL.md) | Codex-specific install notes | -| [MANIFESTO.md](docs/MANIFESTO.md) | Vision, evidence, what exists | -| [ROADMAP.md](docs/ROADMAP.md) | Where SDP is going | -| [PROTOCOL.md](docs/PROTOCOL.md) | Full specification | -| [reference/](docs/reference/) | Principles, glossary, specs | +| [MANIFESTO.md](docs/MANIFESTO.md) | Vision and rationale | +| [ROADMAP.md](docs/ROADMAP.md) | Product direction | ## License diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index 459976ae..2afe1fa5 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -1,31 +1,74 @@ # SDP CLI Reference -Run `sdp --help` for the full command tree and `sdp --help` for flags and subcommands. - -## Core command groups - -| Area | Commands | Purpose | -|------|----------|---------| -| Setup and project state | `sdp init`, `sdp doctor`, `sdp health`, `sdp status`, `sdp next`, `sdp demo`, `sdp hooks`, `sdp completion` | Bootstrap SDP, inspect current state, and manage local tooling | -| Planning and execution | `sdp parse`, `sdp plan`, `sdp build`, `sdp apply`, `sdp orchestrate`, `sdp verify`, `sdp tdd`, `sdp deploy` | Parse workstreams, create plans, execute work, verify completion, and record deployment approvals | -| Guard and context | `sdp guard ...`, `sdp session ...`, `sdp resolve`, `sdp git` | Enforce edit scope, validate context/branch state, resolve task identifiers, and keep session state in sync | -| Evidence and audit | `sdp log ...`, `sdp decisions ...`, `sdp checkpoint ...`, `sdp coordination ...`, `sdp design record`, `sdp idea record` | Inspect evidence, trace decision history, manage checkpoints, and record design/idea evidence | -| Quality and diagnostics | `sdp quality {coverage, complexity, size, types, all}`, `sdp drift detect`, `sdp diagnose`, `sdp watch`, `sdp collision check`, `sdp contract ...`, `sdp acceptance run` | Run quality gates, detect drift, inspect failures, watch files, check collisions, validate contracts, and run smoke acceptance checks | -| Telemetry and metrics | `sdp telemetry {status, consent, enable, disable, analyze, export, upload}`, `sdp metrics {collect, classify, report}` | Manage local opt-in telemetry and derive benchmark/quality metrics | -| Workflow support | `sdp beads ...`, `sdp task create`, `sdp memory ...`, `sdp prd ...`, `sdp prototype`, `sdp skill ...` | Integrate with Beads, manage memory/search, work with PRDs, prototype features, and inspect skills | - -## Frequently used commands - -| Command | Purpose | -|---------|---------| -| `sdp init --auto` | Initialize prompts and SDP scaffolding without prompts | -| `sdp doctor` | Run health checks for hooks, config, telemetry, and repository setup | -| `sdp build ` | Execute a single workstream with guard enforcement and tests | -| `sdp verify ` | Validate workstream completion against evidence and checks | -| `sdp quality all` | Run all quality gates for the current project | -| `sdp telemetry status` | Show telemetry consent, event count, and storage path | -| `sdp telemetry export json` | Export local telemetry to `telemetry_export.json` | -| `sdp log show` | Show paginated evidence events with filters | -| `sdp decisions log` | Record a decision in the audit trail | - -See [reference/skills.md](reference/skills.md) for the skill catalog and [PROTOCOL.md](PROTOCOL.md) for the full protocol spec. +`sdp --help` is the authoritative source for flags and command semantics. This document summarizes the current surfaces that are already present in the CLI. + +## Recommended First-Success Path + +```bash +sdp init --auto +sdp doctor +sdp demo +``` + +Then continue with: + +```bash +sdp plan "Add auth" +sdp apply --dry-run +sdp apply +sdp status --text +sdp next +``` + +## Core Local Workflow + +| Command | Current purpose | +|---------|-----------------| +| `sdp init` | Create `.sdp/` config and guard rules, refresh existing supported IDE integrations, and fall back to `.claude/` only when no integration exists yet | +| `sdp doctor` | Check Git, Go, supported IDE integration, and optional drift state | +| `sdp demo` | Run a temporary first-success walkthrough using `init`, `doctor`, and `status --text` | +| `sdp plan ` | Decompose a feature description into workstreams from the terminal | +| `sdp apply` | Execute ready workstreams with streaming progress output | +| `sdp build ` | Execute one workstream; for the full agent-driven cycle use `/build` or `sdp orchestrate` | +| `sdp verify ` | Verify workstream completion against outputs, verification commands, and coverage threshold | +| `sdp status` | Show project state; default output is a TUI, with `--text` and `--json` for scripts | +| `sdp next` | Recommend the next action based on workstream, git, and config state | +| `sdp log show` | Inspect evidence log events | +| `sdp deploy` | Record an approval event after merge; it does not merge branches or deploy infrastructure | + +## Common Modes and Flags + +| Command | Useful modes | +|---------|--------------| +| `sdp init` | `--auto`, `--dry-run`, `--interactive`, `--headless`, `--force` | +| `sdp doctor` | `--repair`, `--deep`, `--migrate`, `--rollback`, `--drift` | +| `sdp plan` | `--interactive`, `--auto-apply`, `--dry-run`, `--output=json` | +| `sdp apply` | `--ws `, `--retry `, `--dry-run`, `--output=json` | +| `sdp status` | default TUI, `--text`, `--json` | +| `sdp next` | `--json`, `--alternatives` | +| `sdp demo` | `--template`, `--verbose`, `--cleanup=false` | + +## Broader Command Tree + +The top-level help currently exposes these command groups: + +| Area | Commands | +|------|----------| +| Setup and state | `init`, `doctor`, `health`, `status`, `next`, `demo`, `hooks`, `completion` | +| Planning and execution | `parse`, `plan`, `build`, `apply`, `orchestrate`, `verify`, `tdd`, `deploy` | +| Guard and session | `guard`, `session`, `resolve`, `git`, `collision` | +| Evidence and audit | `log`, `decisions`, `checkpoint`, `coordination`, `design`, `idea` | +| Quality and diagnostics | `quality`, `drift`, `diagnose`, `watch`, `contract`, `acceptance` | +| Workflow support | `beads`, `task`, `memory`, `prd`, `prototype`, `skill` | +| Telemetry and metrics | `telemetry`, `metrics` | + +## Relationship to Prompt Surfaces + +The CLI is optional. Core SDP prompt surfaces are installed into the supported integration directory in your repo: + +- `.claude/` +- `.cursor/` +- `.opencode/` +- `.codex/` + +Use [reference/skills.md](reference/skills.md) for the current prompt-surface map and [PROTOCOL.md](PROTOCOL.md) for the current protocol overview. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 24eeefd8..47113f82 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -1,634 +1,76 @@ -# SDP: Spec-Driven Protocol +# SDP Protocol Overview -**Workstream-driven development** for AI agents with multi-agent coordination. +This document describes the current public SDP model at a high level. For exact command behavior, use `sdp --help`. ---- +## What Ships Today -## Multi-Level Architecture +SDP currently ships three layers: -SDP is designed as a multi-level product. Each level builds on the previous, but works independently. +1. **Canonical prompt sources** in `prompts/commands/`, `prompts/skills/`, and `prompts/agents/` +2. **Harness adapters** in `.claude/`, `.cursor/`, `.opencode/`, and `.codex/` +3. **Optional Go CLI** in `sdp-plugin/` for setup, planning, execution, verification, and inspection -``` -┌─────────────────────────────────────────────────────────────────┐ -│ L4: Collaboration (Notifications, Cross-Review) │ -├─────────────────────────────────────────────────────────────────┤ -│ L3: Orchestration (Distributed Agents, k8s) — Future │ -├─────────────────────────────────────────────────────────────────┤ -│ L2: Go Tools (Evidence Log, Guard, Checkpoints) │ -├─────────────────────────────────────────────────────────────────┤ -│ L1: Adapters (Claude Code, Cursor, Windsurf invocation) │ -├─────────────────────────────────────────────────────────────────┤ -│ L0: Protocol (THIS DOCUMENT) │ -│ ├── Workstream format + Quality gates + TDD │ -│ ├── Skills (@build, @review, @oneshot, etc.) │ -│ ├── Agent roles (implementer, reviewer, etc.) │ -│ └── Beads integration (bd create/close/sync) │ -└─────────────────────────────────────────────────────────────────┘ -``` +The CLI is a convenience surface, not the only way to use SDP. Prompt surfaces are installed into supported IDE integrations and can be used independently. -**Key Principle:** L0 works with ANY AI (Opus, GLM, Codex) in ANY tool (Claude Code, Cursor, Windsurf). +## Two Operating Modes -### Level Descriptions +| Mode | Current use | Required components | +|------|-------------|---------------------| +| Local Mode | First-run onboarding and single-repo use | `sdp` CLI plus one supported IDE integration | +| Operator Mode | Queue-backed, multi-session work | Prompt surfaces plus `Beads` | -| Level | What It Provides | Required? | -|-------|------------------|-----------| -| **L0** | Protocol, skills, agents, beads | Yes (foundation) | -| **L1** | Tool-specific invocation adapters | Optional | -| **L2** | Go CLI: evidence, guard, checkpoints | Optional | -| **L3** | Distributed orchestration | Future | -| **L4** | AI-Human collaboration features | Future | +**Local Mode** is the recommended public starting point. -### Skills in L0 +**Operator Mode** is advanced and assumes you already want live queue management. If you want board-backed operation, Beads is part of the contract. -Skills are LLM-agnostic descriptions of workflows: +## Current Stage Model -``` -@build 00-001-01 # Execute workstream with TDD -@review # Multi-agent quality review -@oneshot # Autonomous feature execution -@deploy # Create PR and merge -``` +| Stage | Main surfaces | Current outcome | +|-------|---------------|-----------------| +| Bootstrap | `sdp init`, `sdp doctor`, `sdp demo` | Create `.sdp/` config and verify environment | +| Plan | `sdp plan`, prompt planning commands such as `/feature` | Create or refine workstreams | +| Execute | `sdp apply`, `sdp build`, prompt execution commands such as `/build` and `/oneshot` | Run ready workstreams | +| Verify and inspect | `sdp verify`, `sdp status`, `sdp next`, `sdp log show`, `/review` | Check completion and inspect state | +| Record approval | `sdp deploy`, `/deploy` | Record post-merge approval or follow harness-specific release flow | -Each skill describes WHAT to do. L1 adapters provide HOW to invoke (Task tool, agent panel, etc.). +Important distinction: -### Beads in L0 +- `sdp deploy` records an approval event after merge. +- `sdp deploy` does not merge branches. +- `sdp deploy` does not deploy infrastructure. -Task tracking works without Go tools: +## Supported Integrations -```bash -bd create --title="Fix bug" --priority=1 -bd close sdp-xxx -bd sync -``` +The current public installer supports: -Skills reference beads IDs directly: `@build sdp-xxx` +- `Claude Code` +- `Cursor` +- `OpenCode` / `Windsurf` +- `Codex` ---- +`sdp init` refreshes the supported integration already present in the repo and creates `.claude/` only as a fallback when no supported integration exists yet. -## Quick Start +## Current Artifacts -```bash -# Install (WS-067-11: corrected path to sdp-plugin) -go install github.com/fall-out-bug/sdp/sdp-plugin/cmd/sdp@latest +Common runtime artifacts today: -# Create feature (interactive) -@feature "Add user authentication" +- `.sdp/config.yml` +- `.sdp/guard-rules.yml` +- `.sdp/` evidence and local state files +- workstream documents created by planning flows -# Plan workstreams -@design idea-auth +Canonical prompt content lives in `prompts/`. Tool-specific directories are adapters around that source tree. -# Execute workstream -@build 00-001-01 +## Authoritative Sources -# Or execute all autonomously -@oneshot +Use these sources in order: -# Review quality -@review +1. `sdp --help` for CLI behavior +2. [README.md](../README.md) and [QUICKSTART.md](QUICKSTART.md) for onboarding +3. [CLI_REFERENCE.md](CLI_REFERENCE.md) for the current command map +4. [reference/skills.md](reference/skills.md) for prompt-surface layout +5. `prompts/` source files when you need exact prompt definitions -# Deploy to production -@deploy -``` +## Legacy Note ---- - -## Core Concepts - -### Hierarchy - -| Level | Scope | Size | Example | -|-------|-------|------|---------| -| **Release** | Product milestone | 10-30 Features | R1: Submissions E2E | -| **Feature** | Major feature | 5-30 Workstreams | F24: Unified Workflow | -| **Workstream** | Atomic task | SMALL/MEDIUM/LARGE | WS-060: Domain Model | - -### Workstream Size - -- **SMALL**: < 500 LOC, < 1500 tokens -- **MEDIUM**: 500-1500 LOC, 1500-5000 tokens -- **LARGE**: > 1500 LOC → split into 2+ WS - -⚠️ **NO TIME-BASED ESTIMATES** - Use scope metrics (LOC/tokens) only. - ---- - -## Workstream Flow - -``` -┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ -│ ANALYZE │───→│ PLAN │───→│ EXECUTE │───→│ REVIEW │ -│ (Sonnet) │ │ (Sonnet) │ │ (Auto) │ │ (Sonnet) │ -└────────────┘ └────────────┘ └────────────┘ └────────────┘ - │ │ │ │ - ▼ ▼ ▼ ▼ - Map WS Plan WS Code APPROVED/FIX -``` - ---- - -## Quality Gates - -Every workstream must pass: - -```bash -# Test coverage ≥80% -pytest tests/unit/ --cov=src/ --cov-fail-under=80 - -# Type checking -mypy src/ --strict - -# Linting -ruff check src/ - -# All files <200 LOC -find src/ -name "*.py" -exec wc -l {} + | awk '$1 > 200' -``` - -**Forbidden:** -- ❌ `except: pass` or bare exceptions -- ❌ Files > 200 LOC -- ❌ Coverage < 80% -- ❌ Time-based estimates -- ❌ TODO without followup WS - ---- - -## Unified Workflow (AI-Comm + Beads) - -SDP v0.5+ integrates multi-agent coordination with task tracking. - -### Components - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Unified Orchestrator │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Agent Spawner│──│Message Router│──│ Role Manager │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ Notification Router │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ -│ │ │ Console │ │ Telegram │ │ Mock │ │ │ -│ │ └──────────┘ └──────────┘ └──────────────┘ │ │ -│ └──────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌─────────────┐ - │ Beads CLI │ - │ Task Tracker│ - └─────────────┘ -``` - -### Agent Coordination - -```python -from sdp.unified.agent.spawner import AgentSpawner, AgentConfig - -# Spawn agents -spawner = AgentSpawner() -builder = spawner.spawn_agent(AgentConfig( - name="builder", - prompt="Execute workstreams with TDD...", -)) - -# Send messages -from sdp.unified.agent.router import SendMessageRouter, Message - -router = SendMessageRouter() -router.send_message(Message( - sender="orchestrator", - content="Execute 00-060-01", - recipient=builder, -)) -``` - -### Beads Integration - -```python -from sdp.beads import create_beads_client -from sdp.beads.models import BeadsTaskCreate, BeadsStatus - -# Create client -client = create_beads_client(use_mock=True) - -# Create feature -feature = client.create_task(BeadsTaskCreate( - title="User Authentication", - description="Add OAuth2 login flow", -)) - -# Decompose into workstreams -ws1 = client.create_task(BeadsTaskCreate( - title="Domain model", - parent_id=feature.id, -)) -ws2 = client.create_task(BeadsTaskCreate( - title="Database schema", - parent_id=feature.id, -)) - -# Add dependency -client.add_dependency(ws2.id, ws1.id, dep_type="blocks") - -# Update status -client.update_task_status(ws1.id, BeadsStatus.CLOSED) - -# Get ready tasks -ready = client.get_ready_tasks() # [ws2.id] -``` - -### Telegram Notifications - -```bash -# .env -TELEGRAM_BOT_TOKEN=your_bot_token -TELEGRAM_CHAT_ID=your_chat_id -``` - -```python -from sdp.unified.notifications.telegram import TelegramConfig, TelegramNotifier -from sdp.unified.notifications.provider import Notification, NotificationType - -config = TelegramConfig( - bot_token=os.getenv("TELEGRAM_BOT_TOKEN"), - chat_id=os.getenv("TELEGRAM_CHAT_ID"), -) -notifier = TelegramNotifier(config=config) - -# Send notification -notifier.send(Notification( - type=NotificationType.SUCCESS, - message="Feature completed successfully", -)) -``` - ---- - -## Feature Development Flow - -### 1. Requirements (@feature skill) - -```bash -@feature "Add user authentication" -``` - -Claude asks deep questions: -- Technical approach (JWT vs sessions?) -- UI/UX requirements -- Database schema -- Testing strategy -- Security concerns - -→ Creates: `docs/intent/sdp-XXX.json` -→ Creates: `docs/drafts/beads-sdp-XXX.md` - -### 2. Planning (@design skill) - -```bash -@design beads-sdp-XXX -``` - -Claude explores codebase and creates workstreams: -- 00-XXX-01: Domain model (450 LOC) -- 00-XXX-02: Database schema (300 LOC) -- 00-XXX-03: Repository layer (500 LOC) -- 00-XXX-04: Service layer (600 LOC) -- 00-XXX-05: API endpoints (400 LOC) - -→ Creates: `docs/workstreams/beads-sdp-XXX.md` - -### 3. Contract Tests (@test skill) - -```bash -@test 00-XXX-01 -``` - -Generate contract tests that define **immutable interfaces**: - -- **Function signatures** - Stable API contracts -- **Input/output contracts** - Data format specifications -- **Error conditions** - Expected failure modes -- **Invariants** - Business rules that must hold - -**Workflow:** -1. Analyze interface requirements from spec -2. Design test contracts (signatures, I/O, errors, invariants) -3. Create contract test file: `tests/contract/test_{component}.py` -4. Get stakeholder approval -5. **Lock contracts** - once approved, they CANNOT be modified during /build - -**⚠️ Contract Immutability:** -- ✅ `/build` CAN implement code to pass contracts -- ❌ `/build` CANNOT modify contract test files -- ❌ `/build` CANNOT change function signatures -- ❌ `/build` CANNOT relax error conditions - -**If interface change is needed:** -1. Stop `/build` -2. Create new workstream: "Update contract for {Component}" -3. Run `/test` with revised contracts -4. Get explicit approval -5. Resume `/build` - -Creates: `tests/contract/test_{component}.py` - -### 4. Implementation (@build skill) - -```bash -@build 00-XXX-01 -``` - -Claude follows TDD: -1. **Red** - Write failing test -2. **Green** - Implement minimum code -3. **Refactor** - Improve design - -→ Shows real-time progress -→ Runs tests, mypy, ruff -→ Commits when complete - -**⚠️ Contract Test Enforcement:** -- Guard prevents editing contract test files during `/build` -- Interface changes require new `/test` cycle - -### 5. Autonomous Execution (@oneshot skill) - -```bash -@oneshot sdp-XXX -``` - -Orchestrator agent: -- Executes all WS in dependency order -- Saves checkpoints after each WS -- Sends Telegram notifications -- Resumes from interruption - -### 6. Quality Review (@review skill) - -```bash -@review sdp-XXX -``` - -Validates: -- ✅ All quality gates passed -- ✅ Tests ≥80% coverage -- ✅ No tech debt -- ✅ Clean architecture - -→ Returns: APPROVED / CHANGES_REQUESTED - -### 7. Deployment (@deploy skill) - -```bash -@deploy sdp-XXX -``` - -Generates: -- `docker-compose.yml` -- `.github/workflows/deploy.yml` -- `CHANGELOG.md` entry -- Git tag: `v{version}` - ---- - -## Guardrails - -### YAGNI (You Aren't Gonna Need It) - -- Implement requirements **only** -- No "nice to have" features -- No "we might need this later" -- Delete unused code immediately - -### KISS (Keep It Simple, Stupid) - -- Prefer simple solutions -- Avoid over-engineering -- No premature abstraction -- One-liner > function > class - -### DRY (Don't Repeat Yourself) - -- Extract duplicated code -- Create reusable utilities -- But avoid premature abstraction - -### SOLID Principles - -- **S**ingle Responsibility - One reason to change -- **O**pen/Closed - Open for extension, closed for modification -- **L**iskov Substitution - Subtypes must be substitutable -- **I**nterface Segregation - No fat interfaces -- **D**ependency Inversion - Depend on abstractions - ---- - -## Workstream Naming Convention - -**Format:** `PP-FFF-SS` - -- **PP** - Product/Project (01-99) -- **FFF** - Feature number (001-999) -- **SS** - Workstream sequence (01-99) - -**Examples:** -- `00-001-01` - First workstream of SDP feature 001 -- `02-150-01` - First workstream of hw_checker feature 150 - -**Legacy terms (no longer used):** -- ~~`WS-FFF-SS`~~ — replaced by `PP-FFF-SS` -- ~~`Epic`~~ — replaced by **Feature** -- ~~`Sprint`~~ — not used - -**Migration Features:** -- ✅ `--dry-run` mode for safe preview -- ✅ Updates frontmatter (`ws_id` and `project_id`) -- ✅ Renames files to match new format -- ✅ Updates cross-WS dependencies -- ✅ Comprehensive validation and error reporting -- ✅ Full test coverage (≥80%) - ---- - -## Clean Architecture - -``` -src/ -├── domain/ # Business logic (no framework deps) -│ ├── entities/ # Core business objects -│ └── value_objects/ # Immutable values -├── application/ # Use cases (orchestration) -│ └── services/ # Application services -├── infrastructure/ # External concerns (DB, API) -│ ├── persistence/ # Database access -│ └── api/ # Controllers, views -└── presentation/ # UI layer (optional) -``` - -**Rules:** -- Domain ← No dependencies on other layers -- Application ← Can use Domain -- Infrastructure ← Can use Domain, Application -- Presentation ← Can use all layers - -**Forbidden:** -```python -# ❌ Layer violation -from src.infrastructure.persistence import Database - -class UserEntity: - def save(self): - db = Database() # Domain shouldn't know about DB -``` - -```python -# ✅ Clean separation -class UserEntity: - def __init__(self, name: str, email: str): - self.name = name - self.email = email -``` - ---- - -## Error Handling - -**Forbidden:** -```python -# ❌ Bare except -try: - risky_operation() -except: - pass # SWALLOWS ALL ERRORS -``` - -**Required:** -```python -# ✅ Explicit error handling -try: - risky_operation() -except SpecificError as e: - logger.error(f"Failed: {e}") - raise # Re-raise or handle -``` - ---- - -## Quick Reference - -### Commands - -```bash -# Development -@feature "title" # Gather requirements -@design beads-XXX # Plan workstreams -@build 00-XXX-01 # Execute workstream -@oneshot beads-XXX # Autonomous execution -@review beads-XXX # Quality review -@deploy beads-XXX # Production deployment - -# Debugging -/debug "" # Systematic debugging - -# Issue routing -/issue "" # Classify and route bugs -@hotfix "" # Emergency fix <2h -@bugfix "" # Quality fix <24h -``` - -### Quality Checks - -```bash -# AI-Readiness -find src/ -name "*.py" -exec wc -l {} + | awk '$1 > 200' -ruff check src/ --select=C901 # Complexity - -# Clean Architecture -grep -r "from.*infrastructure" src/domain/ - -# Error handling -grep -rn "except:" src/ -grep -rn "except Exception" src/ | grep -v "exc_info" - -# Test coverage -pytest tests/ --cov=src/ --cov-fail-under=80 - -# Full test suite -pytest -x --tb=short -pytest --cov=src/ --cov-report=term-missing -``` - ---- - -## Feature Branch Rule - -**CRITICAL:** Features MUST be implemented in feature branches. - -### Allowed Branches - -| Branch Type | Purpose | Example | -|-------------|---------|---------| -| `feature/` | Feature implementation | `feature/auth-login` | -| `bugfix/issue-id` | Bug fixes | `bugfix/sdp-1234` | -| `hotfix/issue-id` | Emergency fixes | `hotfix/sdp-1234` | - -### Protected Branches - -| Branch | Allowed Operations | -|--------|-------------------| -| `main` | Merge only (via PR) | -| `dev` | Merge only (via PR) | - -### Enforcement - -- Guard rejects commits to protected branches when `feature_id` is active -- `@build` verifies feature branch before starting work -- Pre-commit hooks block direct commits to `dev`/`main` for feature work - -### Commands - -```bash -# Check if current branch is valid for feature -sdp guard branch check --feature= - -# Validate branch naming convention -sdp guard branch validate feature/ -``` - -### Error Recovery - -If you're on `dev` or `main` when you should be on a feature branch: - -```bash -# Create feature branch -git checkout -b feature/ - -# Or switch to existing branch -git checkout feature/ -``` - ---- - -## Documentation - -- `.claude/agents/README.md` - Agent roles guide -- `README.md` - Project overview - ---- - -## Version - -**SDP v0.9.8** — Multi-Agent Architecture - -Updated: 2026-02-26 - ---- - -**See Also:** -- Agent Roles: `.claude/agents/README.md` -- Reference: `docs/reference/` -- Schema Registry: `docs/reference/schema-registry.md` -- Integration Contracts Guide: `docs/reference/integration-contracts.md` +Older deep reference documents in `docs/reference/` include historical design material from earlier iterations. If a legacy note disagrees with CLI help or the onboarding docs above, trust the runtime help and current onboarding docs. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index a14c1b10..8e98291f 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,40 +1,39 @@ # SDP Quick Start -Get from zero to your first feature in 5 minutes. +Get from zero to a real first success in one repo. -Use this doc when your goal is to adopt SDP in your own repo, not to work on `sdp_lab`. +Use this guide when you are adopting SDP in your own project. If you are contributing to SDP itself, start with `DEVELOPMENT.md` and `CONTRIBUTING.md`. -## 0. Choose Your Starting Point +## 0. Pick the Right Mode -- **Greenfield:** new repo or empty service. Install SDP, run `sdp init --auto`, then start the feature flow. -- **Brownfield:** existing codebase. Install SDP, prefer `sdp init --guided` so you can inspect defaults, then run `sdp doctor` before trusting the flow. +| Mode | Start here when | Requires | +|------|-----------------|----------| +| Local Mode | You want the shortest path to a working setup | Supported IDE integration plus the `sdp` CLI | +| Operator Mode | You already want a queue-backed workflow across sessions | Beads plus prompt-driven workstreams | -SDP installs prompts, hooks, and optional CLI helpers. You still configure your model provider and API keys in your IDE or agent runtime. +**Start with Local Mode unless you already run Beads.** That is the current public happy path. ## 1. Install -**Full project** (prompts + hooks + optional CLI): default install +Default install adds prompts, hooks, and the optional CLI: ```bash -# Into your project (auto-detects Claude Code, Cursor, OpenCode, Codex) curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh ``` -**Binary only** (CLI to ~/.local/bin): +CLI only: ```bash curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only ``` -**Or: submodule** +Vendored as a submodule: ```bash git submodule add https://github.com/fall-out-bug/sdp.git sdp ``` -Use the GitHub URL as the canonical submodule source. A local relative URL like `../sdp` is only a private convenience clone and will break reproducibility for other machines and CI. - -Auto-setup today is first-class for: +Supported integrations: - `Claude Code` - `Cursor` @@ -43,19 +42,27 @@ Auto-setup today is first-class for: If auto-detect misses your tool, rerun with `SDP_IDE=claude|cursor|opencode|codex`. -Skills load from `sdp/.claude/skills/`, `sdp/.cursor/skills/`, `sdp/.opencode/`, or `.codex/skills/sdp/`. +Use the public GitHub URL as the canonical submodule source. Do not point `.gitmodules` at a local sibling checkout such as `../sdp`. -## 2. Initialize +## 2. Initialize the Project ```bash -sdp init --auto # Safe defaults, non-interactive -# or -sdp init --guided # Interactive wizard +sdp init --auto ``` -Creates `.sdp/config.yml`, guard rules, and refreshes the IDE integration already present in the project. If no IDE integration exists yet, `sdp init` falls back to `.claude/`. +Interactive setup is also available: + +```bash +sdp init --guided +``` -*If you get "unknown flag: --auto", upgrade the CLI: `curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only`* +`sdp init` creates `.sdp/config.yml`, `.sdp/guard-rules.yml`, and refreshes the IDE integration already present in the repo. If no supported integration exists yet, it falls back to `.claude/`. + +If your CLI is too old to support `--auto`, reinstall the binary: + +```bash +curl -sSL https://raw.githubusercontent.com/fall-out-bug/sdp/main/install.sh | sh -s -- --binary-only +``` Then verify the environment: @@ -63,74 +70,87 @@ Then verify the environment: sdp doctor ``` -## 3. Create a Feature +## 3. Get a First Success -**Discovery (planning):** +The fastest guided proof that your install works: -``` -@feature "OpenCode plugin for beads visualization" +```bash +sdp demo ``` -This runs: -- `@idea` — Requirements gathering -- `@design` — Workstream decomposition into `docs/workstreams/backlog/00-XXX-YY.md` +The demo creates a temporary project and walks through: -**Delivery (implementation):** +1. `sdp init --guided` +2. `sdp doctor` +3. `sdp status --text` -``` -@oneshot # Autonomous: build all workstreams -@review # Multi-agent quality review -@deploy # Merge to main +If you want to skip the demo and use your real repo immediately, continue with the local CLI flow below. + +## 4. Plan and Execute in Local Mode + +Create workstreams from a feature description: + +```bash +sdp plan "Add OAuth2" ``` -Or step-by-step: +Preview without writing: +```bash +sdp plan "Add OAuth2" --dry-run ``` -@build 00-001-01 # Single workstream with TDD -@build 00-001-02 -@review -@deploy + +Execute ready workstreams: + +```bash +sdp apply --dry-run +sdp apply ``` -## 4. Verify +Execute one workstream directly: ```bash -sdp verify 00-001-01 # Check workstream completion -sdp status # Project state -sdp next # Recommended next action -sdp log show # Evidence log +sdp build 00-001-01 ``` -For a guided dry run of this flow: +Verify and inspect: ```bash -sdp demo +sdp verify 00-001-01 +sdp status --text +sdp next +sdp log show ``` -## 5. Optional: Beads +## 5. Advanced: Operator Mode with Beads + +Use this only if you want a live queue across sessions. -Task tracking for multi-session work: +Install Beads: ```bash brew tap beads-dev/tap && brew install beads -bd ready # Find available tasks -bd create --title="..." # Create task -bd close # Close task ``` -## Flow Summary +Common queue commands: +```bash +bd ready +bd create --title="..." +bd close ``` -@feature "X" → @oneshot → @review → @deploy - │ │ │ │ - ▼ ▼ ▼ ▼ - Workstreams Execute WS APPROVED? Merge PR -``` -**Done = @review APPROVED + @deploy completed.** +Once Beads is in place, SDP also installs prompt surfaces such as `/feature`, `/build`, `/review`, and `/oneshot`. That mode assumes workstreams and operator discipline already exist; it is not required for a first run. + +Important distinction: + +- `/deploy` is a prompt surface in the prompt bundle. +- `sdp deploy` is a CLI command that records an approval event after merge. +- `sdp deploy` does not merge branches or deploy infrastructure. -## Next +## 6. What to Use Next -- [PROTOCOL.md](PROTOCOL.md) — Full specification -- [MANIFESTO.md](MANIFESTO.md) — Vision and evidence -- [reference/](reference/) — Commands, specs, glossary +- Use [CLI_REFERENCE.md](CLI_REFERENCE.md) for current command behavior. +- Use [PROTOCOL.md](PROTOCOL.md) for the current protocol overview. +- Use [reference/README.md](reference/README.md) for the reference index and legacy-doc status. +- Use [MANIFESTO.md](MANIFESTO.md) for vision and rationale. diff --git a/docs/reference/GLOSSARY.md b/docs/reference/GLOSSARY.md index 5f94c509..7d0e20ef 100644 --- a/docs/reference/GLOSSARY.md +++ b/docs/reference/GLOSSARY.md @@ -1,10 +1,12 @@ # SDP Canonical Glossary +> Historical reference. Terms below include older SDP iterations and may not match the current public CLI or onboarding flow. For current behavior, prefer `sdp --help`, `docs/CLI_REFERENCE.md`, and `docs/PROTOCOL.md`. + **Version:** 1.0 **Last Updated:** 2026-01-29 **Maintainer:** SDP Protocol Team -This glossary provides canonical definitions for all SDP (Spec-Driven Protocol) terms, concepts, and conventions. It resolves naming conflicts and serves as the single source of truth for terminology. +This glossary preserves terminology from older SDP iterations. It is useful for historical context, but it is not the primary source of truth for current CLI semantics or onboarding. --- diff --git a/docs/reference/README.md b/docs/reference/README.md index 99a96a1a..cd2eb1bc 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -1,205 +1,34 @@ -# Reference Documentation +# SDP Reference Index -Quick lookup guides for SDP commands, configuration, and quality standards. +This index separates **current reference docs** from **historical design notes**. -**CLI reference:** [../CLI_REFERENCE.md](../CLI_REFERENCE.md) — key `sdp` commands. +When in doubt, trust runtime help first: ---- +1. `sdp --help` +2. [../README.md](../README.md) +3. [../QUICKSTART.md](../QUICKSTART.md) +4. [../CLI_REFERENCE.md](../CLI_REFERENCE.md) -## Contents +## Current Reference Docs -- [Commands](#commands) -- [Quality Gates](#quality-gates) -- [Configuration](#configuration) -- [Pipeline Hooks Security](#pipeline-hooks-security) -- [Error Handling](#error-handling) -- [Hydration](#hydration) -- [Schema Registry](#schema-registry) -- [Integration Contracts](#integration-contracts) -- [Skills](#skills) +| Need | Source | +|------|--------| +| Current CLI behavior | [../CLI_REFERENCE.md](../CLI_REFERENCE.md) | +| Current protocol overview | [../PROTOCOL.md](../PROTOCOL.md) | +| Prompt surface map | [skills.md](skills.md) | +| Prompt source layout | [../../prompts/README.md](../../prompts/README.md) | +| Schema families | [schema-registry.md](schema-registry.md) | +| Hook execution rules | [pipeline-hooks-security.md](pipeline-hooks-security.md) | +| Hydration guarantees | [context-hydration.md](context-hydration.md) | ---- +## Historical Design Notes -## Commands +The documents below contain useful background, but they are **not authoritative for current onboarding or CLI semantics**: -### SDP CLI Commands +- [build-spec.md](build-spec.md) +- [review-spec.md](review-spec.md) +- [design-spec.md](design-spec.md) +- [integration-contracts.md](integration-contracts.md) +- [GLOSSARY.md](GLOSSARY.md) -**Core Commands:** -- `@feature` - Create new feature -- `@design` - Plan workstreams -- `@build` - Execute workstream -- `@review` - Quality review -- `@deploy` - Deploy to production -- `@oneshot` - Autonomous execution - -**Utility Commands:** -- `/debug` - Systematic debugging -- `@issue` - Bug routing -- `@hotfix` - Emergency fix (P0) -- `@bugfix` - Quality fix (P1/P2) - -**See:** [../CLI_REFERENCE.md](../CLI_REFERENCE.md) — SDP CLI commands - ---- - -## Quality Gates - -### Mandatory Checks - -Every workstream must pass: - -1. **TDD** - Tests written before implementation -2. **Coverage ≥80%** - Test coverage threshold -3. **mypy --strict** - Full type hint compliance -4. **ruff** - Code linting -5. **File Size <200 LOC** - Keep code focused -6. **No Bare Exceptions** - Explicit error handling - -**See:** [build-spec.md](build-spec.md) - Complete quality standards - ---- - -## Configuration - -### Quality Gate Configuration - -**File:** `quality-gate.toml` - -```toml -[coverage] -minimum = 80 - -[complexity] -max_cc = 10 - -[file_size] -max_lines = 200 - -[type_hints] -strict_mode = true -``` - -**See:** [../PROTOCOL.md](../PROTOCOL.md) — Protocol and config - ---- - -## Pipeline Hooks Security - -Pipeline hooks run executable commands in build/review/ci phases and are validated in fail-closed mode. - -- shell metacharacters are rejected -- executables must be allowlisted or repository-local scripts -- `on_fail` policy controls halt/warn/ignore behavior - -**See:** [pipeline-hooks-security.md](pipeline-hooks-security.md) — secure hook execution rules - ---- - -## Error Handling - -### SDP Error Framework - -Structured errors with: -- Category classification -- Remediation steps -- Documentation links -- Context information - -**Error Types:** -- `BeadsNotFoundError` - Task not found -- `CoverageTooLowError` - Coverage below threshold -- `QualityGateViolationError` - Quality gate failed -- `WorkstreamValidationError` - Validation failed -- `ConfigurationError` - Config invalid -- `DependencyNotFoundError` - Dependency missing -- `HookExecutionError` - Hook failed -- `TestFailureError` - Tests failed -- `BuildValidationError` - Build check failed -- `ArtifactValidationError` - Artifact invalid - -**See:** [skills.md](skills.md) — Skill contracts and error handling - ---- - -## Hydration - -### Context Packet Reliability - -- Deterministic pre-build/pre-review context packet generation -- Fail-fast read of required quality-gate source (`AGENTS.md`) -- Explicit `ERROR:` capture for dependency and drift collection failures -- Injectable invoker seams for deterministic unit tests - -**See:** [context-hydration.md](context-hydration.md) — hydration guarantees and testability contract - ---- - -## Schema Registry - -### Contracts and Findings - -- Protocol contracts: `schema/contracts/*.schema.json` -- CI findings contracts: `schema/findings/*.schema.json` -- Agent handoff contracts: `schema/handoff-*.schema.json` - -**See:** [schema-registry.md](schema-registry.md) — schema families and usage map - ---- - -## Integration Contracts - -### End-to-End Usage - -- Runtime events and decisions: `schema/contracts/*.schema.json` -- CI findings and examples: `schema/findings/*.schema.json`, `schema/findings/examples/*.json` -- Cross-agent handoffs: `schema/handoff-*.schema.json` -- Evidence provenance (`prompt_hash`, `context_sources`): `schema/evidence-envelope.schema.json` - -**See:** [integration-contracts.md](integration-contracts.md) — integration playbook and rollout checklist - ---- - -## Skills - -### Available Skills - -**Feature Development:** -- `feature` - Unified entry point -- `idea` - Requirements gathering -- `design` - Workstream planning -- `build` - Execute workstream -- `review` - Quality check -- `deploy` - Production deployment - -**Utilities:** -- `oneshot` - Autonomous execution -- `debug` - Systematic debugging -- `issue` - Bug routing -- `hotfix` - Emergency fix -- `bugfix` - Quality fix - -**Internal:** -- `tdd` - TDD enforcement (automatic) - -**See:** [skills.md](skills.md) — Skill catalog - ---- - -## Quick Find - -### Looking For... - -| Need | Doc | -|------|-----| -| Command syntax | [../CLI_REFERENCE.md](../CLI_REFERENCE.md) | -| Quality standards | [build-spec.md](build-spec.md) | -| Hook security rules | [pipeline-hooks-security.md](pipeline-hooks-security.md) | -| Schema map | [schema-registry.md](schema-registry.md) | -| Contracts usage guide | [integration-contracts.md](integration-contracts.md) | -| Skill details | [skills.md](skills.md) | -| Design workflow | [design-spec.md](design-spec.md) | -| Review workflow | [review-spec.md](review-spec.md) | - ---- - -**Version:** SDP v0.9.8 | **Updated:** 2026-02-26 +If one of these documents disagrees with current CLI help or onboarding docs, treat it as historical material and follow the current surfaces instead. diff --git a/docs/reference/build-spec.md b/docs/reference/build-spec.md index d7bb8d90..9a4bdefe 100644 --- a/docs/reference/build-spec.md +++ b/docs/reference/build-spec.md @@ -1,5 +1,7 @@ # Build Command Full Specification +> Historical design note. Do not use this file as the source of truth for current CLI behavior or public onboarding. Prefer `sdp --help`, `docs/CLI_REFERENCE.md`, and `docs/PROTOCOL.md`. + This document contains the complete specification for `@build`. For quick reference, see [SKILL.md](../../.claude/skills/build/SKILL.md). diff --git a/docs/reference/design-spec.md b/docs/reference/design-spec.md index 184dac5b..3ebaeb43 100644 --- a/docs/reference/design-spec.md +++ b/docs/reference/design-spec.md @@ -1,5 +1,7 @@ # Design Command Full Specification +> Historical design note. Do not use this file as the source of truth for current CLI behavior or public onboarding. Prefer `sdp --help`, `docs/CLI_REFERENCE.md`, and `docs/PROTOCOL.md`. + This document contains the complete specification for `@design`. For quick reference, see [SKILL.md](../../.claude/skills/design/SKILL.md). diff --git a/docs/reference/integration-contracts.md b/docs/reference/integration-contracts.md index f29cf3f1..32fecf4a 100644 --- a/docs/reference/integration-contracts.md +++ b/docs/reference/integration-contracts.md @@ -1,5 +1,7 @@ # Integration Contracts Guide +> Historical design note. Do not use this file as the source of truth for current CLI behavior or public onboarding. Prefer `sdp --help`, `docs/CLI_REFERENCE.md`, and `docs/PROTOCOL.md`. + Practical guide for teams integrating SDP protocol artifacts into CI, adapters, status surfaces, and review workflows. ## What This Covers diff --git a/docs/reference/review-spec.md b/docs/reference/review-spec.md index 1b09abee..dee1f975 100644 --- a/docs/reference/review-spec.md +++ b/docs/reference/review-spec.md @@ -1,5 +1,7 @@ # Review Command Full Specification +> Historical design note. Do not use this file as the source of truth for current CLI behavior or public onboarding. Prefer `sdp --help`, `docs/CLI_REFERENCE.md`, and `docs/PROTOCOL.md`. + This document contains the complete specification for `@review`. For quick reference, see [SKILL.md](../../.claude/skills/review/SKILL.md). diff --git a/docs/reference/skills.md b/docs/reference/skills.md index 12c3a3e3..7626ce90 100644 --- a/docs/reference/skills.md +++ b/docs/reference/skills.md @@ -1,477 +1,54 @@ -# SDP Skills Reference +# SDP Prompt Surface Reference -Complete reference for SDP skill system and available skills. +This document maps the prompt surfaces that ship with SDP today. For exact prompt behavior, inspect the source files in `prompts/`. ---- +## Source of Truth -## Table of Contents +Canonical prompt content lives here: -- [Skill System](#skill-system) -- [Feature Skills](#feature-skills) -- [Utility Skills](#utility-skills) -- [Internal Skills](#internal-skills) -- [Skill Development](#skill-development) +- `prompts/commands/` +- `prompts/skills/` +- `prompts/agents/` ---- +Supported adapters expose that content through: -## Skill System +- `.claude/` +- `.cursor/` +- `.opencode/` +- `.codex/` -### What are Skills? +Do not treat adapter directories as the source of truth. Edit `prompts/`. -Skills are Claude Code commands that execute specific SDP workflows. They are defined in `.claude/skills/{name}/SKILL.md` files. +## Common Prompt Surfaces -### Skill Invocation +| Surface | Backing source | Current role | +|---------|----------------|--------------| +| `/feature` | `prompts/commands/feature.md`, `prompts/skills/feature/SKILL.md` | Planning entry point | +| `/idea` | `prompts/commands/idea.md`, `prompts/skills/idea/SKILL.md` | Requirements capture | +| `/design` | `prompts/commands/design.md`, `prompts/skills/design/SKILL.md` | Workstream planning | +| `/build` | `prompts/commands/build.md`, `prompts/skills/build/SKILL.md` | Single-workstream execution | +| `/review` | `prompts/commands/review.md`, `prompts/skills/review/SKILL.md` | Review and verdict loop | +| `/oneshot` | `prompts/commands/oneshot.md`, `prompts/skills/oneshot/SKILL.md` | Outer-loop feature execution | +| `/deploy` | `prompts/commands/deploy.md`, `prompts/skills/deploy/SKILL.md` | Prompt-level release handoff surface | +| `/beads` | `prompts/commands/beads.md`, `prompts/skills/beads/SKILL.md` | Beads task-tracker integration | +| `/debug`, `/hotfix`, `/bugfix`, `/issue` | matching files under `prompts/commands/` and `prompts/skills/` | Investigation and fix flows | -```bash -# Using @ prefix (user-facing) -@feature "Add authentication" +## Current Operating Reality -# Using / prefix (utilities) -/debug "Test fails" -``` +Prompt surfaces and CLI commands are related, but they are not identical. -### Skill Locations +- `sdp init`, `sdp doctor`, `sdp plan`, `sdp apply`, `sdp status`, and `sdp next` are the clearest public Local Mode surfaces. +- Prompt surfaces are the advanced harness-native layer. +- Queue-backed usage expects Beads. -**Feature Skills:** -- `.claude/skills/feature/` -- `.claude/skills/idea/` -- `.claude/skills/design/` -- `.claude/skills/build/` -- `.claude/skills/review/` -- `.claude/skills/deploy/` +Important distinction: -**Utility Skills:** -- `.claude/skills/oneshot/` -- `.claude/skills/debug/` -- `.claude/skills/issue/` -- `.claude/skills/hotfix/` -- `.claude/skills/bugfix/` +- `/deploy` is a prompt surface in the prompt bundle. +- `sdp deploy` is a CLI command with narrower semantics. +- Today `sdp deploy` records an approval event after merge; it does not merge branches or deploy infrastructure. -**Internal Skills:** -- `.claude/skills/tdd/` +## Where to Look Next ---- - -## Feature Skills - -### @feature - -**Location:** `.claude/skills/feature/SKILL.md` - -**Purpose:** Unified entry point for feature development - -**Workflow:** -1. Calls @idea for requirements -2. Generates PRODUCT_VISION.md -3. Creates Beads task -4. Outputs workstream plan - -**Example:** -```bash -@feature "Add user authentication" -``` - -**Output:** -- `docs/drafts/idea-user-auth.md` -- Beads task F{ID} - ---- - -### @idea - -**Location:** `.claude/skills/idea/SKILL.md` - -**Purpose:** Requirements gathering (internal) - -**Process:** -- Deep questioning via AskUserQuestion -- Explores technical approaches -- Identifies tradeoffs -- Generates comprehensive spec - -**Example:** -```bash -@idea "User authentication with OAuth" -``` - -**Called By:** @feature skill - ---- - -### @design - -**Location:** `.claude/skills/design/SKILL.md` - -**Purpose:** Plan workstreams from requirements - -**Process:** -1. Read requirements document -2. Enter Plan Mode for exploration -3. Decompose into workstreams -4. Create dependency graph -5. Request approval - -**Example:** -```bash -@design idea-user-auth -``` - -**Output:** -- `docs/workstreams/backlog/WS-*.md` -- Dependency visualization - ---- - -### @build - -**Location:** `.claude/skills/build/SKILL.md` - -**Purpose:** Execute workstream with TDD - -**Process:** -1. Pre-build validation -2. Red: Write failing test -3. Green: Write minimal code -4. Refactor: Improve code -5. Quality gate checks -6. Git commit -7. Beads update - -**Quality Gates:** -- Coverage ≥80% -- mypy --strict -- ruff clean -- Files <200 LOC -- No bare exceptions - -**Example:** -```bash -@build WS-001-01 -``` - -**Progress Tracking:** Real-time TodoWrite updates - ---- - -### @review - -**Location:** `.claude/skills/review/SKILL.md` - -**Purpose:** Quality check for completed feature - -**Checks:** -- All workstreams completed -- Tests passing -- Coverage ≥80% -- Type hints complete -- No TODO markers -- Clean architecture respected - -**Example:** -```bash -@review -``` - -**Output:** Pass/fail verdict with details - ---- - -### @deploy - -**Location:** `.claude/skills/deploy/SKILL.md` - -**Purpose:** Deploy feature to production - -**Process:** -1. Final quality verification -2. Create git tag -3. Merge to main branch -4. Generate changelog -5. Trigger deployment - -**Example:** -```bash -@deploy -``` - -**Prerequisites:** -- All workstreams completed -- @review passed -- Documentation updated - ---- - -## Utility Skills - -### @oneshot - -**Location:** `.claude/skills/oneshot/SKILL.md` - -**Purpose:** Autonomous feature execution - -**Features:** -- Spawns orchestrator agent -- Executes all workstreams -- Handles dependencies -- Checkpoint save/restore -- Background execution -- Progress notifications - -**Example:** -```bash -@oneshot - -# Background mode -@oneshot --background - -# Resume from checkpoint -@oneshot --resume -``` - -**Output:** Agent ID for resume capability - ---- - -### /debug - -**Location:** `.claude/skills/debug/SKILL.md` - -**Purpose:** Systematic debugging using scientific method - -**Process:** -1. Observe problem -2. Form hypothesis -3. Design experiment -4. Run experiment -5. Update hypothesis - -**Example:** -```bash -/debug "Test fails when running full suite" -``` - -**Method:** Evidence-based root cause analysis - ---- - -### @issue - -**Location:** `.claude/skills/issue/SKILL.md` - -**Purpose:** Bug routing and classification - -**Process:** -1. Analyze bug description -2. Classify severity (P0/P1/P2/P3) -3. Route to appropriate fix command - - P0 → @hotfix - - P1/P2 → @bugfix - - P3 → backlog - -**Example:** -```bash -@issue "Login fails on Firefox with error 500" -``` - -**Severity Classification:** -- **P0** - Critical security, data loss, production down -- **P1** - Major functionality broken -- **P2** - Minor issues, workarounds available -- **P3** - Cosmetic, nice to have - ---- - -### @hotfix - -**Location:** `.claude/skills/hotfix/SKILL.md` - -**Purpose:** Emergency fix for P0 issues - -**Characteristics:** -- Branch from main -- Minimal changes only -- Deploy < 2 hours -- Skip full process -- Direct to production - -**Example:** -```bash -@hotfix "Production database connection fails" -``` - -**Workflow:** -1. Create hotfix branch from main -2. Implement minimal fix -3. Fast verification -4. Deploy immediately -5. Create regular WS for follow-up - ---- - -### @bugfix - -**Location:** `.claude/skills/bugfix/SKILL.md` - -**Purpose:** Quality fix for P1/P2 issues - -**Characteristics:** -- Branch from feature/develop -- Full TDD cycle -- Quality gates enforced -- No production deploy - -**Example:** -```bash -@bugfix "User profile image not loading" -``` - -**Workflow:** -1. Create bugfix branch -2. Full @build process -3. Quality verification -4. Merge to feature branch - ---- - -## Internal Skills - -### /tdd - -**Location:** `.claude/skills/tdd/SKILL.md` - -**Purpose:** TDD cycle enforcement (internal) - -**Process:** -1. **Red** - Write failing test -2. **Green** - Write minimal code -3. **Refactor** - Improve code - -**Called By:** @build skill (automatic) - -**Not for direct user invocation** - ---- - -## Skill Development - -### Creating a New Skill - -**Directory Structure:** -``` -.claude/skills/{skill_name}/ -├── SKILL.md # Skill definition -├── prompt.md # Optional: Additional prompts -└── examples/ # Optional: Usage examples -``` - -**SKILL.md Format:** -```markdown -# @skill-name - -One-line description. - -## Usage -\`\`\`bash -@skill-name "argument" -\`\`\` - -## Process -1. Step one -2. Step two -3. Step three - -## Output -What the skill produces - -## Examples -Common usage examples -``` - -### Skill Best Practices - -**DO ✅:** -- Use clear, descriptive names -- Provide usage examples -- Document all steps -- Handle errors gracefully -- Validate inputs - -**DON'T ❌:** -- Don't create overlapping skills -- Don't skip error handling -- Don't omit documentation -- Don't make skills too complex - ---- - -## Skill Invocation Flow - -### Standard Feature Flow - -``` -@feature - ↓ (calls) -@idea (gathers requirements) - ↓ (outputs) -@design (plans workstreams) - ↓ (outputs) -@build (executes each WS) - ↓ (repeats for all WS) -@review (quality check) - ↓ (if passed) -@deploy (production) -``` - -### Autonomous Flow - -``` -@feature - ↓ -@oneshot (spawns orchestrator) - ↓ -Orchestrator agent executes all workstreams - ↓ -@review + @deploy -``` - -### Bug Fix Flow - -``` -@issue - ↓ (classifies) -@hotfix OR @bugfix - ↓ -@review (quality check) -``` - ---- - -## Quick Reference - -| Skill | Purpose | Time | User-Facing | -|-------|---------|------|-------------| -| `@feature` | Create feature | 10-15 min | ✅ | -| `@idea` | Requirements | 5-10 min | ❌ Internal | -| `@design` | Plan workstreams | 5-10 min | ✅ | -| `@build` | Execute workstream | 30-90 min | ✅ | -| `@review` | Quality check | 5-10 min | ✅ | -| `@deploy` | Deploy feature | 5-10 min | ✅ | -| `@oneshot` | Autonomous exec | 2-6 hours | ✅ | -| `/debug` | Debug issue | 15-60 min | ✅ | -| `@issue` | Report bug | 2-5 min | ✅ | -| `@hotfix` | Emergency fix | < 2 hours | ✅ | -| `@bugfix` | Quality fix | 1-4 hours | ✅ | -| `/tdd` | TDD enforcement | Auto | ❌ Internal | - ---- - -## See Also - -- [commands.md](commands.md) - Command reference -- [quality-gates.md](quality-gates.md) - Quality standards -- [beginner/02-common-tasks.md](../beginner/02-common-tasks.md) - Common workflows - ---- - -**Version:** SDP v0.9.8 -**Updated:** 2026-02-26 +- [../CLI_REFERENCE.md](../CLI_REFERENCE.md) for current CLI surfaces +- [../PROTOCOL.md](../PROTOCOL.md) for the current protocol overview +- [../../prompts/README.md](../../prompts/README.md) for prompt layout diff --git a/prompts/README.md b/prompts/README.md index 26841ee6..10ba2fdc 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -2,6 +2,7 @@ This repository uses a single canonical prompt source: +- Commands: `prompts/commands/*.md` - Skills: `prompts/skills/*/SKILL.md` - Agents: `prompts/agents/*.md`