@the-bearded-bear/claude-craft 8.26.0 → 8.27.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.
Files changed (187) hide show
  1. package/Dev/i18n/base/Common/commands/aliases.md +48 -48
  2. package/Dev/i18n/base/Common/skills/sub-agents-patterns/SKILL.md +1 -0
  3. package/Dev/i18n/de/Common/agents/devops-engineer.md +19 -14
  4. package/Dev/i18n/de/Common/agents/migration-specialist.md +10 -15
  5. package/Dev/i18n/de/Common/agents/mlops-engineer.md +19 -15
  6. package/Dev/i18n/de/Common/agents/ralph-conductor.md +4 -15
  7. package/Dev/i18n/de/Common/agents/refactoring-specialist.md +0 -9
  8. package/Dev/i18n/de/Common/agents/tdd-coach.md +0 -9
  9. package/Dev/i18n/de/Common/agents/uiux-orchestrator.md +0 -9
  10. package/Dev/i18n/de/Common/commands/ralph-run.md +52 -0
  11. package/Dev/i18n/de/Common/commands/research-context7.md +5 -253
  12. package/Dev/i18n/de/Common/rules/12-context-management.md +10 -8
  13. package/Dev/i18n/de/Common/templates/settings.json.template +4 -0
  14. package/Dev/i18n/de/Python/skills/coding-standards/SKILL.md +1 -1
  15. package/Dev/i18n/de/Python/skills/testing-python/SKILL.md +1 -1
  16. package/Dev/i18n/de/Python/skills/tooling/SKILL.md +1 -1
  17. package/Dev/i18n/de/Symfony/commands/check-security.md +20 -20
  18. package/Dev/i18n/de/Symfony/skills/aggregates/SKILL.md +1 -1
  19. package/Dev/i18n/de/Symfony/skills/async/SKILL.md +1 -1
  20. package/Dev/i18n/de/Symfony/skills/cqrs/SKILL.md +1 -1
  21. package/Dev/i18n/de/Symfony/skills/domain-events/SKILL.md +1 -1
  22. package/Dev/i18n/de/Symfony/skills/i18n/SKILL.md +1 -1
  23. package/Dev/i18n/de/Symfony/skills/multitenant/SKILL.md +1 -1
  24. package/Dev/i18n/de/Symfony/skills/value-objects/SKILL.md +1 -1
  25. package/Dev/i18n/de/Team/commands/audit.md +29 -4
  26. package/Dev/i18n/de/Team/commands/delivery.md +29 -4
  27. package/Dev/i18n/de/Team/commands/security.md +31 -4
  28. package/Dev/i18n/de/Team/commands/sprint.md +30 -5
  29. package/Dev/i18n/de/UIUX/commands/audit.md +1 -30
  30. package/Dev/i18n/de/UIUX/commands/component-spec.md +1 -5
  31. package/Dev/i18n/de/UIUX/commands/orchestrator.md +5 -98
  32. package/Dev/i18n/de/Workflow/commands/auto-sprint.md +7 -4
  33. package/Dev/i18n/de/Workflow/commands/plan.md +1 -2
  34. package/Dev/i18n/en/Common/agents/devops-engineer.md +19 -14
  35. package/Dev/i18n/en/Common/agents/migration-specialist.md +10 -15
  36. package/Dev/i18n/en/Common/agents/mlops-engineer.md +19 -15
  37. package/Dev/i18n/en/Common/agents/ralph-conductor.md +4 -15
  38. package/Dev/i18n/en/Common/agents/refactoring-specialist.md +0 -9
  39. package/Dev/i18n/en/Common/agents/tdd-coach.md +0 -9
  40. package/Dev/i18n/en/Common/agents/uiux-orchestrator.md +0 -9
  41. package/Dev/i18n/en/Common/commands/ralph-run.md +52 -0
  42. package/Dev/i18n/en/Common/commands/research-context7.md +5 -253
  43. package/Dev/i18n/en/Common/rules/12-context-management.md +12 -10
  44. package/Dev/i18n/en/Common/templates/settings.json.template +4 -0
  45. package/Dev/i18n/en/Python/skills/coding-standards/SKILL.md +1 -1
  46. package/Dev/i18n/en/Python/skills/testing-python/SKILL.md +1 -1
  47. package/Dev/i18n/en/Python/skills/tooling/SKILL.md +1 -1
  48. package/Dev/i18n/en/Symfony/commands/check-security.md +20 -20
  49. package/Dev/i18n/en/Symfony/skills/aggregates/SKILL.md +1 -1
  50. package/Dev/i18n/en/Symfony/skills/async/SKILL.md +1 -1
  51. package/Dev/i18n/en/Symfony/skills/cqrs/SKILL.md +1 -1
  52. package/Dev/i18n/en/Symfony/skills/domain-events/SKILL.md +1 -1
  53. package/Dev/i18n/en/Symfony/skills/i18n/SKILL.md +1 -1
  54. package/Dev/i18n/en/Symfony/skills/multitenant/SKILL.md +1 -1
  55. package/Dev/i18n/en/Symfony/skills/value-objects/SKILL.md +1 -1
  56. package/Dev/i18n/en/Team/commands/audit.md +29 -4
  57. package/Dev/i18n/en/Team/commands/delivery.md +29 -4
  58. package/Dev/i18n/en/Team/commands/security.md +31 -4
  59. package/Dev/i18n/en/Team/commands/sprint.md +30 -5
  60. package/Dev/i18n/en/UIUX/commands/audit.md +1 -30
  61. package/Dev/i18n/en/UIUX/commands/component-spec.md +1 -5
  62. package/Dev/i18n/en/UIUX/commands/orchestrator.md +5 -98
  63. package/Dev/i18n/en/Workflow/commands/auto-sprint.md +7 -4
  64. package/Dev/i18n/en/Workflow/commands/plan.md +1 -2
  65. package/Dev/i18n/es/Common/agents/devops-engineer.md +19 -14
  66. package/Dev/i18n/es/Common/agents/migration-specialist.md +10 -15
  67. package/Dev/i18n/es/Common/agents/mlops-engineer.md +19 -15
  68. package/Dev/i18n/es/Common/agents/ralph-conductor.md +4 -15
  69. package/Dev/i18n/es/Common/agents/refactoring-specialist.md +0 -9
  70. package/Dev/i18n/es/Common/agents/tdd-coach.md +0 -9
  71. package/Dev/i18n/es/Common/agents/uiux-orchestrator.md +0 -9
  72. package/Dev/i18n/es/Common/commands/ralph-run.md +52 -0
  73. package/Dev/i18n/es/Common/commands/research-context7.md +5 -253
  74. package/Dev/i18n/es/Common/rules/12-context-management.md +9 -7
  75. package/Dev/i18n/es/Common/templates/settings.json.template +4 -0
  76. package/Dev/i18n/es/Python/skills/coding-standards/SKILL.md +1 -1
  77. package/Dev/i18n/es/Python/skills/testing-python/SKILL.md +1 -1
  78. package/Dev/i18n/es/Python/skills/tooling/SKILL.md +1 -1
  79. package/Dev/i18n/es/Symfony/commands/check-security.md +20 -20
  80. package/Dev/i18n/es/Symfony/skills/aggregates/SKILL.md +1 -1
  81. package/Dev/i18n/es/Symfony/skills/async/SKILL.md +1 -1
  82. package/Dev/i18n/es/Symfony/skills/cqrs/SKILL.md +1 -1
  83. package/Dev/i18n/es/Symfony/skills/domain-events/SKILL.md +1 -1
  84. package/Dev/i18n/es/Symfony/skills/i18n/SKILL.md +1 -1
  85. package/Dev/i18n/es/Symfony/skills/multitenant/SKILL.md +1 -1
  86. package/Dev/i18n/es/Symfony/skills/value-objects/SKILL.md +1 -1
  87. package/Dev/i18n/es/Team/commands/audit.md +29 -4
  88. package/Dev/i18n/es/Team/commands/delivery.md +29 -4
  89. package/Dev/i18n/es/Team/commands/security.md +31 -4
  90. package/Dev/i18n/es/Team/commands/sprint.md +30 -5
  91. package/Dev/i18n/es/UIUX/commands/audit.md +1 -30
  92. package/Dev/i18n/es/UIUX/commands/component-spec.md +1 -5
  93. package/Dev/i18n/es/UIUX/commands/orchestrator.md +5 -98
  94. package/Dev/i18n/es/Workflow/commands/auto-sprint.md +7 -4
  95. package/Dev/i18n/es/Workflow/commands/plan.md +1 -2
  96. package/Dev/i18n/fr/Common/agents/devops-engineer.md +19 -14
  97. package/Dev/i18n/fr/Common/agents/migration-specialist.md +10 -15
  98. package/Dev/i18n/fr/Common/agents/mlops-engineer.md +19 -15
  99. package/Dev/i18n/fr/Common/agents/ralph-conductor.md +4 -15
  100. package/Dev/i18n/fr/Common/agents/refactoring-specialist.md +0 -9
  101. package/Dev/i18n/fr/Common/agents/tdd-coach.md +0 -9
  102. package/Dev/i18n/fr/Common/agents/uiux-orchestrator.md +0 -9
  103. package/Dev/i18n/fr/Common/commands/ralph-run.md +52 -0
  104. package/Dev/i18n/fr/Common/commands/research-context7.md +5 -253
  105. package/Dev/i18n/fr/Common/rules/12-context-management.md +10 -8
  106. package/Dev/i18n/fr/Common/templates/settings.json.template +4 -0
  107. package/Dev/i18n/fr/React/commands/check-compliance.md +1 -1
  108. package/Dev/i18n/fr/React/commands/check-security.md +2 -2
  109. package/Dev/i18n/fr/Symfony/commands/check-security.md +20 -20
  110. package/Dev/i18n/fr/Symfony/skills/aggregates/SKILL.md +1 -1
  111. package/Dev/i18n/fr/Symfony/skills/async/SKILL.md +1 -1
  112. package/Dev/i18n/fr/Symfony/skills/cqrs/SKILL.md +1 -1
  113. package/Dev/i18n/fr/Symfony/skills/domain-events/SKILL.md +1 -1
  114. package/Dev/i18n/fr/Symfony/skills/i18n/SKILL.md +1 -1
  115. package/Dev/i18n/fr/Symfony/skills/multitenant/SKILL.md +1 -1
  116. package/Dev/i18n/fr/Symfony/skills/value-objects/SKILL.md +1 -1
  117. package/Dev/i18n/fr/Team/commands/audit.md +29 -4
  118. package/Dev/i18n/fr/Team/commands/delivery.md +29 -4
  119. package/Dev/i18n/fr/Team/commands/security.md +31 -4
  120. package/Dev/i18n/fr/Team/commands/sprint.md +30 -5
  121. package/Dev/i18n/fr/UIUX/commands/audit.md +1 -30
  122. package/Dev/i18n/fr/UIUX/commands/component-spec.md +1 -5
  123. package/Dev/i18n/fr/UIUX/commands/orchestrator.md +5 -98
  124. package/Dev/i18n/fr/Workflow/commands/auto-sprint.md +7 -4
  125. package/Dev/i18n/fr/Workflow/commands/plan.md +1 -2
  126. package/Dev/i18n/pt/Common/agents/devops-engineer.md +19 -14
  127. package/Dev/i18n/pt/Common/agents/migration-specialist.md +10 -15
  128. package/Dev/i18n/pt/Common/agents/mlops-engineer.md +19 -15
  129. package/Dev/i18n/pt/Common/agents/ralph-conductor.md +4 -15
  130. package/Dev/i18n/pt/Common/agents/refactoring-specialist.md +0 -9
  131. package/Dev/i18n/pt/Common/agents/tdd-coach.md +0 -9
  132. package/Dev/i18n/pt/Common/agents/uiux-orchestrator.md +0 -9
  133. package/Dev/i18n/pt/Common/commands/ralph-run.md +52 -0
  134. package/Dev/i18n/pt/Common/commands/research-context7.md +5 -253
  135. package/Dev/i18n/pt/Common/rules/12-context-management.md +9 -7
  136. package/Dev/i18n/pt/Common/templates/settings.json.template +4 -0
  137. package/Dev/i18n/pt/Python/skills/coding-standards/SKILL.md +1 -1
  138. package/Dev/i18n/pt/Python/skills/testing-python/SKILL.md +1 -1
  139. package/Dev/i18n/pt/Python/skills/tooling/SKILL.md +1 -1
  140. package/Dev/i18n/pt/Symfony/commands/check-security.md +20 -20
  141. package/Dev/i18n/pt/Symfony/skills/aggregates/SKILL.md +1 -1
  142. package/Dev/i18n/pt/Symfony/skills/async/SKILL.md +1 -1
  143. package/Dev/i18n/pt/Symfony/skills/cqrs/SKILL.md +1 -1
  144. package/Dev/i18n/pt/Symfony/skills/domain-events/SKILL.md +1 -1
  145. package/Dev/i18n/pt/Symfony/skills/i18n/SKILL.md +1 -1
  146. package/Dev/i18n/pt/Symfony/skills/multitenant/SKILL.md +1 -1
  147. package/Dev/i18n/pt/Symfony/skills/value-objects/SKILL.md +1 -1
  148. package/Dev/i18n/pt/Team/commands/audit.md +29 -4
  149. package/Dev/i18n/pt/Team/commands/delivery.md +29 -4
  150. package/Dev/i18n/pt/Team/commands/security.md +31 -4
  151. package/Dev/i18n/pt/Team/commands/sprint.md +30 -5
  152. package/Dev/i18n/pt/UIUX/commands/audit.md +1 -30
  153. package/Dev/i18n/pt/UIUX/commands/component-spec.md +1 -5
  154. package/Dev/i18n/pt/UIUX/commands/orchestrator.md +5 -98
  155. package/Dev/i18n/pt/Workflow/commands/auto-sprint.md +7 -4
  156. package/Dev/i18n/pt/Workflow/commands/plan.md +1 -2
  157. package/Dev/scripts/install-common-rules.sh +8 -0
  158. package/README.md +12 -8
  159. package/Tools/Ralph/lib/checkpoint.sh +3 -3
  160. package/Tools/Ralph/lib/circuit-breaker.sh +10 -10
  161. package/Tools/Ralph/lib/context-manager.sh +56 -65
  162. package/Tools/Ralph/lib/context-reconstruction.sh +46 -79
  163. package/Tools/Ralph/lib/dod-templates.sh +2 -2
  164. package/Tools/Ralph/lib/dod-validator.sh +18 -18
  165. package/Tools/Ralph/lib/escalation-service.sh +9 -7
  166. package/Tools/Ralph/lib/hooks-generator.sh +2 -1
  167. package/Tools/Ralph/lib/loop-finalizer.sh +1 -1
  168. package/Tools/Ralph/lib/loop.sh +44 -10
  169. package/Tools/Ralph/lib/metrics-exporter.sh +1 -1
  170. package/Tools/Ralph/lib/parallel-manager.sh +173 -88
  171. package/Tools/Ralph/lib/project-detector.sh +3 -3
  172. package/Tools/Ralph/lib/recovery-engine.sh +8 -8
  173. package/Tools/Ralph/lib/session.sh +2 -2
  174. package/Tools/Ralph/lib/sprint-conductor.sh +167 -124
  175. package/Tools/Ralph/lib/sprint-progress.sh +9 -9
  176. package/Tools/Ralph/ralph.sh +66 -38
  177. package/Tools/StatusLine/statusline.sh +0 -0
  178. package/Tools/i18n/ralph/de.sh +165 -33
  179. package/Tools/i18n/ralph/en.sh +132 -0
  180. package/Tools/i18n/ralph/es.sh +209 -77
  181. package/Tools/i18n/ralph/fr.sh +234 -102
  182. package/Tools/i18n/ralph/pt.sh +227 -95
  183. package/bundles/cursor/.cursorrules +3 -3
  184. package/bundles/windsurf/.windsurfrules +3 -3
  185. package/cli/lib/doctor.js +1 -1
  186. package/config/removed-commands.yaml +61 -1
  187. package/package.json +1 -1
@@ -17,11 +17,19 @@ PARALLEL_RESOURCE_LIMIT_CPU="${PARALLEL_RESOURCE_LIMIT_CPU:-80}"
17
17
  PARALLEL_RESOURCE_LIMIT_MEM="${PARALLEL_RESOURCE_LIMIT_MEM:-80}"
18
18
 
19
19
  # Session tracking
20
- declare -A PARALLEL_SESSIONS
21
- declare -A PARALLEL_PIDS
20
+ # `=()`: a declared-but-unassigned array is "unbound" for `${#arr[@]}` under set -u
21
+ declare -gA PARALLEL_SESSIONS=()
22
+ declare -gA PARALLEL_PIDS=()
22
23
  PARALLEL_SESSION_COUNT=0
23
24
  PARALLEL_COMPLETED_COUNT=0
24
25
  PARALLEL_FAILED_COUNT=0
26
+ # Results returned through globals: spawn_session and collect_completed_sessions
27
+ # must run in the leader shell, never in `$(...)` (the worker would hold the
28
+ # substitution pipe open, and the tracking arrays would be lost).
29
+ PARALLEL_SPAWNED_SESSION_ID=""
30
+ # One story id per line (ids may contain spaces).
31
+ PARALLEL_COLLECTED_STORIES=""
32
+ PARALLEL_FAILED_STORIES=""
25
33
 
26
34
  # Paths
27
35
  PARALLEL_BASE_DIR=""
@@ -36,6 +44,8 @@ PARALLEL_WORKER_DIR=""
36
44
  init_parallel_manager() {
37
45
  PARALLEL_BASE_DIR="${RALPH_SESSION_BASE:-.ralph}/parallel"
38
46
  mkdir -p "$PARALLEL_BASE_DIR"
47
+ # Absolute: workers cd into their own worktree
48
+ PARALLEL_BASE_DIR="$(cd "$PARALLEL_BASE_DIR" && pwd)"
39
49
 
40
50
  PARALLEL_STATE_FILE="$PARALLEL_BASE_DIR/state.yaml"
41
51
  PARALLEL_DEPENDENCY_GRAPH="$PARALLEL_BASE_DIR/dependencies.json"
@@ -98,32 +108,41 @@ EOF
98
108
 
99
109
  # Build dependency graph from stories
100
110
  build_dependency_graph() {
101
- local stories="$1" # Space-separated list of story IDs
111
+ local stories="$1" # Story IDs, one per line
102
112
  local bmad_dir="${BMAD_DIR:-.bmad}"
103
113
  local status_file="$bmad_dir/sprint-status.yaml"
104
114
 
115
+ # Without a status file (or yq), all stories are independent nodes: the
116
+ # graph must still list them, get_ready_stories reads it.
117
+ local with_deps=true
105
118
  if [[ ! -f "$status_file" ]] || ! command -v yq &>/dev/null; then
106
- # No dependencies, all stories are independent
107
- echo '{"nodes":{},"edges":[]}'
108
- return
119
+ with_deps=false
109
120
  fi
110
121
 
111
122
  local nodes="{}"
112
123
  local edges="[]"
113
124
 
114
- for story_id in $stories; do
125
+ local -a story_list=()
126
+ mapfile -t story_list <<< "$stories"
127
+ for story_id in "${story_list[@]}"; do
128
+ [[ -z "$story_id" ]] && continue
115
129
  # Get story dependencies
116
- local deps
117
- deps=$(yq ".stories.${story_id}.blocked_by // []" "$status_file" 2>/dev/null)
130
+ local deps="[]"
131
+ if [[ "$with_deps" == "true" ]]; then
132
+ deps=$(STORY_ID="$story_id" yq '.stories[strenv(STORY_ID)].blocked_by // []' "$status_file" 2>/dev/null) || deps="[]"
133
+ fi
118
134
 
119
- # Add node
120
- nodes=$(echo "$nodes" | jq --arg id "$story_id" '. + {($id): {"status": "pending", "deps": []}}')
135
+ # Add node (on jq failure, keep the previous value: `next` buffers it)
136
+ local next
137
+ next=$(echo "$nodes" | jq --arg id "$story_id" '. + {($id): {"status": "pending", "deps": []}}') && nodes="$next"
121
138
 
122
139
  # Add edges for dependencies
123
140
  if [[ "$deps" != "[]" && "$deps" != "null" ]]; then
124
- for dep in $(echo "$deps" | jq -r '.[]' 2>/dev/null); do
125
- edges=$(echo "$edges" | jq --arg from "$dep" --arg to "$story_id" '. + [{"from": $from, "to": $to}]')
126
- nodes=$(echo "$nodes" | jq --arg id "$story_id" --arg dep "$dep" '.[$id].deps += [$dep]')
141
+ local -a dep_list=()
142
+ mapfile -t dep_list < <(echo "$deps" | jq -r '.[]' 2>/dev/null || true)
143
+ for dep in "${dep_list[@]}"; do
144
+ next=$(echo "$edges" | jq --arg from "$dep" --arg to "$story_id" '. + [{"from": $from, "to": $to}]') && edges="$next"
145
+ next=$(echo "$nodes" | jq --arg id "$story_id" --arg dep "$dep" '.[$id].deps += [$dep]') && nodes="$next"
127
146
  done
128
147
  fi
129
148
  done
@@ -144,18 +163,20 @@ get_ready_stories() {
144
163
  fi
145
164
 
146
165
  # Find nodes with status=pending and all deps completed
166
+ # ($root: jq has no `$.` root accessor — that syntax failed to compile)
147
167
  jq -r '
168
+ . as $root |
148
169
  .nodes | to_entries[] |
149
170
  select(.value.status == "pending") |
150
171
  select(
151
172
  (.value.deps | length) == 0 or
152
173
  (
153
174
  .value.deps as $deps |
154
- ($deps | map(. as $d | $.nodes[$d].status == "completed") | all)
175
+ ($deps | map(. as $d | $root.nodes[$d].status == "completed") | all)
155
176
  )
156
177
  ) |
157
178
  .key
158
- ' "$PARALLEL_DEPENDENCY_GRAPH" 2>/dev/null
179
+ ' "$PARALLEL_DEPENDENCY_GRAPH" 2>/dev/null || true
159
180
  }
160
181
 
161
182
  # Mark story as completed in dependency graph
@@ -171,6 +192,19 @@ mark_story_completed() {
171
192
  echo "$updated" > "$PARALLEL_DEPENDENCY_GRAPH"
172
193
  }
173
194
 
195
+ # Mark story as running in dependency graph (leader only, at spawn time)
196
+ mark_story_running() {
197
+ local story_id="$1"
198
+
199
+ if [[ ! -f "$PARALLEL_DEPENDENCY_GRAPH" ]] || ! command -v jq &>/dev/null; then
200
+ return 0
201
+ fi
202
+
203
+ local updated
204
+ updated=$(jq --arg id "$story_id" '.nodes[$id].status = "running"' "$PARALLEL_DEPENDENCY_GRAPH")
205
+ echo "$updated" > "$PARALLEL_DEPENDENCY_GRAPH"
206
+ }
207
+
174
208
  # Mark story as failed in dependency graph
175
209
  mark_story_failed() {
176
210
  local story_id="$1"
@@ -211,11 +245,11 @@ check_resource_availability() {
211
245
  mem_usage=$(free | grep Mem | awk '{print int($3/$2 * 100)}' 2>/dev/null || echo "0")
212
246
  elif [[ "$(uname)" == "Darwin" ]]; then
213
247
  local pages_free pages_active pages_inactive pages_speculative pages_wired
214
- pages_free=$(vm_stat | grep "Pages free" | awk '{print $3}' | tr -d '.')
215
- pages_active=$(vm_stat | grep "Pages active" | awk '{print $3}' | tr -d '.')
216
- pages_inactive=$(vm_stat | grep "Pages inactive" | awk '{print $3}' | tr -d '.')
217
- pages_speculative=$(vm_stat | grep "Pages speculative" | awk '{print $3}' | tr -d '.')
218
- pages_wired=$(vm_stat | grep "Pages wired" | awk '{print $4}' | tr -d '.')
248
+ pages_free=$(vm_stat | grep "Pages free" | awk '{print $3}' | tr -d '.') || pages_free=0
249
+ pages_active=$(vm_stat | grep "Pages active" | awk '{print $3}' | tr -d '.') || pages_active=0
250
+ pages_inactive=$(vm_stat | grep "Pages inactive" | awk '{print $3}' | tr -d '.') || pages_inactive=0
251
+ pages_speculative=$(vm_stat | grep "Pages speculative" | awk '{print $3}' | tr -d '.') || pages_speculative=0
252
+ pages_wired=$(vm_stat | grep "Pages wired" | awk '{print $4}' | tr -d '.') || pages_wired=0
219
253
  local total=$((pages_free + pages_active + pages_inactive + pages_speculative + pages_wired))
220
254
  local used=$((pages_active + pages_wired))
221
255
  mem_usage=$((used * 100 / total))
@@ -232,9 +266,7 @@ check_resource_availability() {
232
266
  # Get number of available slots
233
267
  get_available_slots() {
234
268
  local active_count=0
235
- if [[ -v PARALLEL_PIDS ]]; then
236
- active_count=${#PARALLEL_PIDS[@]}
237
- fi
269
+ active_count=${#PARALLEL_PIDS[@]}
238
270
  local available=$((PARALLEL_MAX_CONCURRENT - active_count))
239
271
 
240
272
  # Further limit if resources are constrained
@@ -250,14 +282,18 @@ get_available_slots() {
250
282
  # Session Spawning
251
283
  # =============================================================================
252
284
 
253
- # Spawn a new Ralph session for a story
285
+ # Spawn a new Ralph session for a story (id in PARALLEL_SPAWNED_SESSION_ID)
254
286
  spawn_session() {
255
287
  local story_id="$1"
256
288
  local prompt="$2"
257
289
  local config="${3:-}"
258
290
 
259
- # Generate session ID
260
- local session_id="PAR-${story_id}-$(date +%s)"
291
+ # Generate session ID. It names a directory and a git branch: the story id
292
+ # is reduced to [A-Za-z0-9_-] (no `/`, `..` nor space), and the spawn
293
+ # counter keeps ids that sanitize alike ("US 1", "US/1") apart.
294
+ local session_id
295
+ session_id="PAR-${story_id//[^A-Za-z0-9_-]/-}-$(date +%s)-${PARALLEL_SESSION_COUNT}"
296
+ PARALLEL_SPAWNED_SESSION_ID=""
261
297
 
262
298
  print_info "Spawning parallel session: $session_id for $story_id"
263
299
 
@@ -265,21 +301,22 @@ spawn_session() {
265
301
  local session_dir="$PARALLEL_BASE_DIR/sessions/$session_id"
266
302
  mkdir -p "$session_dir"
267
303
 
268
- # Write session info
269
- cat > "$session_dir/info.yaml" << EOF
270
- session_id: "$session_id"
271
- story_id: "$story_id"
272
- status: "running"
273
- started_at: "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
274
- pid: ""
275
- exit_code: ""
276
- EOF
304
+ # Write session info (ids through strenv(): a `"` must not break the YAML)
305
+ if command -v yq &>/dev/null; then
306
+ SESSION_ID_ARG="$session_id" STORY_ID="$story_id" TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" yq -n '
307
+ .session_id = strenv(SESSION_ID_ARG) |
308
+ .story_id = strenv(STORY_ID) |
309
+ .status = "running" |
310
+ .started_at = strenv(TS) |
311
+ .pid = "" |
312
+ .exit_code = ""' > "$session_dir/info.yaml"
313
+ fi
277
314
 
278
315
  # Find Ralph script
279
316
  local ralph_script=""
280
317
  for path in "${SCRIPT_DIR}/ralph.sh" "Tools/Ralph/ralph.sh" "$(dirname "${BASH_SOURCE[0]}")/../ralph.sh"; do
281
318
  if [[ -f "$path" ]]; then
282
- ralph_script="$path"
319
+ ralph_script="$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
283
320
  break
284
321
  fi
285
322
  done
@@ -290,19 +327,37 @@ EOF
290
327
  fi
291
328
 
292
329
  # Build command
293
- local cmd="$ralph_script"
294
- [[ -n "$config" ]] && cmd="$cmd --config=$config"
330
+ local -a cmd=("$ralph_script")
331
+ [[ -n "${LANG_ARG:-}" ]] && cmd+=("--lang=$LANG_ARG")
332
+ [[ -n "$config" ]] && cmd+=("--config=$config")
333
+
334
+ # Isolate the worker's working tree (one git worktree per session)
335
+ local worktree_dir=""
336
+ if ! worktree_dir=$(create_worker_worktree "$session_id"); then
337
+ worktree_dir=""
338
+ print_warning "No git worktree for $session_id: worker shares the current directory"
339
+ fi
295
340
 
296
341
  # Spawn in background
297
342
  # Workers write to individual temp files (single-writer pattern).
298
343
  # The leader merges results after all workers complete.
299
- local worker_id="${story_id}-$$-$(date +%s)"
344
+ # Derived from the (sanitized) session id: it names the report file
345
+ local worker_id="${session_id}-$$"
300
346
  (
301
- cd "$(pwd)" || exit 1
347
+ if [[ -n "$worktree_dir" ]]; then
348
+ cd "$worktree_dir" || exit 1
349
+ fi
350
+ # Per-worker Ralph state (incl. the Claude session UUID) under the
351
+ # leader's .ralph/, so it survives the worktree removal.
352
+ export RALPH_SESSION_BASE="$session_dir/ralph"
302
353
 
303
354
  local output
304
- output=$($cmd "$prompt" 2>&1)
305
- local status=$?
355
+ local status=0
356
+ output=$("${cmd[@]}" "$prompt" 2>&1) || status=$?
357
+
358
+ if [[ -n "$worktree_dir" ]] && ! cleanup_worker_worktree "$worktree_dir" "$story_id"; then
359
+ output+=$'\n'"[WARN] Worktree kept: $worktree_dir"
360
+ fi
306
361
 
307
362
  # Save output (per-session dir, no conflict)
308
363
  echo "$output" > "$session_dir/output.log"
@@ -337,7 +392,31 @@ EOF
337
392
  # Update state file
338
393
  update_parallel_state "spawn" "$session_id" "$story_id"
339
394
 
340
- echo "$session_id"
395
+ PARALLEL_SPAWNED_SESSION_ID="$session_id"
396
+ }
397
+
398
+ # Create a dedicated git worktree on branch ralph/<session_id> so parallel
399
+ # workers never share a working tree. Prints its path; fails outside git.
400
+ create_worker_worktree() {
401
+ local session_id="$1"
402
+ local worktree_dir="$PARALLEL_BASE_DIR/worktrees/$session_id"
403
+
404
+ git rev-parse --is-inside-work-tree &> /dev/null || return 1
405
+ git worktree add -q -b "ralph/$session_id" "$worktree_dir" HEAD &> /dev/null || return 1
406
+ echo "$worktree_dir"
407
+ }
408
+
409
+ # Commit leftover changes as WIP on the worker branch, then remove the
410
+ # worktree. If the commit fails, the dirty worktree is kept (no data loss).
411
+ cleanup_worker_worktree() {
412
+ local worktree_dir="$1"
413
+ local story_id="$2"
414
+
415
+ if [[ -n "$(git -C "$worktree_dir" status --porcelain)" ]]; then
416
+ git -C "$worktree_dir" add -A
417
+ git -C "$worktree_dir" commit -q -m "wip(ralph): $story_id" || return 1
418
+ fi
419
+ git -C "$worktree_dir" worktree remove "$worktree_dir"
341
420
  }
342
421
 
343
422
  # =============================================================================
@@ -347,6 +426,8 @@ EOF
347
426
  # Check and collect completed sessions.
348
427
  # In the single-writer pattern, this function collects finished PIDs
349
428
  # and then merges worker reports written by the background processes.
429
+ # Completed (failed) stories are also appended, one per line, to
430
+ # PARALLEL_COLLECTED_STORIES (PARALLEL_FAILED_STORIES).
350
431
  collect_completed_sessions() {
351
432
  local completed=()
352
433
  local failed=()
@@ -359,16 +440,18 @@ collect_completed_sessions() {
359
440
  local session_id="${PARALLEL_PIDS[$pid]}"
360
441
  local story_id="${PARALLEL_SESSIONS[$session_id]}"
361
442
 
362
- # Check exit status
363
- wait "$pid" 2>/dev/null
364
- local exit_code=$?
443
+ # Check exit status (a failed worker must not kill the leader)
444
+ local exit_code=0
445
+ wait "$pid" 2>/dev/null || exit_code=$?
365
446
 
366
447
  if [[ $exit_code -eq 0 ]]; then
367
448
  completed+=("$story_id")
449
+ PARALLEL_COLLECTED_STORIES+="$story_id"$'\n'
368
450
  PARALLEL_COMPLETED_COUNT=$((PARALLEL_COMPLETED_COUNT + 1))
369
451
  print_success "Session completed: $session_id ($story_id)"
370
452
  else
371
453
  failed+=("$story_id")
454
+ PARALLEL_FAILED_STORIES+="$story_id"$'\n'
372
455
  PARALLEL_FAILED_COUNT=$((PARALLEL_FAILED_COUNT + 1))
373
456
  print_error "Session failed: $session_id ($story_id)"
374
457
  fi
@@ -395,9 +478,7 @@ wait_all_sessions() {
395
478
  start_time=$(date +%s)
396
479
 
397
480
  local active_count=0
398
- if [[ -v PARALLEL_PIDS ]]; then
399
- active_count=${#PARALLEL_PIDS[@]}
400
- fi
481
+ active_count=${#PARALLEL_PIDS[@]}
401
482
 
402
483
  while [[ $active_count -gt 0 ]]; do
403
484
  collect_completed_sessions > /dev/null
@@ -415,9 +496,7 @@ wait_all_sessions() {
415
496
 
416
497
  # Update active count
417
498
  active_count=0
418
- if [[ -v PARALLEL_PIDS ]]; then
419
- active_count=${#PARALLEL_PIDS[@]}
420
- fi
499
+ active_count=${#PARALLEL_PIDS[@]}
421
500
  done
422
501
 
423
502
  return 0
@@ -426,9 +505,7 @@ wait_all_sessions() {
426
505
  # Get active session count
427
506
  get_active_count() {
428
507
  local count=0
429
- if [[ -v PARALLEL_PIDS ]]; then
430
- count=${#PARALLEL_PIDS[@]}
431
- fi
508
+ count=${#PARALLEL_PIDS[@]}
432
509
  echo "$count"
433
510
  }
434
511
 
@@ -450,12 +527,12 @@ process_stories_parallel() {
450
527
  print_info "Stories: $(echo "$stories" | wc -w | tr -d ' ')"
451
528
  print_info "Max concurrent: $PARALLEL_MAX_CONCURRENT"
452
529
 
453
- # Build dependency graph
454
- build_dependency_graph "$stories" > /dev/null
530
+ # Build dependency graph (it reads one id per line)
531
+ build_dependency_graph "$(tr -s ' ' '\n' <<< "$stories")" > /dev/null
455
532
 
456
533
  local processed=0
457
534
  local total
458
- total=$(echo "$stories" | wc -w | tr -d ' ')
535
+ total=$(echo "$stories" | wc -w | tr -d ' ') || total=0
459
536
 
460
537
  while [[ $processed -lt $total ]]; do
461
538
  # Collect completed sessions
@@ -473,9 +550,6 @@ process_stories_parallel() {
473
550
  for story_id in $ready; do
474
551
  [[ $slots -le 0 ]] && break
475
552
 
476
- # Skip if already processed
477
- [[ -n "${PARALLEL_SESSIONS[$story_id]:-}" ]] && continue
478
-
479
553
  # Build prompt
480
554
  local prompt="$prompt_template $story_id"
481
555
 
@@ -484,14 +558,19 @@ process_stories_parallel() {
484
558
  local status_file="$bmad_dir/sprint-status.yaml"
485
559
  if [[ -f "$status_file" ]] && command -v yq &>/dev/null; then
486
560
  local title
487
- title=$(yq ".stories.${story_id}.title" "$status_file" 2>/dev/null)
561
+ title=$(STORY_ID="$story_id" yq '.stories[strenv(STORY_ID)].title' "$status_file" 2>/dev/null)
488
562
  prompt="Implement user story $story_id: $title. Follow TDD approach."
489
563
  fi
490
564
 
491
- # Spawn session
492
- spawn_session "$story_id" "$prompt"
493
- ((slots--))
494
- ((processed++))
565
+ # Spawn session. "running" keeps get_ready_stories (pending
566
+ # only) from re-spawning it before its report is merged.
567
+ if spawn_session "$story_id" "$prompt"; then
568
+ mark_story_running "$story_id"
569
+ else
570
+ mark_story_failed "$story_id"
571
+ fi
572
+ slots=$((slots - 1))
573
+ processed=$((processed + 1))
495
574
  done
496
575
  fi
497
576
 
@@ -503,6 +582,9 @@ process_stories_parallel() {
503
582
  print_info "Waiting for remaining sessions to complete..."
504
583
  wait_all_sessions
505
584
 
585
+ # Drop stale worktree metadata (e.g. a worker killed before its cleanup)
586
+ git worktree prune &> /dev/null || true
587
+
506
588
  # Print summary
507
589
  print_parallel_summary
508
590
  }
@@ -520,16 +602,14 @@ write_worker_report() {
520
602
  local status="$4" # "completed" or "failed"
521
603
 
522
604
  local report_file="${PARALLEL_WORKER_DIR}/worker-${worker_id}.yaml"
523
- local ts
524
- ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
525
-
526
- cat > "$report_file" << EOF
527
- worker_id: "${worker_id}"
528
- session_id: "${session_id}"
529
- story_id: "${story_id}"
530
- status: "${status}"
531
- reported_at: "${ts}"
532
- EOF
605
+ # Values through strenv(): a `"` in an id must not break the YAML
606
+ WORKER_ID="$worker_id" SESSION_ID_ARG="$session_id" STORY_ID="$story_id" \
607
+ STATUS="$status" TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" yq -n '
608
+ .worker_id = strenv(WORKER_ID) |
609
+ .session_id = strenv(SESSION_ID_ARG) |
610
+ .story_id = strenv(STORY_ID) |
611
+ .status = strenv(STATUS) |
612
+ .reported_at = strenv(TS)' > "$report_file"
533
613
  }
534
614
 
535
615
  # Merge all worker reports into the shared state file (leader only).
@@ -567,18 +647,18 @@ merge_worker_reports() {
567
647
  if [[ "$dry_run" == "true" ]]; then
568
648
  print_info "DRY RUN - Would merge: session=$session_id story=$story_id status=$status" 2>/dev/null \
569
649
  || echo "[INFO] DRY RUN - Would merge: session=$session_id story=$story_id status=$status"
570
- ((merged_count++))
650
+ merged_count=$((merged_count + 1))
571
651
  continue
572
652
  fi
573
653
 
574
654
  # Update state file (single writer - no race condition)
575
655
  if [[ -f "$PARALLEL_STATE_FILE" ]]; then
576
- yq -i ".sessions.active -= [\"$session_id\"]" "$PARALLEL_STATE_FILE"
656
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.active -= [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
577
657
  if [[ "$status" == "completed" ]]; then
578
- yq -i ".sessions.completed += [\"$session_id\"]" "$PARALLEL_STATE_FILE"
658
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.completed += [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
579
659
  yq -i ".metrics.total_completed += 1" "$PARALLEL_STATE_FILE"
580
660
  else
581
- yq -i ".sessions.failed += [\"$session_id\"]" "$PARALLEL_STATE_FILE"
661
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.failed += [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
582
662
  yq -i ".metrics.total_failed += 1" "$PARALLEL_STATE_FILE"
583
663
  fi
584
664
  fi
@@ -591,7 +671,7 @@ merge_worker_reports() {
591
671
  fi
592
672
 
593
673
  rm -f "$report_file"
594
- ((merged_count++))
674
+ merged_count=$((merged_count + 1))
595
675
  done
596
676
 
597
677
  # Update efficiency after merge
@@ -640,19 +720,19 @@ update_parallel_state() {
640
720
  case "$action" in
641
721
  spawn)
642
722
  # Spawn is always called from the leader, safe to write directly
643
- yq -i ".sessions.active += [\"$session_id\"]" "$PARALLEL_STATE_FILE"
723
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.active += [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
644
724
  yq -i ".metrics.total_spawned += 1" "$PARALLEL_STATE_FILE"
645
725
  ;;
646
726
  complete|fail)
647
727
  # DEPRECATED for parallel mode: use write_worker_report() + merge_worker_reports()
648
728
  # Kept for backward compatibility with sequential mode
649
729
  if [[ "$action" == "complete" ]]; then
650
- yq -i ".sessions.active -= [\"$session_id\"]" "$PARALLEL_STATE_FILE"
651
- yq -i ".sessions.completed += [\"$session_id\"]" "$PARALLEL_STATE_FILE"
730
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.active -= [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
731
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.completed += [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
652
732
  yq -i ".metrics.total_completed += 1" "$PARALLEL_STATE_FILE"
653
733
  else
654
- yq -i ".sessions.active -= [\"$session_id\"]" "$PARALLEL_STATE_FILE"
655
- yq -i ".sessions.failed += [\"$session_id\"]" "$PARALLEL_STATE_FILE"
734
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.active -= [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
735
+ SESSION_ID_ARG="$session_id" yq -i '.sessions.failed += [strenv(SESSION_ID_ARG)]' "$PARALLEL_STATE_FILE"
656
736
  yq -i ".metrics.total_failed += 1" "$PARALLEL_STATE_FILE"
657
737
  fi
658
738
  ;;
@@ -746,7 +826,7 @@ cmd_parallel_manager() {
746
826
  echo "Usage: parallel-manager.sh spawn <story-id> [prompt]"
747
827
  return 1
748
828
  fi
749
- spawn_session "$story_id" "$prompt"
829
+ spawn_session "$story_id" "$prompt" && echo "$PARALLEL_SPAWNED_SESSION_ID"
750
830
  ;;
751
831
  wait)
752
832
  wait_all_sessions "${1:-0}"
@@ -781,6 +861,11 @@ cmd_parallel_manager() {
781
861
  echo "Single-Writer Pattern:"
782
862
  echo " Background workers write to .ralph/parallel/workers/worker-{id}.yaml"
783
863
  echo " Only the leader merges results into shared state files"
864
+ echo ""
865
+ echo "Isolation:"
866
+ echo " Each worker runs in its own git worktree (branch ralph/<session>),"
867
+ echo " removed at the end of the run (leftover changes committed as WIP)."
868
+ echo " Each Ralph session pins its own Claude session (--session-id, then --resume)."
784
869
  ;;
785
870
  *)
786
871
  echo "Usage: parallel-manager.sh <command> [args]"
@@ -72,7 +72,7 @@ detect_symfony() {
72
72
 
73
73
  # Detect Symfony version
74
74
  local version
75
- version=$(grep -o '"symfony/framework-bundle"[[:space:]]*:[[:space:]]*"[^"]*"' "$composer_file" | grep -o '[0-9]\+\.[0-9]\+' | head -1)
75
+ version=$(grep -o '"symfony/framework-bundle"[[:space:]]*:[[:space:]]*"[^"]*"' "$composer_file" | grep -o '[0-9]\+\.[0-9]\+' | head -1) || true
76
76
  if [[ -n "$version" ]]; then
77
77
  DETECTED_PROJECT_TYPE="symfony-$version"
78
78
  fi
@@ -325,7 +325,7 @@ detect_dotnet() {
325
325
 
326
326
  # Look for .csproj files
327
327
  local csproj_count
328
- csproj_count=$(find "$project_dir" -maxdepth 3 -name "*.csproj" 2>/dev/null | wc -l)
328
+ csproj_count=$(find "$project_dir" -maxdepth 3 -name "*.csproj" 2>/dev/null | wc -l) || csproj_count=0
329
329
 
330
330
  if [[ $csproj_count -gt 0 ]]; then
331
331
  DETECTED_PROJECT_TYPE="dotnet"
@@ -340,7 +340,7 @@ detect_dotnet() {
340
340
 
341
341
  # Detect specific framework
342
342
  local csproj_file
343
- csproj_file=$(find "$project_dir" -maxdepth 3 -name "*.csproj" 2>/dev/null | head -1)
343
+ csproj_file=$(find "$project_dir" -maxdepth 3 -name "*.csproj" 2>/dev/null | head -1) || true
344
344
  if [[ -f "$csproj_file" ]]; then
345
345
  if grep -q 'Microsoft.NET.Sdk.Web' "$csproj_file" 2>/dev/null; then
346
346
  DETECTED_PROJECT_TYPE="dotnet-web"
@@ -17,7 +17,7 @@ RECOVERY_AUTO_FIX_TESTS="${RECOVERY_AUTO_FIX_TESTS:-retry_tdd}"
17
17
  RECOVERY_AUTO_RETRY_TRANSIENT="${RECOVERY_AUTO_RETRY_TRANSIENT:-true}"
18
18
 
19
19
  # Error classification levels
20
- declare -A RECOVERY_LEVELS=(
20
+ declare -gA RECOVERY_LEVELS=(
21
21
  [0]="transient" # Auto-retry immediately
22
22
  [1]="recoverable" # Auto-fix possible
23
23
  [2]="degraded" # Continue with warning
@@ -25,14 +25,14 @@ declare -A RECOVERY_LEVELS=(
25
25
  )
26
26
 
27
27
  # Error patterns for classification
28
- declare -A ERROR_PATTERNS_TRANSIENT=(
28
+ declare -gA ERROR_PATTERNS_TRANSIENT=(
29
29
  ["timeout"]="timeout|timed out|ETIMEDOUT"
30
30
  ["rate_limit"]="rate limit|429|too many requests"
31
31
  ["network"]="ECONNRESET|ECONNREFUSED|network error"
32
32
  ["temporary"]="temporary failure|try again|service unavailable|503"
33
33
  )
34
34
 
35
- declare -A ERROR_PATTERNS_RECOVERABLE=(
35
+ declare -gA ERROR_PATTERNS_RECOVERABLE=(
36
36
  ["lint"]="lint error|eslint|phpcs|flake8|prettier|stylelint"
37
37
  ["test_fail"]="test failed|assertion failed|tests? (failed|failing)"
38
38
  ["deps"]="dependency|package not found|module not found|could not resolve"
@@ -40,14 +40,14 @@ declare -A ERROR_PATTERNS_RECOVERABLE=(
40
40
  ["type_error"]="type error|typescript error|type mismatch"
41
41
  )
42
42
 
43
- declare -A ERROR_PATTERNS_DEGRADED=(
43
+ declare -gA ERROR_PATTERNS_DEGRADED=(
44
44
  ["docs"]="documentation|readme|changelog"
45
45
  ["optional_gate"]="optional.*gate|non-blocking"
46
46
  ["coverage"]="coverage below|coverage threshold"
47
47
  ["warning"]="warning:|deprecated"
48
48
  )
49
49
 
50
- declare -A ERROR_PATTERNS_BLOCKED=(
50
+ declare -gA ERROR_PATTERNS_BLOCKED=(
51
51
  ["security"]="security vulnerability|CVE-|critical security|injection"
52
52
  ["architecture"]="architecture violation|circular dependency|breaking change"
53
53
  ["auth"]="authentication failed|unauthorized|permission denied"
@@ -55,7 +55,7 @@ declare -A ERROR_PATTERNS_BLOCKED=(
55
55
  )
56
56
 
57
57
  # Recovery strategies
58
- declare -A RECOVERY_STRATEGIES=(
58
+ declare -gA RECOVERY_STRATEGIES=(
59
59
  ["lint"]="run_auto_lint_fix"
60
60
  ["test_fail"]="run_tdd_retry"
61
61
  ["deps"]="run_deps_install"
@@ -124,7 +124,7 @@ init_recovery_engine() {
124
124
  classify_error() {
125
125
  local error_output="$1"
126
126
  local error_lower
127
- error_lower=$(echo "$error_output" | tr '[:upper:]' '[:lower:]')
127
+ error_lower=$(echo "$error_output" | tr '[:upper:]' '[:lower:]') || true
128
128
 
129
129
  # Check Level 3 - Blocked (security, architecture)
130
130
  for pattern_name in "${!ERROR_PATTERNS_BLOCKED[@]}"; do
@@ -415,7 +415,7 @@ run_tdd_retry() {
415
415
 
416
416
  # Try to parse test output
417
417
  if echo "$error_output" | grep -qE "FAIL|FAILED|Error:"; then
418
- failing_tests=$(echo "$error_output" | grep -E "FAIL|FAILED|Error:" | head -5)
418
+ failing_tests=$(echo "$error_output" | grep -E "FAIL|FAILED|Error:" | head -5) || true
419
419
  fi
420
420
 
421
421
  # Update sprint progress with TDD phase
@@ -23,10 +23,10 @@ generate_session_id() {
23
23
  elif command -v xxd &>/dev/null; then
24
24
  random=$(head -c 4 /dev/urandom | xxd -p)
25
25
  elif command -v od &>/dev/null; then
26
- random=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
26
+ random=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n') || true
27
27
  else
28
28
  # Fallback: use $RANDOM (less entropy but portable)
29
- random=$(printf '%04x%04x' $RANDOM $RANDOM | head -c 8)
29
+ random=$(printf '%04x%04x' $RANDOM $RANDOM | head -c 8) || true
30
30
  fi
31
31
 
32
32
  echo "ralph-${timestamp}-${random}"