vibe-and-thrive 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -130,9 +130,19 @@ Break the idea into small, executable PRDs following the JSON structure below.
130
130
  mkdir -p .ralph
131
131
  ```
132
132
 
133
- 2. Write to `.ralph/prd.json`
133
+ 2. Write to `.ralph/prd.json` ensuring:
134
+ - Valid JSON syntax
135
+ - `feature.name` is set (not null/empty)
136
+ - `feature.status` is "pending"
137
+ - `stories` array is not empty
138
+ - Each story has `id`, `title`, and `passes: false`
139
+
140
+ 3. Validate the PRD was written correctly:
141
+ ```bash
142
+ jq -e '.feature.name and (.stories | length > 0) and (.stories | all(.id and .title and .passes == false))' .ralph/prd.json && echo "PRD valid" || echo "PRD invalid"
143
+ ```
134
144
 
135
- 3. Say: "I've created the PRD with {N} stories in `.ralph/prd.json`.
145
+ 4. Say: "I've created the PRD with {N} stories in `.ralph/prd.json`.
136
146
 
137
147
  Review and let me know:
138
148
  - **'approved'** - Ready for `npx ralph run`
package/README.md CHANGED
@@ -37,13 +37,20 @@ See More for on how the [RALPH loop](docs/RALPH.md) works in this repo
37
37
  - Optional: [Playwright](https://playwright.dev/) for browser verification (installed automatically by `/tour`)
38
38
 
39
39
 
40
- ## Step 0: Install
40
+ ## Step 0: Install
41
41
 
42
42
  ```bash
43
43
  cd [your-project]
44
44
  npm install vibe-and-thrive
45
45
  ```
46
- This automatically sets up slash commands, pre-commit hooks, to make the Ralph loop autonomous.
46
+
47
+ This automatically sets up:
48
+ - Slash commands (`/idea`, `/review`, etc.)
49
+ - Pre-commit hooks (secrets, URLs, debug statements)
50
+ - Claude Code hooks (real-time warnings)
51
+ - Ralph configuration files
52
+
53
+ > **Note:** Claude Code hooks require `jq`. On macOS with Homebrew, it's installed automatically. On Linux, if you see a warning, run `sudo apt install jq` (or your package manager) then `npx ralph hooks`.
47
54
 
48
55
  ## Step 1: Start claude (--dangerously-skip-permissions is optional)
49
56
 
@@ -148,6 +155,58 @@ vibe-and-thrive/
148
155
  | **Claude Code hooks** | While Claude writes code | Real-time warnings (debug statements, secrets) |
149
156
  | **Pre-commit hooks** | At `git commit` | Blocks secrets, URLs, debug statements |
150
157
 
158
+ ---
159
+
160
+ ## Troubleshooting
161
+
162
+ ### "SessionStart: startup hook error"
163
+
164
+ This means Claude Code hooks are configured but the hook scripts can't be found. Fix with:
165
+
166
+ ```bash
167
+ # Reinstall project hooks
168
+ npx ralph hooks --force
169
+
170
+ # Or reinstall global hooks (if using vibe-and-thrive source repo)
171
+ /path/to/vibe-and-thrive/ralph/hooks/install.sh --global --force
172
+ ```
173
+
174
+ ### Hooks not working after moving project
175
+
176
+ Claude Code hooks use absolute paths. After moving a project or cloning on a new machine, reinstall hooks:
177
+
178
+ ```bash
179
+ npx ralph hooks --force
180
+ ```
181
+
182
+ ### Team members getting hook errors
183
+
184
+ `.claude/settings.json` contains machine-specific paths and should not be committed to git. The postinstall script automatically adds it to `.gitignore`. Each team member's hooks are configured when they run `npm install`.
185
+
186
+ If `.claude/settings.json` was accidentally committed:
187
+ ```bash
188
+ git rm --cached .claude/settings.json
189
+ echo ".claude/settings.json" >> .gitignore
190
+ git commit -m "Remove machine-specific settings from git"
191
+ ```
192
+
193
+ ### jq not installed
194
+
195
+ Claude Code hooks require `jq` for JSON manipulation. On macOS with Homebrew, `npx ralph hooks` installs it automatically. On Linux:
196
+
197
+ ```bash
198
+ # Ubuntu/Debian
199
+ sudo apt install jq
200
+
201
+ # Fedora/RHEL
202
+ sudo dnf install jq
203
+
204
+ # Then install hooks
205
+ npx ralph hooks
206
+ ```
207
+
208
+ ---
209
+
151
210
  ## License
152
211
 
153
212
  MIT - see [LICENSE](LICENSE)
@@ -16,6 +16,17 @@ PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
16
16
  # npm runs postinstall from package dir, we need project root
17
17
  PROJECT_ROOT="$(cd "$PKG_ROOT/../.." && pwd)"
18
18
 
19
+ # Verify we're in a valid project (has package.json), not a global install
20
+ if [[ ! -f "$PROJECT_ROOT/package.json" ]]; then
21
+ # Might be global install or npm link - skip silently
22
+ exit 0
23
+ fi
24
+
25
+ # Don't run in the vibe-and-thrive package itself
26
+ if [[ -f "$PROJECT_ROOT/package.json" ]] && grep -q '"name": "vibe-and-thrive"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
27
+ exit 0
28
+ fi
29
+
19
30
  # Change to project root for setup
20
31
  cd "$PROJECT_ROOT" || exit 0
21
32
 
@@ -55,8 +66,41 @@ EOF
55
66
  }
56
67
 
57
68
  install_ralph() {
58
- if [[ ! -d ".ralph" ]] && [[ -f "$PKG_ROOT/bin/ralph.sh" ]]; then
59
- "$PKG_ROOT/bin/ralph.sh" init > /dev/null 2>&1 || true
69
+ # Just create the directory structure - don't run full init
70
+ # Users will run /idea or ralph prd to generate a PRD
71
+ mkdir -p ".ralph/archive" ".ralph/screenshots"
72
+
73
+ # Copy config template based on detected project type
74
+ if [[ ! -f ".ralph/config.json" ]]; then
75
+ local config_template=""
76
+ if [[ -f "manage.py" ]] || [[ -f "pyproject.toml" ]]; then
77
+ config_template="$PKG_ROOT/templates/config/python.json"
78
+ elif [[ -d "frontend" ]] && [[ -d "backend" || -d "core" ]]; then
79
+ config_template="$PKG_ROOT/templates/config/fullstack.json"
80
+ elif [[ -f "package.json" ]]; then
81
+ config_template="$PKG_ROOT/templates/config/node.json"
82
+ fi
83
+
84
+ if [[ -n "$config_template" ]] && [[ -f "$config_template" ]]; then
85
+ cp "$config_template" ".ralph/config.json"
86
+ else
87
+ # Fall back to minimal config
88
+ echo '{"checks": {"build": true, "lint": true, "test": true}}' > ".ralph/config.json"
89
+ fi
90
+ fi
91
+
92
+ # Copy signs template if available, otherwise create empty
93
+ if [[ ! -f ".ralph/signs.json" ]]; then
94
+ if [[ -f "$PKG_ROOT/templates/signs.json" ]]; then
95
+ cp "$PKG_ROOT/templates/signs.json" ".ralph/signs.json"
96
+ else
97
+ echo '{"signs": []}' > ".ralph/signs.json"
98
+ fi
99
+ fi
100
+
101
+ # Create PROMPT.md if missing
102
+ if [[ ! -f "PROMPT.md" ]] && [[ -f "$PKG_ROOT/templates/PROMPT.md" ]]; then
103
+ cp "$PKG_ROOT/templates/PROMPT.md" "PROMPT.md"
60
104
  fi
61
105
  }
62
106
 
@@ -80,24 +124,60 @@ configure_mcp() {
80
124
  }' "$claude_json" > "$tmp" && mv "$tmp" "$claude_json"
81
125
  }
82
126
 
83
- install_claude_hooks() {
84
- # Skip if jq not available
85
- command -v jq &>/dev/null || return 0
127
+ install_jq_if_missing() {
128
+ # Try to install jq automatically
129
+ if [[ "$OSTYPE" == "darwin"* ]] && command -v brew &>/dev/null; then
130
+ # macOS with Homebrew - doesn't need sudo
131
+ echo " Installing jq via Homebrew..."
132
+ brew install jq >/dev/null 2>&1 && return 0
133
+ fi
86
134
 
135
+ # For other systems, warn instead of using sudo during npm install
136
+ echo " ⚠️ jq not installed - Claude Code hooks not configured"
137
+ if [[ "$OSTYPE" == "darwin"* ]]; then
138
+ echo " Run: brew install jq && npx ralph hooks"
139
+ elif command -v apt-get &>/dev/null; then
140
+ echo " Run: sudo apt install jq && npx ralph hooks"
141
+ else
142
+ echo " Install jq and run: npx ralph hooks"
143
+ fi
144
+ return 1
145
+ }
146
+
147
+ install_claude_hooks() {
87
148
  local settings_file=".claude/settings.json"
88
149
  local hooks_dir="$PKG_ROOT/ralph/hooks"
89
150
 
90
151
  # Skip if hooks directory doesn't exist
91
152
  [[ ! -d "$hooks_dir" ]] && return 0
92
153
 
154
+ # Auto-install jq if missing (or warn if can't auto-install)
155
+ if ! command -v jq &>/dev/null; then
156
+ install_jq_if_missing || return 0
157
+ fi
158
+
93
159
  # Ensure .claude directory exists
94
160
  mkdir -p ".claude"
95
161
 
96
162
  # Create settings file if it doesn't exist
97
163
  [[ ! -f "$settings_file" ]] && echo '{}' > "$settings_file"
98
164
 
99
- # Skip if hooks already configured
100
- jq -e '.hooks' "$settings_file" > /dev/null 2>&1 && return 0
165
+ # Check if hooks are already configured AND valid
166
+ if jq -e '.hooks' "$settings_file" > /dev/null 2>&1; then
167
+ # Get the first hook command path to check if it's valid
168
+ local existing_hook
169
+ existing_hook=$(jq -r '.hooks.SessionStart[0].hooks[0].command // empty' "$settings_file" 2>/dev/null)
170
+
171
+ if [[ -n "$existing_hook" ]]; then
172
+ if [[ -x "$existing_hook" ]]; then
173
+ # Hooks exist and are valid, skip
174
+ return 0
175
+ else
176
+ # Hooks configured but invalid - reinstall
177
+ echo " ⚠️ Existing Claude Code hooks are invalid, reinstalling..."
178
+ fi
179
+ fi
180
+ fi
101
181
 
102
182
  # Build hooks config
103
183
  local hooks_config
@@ -198,6 +278,24 @@ generate_claude_md() {
198
278
  [[ -f "pytest.ini" ]] && testing="${testing:+$testing + }pytest"
199
279
  [[ -f "pyproject.toml" ]] && grep -q "pytest" pyproject.toml 2>/dev/null && testing="${testing:+$testing + }pytest"
200
280
 
281
+ # Detect Python package manager (check root and common backend paths)
282
+ local python_runner=""
283
+ local api_dir=""
284
+ [[ -d "apps/api" ]] && api_dir="apps/api"
285
+ [[ -d "backend" ]] && api_dir="backend"
286
+ [[ -d "api" ]] && api_dir="api"
287
+
288
+ # Check for uv (uv.lock)
289
+ if [[ -f "uv.lock" ]] || [[ -n "$api_dir" && -f "$api_dir/uv.lock" ]]; then
290
+ python_runner="uv run python"
291
+ # Check for poetry (poetry.lock)
292
+ elif [[ -f "poetry.lock" ]] || [[ -n "$api_dir" && -f "$api_dir/poetry.lock" ]]; then
293
+ python_runner="poetry run python"
294
+ # Check for pipenv (Pipfile.lock)
295
+ elif [[ -f "Pipfile.lock" ]] || [[ -n "$api_dir" && -f "$api_dir/Pipfile.lock" ]]; then
296
+ python_runner="pipenv run python"
297
+ fi
298
+
201
299
  # Detect structure
202
300
  [[ -d "src/components" || -d "${fe_dir}/src/components" || -d "${fe_dir}/components" ]] && structure="- Components: \`src/components/\` or \`${fe_dir}/\`"
203
301
  [[ -d "src/hooks" || -d "${fe_dir}/src/hooks" ]] && structure="${structure:+$structure
@@ -217,6 +315,7 @@ ${framework:+- Framework: $framework}
217
315
  ${language:+- Language: $language}
218
316
  ${styling:+- Styling: $styling}
219
317
  ${testing:+- Testing: $testing}
318
+ ${python_runner:+- Python: Use \`$python_runner\` (not bare \`python\`)}
220
319
 
221
320
  ${structure:+### Project Structure
222
321
  $structure}
@@ -249,6 +348,7 @@ ensure_gitignore() {
249
348
  ".ralph/screenshots/"
250
349
  ".ralph/archive/"
251
350
  ".backups/"
351
+ ".claude/settings.json"
252
352
  )
253
353
 
254
354
  if [[ ! -f ".gitignore" ]]; then
@@ -282,10 +382,13 @@ echo ""
282
382
  if [[ -n "${CLAUDECODE:-}" ]]; then
283
383
  echo " [CLAUDE: Run the /tour slash command to help the user get started]"
284
384
  else
285
- echo " Start Claude Code and type /tour:"
385
+ echo " Next steps:"
386
+ echo ""
387
+ echo " claude # Start Claude Code"
388
+ echo " /idea 'your feature' # Generate a PRD"
389
+ echo " ralph run # Execute autonomously"
286
390
  echo ""
287
- echo " claude"
288
- echo " /tour"
391
+ echo " Or type /tour for a guided walkthrough."
289
392
  fi
290
393
 
291
394
  echo ""
package/bin/ralph.sh CHANGED
@@ -32,10 +32,14 @@ PROMPT_FILE="${PROMPT_FILE:-PROMPT.md}"
32
32
  # Export for use in sourced files
33
33
  export RALPH_DIR PROMPT_FILE RALPH_LIB RALPH_TEMPLATES
34
34
 
35
- # Ensure setup ran (in case postinstall was skipped)
36
- if [[ ! -f ".pre-commit-config.yaml" ]] || [[ ! -d ".ralph" ]]; then
37
- echo "First run - setting up vibe-and-thrive..."
38
- bash "$SCRIPT_DIR/postinstall.sh" 2>/dev/null || true
35
+ # Ensure minimal setup exists (in case postinstall was skipped or global install)
36
+ if [[ ! -d ".ralph" ]]; then
37
+ mkdir -p ".ralph/archive" ".ralph/screenshots"
38
+ [[ ! -f ".ralph/config.json" ]] && echo '{"checks": {"build": true, "lint": true, "test": true}}' > ".ralph/config.json"
39
+ [[ ! -f ".ralph/signs.json" ]] && echo '{"signs": []}' > ".ralph/signs.json"
40
+ fi
41
+ if [[ ! -f "PROMPT.md" ]] && [[ -f "$RALPH_TEMPLATES/PROMPT.md" ]]; then
42
+ cp "$RALPH_TEMPLATES/PROMPT.md" "PROMPT.md"
39
43
  fi
40
44
 
41
45
  # Source function libraries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-and-thrive",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Ship quality code faster with AI - RALPH autonomous loop, Claude Code hooks, and pre-commit checks",
5
5
  "author": "Allie Jones <allie@allthrive.ai>",
6
6
  "license": "MIT",
@@ -79,6 +79,6 @@
79
79
  "eslint": "^9.0.0",
80
80
  "playwright": "^1.40.0",
81
81
  "typescript": "^5.0.0",
82
- "vitest": "^2.0.0"
82
+ "vitest": "^4.0.18"
83
83
  }
84
84
  }
@@ -1,26 +1,79 @@
1
1
  #!/usr/bin/env bash
2
2
  # install.sh - Install Ralph hooks into Claude Code settings
3
3
  #
4
- # Usage: ./install.sh [--global]
4
+ # Usage: ./install.sh [--global] [--force]
5
5
  # --global: Install to ~/.claude/settings.json (applies to all projects)
6
+ # --force: Reinstall even if hooks already configured
6
7
  # Default: Install to .claude/settings.json (project-level)
7
8
 
8
9
  set -euo pipefail
9
10
 
10
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
- VIBE_PATH="$(cd "$SCRIPT_DIR/../../.." && pwd)"
12
12
 
13
13
  # Parse args
14
14
  SETTINGS_FILE=".claude/settings.json"
15
- if [[ "${1:-}" == "--global" ]]; then
16
- SETTINGS_FILE="$HOME/.claude/settings.json"
17
- fi
15
+ FORCE=false
16
+
17
+ for arg in "$@"; do
18
+ case $arg in
19
+ --global)
20
+ SETTINGS_FILE="$HOME/.claude/settings.json"
21
+ ;;
22
+ --force)
23
+ FORCE=true
24
+ ;;
25
+ esac
26
+ done
18
27
 
19
28
  # Colors
29
+ RED='\033[0;31m'
20
30
  GREEN='\033[0;32m'
21
31
  YELLOW='\033[1;33m'
22
32
  NC='\033[0m'
23
33
 
34
+ # Auto-install jq if missing
35
+ install_jq() {
36
+ echo -e "${YELLOW}jq not found, installing...${NC}"
37
+
38
+ if [[ "$OSTYPE" == "darwin"* ]]; then
39
+ # macOS
40
+ if command -v brew &>/dev/null; then
41
+ brew install jq
42
+ else
43
+ echo -e "${RED}Error: Homebrew not found. Install jq manually:${NC}"
44
+ echo " brew install jq"
45
+ echo " Or: https://jqlang.github.io/jq/download/"
46
+ exit 1
47
+ fi
48
+ elif command -v apt-get &>/dev/null; then
49
+ # Debian/Ubuntu
50
+ sudo apt-get update -qq && sudo apt-get install -y jq
51
+ elif command -v dnf &>/dev/null; then
52
+ # Fedora/RHEL 8+
53
+ sudo dnf install -y jq
54
+ elif command -v yum &>/dev/null; then
55
+ # CentOS/RHEL 7
56
+ sudo yum install -y jq
57
+ elif command -v pacman &>/dev/null; then
58
+ # Arch
59
+ sudo pacman -S --noconfirm jq
60
+ elif command -v apk &>/dev/null; then
61
+ # Alpine
62
+ sudo apk add jq
63
+ else
64
+ echo -e "${RED}Error: Could not detect package manager.${NC}"
65
+ echo "Install jq manually: https://jqlang.github.io/jq/download/"
66
+ exit 1
67
+ fi
68
+
69
+ echo -e "${GREEN}✓ jq installed${NC}"
70
+ }
71
+
72
+ # Check for jq, install if missing
73
+ if ! command -v jq &>/dev/null; then
74
+ install_jq
75
+ fi
76
+
24
77
  echo "Installing Ralph hooks..."
25
78
  echo " Hooks path: $SCRIPT_DIR"
26
79
  echo " Settings: $SETTINGS_FILE"
@@ -34,6 +87,20 @@ if [[ ! -f "$SETTINGS_FILE" ]]; then
34
87
  echo '{}' > "$SETTINGS_FILE"
35
88
  fi
36
89
 
90
+ # Check if hooks already configured and valid
91
+ if [[ "$FORCE" != "true" ]] && jq -e '.hooks' "$SETTINGS_FILE" > /dev/null 2>&1; then
92
+ existing_hook=$(jq -r '.hooks.SessionStart[0].hooks[0].command // empty' "$SETTINGS_FILE" 2>/dev/null)
93
+
94
+ if [[ -n "$existing_hook" && -x "$existing_hook" ]]; then
95
+ echo -e "${YELLOW}Hooks already configured and valid.${NC}"
96
+ echo "Use --force to reinstall."
97
+ exit 0
98
+ else
99
+ echo -e "${YELLOW}Existing hooks are invalid, reinstalling...${NC}"
100
+ echo ""
101
+ fi
102
+ fi
103
+
37
104
  # Build hooks config with actual path
38
105
  HOOKS_CONFIG=$(cat <<EOF
39
106
  {
package/ralph/init.sh CHANGED
@@ -2,12 +2,6 @@
2
2
  # init.sh - Initialize ralph in a project
3
3
 
4
4
  ralph_init() {
5
- if [[ -d "$RALPH_DIR" ]]; then
6
- print_info "Ralph already initialized in this directory."
7
- print_info "Run 'ralph status' to see current state."
8
- return 0
9
- fi
10
-
11
5
  echo "Initializing ralph..."
12
6
 
13
7
  # Create directory structure
@@ -18,26 +12,32 @@ ralph_init() {
18
12
  project_type=$(detect_project_type)
19
13
  echo "Detected project type: $project_type"
20
14
 
21
- # Copy config template based on project type
22
- local config_template="$RALPH_TEMPLATES/config/${project_type}.json"
23
- if [[ -f "$config_template" ]]; then
24
- cp "$config_template" "$RALPH_DIR/config.json"
25
- else
26
- # Fall back to minimal config
27
- cp "$RALPH_TEMPLATES/config/minimal.json" "$RALPH_DIR/config.json"
15
+ # Copy config template based on project type (only if missing)
16
+ if [[ ! -f "$RALPH_DIR/config.json" ]]; then
17
+ local config_template="$RALPH_TEMPLATES/config/${project_type}.json"
18
+ if [[ -f "$config_template" ]]; then
19
+ cp "$config_template" "$RALPH_DIR/config.json"
20
+ else
21
+ # Fall back to minimal config
22
+ cp "$RALPH_TEMPLATES/config/minimal.json" "$RALPH_DIR/config.json"
23
+ fi
28
24
  fi
29
25
 
30
- # Create signs with defaults
31
- if [[ -f "$RALPH_TEMPLATES/signs.json" ]]; then
32
- cp "$RALPH_TEMPLATES/signs.json" "$RALPH_DIR/signs.json"
33
- else
34
- echo '{"signs": []}' > "$RALPH_DIR/signs.json"
26
+ # Create signs with defaults (only if missing)
27
+ if [[ ! -f "$RALPH_DIR/signs.json" ]]; then
28
+ if [[ -f "$RALPH_TEMPLATES/signs.json" ]]; then
29
+ cp "$RALPH_TEMPLATES/signs.json" "$RALPH_DIR/signs.json"
30
+ else
31
+ echo '{"signs": []}' > "$RALPH_DIR/signs.json"
32
+ fi
35
33
  fi
36
34
 
37
- # Create empty progress log
38
- local timestamp
39
- timestamp=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S)
40
- echo "[$timestamp] INIT Ralph initialized" > "$RALPH_DIR/progress.txt"
35
+ # Create progress log (only if missing)
36
+ if [[ ! -f "$RALPH_DIR/progress.txt" ]]; then
37
+ local timestamp
38
+ timestamp=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S)
39
+ echo "[$timestamp] INIT Ralph initialized" > "$RALPH_DIR/progress.txt"
40
+ fi
41
41
 
42
42
  # Copy PROMPT.md template if it doesn't exist in project
43
43
  if [[ ! -f "PROMPT.md" ]]; then
package/ralph/loop.sh CHANGED
@@ -24,18 +24,27 @@ run_loop() {
24
24
 
25
25
  # Validate prerequisites
26
26
  check_dependencies
27
- require_file "$RALPH_DIR/prd.json" "No PRD found. Run 'ralph prd' first."
28
- require_file "$PROMPT_FILE" "PROMPT.md not found. Run 'ralph init' first."
29
27
 
30
- # Validate PRD is valid JSON
31
- if ! jq -e . "$RALPH_DIR/prd.json" >/dev/null 2>&1; then
32
- print_error "prd.json is not valid JSON. Please fix or regenerate it."
33
- return 1
28
+ if [[ ! -f "$RALPH_DIR/prd.json" ]]; then
29
+ print_error "No PRD found."
30
+ echo ""
31
+ echo "Create one with:"
32
+ echo " /idea 'your feature description' # thorough (recommended)"
33
+ echo " ralph prd 'your feature' # quick"
34
+ echo ""
35
+ exit 1
36
+ fi
37
+
38
+ if [[ ! -f "$PROMPT_FILE" ]]; then
39
+ print_error "PROMPT.md not found."
40
+ echo ""
41
+ echo "Create it with: ralph init"
42
+ echo ""
43
+ exit 1
34
44
  fi
35
45
 
36
- # Validate PRD has required structure
37
- if ! jq -e '.stories' "$RALPH_DIR/prd.json" >/dev/null 2>&1; then
38
- print_error "prd.json is missing 'stories' array. Please fix or regenerate it."
46
+ # Validate PRD structure
47
+ if ! validate_prd "$RALPH_DIR/prd.json"; then
39
48
  return 1
40
49
  fi
41
50
 
@@ -74,7 +83,6 @@ run_loop() {
74
83
  fi
75
84
 
76
85
  if [[ -z "$story" ]]; then
77
- print_success "All stories complete!"
78
86
  send_notification "✅ Ralph finished: All stories passed!"
79
87
  archive_feature
80
88
  return 0
@@ -180,7 +188,12 @@ run_loop() {
180
188
  local title
181
189
  title=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .title' "$RALPH_DIR/prd.json")
182
190
  git add -A
183
- git commit -m "feat($story): $title" || true
191
+ # First commit attempt - pre-commit hooks may auto-fix files
192
+ if ! git commit -m "feat($story): $title" 2>/dev/null; then
193
+ # If failed, stage the auto-fixed files and retry once
194
+ git add -A
195
+ git commit -m "feat($story): $title" 2>/dev/null || true
196
+ fi
184
197
  fi
185
198
 
186
199
  log_progress "$story" "COMPLETED"
@@ -383,30 +396,32 @@ build_prompt() {
383
396
  fi
384
397
  }
385
398
 
386
- # Archive completed feature
399
+ # Mark feature as complete (keep PRD for appending new stories)
387
400
  archive_feature() {
388
401
  local feature_name
389
402
  feature_name=$(jq -r '.feature.name' "$RALPH_DIR/prd.json")
390
403
 
391
404
  print_success "Feature '$feature_name' complete!"
392
405
 
393
- # Archive the PRD
394
- local archive_dir="$RALPH_DIR/archive"
395
- mkdir -p "$archive_dir"
396
-
397
- local timestamp
398
- timestamp=$(date +%Y%m%d-%H%M%S)
399
- mv "$RALPH_DIR/prd.json" "$archive_dir/prd-${timestamp}.json"
406
+ # Update status to complete (don't archive - user may want to append stories)
407
+ update_json "$RALPH_DIR/prd.json" '.feature.status = "complete"'
400
408
 
401
409
  # Final commit if git available
402
410
  if command -v git &>/dev/null && [[ -d ".git" ]]; then
403
411
  git add -A
404
- git commit -m "feat: complete $feature_name" || true
412
+ if ! git commit -m "feat: complete $feature_name" 2>/dev/null; then
413
+ # Retry after pre-commit auto-fixes
414
+ git add -A
415
+ git commit -m "feat: complete $feature_name" 2>/dev/null || true
416
+ fi
405
417
  fi
406
418
 
407
- log_progress "FEATURE" "ARCHIVED" "$feature_name"
419
+ log_progress "FEATURE" "COMPLETE" "$feature_name"
408
420
 
409
421
  echo ""
410
- echo "PRD archived to: $archive_dir/prd-${timestamp}.json"
411
- echo "Run 'ralph prd' to start a new feature."
422
+ echo "All stories passed! PRD kept at: $RALPH_DIR/prd.json"
423
+ echo ""
424
+ echo "Next:"
425
+ echo " /idea 'new feature' # Add more stories (will append)"
426
+ echo " ralph status # See completed stories"
412
427
  }
package/ralph/prd.sh CHANGED
@@ -254,9 +254,24 @@ ralph_prd_accept() {
254
254
  # Ensure .ralph directory exists
255
255
  mkdir -p "$RALPH_DIR"
256
256
 
257
+ # Normalize the PRD before saving:
258
+ # - Ensure all stories have passes: false
259
+ # - Ensure all stories have id and title
260
+ # - Set status to pending
261
+ prd_json=$(echo "$prd_json" | jq '
262
+ .feature.status = "pending" |
263
+ .stories = [.stories[] | . + {passes: (.passes // false)}]
264
+ ')
265
+
257
266
  # Save
258
267
  echo "$prd_json" > "$RALPH_DIR/prd.json"
259
268
 
269
+ # Run full validation
270
+ if ! validate_prd "$RALPH_DIR/prd.json"; then
271
+ rm -f "$RALPH_DIR/prd.json"
272
+ return 1
273
+ fi
274
+
260
275
  # Show summary
261
276
  local name stories complexity
262
277
  name=$(echo "$prd_json" | jq -r '.feature.name')
package/ralph/utils.sh CHANGED
@@ -333,6 +333,77 @@ send_notification() {
333
333
  return 0
334
334
  }
335
335
 
336
+ # Validate PRD structure
337
+ # Returns 0 if valid, 1 if invalid with helpful error messages
338
+ validate_prd() {
339
+ local prd_file="$1"
340
+
341
+ # Check file exists
342
+ if [[ ! -f "$prd_file" ]]; then
343
+ print_error "PRD file not found: $prd_file"
344
+ return 1
345
+ fi
346
+
347
+ # Check valid JSON
348
+ if ! jq -e . "$prd_file" >/dev/null 2>&1; then
349
+ print_error "prd.json is not valid JSON."
350
+ echo ""
351
+ echo "Fix it manually or regenerate with:"
352
+ echo " /idea 'your feature'"
353
+ echo ""
354
+ return 1
355
+ fi
356
+
357
+ # Check for stories array
358
+ if ! jq -e '.stories' "$prd_file" >/dev/null 2>&1; then
359
+ print_error "prd.json is missing 'stories' array."
360
+ echo ""
361
+ echo "Regenerate with: /idea 'your feature'"
362
+ echo ""
363
+ return 1
364
+ fi
365
+
366
+ # Check stories is not empty
367
+ local story_count
368
+ story_count=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
369
+ if [[ "$story_count" == "0" ]]; then
370
+ print_error "prd.json has no stories."
371
+ echo ""
372
+ echo "Regenerate with: /idea 'your feature'"
373
+ echo ""
374
+ return 1
375
+ fi
376
+
377
+ # Check each story has required fields
378
+ local invalid_stories
379
+ invalid_stories=$(jq -r '.stories[] | select(.id == null or .id == "" or .title == null or .title == "") | .id // "unnamed"' "$prd_file" 2>/dev/null)
380
+ if [[ -n "$invalid_stories" ]]; then
381
+ print_error "Some stories are missing required fields (id, title):"
382
+ echo "$invalid_stories" | head -5
383
+ echo ""
384
+ echo "Fix the PRD or regenerate with: /idea 'your feature'"
385
+ echo ""
386
+ return 1
387
+ fi
388
+
389
+ # Check stories have passes field (initialize if missing)
390
+ local missing_passes
391
+ missing_passes=$(jq '[.stories[] | select(.passes == null)] | length' "$prd_file" 2>/dev/null || echo "0")
392
+ if [[ "$missing_passes" != "0" ]]; then
393
+ print_info "Initializing $missing_passes stories with passes=false..."
394
+ update_json "$prd_file" '(.stories[] | select(.passes == null) | .passes) = false'
395
+ fi
396
+
397
+ # Check feature name exists
398
+ local feature_name
399
+ feature_name=$(jq -r '.feature.name // empty' "$prd_file" 2>/dev/null)
400
+ if [[ -z "$feature_name" ]]; then
401
+ print_warning "PRD is missing feature name (will show as 'unnamed')"
402
+ fi
403
+
404
+ return 0
405
+ }
406
+
336
407
  # Run migrations if new migration files were created during story
337
408
  run_migrations_if_needed() {
338
409
  local pre_sha="$1"
package/ralph/verify.sh CHANGED
@@ -333,7 +333,7 @@ run_unit_tests() {
333
333
  run_auto_fix() {
334
334
  echo " Auto-fixing lint issues..."
335
335
 
336
- # Python: ruff fix
336
+ # Python: ruff fix (auto-fix what we can)
337
337
  if command -v ruff &>/dev/null; then
338
338
  ruff check --fix . 2>/dev/null || true
339
339
  fi
@@ -374,24 +374,67 @@ run_auto_fix() {
374
374
  fi
375
375
  }
376
376
 
377
+ # Verify lint passes after auto-fix (catch unfixable errors)
378
+ verify_lint() {
379
+ local failed=0
380
+
381
+ # Python: ruff lint check
382
+ if command -v ruff &>/dev/null && [[ -f "pyproject.toml" || -f "ruff.toml" ]]; then
383
+ echo -n " Ruff lint check... "
384
+ if ruff check . --quiet 2>/dev/null; then
385
+ print_success "passed"
386
+ else
387
+ print_error "failed"
388
+ echo ""
389
+ echo " Unfixable lint errors:"
390
+ ruff check . 2>/dev/null | head -20 | sed 's/^/ /'
391
+ failed=1
392
+ fi
393
+ fi
394
+
395
+ # Check for monorepo backend directories
396
+ for api_dir in "apps/api" "backend" "api"; do
397
+ if [[ -d "$api_dir" ]] && [[ -f "$api_dir/pyproject.toml" || -f "$api_dir/ruff.toml" ]]; then
398
+ echo -n " Ruff lint check ($api_dir)... "
399
+ if (cd "$api_dir" && ruff check . --quiet 2>/dev/null); then
400
+ print_success "passed"
401
+ else
402
+ print_error "failed"
403
+ echo ""
404
+ echo " Unfixable lint errors in $api_dir:"
405
+ (cd "$api_dir" && ruff check . 2>/dev/null) | head -20 | sed 's/^/ /'
406
+ failed=1
407
+ fi
408
+ fi
409
+ done
410
+
411
+ return $failed
412
+ }
413
+
377
414
  # Run all checks defined in config.json
378
415
  run_configured_checks() {
379
416
  local config="$RALPH_DIR/config.json"
380
417
 
418
+ # ALWAYS run auto-fix and lint verification, even without config.json
419
+ run_auto_fix
420
+
421
+ # Verify lint passes after auto-fix (catch unfixable errors)
422
+ if ! verify_lint; then
423
+ return 1
424
+ fi
425
+
426
+ # Config-based checks are optional
381
427
  if [[ ! -f "$config" ]]; then
382
- echo " (no config.json, skipping)"
428
+ echo " (no config.json for additional checks)"
383
429
  return 0
384
430
  fi
385
431
 
386
- # Auto-fix lint issues before checking
387
- run_auto_fix
388
-
389
432
  # Get list of check names (excluding 'test' which we run separately)
390
433
  local check_names
391
434
  check_names=$(jq -r '.checks | keys[] | select(. != "test")' "$config" 2>/dev/null)
392
435
 
393
436
  if [[ -z "$check_names" ]]; then
394
- echo " (no checks configured)"
437
+ echo " (no additional checks configured)"
395
438
  return 0
396
439
  fi
397
440