@the-bearded-bear/claude-craft 8.26.0-next.2a54921 → 8.26.0-next.defc34b
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.
- package/Dev/i18n/de/Common/commands/ralph-run.md +6 -0
- package/Dev/i18n/en/Common/commands/ralph-run.md +6 -0
- package/Dev/i18n/es/Common/commands/ralph-run.md +6 -0
- package/Dev/i18n/fr/Common/commands/ralph-run.md +6 -0
- package/Dev/i18n/pt/Common/commands/ralph-run.md +6 -0
- package/Tools/Ralph/lib/checkpoint.sh +3 -3
- package/Tools/Ralph/lib/circuit-breaker.sh +10 -10
- package/Tools/Ralph/lib/context-manager.sh +37 -22
- package/Tools/Ralph/lib/context-reconstruction.sh +1 -1
- package/Tools/Ralph/lib/dod-templates.sh +2 -2
- package/Tools/Ralph/lib/dod-validator.sh +18 -18
- package/Tools/Ralph/lib/escalation-service.sh +5 -5
- package/Tools/Ralph/lib/hooks-generator.sh +2 -1
- package/Tools/Ralph/lib/loop-finalizer.sh +1 -1
- package/Tools/Ralph/lib/loop.sh +44 -10
- package/Tools/Ralph/lib/metrics-exporter.sh +1 -1
- package/Tools/Ralph/lib/parallel-manager.sh +114 -50
- package/Tools/Ralph/lib/project-detector.sh +3 -3
- package/Tools/Ralph/lib/recovery-engine.sh +8 -8
- package/Tools/Ralph/lib/session.sh +2 -2
- package/Tools/Ralph/lib/sprint-conductor.sh +2 -2
- package/Tools/Ralph/lib/sprint-progress.sh +9 -9
- package/Tools/Ralph/ralph.sh +14 -11
- package/Tools/StatusLine/statusline.sh +0 -0
- package/package.json +1 -1
|
@@ -51,6 +51,12 @@ Führt Claude in einer kontinuierlichen Schleife aus, bis die Aufgabe abgeschlos
|
|
|
51
51
|
- Maximale Iterationen, Timeouts, DoD-Kriterien festlegen
|
|
52
52
|
- Hooks initialisieren, falls aktiviert
|
|
53
53
|
|
|
54
|
+
#### Isolierung von Sessions und parallelen Workern
|
|
55
|
+
|
|
56
|
+
- **Explizite Claude-Session**: Jede Ralph-Session legt ihre eigene Claude-Konversations-ID fest — `claude --session-id <uuid>` in der ersten Iteration, danach `claude --resume <uuid>`. Die UUID wird in `.ralph/sessions/<session>/claude_session_id` gespeichert, sodass `--continue=<session>` die richtige Konversation fortsetzt. Ralph verwendet `claude --continue` nicht mehr, das „die neueste Konversation im Verzeichnis“ fortsetzte (geteilt zwischen Workern oder sogar mit der interaktiven Session des Benutzers).
|
|
57
|
+
- **Ein Git-Worktree pro Worker** (paralleler Modus, `lib/parallel-manager.sh`): Jede Story läuft in einem eigenen Worktree `.ralph/parallel/worktrees/<session>` auf dem Branch `ralph/<session>`, mit ihrem Ralph-Zustand unter `.ralph/parallel/sessions/<session>/ralph/`. Am Ende des Laufs werden verbleibende Änderungen als `wip(ralph): <story>` auf diesem Branch committet, danach wird der Worktree entfernt (`git worktree remove`). Schlägt dieser Commit fehl, bleibt der Worktree erhalten (kein Datenverlust) und wird in `output.log` gemeldet. Die Arbeit wird durch Mergen der `ralph/*`-Branches übernommen.
|
|
58
|
+
- **Außerhalb eines Git-Repositorys**: kein Worktree; der Worker läuft im aktuellen Verzeichnis (eine Warnung wird ausgegeben), die Isolierung der Claude-Sessions bleibt bestehen.
|
|
59
|
+
|
|
54
60
|
### 2. Hauptschleife mit Dashboard
|
|
55
61
|
|
|
56
62
|
```
|
|
@@ -51,6 +51,12 @@ Execute Claude in a continuous loop until the task is complete or the Definition
|
|
|
51
51
|
- Set max iterations, timeouts, DoD criteria
|
|
52
52
|
- Initialize hooks if enabled
|
|
53
53
|
|
|
54
|
+
#### Session and parallel worker isolation
|
|
55
|
+
|
|
56
|
+
- **Explicit Claude session**: each Ralph session pins its own Claude conversation ID — `claude --session-id <uuid>` on the first iteration, then `claude --resume <uuid>`. The UUID is stored in `.ralph/sessions/<session>/claude_session_id`, so `--continue=<session>` resumes the right conversation. Ralph no longer uses `claude --continue`, which resumed "the most recent conversation in the directory" (shared between workers, or even with the user's interactive session).
|
|
57
|
+
- **One git worktree per worker** (parallel mode, `lib/parallel-manager.sh`): each story runs in a dedicated worktree `.ralph/parallel/worktrees/<session>` on branch `ralph/<session>`, with its Ralph state under `.ralph/parallel/sessions/<session>/ralph/`. At the end of the run, leftover changes are committed as `wip(ralph): <story>` on that branch, then the worktree is removed (`git worktree remove`). If that commit fails, the worktree is kept (no data loss) and reported in `output.log`. Collect the work by merging the `ralph/*` branches.
|
|
58
|
+
- **Outside a git repository**: no worktree; the worker runs in the current directory (a warning is printed), Claude session isolation still applies.
|
|
59
|
+
|
|
54
60
|
### 2. Main Loop with Dashboard
|
|
55
61
|
|
|
56
62
|
```
|
|
@@ -51,6 +51,12 @@ Ejecuta Claude en un bucle continuo hasta que la tarea esté completa o se cumpl
|
|
|
51
51
|
- Establecer máximo de iteraciones, timeouts y criterios DoD
|
|
52
52
|
- Inicializar hooks si están habilitados
|
|
53
53
|
|
|
54
|
+
#### Aislamiento de sesiones y workers paralelos
|
|
55
|
+
|
|
56
|
+
- **Sesión de Claude explícita**: cada sesión de Ralph fija su propio identificador de conversación de Claude — `claude --session-id <uuid>` en la primera iteración y luego `claude --resume <uuid>`. El UUID se guarda en `.ralph/sessions/<session>/claude_session_id`, de modo que `--continue=<session>` reanuda la conversación correcta. Ralph ya no usa `claude --continue`, que reanudaba «la conversación más reciente del directorio» (compartida entre workers, o incluso con la sesión interactiva del usuario).
|
|
57
|
+
- **Un worktree de git por worker** (modo paralelo, `lib/parallel-manager.sh`): cada historia se ejecuta en un worktree dedicado `.ralph/parallel/worktrees/<session>` sobre la rama `ralph/<session>`, con su estado de Ralph en `.ralph/parallel/sessions/<session>/ralph/`. Al final de la ejecución, los cambios pendientes se confirman como `wip(ralph): <story>` en esa rama y después se elimina el worktree (`git worktree remove`). Si ese commit falla, el worktree se conserva (sin pérdida de datos) y se indica en `output.log`. El trabajo se recupera fusionando las ramas `ralph/*`.
|
|
58
|
+
- **Fuera de un repositorio git**: sin worktree; el worker se ejecuta en el directorio actual (se muestra un aviso) y el aislamiento de sesiones de Claude se mantiene.
|
|
59
|
+
|
|
54
60
|
### 2. Bucle Principal con Dashboard
|
|
55
61
|
|
|
56
62
|
```
|
|
@@ -51,6 +51,12 @@ Exécuter Claude en boucle continue jusqu'à ce que la tâche soit terminée ou
|
|
|
51
51
|
- Définir les itérations max, les timeouts, les critères DoD
|
|
52
52
|
- Initialiser les hooks si activés
|
|
53
53
|
|
|
54
|
+
#### Isolation des sessions et des workers parallèles
|
|
55
|
+
|
|
56
|
+
- **Session Claude explicite** : chaque session Ralph fixe son propre identifiant de conversation Claude — `claude --session-id <uuid>` à la première itération, puis `claude --resume <uuid>`. L'UUID est stocké dans `.ralph/sessions/<session>/claude_session_id`, ce qui permet à `--continue=<session>` de reprendre la bonne conversation. Ralph n'utilise plus `claude --continue`, qui reprenait « la conversation la plus récente du répertoire » (partagée entre workers, voire avec une session interactive de l'utilisateur).
|
|
57
|
+
- **Worktree git par worker** (mode parallèle, `lib/parallel-manager.sh`) : chaque story s'exécute dans un worktree dédié `.ralph/parallel/worktrees/<session>` sur la branche `ralph/<session>`, avec son état Ralph sous `.ralph/parallel/sessions/<session>/ralph/`. En fin de run, les modifications restantes sont commitées en `wip(ralph): <story>` sur cette branche, puis le worktree est retiré (`git worktree remove`). Si ce commit échoue, le worktree est conservé (aucune perte) et signalé dans `output.log`. Le travail se récupère en fusionnant les branches `ralph/*`.
|
|
58
|
+
- **Hors dépôt git** : pas de worktree ; le worker s'exécute dans le répertoire courant (avertissement affiché), l'isolation des sessions Claude restant assurée.
|
|
59
|
+
|
|
54
60
|
### 2. Boucle principale avec Dashboard
|
|
55
61
|
|
|
56
62
|
```
|
|
@@ -51,6 +51,12 @@ Executa o Claude em loop contínuo até que a tarefa seja concluída ou os crit
|
|
|
51
51
|
- Definir máximo de iterações, timeouts e critérios de DoD
|
|
52
52
|
- Inicializar hooks se habilitados
|
|
53
53
|
|
|
54
|
+
#### Isolamento de sessões e workers paralelos
|
|
55
|
+
|
|
56
|
+
- **Sessão Claude explícita**: cada sessão Ralph fixa seu próprio identificador de conversa Claude — `claude --session-id <uuid>` na primeira iteração e depois `claude --resume <uuid>`. O UUID é armazenado em `.ralph/sessions/<session>/claude_session_id`, para que `--continue=<session>` retome a conversa correta. O Ralph não usa mais `claude --continue`, que retomava "a conversa mais recente do diretório" (compartilhada entre workers, ou até com a sessão interativa do usuário).
|
|
57
|
+
- **Um worktree git por worker** (modo paralelo, `lib/parallel-manager.sh`): cada história é executada em um worktree dedicado `.ralph/parallel/worktrees/<session>` na branch `ralph/<session>`, com seu estado Ralph em `.ralph/parallel/sessions/<session>/ralph/`. No fim da execução, as alterações restantes são commitadas como `wip(ralph): <story>` nessa branch e o worktree é removido (`git worktree remove`). Se esse commit falhar, o worktree é mantido (sem perda de dados) e sinalizado em `output.log`. O trabalho é recuperado fazendo merge das branches `ralph/*`.
|
|
58
|
+
- **Fora de um repositório git**: sem worktree; o worker é executado no diretório atual (um aviso é exibido) e o isolamento das sessões Claude continua garantido.
|
|
59
|
+
|
|
54
60
|
### 2. Loop Principal com Painel
|
|
55
61
|
|
|
56
62
|
```
|
|
@@ -207,12 +207,12 @@ list_checkpoints() {
|
|
|
207
207
|
|
|
208
208
|
# Find commits matching the checkpoint pattern
|
|
209
209
|
local commits
|
|
210
|
-
commits=$(git log --oneline --grep="checkpoint: Ralph" 2>/dev/null | head -20)
|
|
210
|
+
commits=$(git log --oneline --grep="checkpoint: Ralph" 2>/dev/null | head -20) || true
|
|
211
211
|
|
|
212
212
|
if [[ -n "$commits" ]]; then
|
|
213
213
|
echo "$commits" | while read -r line; do
|
|
214
|
-
local hash=$(echo "$line" | cut -d' ' -f1)
|
|
215
|
-
local msg=$(echo "$line" | cut -d' ' -f2-)
|
|
214
|
+
local hash=$(echo "$line" | cut -d' ' -f1) || true
|
|
215
|
+
local msg=$(echo "$line" | cut -d' ' -f2-) || true
|
|
216
216
|
echo "{\"hash\":\"$hash\",\"message\":\"$msg\"}"
|
|
217
217
|
done | jq -s '.'
|
|
218
218
|
else
|
|
@@ -30,35 +30,35 @@ CB_LEARNING_MIN_SAMPLES=5
|
|
|
30
30
|
# =============================================================================
|
|
31
31
|
# Each profile defines thresholds optimized for different task types
|
|
32
32
|
|
|
33
|
-
declare -
|
|
33
|
+
declare -gA CB_PROFILE_QUICK_FIX=(
|
|
34
34
|
["no_changes"]=2
|
|
35
35
|
["errors"]=3
|
|
36
36
|
["max_iterations"]=10
|
|
37
37
|
["keywords"]="fix bug typo hotfix patch"
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
declare -
|
|
40
|
+
declare -gA CB_PROFILE_SMALL_FEATURE=(
|
|
41
41
|
["no_changes"]=3
|
|
42
42
|
["errors"]=4
|
|
43
43
|
["max_iterations"]=15
|
|
44
44
|
["keywords"]="add implement small simple"
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
declare -
|
|
47
|
+
declare -gA CB_PROFILE_MEDIUM_FEATURE=(
|
|
48
48
|
["no_changes"]=4
|
|
49
49
|
["errors"]=6
|
|
50
50
|
["max_iterations"]=25
|
|
51
51
|
["keywords"]="feature create with tests"
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
declare -
|
|
54
|
+
declare -gA CB_PROFILE_LARGE_FEATURE=(
|
|
55
55
|
["no_changes"]=5
|
|
56
56
|
["errors"]=8
|
|
57
57
|
["max_iterations"]=50
|
|
58
58
|
["keywords"]="refactor migrate complex large"
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
declare -
|
|
61
|
+
declare -gA CB_PROFILE_EXPLORATION=(
|
|
62
62
|
["no_changes"]=10
|
|
63
63
|
["errors"]=15
|
|
64
64
|
["max_iterations"]=100
|
|
@@ -66,7 +66,7 @@ declare -A CB_PROFILE_EXPLORATION=(
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
# Autonomous profile for overnight/unattended runs
|
|
69
|
-
declare -
|
|
69
|
+
declare -gA CB_PROFILE_AUTONOMOUS=(
|
|
70
70
|
["no_changes"]=5
|
|
71
71
|
["errors"]=8
|
|
72
72
|
["max_iterations"]=50
|
|
@@ -162,7 +162,7 @@ init_circuit_breaker() {
|
|
|
162
162
|
detect_task_profile() {
|
|
163
163
|
local prompt="$1"
|
|
164
164
|
local prompt_lower
|
|
165
|
-
prompt_lower=$(echo "$prompt" | tr '[:upper:]' '[:lower:]')
|
|
165
|
+
prompt_lower=$(echo "$prompt" | tr '[:upper:]' '[:lower:]') || true
|
|
166
166
|
|
|
167
167
|
# Check each profile's keywords
|
|
168
168
|
local profiles=("quick_fix" "small_feature" "medium_feature" "large_feature" "exploration" "autonomous")
|
|
@@ -265,7 +265,7 @@ apply_learned_adjustments() {
|
|
|
265
265
|
' "$history_file" 2>/dev/null)
|
|
266
266
|
|
|
267
267
|
local samples
|
|
268
|
-
samples=$(echo "$profile_data" | jq -r '.samples // 0')
|
|
268
|
+
samples=$(echo "$profile_data" | jq -r '.samples // 0') || samples=0
|
|
269
269
|
|
|
270
270
|
if [[ $samples -lt $CB_LEARNING_MIN_SAMPLES ]]; then
|
|
271
271
|
print_verbose "Not enough samples for learning ($samples < $CB_LEARNING_MIN_SAMPLES)"
|
|
@@ -274,10 +274,10 @@ apply_learned_adjustments() {
|
|
|
274
274
|
|
|
275
275
|
# Apply adjustments based on historical success rate
|
|
276
276
|
local success_rate
|
|
277
|
-
success_rate=$(echo "$profile_data" | jq -r '.success_rate // 0')
|
|
277
|
+
success_rate=$(echo "$profile_data" | jq -r '.success_rate // 0') || success_rate=0
|
|
278
278
|
|
|
279
279
|
local avg_iterations
|
|
280
|
-
avg_iterations=$(echo "$profile_data" | jq -r '.avg_iterations // 0')
|
|
280
|
+
avg_iterations=$(echo "$profile_data" | jq -r '.avg_iterations // 0') || avg_iterations=0
|
|
281
281
|
|
|
282
282
|
# If success rate is low, increase thresholds
|
|
283
283
|
if [[ $success_rate -lt 50 ]]; then
|
|
@@ -106,6 +106,17 @@ init_context_manager() {
|
|
|
106
106
|
print_verbose " - Smart reconstruction: $CONTEXT_SMART_RECONSTRUCTION"
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
# Print the first lines of `/context` for a Claude session UUID.
|
|
110
|
+
# Best effort: prints nothing on failure, always succeeds (set -e safe).
|
|
111
|
+
probe_context_usage() {
|
|
112
|
+
local claude_uuid="$1"
|
|
113
|
+
if command -v timeout &> /dev/null; then
|
|
114
|
+
timeout 10s "${CLAUDE_CMD[@]}" --resume "$claude_uuid" -p "/context" 2>/dev/null | head -5 || true
|
|
115
|
+
else
|
|
116
|
+
"${CLAUDE_CMD[@]}" --resume "$claude_uuid" -p "/context" 2>/dev/null | head -5 || true
|
|
117
|
+
fi
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
# =============================================================================
|
|
110
121
|
# Auto-Compact
|
|
111
122
|
# =============================================================================
|
|
@@ -120,16 +131,21 @@ run_auto_compact() {
|
|
|
120
131
|
return 1
|
|
121
132
|
fi
|
|
122
133
|
|
|
134
|
+
# /context and /compact must target this Ralph session's Claude session,
|
|
135
|
+
# never "the most recent conversation in this directory" (--continue).
|
|
136
|
+
local claude_uuid
|
|
137
|
+
claude_uuid=$(get_claude_session_uuid "$session_id")
|
|
138
|
+
if [[ -z "$claude_uuid" ]]; then
|
|
139
|
+
log_session "$session_id" "WARN" "No Claude session yet, nothing to compact"
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
|
|
123
143
|
# Check if context is above minimum threshold before compacting
|
|
124
144
|
if [[ "$CONTEXT_MIN_THRESHOLD" -gt 0 ]]; then
|
|
125
145
|
local context_info
|
|
126
|
-
|
|
127
|
-
context_info=$(timeout 10s "${CLAUDE_CMD[@]}" -p "/context" 2>/dev/null | head -5)
|
|
128
|
-
else
|
|
129
|
-
context_info=$("${CLAUDE_CMD[@]}" -p "/context" 2>/dev/null | head -5)
|
|
130
|
-
fi
|
|
146
|
+
context_info=$(probe_context_usage "$claude_uuid")
|
|
131
147
|
local current_usage
|
|
132
|
-
current_usage=$(echo "$context_info" | grep -oP '\d+(?=%)' | head -1)
|
|
148
|
+
current_usage=$(echo "$context_info" | grep -oP '\d+(?=%)' | head -1 || true)
|
|
133
149
|
|
|
134
150
|
if [[ -n "$current_usage" && "$current_usage" -lt "$CONTEXT_MIN_THRESHOLD" ]]; then
|
|
135
151
|
log_session "$session_id" "INFO" "Context at ${current_usage}% - below minimum threshold (${CONTEXT_MIN_THRESHOLD}%), skipping compact"
|
|
@@ -145,8 +161,8 @@ run_auto_compact() {
|
|
|
145
161
|
|
|
146
162
|
# Handle overflow strategy
|
|
147
163
|
local overflow_result
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
local overflow_status=0
|
|
165
|
+
overflow_result=$(handle_max_compacts_reached "$session_id") || overflow_status=$?
|
|
150
166
|
|
|
151
167
|
if [[ $overflow_status -eq 0 && -n "$overflow_result" ]]; then
|
|
152
168
|
# New session ID returned - signal continuation
|
|
@@ -185,11 +201,10 @@ run_auto_compact() {
|
|
|
185
201
|
|
|
186
202
|
# Execute /compact via Claude CLI
|
|
187
203
|
local compact_output
|
|
188
|
-
local compact_status
|
|
204
|
+
local compact_status=0
|
|
189
205
|
|
|
190
206
|
if command -v timeout &> /dev/null; then
|
|
191
|
-
compact_output=$(timeout 60s "${CLAUDE_CMD[@]}" --
|
|
192
|
-
compact_status=$?
|
|
207
|
+
compact_output=$(timeout 60s "${CLAUDE_CMD[@]}" --resume "$claude_uuid" -p "/compact" 2>&1) || compact_status=$?
|
|
193
208
|
|
|
194
209
|
# Handle timeout specifically (exit code 124)
|
|
195
210
|
if [[ $compact_status -eq 124 ]]; then
|
|
@@ -198,8 +213,7 @@ run_auto_compact() {
|
|
|
198
213
|
return 1
|
|
199
214
|
fi
|
|
200
215
|
else
|
|
201
|
-
compact_output=$("${CLAUDE_CMD[@]}" --
|
|
202
|
-
compact_status=$?
|
|
216
|
+
compact_output=$("${CLAUDE_CMD[@]}" --resume "$claude_uuid" -p "/compact" 2>&1) || compact_status=$?
|
|
203
217
|
fi
|
|
204
218
|
|
|
205
219
|
if [[ $compact_status -eq 0 ]]; then
|
|
@@ -359,10 +373,10 @@ create_continuation_session() {
|
|
|
359
373
|
elif command -v xxd &>/dev/null; then
|
|
360
374
|
random_suffix=$(head -c 4 /dev/urandom | xxd -p)
|
|
361
375
|
elif command -v od &>/dev/null; then
|
|
362
|
-
random_suffix=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
376
|
+
random_suffix=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n') || true
|
|
363
377
|
else
|
|
364
378
|
# Fallback: use $RANDOM (less entropy but portable)
|
|
365
|
-
random_suffix=$(printf '%04x%04x' $RANDOM $RANDOM | head -c 8)
|
|
379
|
+
random_suffix=$(printf '%04x%04x' $RANDOM $RANDOM | head -c 8) || true
|
|
366
380
|
fi
|
|
367
381
|
|
|
368
382
|
local new_session_id="ralph-cont-${timestamp}-${random_suffix}"
|
|
@@ -404,18 +418,19 @@ check_preventive_compact() {
|
|
|
404
418
|
return 1
|
|
405
419
|
fi
|
|
406
420
|
|
|
421
|
+
# Nothing to measure before the first Claude call of this session.
|
|
422
|
+
local claude_uuid
|
|
423
|
+
claude_uuid=$(get_claude_session_uuid "$session_id")
|
|
424
|
+
[[ -z "$claude_uuid" ]] && return 1
|
|
425
|
+
|
|
407
426
|
# Try to get context usage from Claude CLI
|
|
408
427
|
# Note: This is a best-effort check, Claude may not always report usage
|
|
409
428
|
local context_info
|
|
410
|
-
|
|
411
|
-
context_info=$(timeout 10s "${CLAUDE_CMD[@]}" -p "/context" 2>/dev/null | head -5)
|
|
412
|
-
else
|
|
413
|
-
context_info=$("${CLAUDE_CMD[@]}" -p "/context" 2>/dev/null | head -5)
|
|
414
|
-
fi
|
|
429
|
+
context_info=$(probe_context_usage "$claude_uuid")
|
|
415
430
|
|
|
416
431
|
# Try to extract percentage from output
|
|
417
432
|
local usage
|
|
418
|
-
usage=$(echo "$context_info" | grep -oP '\d+(?=%)' | head -1)
|
|
433
|
+
usage=$(echo "$context_info" | grep -oP '\d+(?=%)' | head -1 || true)
|
|
419
434
|
|
|
420
435
|
if [[ -n "$usage" && "$usage" -ge "$CONTEXT_PREVENTIVE_THRESHOLD" ]]; then
|
|
421
436
|
print_warning "${MSG_CONTEXT_PREVENTIVE:-Context at ${usage}% - running preventive compact}"
|
|
@@ -423,7 +438,7 @@ check_preventive_compact() {
|
|
|
423
438
|
|
|
424
439
|
# Save progress before preventive compact
|
|
425
440
|
if type save_sprint_progress &>/dev/null; then
|
|
426
|
-
save_sprint_progress "$session_id"
|
|
441
|
+
save_sprint_progress "$session_id" || print_verbose "No sprint progress file to save"
|
|
427
442
|
fi
|
|
428
443
|
|
|
429
444
|
run_auto_compact "$session_id"
|
|
@@ -109,7 +109,7 @@ build_git_section() {
|
|
|
109
109
|
echo ""
|
|
110
110
|
echo "### Uncommitted Changes"
|
|
111
111
|
local status
|
|
112
|
-
status=$(git status --porcelain 2>/dev/null | head -10)
|
|
112
|
+
status=$(git status --porcelain 2>/dev/null | head -10) || true
|
|
113
113
|
if [[ -n "$status" ]]; then
|
|
114
114
|
echo "\`\`\`"
|
|
115
115
|
echo "$status"
|
|
@@ -18,7 +18,7 @@ get_dod_template() {
|
|
|
18
18
|
|
|
19
19
|
# Normalize project type (remove version suffixes)
|
|
20
20
|
local base_type
|
|
21
|
-
base_type=$(echo "$project_type" | sed 's/-[0-9.]*$//' | sed 's/-typescript$//')
|
|
21
|
+
base_type=$(echo "$project_type" | sed 's/-[0-9.]*$//' | sed 's/-typescript$//') || true
|
|
22
22
|
|
|
23
23
|
# Map to template file
|
|
24
24
|
local template_file=""
|
|
@@ -69,7 +69,7 @@ get_dod_template_path() {
|
|
|
69
69
|
local templates_dir="${2:-$DOD_TEMPLATES_DIR}"
|
|
70
70
|
|
|
71
71
|
local base_type
|
|
72
|
-
base_type=$(echo "$project_type" | sed 's/-[0-9.]*$//' | sed 's/-typescript$//')
|
|
72
|
+
base_type=$(echo "$project_type" | sed 's/-[0-9.]*$//' | sed 's/-typescript$//') || true
|
|
73
73
|
|
|
74
74
|
case "$base_type" in
|
|
75
75
|
symfony|laravel|php)
|
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
DOD_COMPLETION_MARKER="${DOD_COMPLETION_MARKER:-<promise>COMPLETE</promise>}"
|
|
10
10
|
|
|
11
11
|
# DoD validator types
|
|
12
|
-
declare -
|
|
12
|
+
declare -gA DOD_VALIDATORS=(
|
|
13
13
|
["command"]="validate_command"
|
|
14
14
|
["output_contains"]="validate_output_contains"
|
|
15
15
|
["file_changed"]="validate_file_changed"
|
|
@@ -27,12 +27,12 @@ validate_dod() {
|
|
|
27
27
|
|
|
28
28
|
# If no config, use simple completion marker check
|
|
29
29
|
if [[ -z "$config_file" || ! -f "$config_file" ]]; then
|
|
30
|
-
|
|
30
|
+
validate_simple_completion "$output"; return
|
|
31
31
|
fi
|
|
32
32
|
|
|
33
33
|
# Check if yq is available for YAML parsing
|
|
34
34
|
if ! command -v yq &> /dev/null; then
|
|
35
|
-
|
|
35
|
+
validate_simple_completion "$output"; return
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
38
|
# Get DoD checklist from config
|
|
@@ -44,7 +44,7 @@ validate_dod() {
|
|
|
44
44
|
if [[ -n "$marker" ]]; then
|
|
45
45
|
DOD_COMPLETION_MARKER="$marker"
|
|
46
46
|
fi
|
|
47
|
-
|
|
47
|
+
validate_simple_completion "$output"; return
|
|
48
48
|
fi
|
|
49
49
|
|
|
50
50
|
# Process checklist items
|
|
@@ -52,14 +52,14 @@ validate_dod() {
|
|
|
52
52
|
local results=()
|
|
53
53
|
|
|
54
54
|
# Get count of checklist items
|
|
55
|
-
local count=$(echo "$checklist" | yq e 'length' -)
|
|
55
|
+
local count=$(echo "$checklist" | yq e 'length' -) || true
|
|
56
56
|
|
|
57
57
|
for ((i=0; i<count; i++)); do
|
|
58
58
|
local item=$(yq e ".definition_of_done.checklist[$i]" "$config_file")
|
|
59
|
-
local id=$(echo "$item" | yq e '.id' -)
|
|
60
|
-
local name=$(echo "$item" | yq e '.name' -)
|
|
61
|
-
local type=$(echo "$item" | yq e '.type' -)
|
|
62
|
-
local required=$(echo "$item" | yq e '.required // true' -)
|
|
59
|
+
local id=$(echo "$item" | yq e '.id' -) || true
|
|
60
|
+
local name=$(echo "$item" | yq e '.name' -) || true
|
|
61
|
+
local type=$(echo "$item" | yq e '.type' -) || true
|
|
62
|
+
local required=$(echo "$item" | yq e '.required // true' -) || true
|
|
63
63
|
|
|
64
64
|
# Validate based on type
|
|
65
65
|
local passed=false
|
|
@@ -67,7 +67,7 @@ validate_dod() {
|
|
|
67
67
|
|
|
68
68
|
case "$type" in
|
|
69
69
|
command)
|
|
70
|
-
local cmd=$(echo "$item" | yq e '.command' -)
|
|
70
|
+
local cmd=$(echo "$item" | yq e '.command' -) || true
|
|
71
71
|
if validate_command "$cmd"; then
|
|
72
72
|
passed=true
|
|
73
73
|
message="Command passed"
|
|
@@ -76,7 +76,7 @@ validate_dod() {
|
|
|
76
76
|
fi
|
|
77
77
|
;;
|
|
78
78
|
output_contains)
|
|
79
|
-
local pattern=$(echo "$item" | yq e '.pattern' -)
|
|
79
|
+
local pattern=$(echo "$item" | yq e '.pattern' -) || true
|
|
80
80
|
if validate_output_contains "$output" "$pattern"; then
|
|
81
81
|
passed=true
|
|
82
82
|
message="Pattern found"
|
|
@@ -85,7 +85,7 @@ validate_dod() {
|
|
|
85
85
|
fi
|
|
86
86
|
;;
|
|
87
87
|
file_changed)
|
|
88
|
-
local pattern=$(echo "$item" | yq e '.pattern' -)
|
|
88
|
+
local pattern=$(echo "$item" | yq e '.pattern' -) || true
|
|
89
89
|
if validate_file_changed "$pattern"; then
|
|
90
90
|
passed=true
|
|
91
91
|
message="Files changed"
|
|
@@ -94,7 +94,7 @@ validate_dod() {
|
|
|
94
94
|
fi
|
|
95
95
|
;;
|
|
96
96
|
hook)
|
|
97
|
-
local script=$(echo "$item" | yq e '.script' -)
|
|
97
|
+
local script=$(echo "$item" | yq e '.script' -) || true
|
|
98
98
|
if validate_hook "$script" "$output"; then
|
|
99
99
|
passed=true
|
|
100
100
|
message="Hook passed"
|
|
@@ -103,7 +103,7 @@ validate_dod() {
|
|
|
103
103
|
fi
|
|
104
104
|
;;
|
|
105
105
|
human)
|
|
106
|
-
local prompt=$(echo "$item" | yq e '.prompt // ""' -)
|
|
106
|
+
local prompt=$(echo "$item" | yq e '.prompt // ""' -) || true
|
|
107
107
|
if validate_human "$name" "$prompt"; then
|
|
108
108
|
passed=true
|
|
109
109
|
message="Human approved"
|
|
@@ -209,13 +209,13 @@ validate_file_changed() {
|
|
|
209
209
|
|
|
210
210
|
# Use git to check for changes matching pattern
|
|
211
211
|
if command -v git &> /dev/null && git rev-parse --git-dir &> /dev/null; then
|
|
212
|
-
local changes=$(git status --porcelain 2>/dev/null | grep -E "$pattern" | wc -l)
|
|
212
|
+
local changes=$(git status --porcelain 2>/dev/null | grep -E "$pattern" | wc -l) || true
|
|
213
213
|
if [[ $changes -gt 0 ]]; then
|
|
214
214
|
return 0
|
|
215
215
|
fi
|
|
216
216
|
else
|
|
217
217
|
# Fallback: check for recently modified files
|
|
218
|
-
local changes=$(find . -name "$pattern" -mmin -5 2>/dev/null | wc -l)
|
|
218
|
+
local changes=$(find . -name "$pattern" -mmin -5 2>/dev/null | wc -l) || true
|
|
219
219
|
if [[ $changes -gt 0 ]]; then
|
|
220
220
|
return 0
|
|
221
221
|
fi
|
|
@@ -381,7 +381,7 @@ export_dod_status() {
|
|
|
381
381
|
|
|
382
382
|
# Count checks
|
|
383
383
|
local total
|
|
384
|
-
total=$(echo "$checklist" | yq e 'length' -)
|
|
384
|
+
total=$(echo "$checklist" | yq e 'length' -) || total=0
|
|
385
385
|
|
|
386
386
|
local required=0
|
|
387
387
|
for ((i=0; i<total; i++)); do
|
|
@@ -424,7 +424,7 @@ get_dod_summary() {
|
|
|
424
424
|
|
|
425
425
|
# Build summary
|
|
426
426
|
local count
|
|
427
|
-
count=$(echo "$checklist" | yq e 'length' -)
|
|
427
|
+
count=$(echo "$checklist" | yq e 'length' -) || count=0
|
|
428
428
|
|
|
429
429
|
local checks="["
|
|
430
430
|
for ((i=0; i<count; i++)); do
|
|
@@ -21,7 +21,7 @@ ESCALATION_RESOLVED_DIR=""
|
|
|
21
21
|
ESCALATION_AUDIT_FILE=""
|
|
22
22
|
|
|
23
23
|
# Escalation priorities
|
|
24
|
-
declare -
|
|
24
|
+
declare -gA ESCALATION_PRIORITIES=(
|
|
25
25
|
["critical"]=1
|
|
26
26
|
["high"]=2
|
|
27
27
|
["medium"]=3
|
|
@@ -421,7 +421,7 @@ build_slack_payload() {
|
|
|
421
421
|
|
|
422
422
|
local title="Ralph Escalation: $event"
|
|
423
423
|
local truncated_details
|
|
424
|
-
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ')
|
|
424
|
+
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ') || true
|
|
425
425
|
|
|
426
426
|
jq -n \
|
|
427
427
|
--arg color "$color" \
|
|
@@ -441,7 +441,7 @@ build_teams_payload() {
|
|
|
441
441
|
[[ "$event" == "resolved" ]] && color="good"
|
|
442
442
|
|
|
443
443
|
local truncated_details
|
|
444
|
-
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ')
|
|
444
|
+
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ') || true
|
|
445
445
|
|
|
446
446
|
jq -n \
|
|
447
447
|
--arg type "MessageCard" \
|
|
@@ -462,7 +462,7 @@ build_discord_payload() {
|
|
|
462
462
|
[[ "$event" == "resolved" ]] && color=65280 # Green
|
|
463
463
|
|
|
464
464
|
local truncated_details
|
|
465
|
-
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ')
|
|
465
|
+
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ') || true
|
|
466
466
|
|
|
467
467
|
jq -n \
|
|
468
468
|
--arg title "Ralph Escalation: $event" \
|
|
@@ -479,7 +479,7 @@ build_generic_payload() {
|
|
|
479
479
|
local id="$1" event="$2" level="$3" error="$4" details="$5" session="$6"
|
|
480
480
|
|
|
481
481
|
local truncated_details
|
|
482
|
-
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ')
|
|
482
|
+
truncated_details=$(echo "$details" | head -c 500 | tr '\n' ' ') || true
|
|
483
483
|
|
|
484
484
|
jq -n \
|
|
485
485
|
--arg event "$event" \
|
|
@@ -95,7 +95,8 @@ generate_hooks_config() {
|
|
|
95
95
|
# Merge with existing settings
|
|
96
96
|
local merged
|
|
97
97
|
if command -v jq &>/dev/null; then
|
|
98
|
-
merged=$(echo "$existing_settings" | jq --argjson hooks "$hooks_config" '. + {hooks: $hooks}')
|
|
98
|
+
merged=$(echo "$existing_settings" | jq --argjson hooks "$hooks_config" '. + {hooks: $hooks}') \
|
|
99
|
+
|| merged="{\"hooks\": $hooks_config}"
|
|
99
100
|
else
|
|
100
101
|
# Fallback: just write hooks config
|
|
101
102
|
merged="{\"hooks\": $hooks_config}"
|
|
@@ -56,7 +56,7 @@ ralph_finalize_loop() {
|
|
|
56
56
|
|
|
57
57
|
# Save sprint progress before summary
|
|
58
58
|
if type save_sprint_progress &>/dev/null; then
|
|
59
|
-
save_sprint_progress "$session_id"
|
|
59
|
+
save_sprint_progress "$session_id" || print_verbose "No sprint progress file to save"
|
|
60
60
|
fi
|
|
61
61
|
|
|
62
62
|
# Save metrics export
|
package/Tools/Ralph/lib/loop.sh
CHANGED
|
@@ -13,6 +13,30 @@ CLAUDE_ARGS="${CLAUDE_ARGS:-}"
|
|
|
13
13
|
# Claude Invocation
|
|
14
14
|
# =============================================================================
|
|
15
15
|
|
|
16
|
+
# Print a random RFC 4122 v4 UUID (`claude --session-id` requires a valid UUID).
|
|
17
|
+
generate_uuid() {
|
|
18
|
+
if [[ -r /proc/sys/kernel/random/uuid ]]; then
|
|
19
|
+
cat /proc/sys/kernel/random/uuid
|
|
20
|
+
elif command -v uuidgen &> /dev/null; then
|
|
21
|
+
uuidgen | tr '[:upper:]' '[:lower:]'
|
|
22
|
+
else
|
|
23
|
+
local hex
|
|
24
|
+
hex=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n') || true
|
|
25
|
+
printf '%s-%s-4%s-%x%s-%s\n' "${hex:0:8}" "${hex:8:4}" "${hex:13:3}" \
|
|
26
|
+
$(( (0x${hex:16:1} & 0x3) | 0x8 )) "${hex:17:3}" "${hex:20:12}"
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Print the Claude session UUID pinned to a Ralph session, or nothing if
|
|
31
|
+
# Claude has not been invoked yet for it. Always succeeds (set -e safe).
|
|
32
|
+
get_claude_session_uuid() {
|
|
33
|
+
local id_file="$RALPH_SESSION_BASE/sessions/$1/claude_session_id"
|
|
34
|
+
if [[ -n "$1" && -s "$id_file" ]]; then
|
|
35
|
+
cat "$id_file"
|
|
36
|
+
fi
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
invoke_claude() {
|
|
17
41
|
local session_id="$1"
|
|
18
42
|
local prompt="$2"
|
|
@@ -25,9 +49,21 @@ invoke_claude() {
|
|
|
25
49
|
local -a cmd
|
|
26
50
|
IFS=' ' read -r -a cmd <<< "${CLAUDE_COMMAND:-claude}"
|
|
27
51
|
|
|
28
|
-
#
|
|
52
|
+
# Pin an explicit Claude session per Ralph session. `--continue` resumes
|
|
53
|
+
# "the most recent conversation in this directory", which parallel workers
|
|
54
|
+
# (and unrelated user sessions) would share.
|
|
29
55
|
if [[ -n "$session_id" ]]; then
|
|
30
|
-
|
|
56
|
+
local claude_session_uuid
|
|
57
|
+
claude_session_uuid=$(get_claude_session_uuid "$session_id")
|
|
58
|
+
if [[ -n "$claude_session_uuid" ]]; then
|
|
59
|
+
cmd+=(--resume "$claude_session_uuid")
|
|
60
|
+
else
|
|
61
|
+
local id_file="$RALPH_SESSION_BASE/sessions/$session_id/claude_session_id"
|
|
62
|
+
claude_session_uuid=$(generate_uuid)
|
|
63
|
+
mkdir -p "$(dirname "$id_file")"
|
|
64
|
+
echo "$claude_session_uuid" > "$id_file"
|
|
65
|
+
cmd+=(--session-id "$claude_session_uuid")
|
|
66
|
+
fi
|
|
31
67
|
fi
|
|
32
68
|
|
|
33
69
|
# Add prompt
|
|
@@ -39,12 +75,11 @@ invoke_claude() {
|
|
|
39
75
|
# Execute with timeout
|
|
40
76
|
local timeout_sec=$((timeout / 1000))
|
|
41
77
|
local output
|
|
42
|
-
local exit_code
|
|
78
|
+
local exit_code=0
|
|
43
79
|
|
|
44
80
|
# Use timeout command if available
|
|
45
81
|
if command -v timeout &> /dev/null; then
|
|
46
|
-
output=$(timeout "${timeout_sec}s" "${cmd[@]}" "$prompt" 2>&1)
|
|
47
|
-
exit_code=$?
|
|
82
|
+
output=$(timeout "${timeout_sec}s" "${cmd[@]}" "$prompt" 2>&1) || exit_code=$?
|
|
48
83
|
|
|
49
84
|
# Check for timeout (exit code 124)
|
|
50
85
|
if [[ $exit_code -eq 124 ]]; then
|
|
@@ -53,8 +88,7 @@ invoke_claude() {
|
|
|
53
88
|
fi
|
|
54
89
|
else
|
|
55
90
|
# Fallback without timeout
|
|
56
|
-
output=$("${cmd[@]}" "$prompt" 2>&1)
|
|
57
|
-
exit_code=$?
|
|
91
|
+
output=$("${cmd[@]}" "$prompt" 2>&1) || exit_code=$?
|
|
58
92
|
fi
|
|
59
93
|
|
|
60
94
|
# Log result
|
|
@@ -104,7 +138,7 @@ detect_file_changes() {
|
|
|
104
138
|
|
|
105
139
|
# Use git to detect changes if available
|
|
106
140
|
if command -v git &> /dev/null && git rev-parse --git-dir &> /dev/null; then
|
|
107
|
-
local changes=$(git status --porcelain 2>/dev/null | wc -l)
|
|
141
|
+
local changes=$(git status --porcelain 2>/dev/null | wc -l) || true
|
|
108
142
|
log_session "$session_id" "DEBUG" "Detected $changes file changes"
|
|
109
143
|
echo "$changes"
|
|
110
144
|
else
|
|
@@ -112,7 +146,7 @@ detect_file_changes() {
|
|
|
112
146
|
local changes=0
|
|
113
147
|
for dir in src lib app tests; do
|
|
114
148
|
if [[ -d "$dir" ]]; then
|
|
115
|
-
local recent=$(find "$dir" -type f -mmin -1 2>/dev/null | wc -l)
|
|
149
|
+
local recent=$(find "$dir" -type f -mmin -1 2>/dev/null | wc -l) || true
|
|
116
150
|
changes=$((changes + recent))
|
|
117
151
|
fi
|
|
118
152
|
done
|
|
@@ -219,7 +253,7 @@ get_loop_state() {
|
|
|
219
253
|
status: (.status // "unknown"),
|
|
220
254
|
circuit_breaker: (.circuit_breaker // {}),
|
|
221
255
|
dod_results: (.dod_results // [])
|
|
222
|
-
}' 2>/dev/null)
|
|
256
|
+
}' 2>/dev/null) || result=""
|
|
223
257
|
|
|
224
258
|
if [[ -n "$result" ]]; then
|
|
225
259
|
echo "$result"
|
|
@@ -12,7 +12,7 @@ METRICS_REALTIME_ENABLED="${METRICS_REALTIME_ENABLED:-true}"
|
|
|
12
12
|
METRICS_REALTIME_INTERVAL="${METRICS_REALTIME_INTERVAL:-1000}"
|
|
13
13
|
|
|
14
14
|
# Metrics data
|
|
15
|
-
declare -
|
|
15
|
+
declare -gA METRICS_DATA
|
|
16
16
|
METRICS_START_TIME=0
|
|
17
17
|
METRICS_ITERATION_TIMES=()
|
|
18
18
|
METRICS_CONTEXT_SAMPLES=()
|
|
@@ -17,8 +17,9 @@ 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
|
-
|
|
21
|
-
declare -
|
|
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
|
|
@@ -36,6 +37,8 @@ PARALLEL_WORKER_DIR=""
|
|
|
36
37
|
init_parallel_manager() {
|
|
37
38
|
PARALLEL_BASE_DIR="${RALPH_SESSION_BASE:-.ralph}/parallel"
|
|
38
39
|
mkdir -p "$PARALLEL_BASE_DIR"
|
|
40
|
+
# Absolute: workers cd into their own worktree
|
|
41
|
+
PARALLEL_BASE_DIR="$(cd "$PARALLEL_BASE_DIR" && pwd)"
|
|
39
42
|
|
|
40
43
|
PARALLEL_STATE_FILE="$PARALLEL_BASE_DIR/state.yaml"
|
|
41
44
|
PARALLEL_DEPENDENCY_GRAPH="$PARALLEL_BASE_DIR/dependencies.json"
|
|
@@ -102,10 +105,11 @@ build_dependency_graph() {
|
|
|
102
105
|
local bmad_dir="${BMAD_DIR:-.bmad}"
|
|
103
106
|
local status_file="$bmad_dir/sprint-status.yaml"
|
|
104
107
|
|
|
108
|
+
# Without a status file (or yq), all stories are independent nodes: the
|
|
109
|
+
# graph must still list them, get_ready_stories reads it.
|
|
110
|
+
local with_deps=true
|
|
105
111
|
if [[ ! -f "$status_file" ]] || ! command -v yq &>/dev/null; then
|
|
106
|
-
|
|
107
|
-
echo '{"nodes":{},"edges":[]}'
|
|
108
|
-
return
|
|
112
|
+
with_deps=false
|
|
109
113
|
fi
|
|
110
114
|
|
|
111
115
|
local nodes="{}"
|
|
@@ -113,17 +117,20 @@ build_dependency_graph() {
|
|
|
113
117
|
|
|
114
118
|
for story_id in $stories; do
|
|
115
119
|
# Get story dependencies
|
|
116
|
-
local deps
|
|
117
|
-
|
|
120
|
+
local deps="[]"
|
|
121
|
+
if [[ "$with_deps" == "true" ]]; then
|
|
122
|
+
deps=$(yq ".stories.${story_id}.blocked_by // []" "$status_file" 2>/dev/null) || deps="[]"
|
|
123
|
+
fi
|
|
118
124
|
|
|
119
|
-
# Add node
|
|
120
|
-
|
|
125
|
+
# Add node (on jq failure, keep the previous value: `next` buffers it)
|
|
126
|
+
local next
|
|
127
|
+
next=$(echo "$nodes" | jq --arg id "$story_id" '. + {($id): {"status": "pending", "deps": []}}') && nodes="$next"
|
|
121
128
|
|
|
122
129
|
# Add edges for dependencies
|
|
123
130
|
if [[ "$deps" != "[]" && "$deps" != "null" ]]; then
|
|
124
|
-
for dep in $(echo "$deps" | jq -r '.[]' 2>/dev/null); do
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
for dep in $(echo "$deps" | jq -r '.[]' 2>/dev/null || true); do
|
|
132
|
+
next=$(echo "$edges" | jq --arg from "$dep" --arg to "$story_id" '. + [{"from": $from, "to": $to}]') && edges="$next"
|
|
133
|
+
next=$(echo "$nodes" | jq --arg id "$story_id" --arg dep "$dep" '.[$id].deps += [$dep]') && nodes="$next"
|
|
127
134
|
done
|
|
128
135
|
fi
|
|
129
136
|
done
|
|
@@ -144,18 +151,20 @@ get_ready_stories() {
|
|
|
144
151
|
fi
|
|
145
152
|
|
|
146
153
|
# Find nodes with status=pending and all deps completed
|
|
154
|
+
# ($root: jq has no `$.` root accessor — that syntax failed to compile)
|
|
147
155
|
jq -r '
|
|
156
|
+
. as $root |
|
|
148
157
|
.nodes | to_entries[] |
|
|
149
158
|
select(.value.status == "pending") |
|
|
150
159
|
select(
|
|
151
160
|
(.value.deps | length) == 0 or
|
|
152
161
|
(
|
|
153
162
|
.value.deps as $deps |
|
|
154
|
-
($deps | map(. as $d |
|
|
163
|
+
($deps | map(. as $d | $root.nodes[$d].status == "completed") | all)
|
|
155
164
|
)
|
|
156
165
|
) |
|
|
157
166
|
.key
|
|
158
|
-
' "$PARALLEL_DEPENDENCY_GRAPH" 2>/dev/null
|
|
167
|
+
' "$PARALLEL_DEPENDENCY_GRAPH" 2>/dev/null || true
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
# Mark story as completed in dependency graph
|
|
@@ -171,6 +180,19 @@ mark_story_completed() {
|
|
|
171
180
|
echo "$updated" > "$PARALLEL_DEPENDENCY_GRAPH"
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
# Mark story as running in dependency graph (leader only, at spawn time)
|
|
184
|
+
mark_story_running() {
|
|
185
|
+
local story_id="$1"
|
|
186
|
+
|
|
187
|
+
if [[ ! -f "$PARALLEL_DEPENDENCY_GRAPH" ]] || ! command -v jq &>/dev/null; then
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
local updated
|
|
192
|
+
updated=$(jq --arg id "$story_id" '.nodes[$id].status = "running"' "$PARALLEL_DEPENDENCY_GRAPH")
|
|
193
|
+
echo "$updated" > "$PARALLEL_DEPENDENCY_GRAPH"
|
|
194
|
+
}
|
|
195
|
+
|
|
174
196
|
# Mark story as failed in dependency graph
|
|
175
197
|
mark_story_failed() {
|
|
176
198
|
local story_id="$1"
|
|
@@ -211,11 +233,11 @@ check_resource_availability() {
|
|
|
211
233
|
mem_usage=$(free | grep Mem | awk '{print int($3/$2 * 100)}' 2>/dev/null || echo "0")
|
|
212
234
|
elif [[ "$(uname)" == "Darwin" ]]; then
|
|
213
235
|
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 '.')
|
|
236
|
+
pages_free=$(vm_stat | grep "Pages free" | awk '{print $3}' | tr -d '.') || pages_free=0
|
|
237
|
+
pages_active=$(vm_stat | grep "Pages active" | awk '{print $3}' | tr -d '.') || pages_active=0
|
|
238
|
+
pages_inactive=$(vm_stat | grep "Pages inactive" | awk '{print $3}' | tr -d '.') || pages_inactive=0
|
|
239
|
+
pages_speculative=$(vm_stat | grep "Pages speculative" | awk '{print $3}' | tr -d '.') || pages_speculative=0
|
|
240
|
+
pages_wired=$(vm_stat | grep "Pages wired" | awk '{print $4}' | tr -d '.') || pages_wired=0
|
|
219
241
|
local total=$((pages_free + pages_active + pages_inactive + pages_speculative + pages_wired))
|
|
220
242
|
local used=$((pages_active + pages_wired))
|
|
221
243
|
mem_usage=$((used * 100 / total))
|
|
@@ -232,9 +254,7 @@ check_resource_availability() {
|
|
|
232
254
|
# Get number of available slots
|
|
233
255
|
get_available_slots() {
|
|
234
256
|
local active_count=0
|
|
235
|
-
|
|
236
|
-
active_count=${#PARALLEL_PIDS[@]}
|
|
237
|
-
fi
|
|
257
|
+
active_count=${#PARALLEL_PIDS[@]}
|
|
238
258
|
local available=$((PARALLEL_MAX_CONCURRENT - active_count))
|
|
239
259
|
|
|
240
260
|
# Further limit if resources are constrained
|
|
@@ -279,7 +299,7 @@ EOF
|
|
|
279
299
|
local ralph_script=""
|
|
280
300
|
for path in "${SCRIPT_DIR}/ralph.sh" "Tools/Ralph/ralph.sh" "$(dirname "${BASH_SOURCE[0]}")/../ralph.sh"; do
|
|
281
301
|
if [[ -f "$path" ]]; then
|
|
282
|
-
ralph_script="$path"
|
|
302
|
+
ralph_script="$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
|
|
283
303
|
break
|
|
284
304
|
fi
|
|
285
305
|
done
|
|
@@ -290,19 +310,35 @@ EOF
|
|
|
290
310
|
fi
|
|
291
311
|
|
|
292
312
|
# Build command
|
|
293
|
-
local cmd="$ralph_script"
|
|
294
|
-
[[ -n "$config" ]] && cmd
|
|
313
|
+
local -a cmd=("$ralph_script")
|
|
314
|
+
[[ -n "$config" ]] && cmd+=("--config=$config")
|
|
315
|
+
|
|
316
|
+
# Isolate the worker's working tree (one git worktree per session)
|
|
317
|
+
local worktree_dir=""
|
|
318
|
+
if ! worktree_dir=$(create_worker_worktree "$session_id"); then
|
|
319
|
+
worktree_dir=""
|
|
320
|
+
print_warning "No git worktree for $session_id: worker shares the current directory"
|
|
321
|
+
fi
|
|
295
322
|
|
|
296
323
|
# Spawn in background
|
|
297
324
|
# Workers write to individual temp files (single-writer pattern).
|
|
298
325
|
# The leader merges results after all workers complete.
|
|
299
326
|
local worker_id="${story_id}-$$-$(date +%s)"
|
|
300
327
|
(
|
|
301
|
-
|
|
328
|
+
if [[ -n "$worktree_dir" ]]; then
|
|
329
|
+
cd "$worktree_dir" || exit 1
|
|
330
|
+
fi
|
|
331
|
+
# Per-worker Ralph state (incl. the Claude session UUID) under the
|
|
332
|
+
# leader's .ralph/, so it survives the worktree removal.
|
|
333
|
+
export RALPH_SESSION_BASE="$session_dir/ralph"
|
|
302
334
|
|
|
303
335
|
local output
|
|
304
|
-
|
|
305
|
-
|
|
336
|
+
local status=0
|
|
337
|
+
output=$("${cmd[@]}" "$prompt" 2>&1) || status=$?
|
|
338
|
+
|
|
339
|
+
if [[ -n "$worktree_dir" ]] && ! cleanup_worker_worktree "$worktree_dir" "$story_id"; then
|
|
340
|
+
output+=$'\n'"[WARN] Worktree kept: $worktree_dir"
|
|
341
|
+
fi
|
|
306
342
|
|
|
307
343
|
# Save output (per-session dir, no conflict)
|
|
308
344
|
echo "$output" > "$session_dir/output.log"
|
|
@@ -340,6 +376,30 @@ EOF
|
|
|
340
376
|
echo "$session_id"
|
|
341
377
|
}
|
|
342
378
|
|
|
379
|
+
# Create a dedicated git worktree on branch ralph/<session_id> so parallel
|
|
380
|
+
# workers never share a working tree. Prints its path; fails outside git.
|
|
381
|
+
create_worker_worktree() {
|
|
382
|
+
local session_id="$1"
|
|
383
|
+
local worktree_dir="$PARALLEL_BASE_DIR/worktrees/$session_id"
|
|
384
|
+
|
|
385
|
+
git rev-parse --is-inside-work-tree &> /dev/null || return 1
|
|
386
|
+
git worktree add -q -b "ralph/$session_id" "$worktree_dir" HEAD &> /dev/null || return 1
|
|
387
|
+
echo "$worktree_dir"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Commit leftover changes as WIP on the worker branch, then remove the
|
|
391
|
+
# worktree. If the commit fails, the dirty worktree is kept (no data loss).
|
|
392
|
+
cleanup_worker_worktree() {
|
|
393
|
+
local worktree_dir="$1"
|
|
394
|
+
local story_id="$2"
|
|
395
|
+
|
|
396
|
+
if [[ -n "$(git -C "$worktree_dir" status --porcelain)" ]]; then
|
|
397
|
+
git -C "$worktree_dir" add -A
|
|
398
|
+
git -C "$worktree_dir" commit -q -m "wip(ralph): $story_id" || return 1
|
|
399
|
+
fi
|
|
400
|
+
git -C "$worktree_dir" worktree remove "$worktree_dir"
|
|
401
|
+
}
|
|
402
|
+
|
|
343
403
|
# =============================================================================
|
|
344
404
|
# Session Management
|
|
345
405
|
# =============================================================================
|
|
@@ -359,9 +419,9 @@ collect_completed_sessions() {
|
|
|
359
419
|
local session_id="${PARALLEL_PIDS[$pid]}"
|
|
360
420
|
local story_id="${PARALLEL_SESSIONS[$session_id]}"
|
|
361
421
|
|
|
362
|
-
# Check exit status
|
|
363
|
-
|
|
364
|
-
|
|
422
|
+
# Check exit status (a failed worker must not kill the leader)
|
|
423
|
+
local exit_code=0
|
|
424
|
+
wait "$pid" 2>/dev/null || exit_code=$?
|
|
365
425
|
|
|
366
426
|
if [[ $exit_code -eq 0 ]]; then
|
|
367
427
|
completed+=("$story_id")
|
|
@@ -395,9 +455,7 @@ wait_all_sessions() {
|
|
|
395
455
|
start_time=$(date +%s)
|
|
396
456
|
|
|
397
457
|
local active_count=0
|
|
398
|
-
|
|
399
|
-
active_count=${#PARALLEL_PIDS[@]}
|
|
400
|
-
fi
|
|
458
|
+
active_count=${#PARALLEL_PIDS[@]}
|
|
401
459
|
|
|
402
460
|
while [[ $active_count -gt 0 ]]; do
|
|
403
461
|
collect_completed_sessions > /dev/null
|
|
@@ -415,9 +473,7 @@ wait_all_sessions() {
|
|
|
415
473
|
|
|
416
474
|
# Update active count
|
|
417
475
|
active_count=0
|
|
418
|
-
|
|
419
|
-
active_count=${#PARALLEL_PIDS[@]}
|
|
420
|
-
fi
|
|
476
|
+
active_count=${#PARALLEL_PIDS[@]}
|
|
421
477
|
done
|
|
422
478
|
|
|
423
479
|
return 0
|
|
@@ -426,9 +482,7 @@ wait_all_sessions() {
|
|
|
426
482
|
# Get active session count
|
|
427
483
|
get_active_count() {
|
|
428
484
|
local count=0
|
|
429
|
-
|
|
430
|
-
count=${#PARALLEL_PIDS[@]}
|
|
431
|
-
fi
|
|
485
|
+
count=${#PARALLEL_PIDS[@]}
|
|
432
486
|
echo "$count"
|
|
433
487
|
}
|
|
434
488
|
|
|
@@ -455,7 +509,7 @@ process_stories_parallel() {
|
|
|
455
509
|
|
|
456
510
|
local processed=0
|
|
457
511
|
local total
|
|
458
|
-
total=$(echo "$stories" | wc -w | tr -d ' ')
|
|
512
|
+
total=$(echo "$stories" | wc -w | tr -d ' ') || total=0
|
|
459
513
|
|
|
460
514
|
while [[ $processed -lt $total ]]; do
|
|
461
515
|
# Collect completed sessions
|
|
@@ -473,9 +527,6 @@ process_stories_parallel() {
|
|
|
473
527
|
for story_id in $ready; do
|
|
474
528
|
[[ $slots -le 0 ]] && break
|
|
475
529
|
|
|
476
|
-
# Skip if already processed
|
|
477
|
-
[[ -n "${PARALLEL_SESSIONS[$story_id]:-}" ]] && continue
|
|
478
|
-
|
|
479
530
|
# Build prompt
|
|
480
531
|
local prompt="$prompt_template $story_id"
|
|
481
532
|
|
|
@@ -488,10 +539,15 @@ process_stories_parallel() {
|
|
|
488
539
|
prompt="Implement user story $story_id: $title. Follow TDD approach."
|
|
489
540
|
fi
|
|
490
541
|
|
|
491
|
-
# Spawn session
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
542
|
+
# Spawn session. "running" keeps get_ready_stories (pending
|
|
543
|
+
# only) from re-spawning it before its report is merged.
|
|
544
|
+
if spawn_session "$story_id" "$prompt"; then
|
|
545
|
+
mark_story_running "$story_id"
|
|
546
|
+
else
|
|
547
|
+
mark_story_failed "$story_id"
|
|
548
|
+
fi
|
|
549
|
+
slots=$((slots - 1))
|
|
550
|
+
processed=$((processed + 1))
|
|
495
551
|
done
|
|
496
552
|
fi
|
|
497
553
|
|
|
@@ -503,6 +559,9 @@ process_stories_parallel() {
|
|
|
503
559
|
print_info "Waiting for remaining sessions to complete..."
|
|
504
560
|
wait_all_sessions
|
|
505
561
|
|
|
562
|
+
# Drop stale worktree metadata (e.g. a worker killed before its cleanup)
|
|
563
|
+
git worktree prune &> /dev/null || true
|
|
564
|
+
|
|
506
565
|
# Print summary
|
|
507
566
|
print_parallel_summary
|
|
508
567
|
}
|
|
@@ -567,7 +626,7 @@ merge_worker_reports() {
|
|
|
567
626
|
if [[ "$dry_run" == "true" ]]; then
|
|
568
627
|
print_info "DRY RUN - Would merge: session=$session_id story=$story_id status=$status" 2>/dev/null \
|
|
569
628
|
|| echo "[INFO] DRY RUN - Would merge: session=$session_id story=$story_id status=$status"
|
|
570
|
-
((merged_count
|
|
629
|
+
merged_count=$((merged_count + 1))
|
|
571
630
|
continue
|
|
572
631
|
fi
|
|
573
632
|
|
|
@@ -591,7 +650,7 @@ merge_worker_reports() {
|
|
|
591
650
|
fi
|
|
592
651
|
|
|
593
652
|
rm -f "$report_file"
|
|
594
|
-
((merged_count
|
|
653
|
+
merged_count=$((merged_count + 1))
|
|
595
654
|
done
|
|
596
655
|
|
|
597
656
|
# Update efficiency after merge
|
|
@@ -781,6 +840,11 @@ cmd_parallel_manager() {
|
|
|
781
840
|
echo "Single-Writer Pattern:"
|
|
782
841
|
echo " Background workers write to .ralph/parallel/workers/worker-{id}.yaml"
|
|
783
842
|
echo " Only the leader merges results into shared state files"
|
|
843
|
+
echo ""
|
|
844
|
+
echo "Isolation:"
|
|
845
|
+
echo " Each worker runs in its own git worktree (branch ralph/<session>),"
|
|
846
|
+
echo " removed at the end of the run (leftover changes committed as WIP)."
|
|
847
|
+
echo " Each Ralph session pins its own Claude session (--session-id, then --resume)."
|
|
784
848
|
;;
|
|
785
849
|
*)
|
|
786
850
|
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 -
|
|
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 -
|
|
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 -
|
|
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 -
|
|
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 -
|
|
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 -
|
|
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}"
|
|
@@ -196,7 +196,7 @@ run_sprint_conductor_parallel() {
|
|
|
196
196
|
fi
|
|
197
197
|
|
|
198
198
|
local story_count
|
|
199
|
-
story_count=$(echo "$all_stories" | wc -w | tr -d ' ')
|
|
199
|
+
story_count=$(echo "$all_stories" | wc -w | tr -d ' ') || story_count=0
|
|
200
200
|
log_conductor "Found $story_count stories for parallel processing"
|
|
201
201
|
|
|
202
202
|
# Build dependency graph
|
|
@@ -749,7 +749,7 @@ build_story_prompt() {
|
|
|
749
749
|
local title description acceptance_criteria
|
|
750
750
|
title=$(yq ".stories.${story_id}.title" "$status_file" 2>/dev/null)
|
|
751
751
|
description=$(yq ".stories.${story_id}.description" "$status_file" 2>/dev/null)
|
|
752
|
-
acceptance_criteria=$(yq ".stories.${story_id}.acceptance_criteria[]" "$status_file" 2>/dev/null | tr '\n' '; ')
|
|
752
|
+
acceptance_criteria=$(yq ".stories.${story_id}.acceptance_criteria[]" "$status_file" 2>/dev/null | tr '\n' '; ') || true
|
|
753
753
|
|
|
754
754
|
prompt="Implement user story: $title
|
|
755
755
|
|
|
@@ -288,7 +288,7 @@ get_completed_tasks_count() {
|
|
|
288
288
|
|
|
289
289
|
update_sprint_progress() {
|
|
290
290
|
local section="$1"
|
|
291
|
-
local content="$2"
|
|
291
|
+
local content="${2:-}" # optional: "compact" is called without content
|
|
292
292
|
|
|
293
293
|
if [[ "$SPRINT_PROGRESS_ENABLED" != "true" ]]; then
|
|
294
294
|
return 0
|
|
@@ -343,10 +343,10 @@ update_state_section() {
|
|
|
343
343
|
local state_json="$1"
|
|
344
344
|
|
|
345
345
|
# Parse JSON and update each field
|
|
346
|
-
local sprint=$(echo "$state_json" | grep -oP '"sprint":\s*"\K[^"]*' 2>/dev/null)
|
|
347
|
-
local current_us=$(echo "$state_json" | grep -oP '"current_us":\s*"\K[^"]*' 2>/dev/null)
|
|
348
|
-
local current_task=$(echo "$state_json" | grep -oP '"current_task":\s*"\K[^"]*' 2>/dev/null)
|
|
349
|
-
local phase=$(echo "$state_json" | grep -oP '"phase":\s*"\K[^"]*' 2>/dev/null)
|
|
346
|
+
local sprint=$(echo "$state_json" | grep -oP '"sprint":\s*"\K[^"]*' 2>/dev/null) || true
|
|
347
|
+
local current_us=$(echo "$state_json" | grep -oP '"current_us":\s*"\K[^"]*' 2>/dev/null) || true
|
|
348
|
+
local current_task=$(echo "$state_json" | grep -oP '"current_task":\s*"\K[^"]*' 2>/dev/null) || true
|
|
349
|
+
local phase=$(echo "$state_json" | grep -oP '"phase":\s*"\K[^"]*' 2>/dev/null) || true
|
|
350
350
|
|
|
351
351
|
# Build list of sed patterns (escape special chars)
|
|
352
352
|
local patterns=()
|
|
@@ -657,7 +657,7 @@ compact_on_sprint_start() {
|
|
|
657
657
|
print_info "${MSG_CONTEXT_SPRINT_START:-Compact at sprint start for clean context}..."
|
|
658
658
|
|
|
659
659
|
# Save initial state
|
|
660
|
-
save_sprint_progress "$session_id"
|
|
660
|
+
save_sprint_progress "$session_id" || print_verbose "No sprint progress file to save"
|
|
661
661
|
|
|
662
662
|
# Run compact if available
|
|
663
663
|
if type run_auto_compact &>/dev/null; then
|
|
@@ -688,7 +688,7 @@ compact_on_task_complete() {
|
|
|
688
688
|
if [[ -n "$task_info" ]]; then
|
|
689
689
|
update_sprint_progress "task_complete" "$task_info"
|
|
690
690
|
fi
|
|
691
|
-
save_sprint_progress "$session_id"
|
|
691
|
+
save_sprint_progress "$session_id" || print_verbose "No sprint progress file to save"
|
|
692
692
|
|
|
693
693
|
# Run compact
|
|
694
694
|
if type run_auto_compact &>/dev/null; then
|
|
@@ -710,7 +710,7 @@ compact_on_us_complete() {
|
|
|
710
710
|
|
|
711
711
|
if [[ "$SPRINT_COMPACT_ON_US_COMPLETE" != "true" ]]; then
|
|
712
712
|
# Even if compact is disabled, we should save and checkpoint
|
|
713
|
-
save_sprint_progress "$session_id"
|
|
713
|
+
save_sprint_progress "$session_id" || print_verbose "No sprint progress file to save"
|
|
714
714
|
if type create_checkpoint &>/dev/null; then
|
|
715
715
|
create_checkpoint "$session_id" "us-complete-$us_id"
|
|
716
716
|
fi
|
|
@@ -721,7 +721,7 @@ compact_on_us_complete() {
|
|
|
721
721
|
print_info "${MSG_CONTEXT_US_COMPLETE:-US complete - running strategic compact}..."
|
|
722
722
|
|
|
723
723
|
# Save and checkpoint
|
|
724
|
-
save_sprint_progress "$session_id"
|
|
724
|
+
save_sprint_progress "$session_id" || print_verbose "No sprint progress file to save"
|
|
725
725
|
if type create_checkpoint &>/dev/null; then
|
|
726
726
|
create_checkpoint "$session_id" "us-complete-$us_id"
|
|
727
727
|
fi
|
package/Tools/Ralph/ralph.sh
CHANGED
|
@@ -499,14 +499,15 @@ run_ralph() {
|
|
|
499
499
|
|
|
500
500
|
# Check for preventive compact (before hitting the limit)
|
|
501
501
|
if type check_preventive_compact &>/dev/null; then
|
|
502
|
-
check_preventive_compact "$SESSION_ID"
|
|
502
|
+
check_preventive_compact "$SESSION_ID" || true
|
|
503
503
|
fi
|
|
504
504
|
|
|
505
505
|
# Check if we have a continuation prompt (after compact/session handoff)
|
|
506
506
|
local effective_prompt="$PROMPT"
|
|
507
507
|
if type get_context_continuation_prompt &>/dev/null; then
|
|
508
508
|
local continuation_prompt
|
|
509
|
-
|
|
509
|
+
# Returns 1 when there is no continuation context (nominal).
|
|
510
|
+
continuation_prompt=$(get_context_continuation_prompt) || true
|
|
510
511
|
if [[ -n "$continuation_prompt" ]]; then
|
|
511
512
|
# Prepend continuation context to the prompt
|
|
512
513
|
effective_prompt="$continuation_prompt
|
|
@@ -520,9 +521,11 @@ $PROMPT"
|
|
|
520
521
|
|
|
521
522
|
# Invoke Claude
|
|
522
523
|
print_info "${MSG_INVOKING_CLAUDE}"
|
|
524
|
+
# `|| invoke_status=$?`: a failed call must reach the circuit breaker,
|
|
525
|
+
# not kill the loop through errexit.
|
|
523
526
|
local response
|
|
524
|
-
|
|
525
|
-
|
|
527
|
+
local invoke_status=0
|
|
528
|
+
response=$(invoke_claude "$SESSION_ID" "$effective_prompt" "$TIMEOUT") || invoke_status=$?
|
|
526
529
|
|
|
527
530
|
if [[ $invoke_status -ne 0 ]]; then
|
|
528
531
|
print_error "${MSG_ERROR}: Claude invocation failed"
|
|
@@ -536,8 +539,8 @@ $PROMPT"
|
|
|
536
539
|
|
|
537
540
|
if type run_auto_compact &>/dev/null; then
|
|
538
541
|
local compact_result
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
local compact_status=0
|
|
543
|
+
compact_result=$(run_auto_compact "$SESSION_ID") || compact_status=$?
|
|
541
544
|
|
|
542
545
|
case $compact_status in
|
|
543
546
|
0)
|
|
@@ -548,7 +551,7 @@ $PROMPT"
|
|
|
548
551
|
local retry_prompt="$PROMPT"
|
|
549
552
|
if type get_context_continuation_prompt &>/dev/null; then
|
|
550
553
|
local cont_prompt
|
|
551
|
-
cont_prompt=$(get_context_continuation_prompt)
|
|
554
|
+
cont_prompt=$(get_context_continuation_prompt) || true
|
|
552
555
|
if [[ -n "$cont_prompt" ]]; then
|
|
553
556
|
retry_prompt="$cont_prompt
|
|
554
557
|
|
|
@@ -558,8 +561,8 @@ $PROMPT"
|
|
|
558
561
|
fi
|
|
559
562
|
fi
|
|
560
563
|
|
|
561
|
-
|
|
562
|
-
invoke_status=$?
|
|
564
|
+
invoke_status=0
|
|
565
|
+
response=$(invoke_claude "$SESSION_ID" "$retry_prompt" "$TIMEOUT") || invoke_status=$?
|
|
563
566
|
|
|
564
567
|
if [[ $invoke_status -ne 0 ]]; then
|
|
565
568
|
print_error "${MSG_ERROR}: Claude invocation failed after compact"
|
|
@@ -581,10 +584,10 @@ $PROMPT"
|
|
|
581
584
|
fi
|
|
582
585
|
|
|
583
586
|
# Retry with new session and handoff context
|
|
587
|
+
invoke_status=0
|
|
584
588
|
response=$(invoke_claude "$SESSION_ID" "$handoff_prompt
|
|
585
589
|
|
|
586
|
-
$PROMPT" "$TIMEOUT")
|
|
587
|
-
invoke_status=$?
|
|
590
|
+
$PROMPT" "$TIMEOUT") || invoke_status=$?
|
|
588
591
|
|
|
589
592
|
if [[ $invoke_status -ne 0 ]]; then
|
|
590
593
|
print_error "${MSG_ERROR}: Claude invocation failed in continuation session"
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-bearded-bear/claude-craft",
|
|
3
|
-
"version": "8.26.0-next.
|
|
3
|
+
"version": "8.26.0-next.defc34b",
|
|
4
4
|
"description": "A comprehensive framework for AI-assisted development with Claude Code. Install standardized rules, agents, and commands for your projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "cli/index.js",
|