prizmkit 1.1.26 → 1.1.29
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/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/run-bugfix.sh +3 -8
- package/bundled/dev-pipeline/run-feature.sh +3 -8
- package/bundled/dev-pipeline/run-recovery.sh +691 -0
- package/bundled/dev-pipeline/run-refactor.sh +3 -8
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +759 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/recovery-workflow/SKILL.md +35 -0
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +111 -42
- package/package.json +1 -1
- package/src/upgrade.js +18 -11
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# ============================================================
|
|
5
|
+
# dev-pipeline/run-recovery.sh - Recover Interrupted Workflow Sessions
|
|
6
|
+
#
|
|
7
|
+
# Auto-detects which workflow (feature-workflow, bug-fix-workflow,
|
|
8
|
+
# refactor-workflow) was interrupted and what phase it reached,
|
|
9
|
+
# then generates a comprehensive bootstrap prompt and spawns an
|
|
10
|
+
# AI CLI session to complete all remaining phases.
|
|
11
|
+
#
|
|
12
|
+
# Unlike run-feature.sh / run-bugfix.sh / run-refactor.sh, this
|
|
13
|
+
# is a single-shot operation (no pipeline loop) for one detected
|
|
14
|
+
# interrupted workflow.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# ./run-recovery.sh Auto-detect and recover
|
|
18
|
+
# ./run-recovery.sh run Same as above
|
|
19
|
+
# ./run-recovery.sh run --dry-run Generate prompt only
|
|
20
|
+
# ./run-recovery.sh run --yes Skip confirmation
|
|
21
|
+
# ./run-recovery.sh run --model <model> Override AI model
|
|
22
|
+
# ./run-recovery.sh detect Detection report only
|
|
23
|
+
# ./run-recovery.sh help Show help
|
|
24
|
+
#
|
|
25
|
+
# Environment Variables:
|
|
26
|
+
# SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
|
|
27
|
+
# AI_CLI AI CLI command name (auto-detected: cbc or claude)
|
|
28
|
+
# PRIZMKIT_PLATFORM Force platform: 'codebuddy' or 'claude' (auto-detected)
|
|
29
|
+
# MODEL AI model to use (e.g. claude-opus-4.6)
|
|
30
|
+
# VERBOSE Set to 1 to enable --verbose on AI CLI
|
|
31
|
+
# HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
|
|
32
|
+
# STALE_KILL_THRESHOLD Auto-kill after N seconds of no progress (default: 900)
|
|
33
|
+
# AUTO_PUSH Auto-push after successful recovery (default: 0)
|
|
34
|
+
# ============================================================
|
|
35
|
+
|
|
36
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
38
|
+
RECOVERY_STATE_DIR="${PROJECT_ROOT}/.prizmkit/state/recovery"
|
|
39
|
+
SCRIPTS_DIR="$SCRIPT_DIR/scripts"
|
|
40
|
+
RECOVERY_DETECT_SCRIPT="${PROJECT_ROOT}/core/skills/orchestration-skill/workflows/recovery-workflow/scripts/detect-recovery-state.py"
|
|
41
|
+
|
|
42
|
+
# Configuration
|
|
43
|
+
SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
|
|
44
|
+
HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
|
|
45
|
+
STALE_KILL_THRESHOLD=${STALE_KILL_THRESHOLD:-900}
|
|
46
|
+
VERBOSE=${VERBOSE:-0}
|
|
47
|
+
MODEL=${MODEL:-""}
|
|
48
|
+
AUTO_PUSH=${AUTO_PUSH:-0}
|
|
49
|
+
|
|
50
|
+
# Source shared common helpers (CLI/platform detection + logs + deps)
|
|
51
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
52
|
+
prizm_detect_cli_and_platform
|
|
53
|
+
|
|
54
|
+
# Source shared heartbeat library
|
|
55
|
+
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
56
|
+
|
|
57
|
+
# Source shared branch library
|
|
58
|
+
source "$SCRIPT_DIR/lib/branch.sh"
|
|
59
|
+
|
|
60
|
+
# Detect stream-json support
|
|
61
|
+
detect_stream_json_support "$CLI_CMD"
|
|
62
|
+
|
|
63
|
+
# Session tracking (for cleanup)
|
|
64
|
+
_SESSION_DIR=""
|
|
65
|
+
_SESSION_PID=""
|
|
66
|
+
|
|
67
|
+
# ============================================================
|
|
68
|
+
# Help
|
|
69
|
+
# ============================================================
|
|
70
|
+
|
|
71
|
+
show_help() {
|
|
72
|
+
cat <<'HELP'
|
|
73
|
+
Usage: ./run-recovery.sh [command] [options]
|
|
74
|
+
|
|
75
|
+
Commands:
|
|
76
|
+
run Auto-detect interrupted workflow and recover (default)
|
|
77
|
+
detect Show detection report only (no execution)
|
|
78
|
+
help Show this help
|
|
79
|
+
|
|
80
|
+
Options (for 'run' command):
|
|
81
|
+
--dry-run Generate bootstrap prompt without spawning AI session
|
|
82
|
+
--yes Skip user confirmation
|
|
83
|
+
--model M Override AI model (e.g. claude-opus-4.6)
|
|
84
|
+
|
|
85
|
+
Environment Variables:
|
|
86
|
+
SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
|
|
87
|
+
MODEL AI model to use
|
|
88
|
+
VERBOSE Set to 1 for verbose AI CLI output
|
|
89
|
+
HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
|
|
90
|
+
STALE_KILL_THRESHOLD Auto-kill after N seconds no progress (default: 900)
|
|
91
|
+
AUTO_PUSH Auto-push after success (default: 0)
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
./run-recovery.sh # Auto-detect and recover
|
|
95
|
+
./run-recovery.sh detect # Show what would be recovered
|
|
96
|
+
./run-recovery.sh run --dry-run # Generate prompt, don't execute
|
|
97
|
+
./run-recovery.sh run --yes # Skip confirmation prompt
|
|
98
|
+
./run-recovery.sh run --model claude-opus-4.6
|
|
99
|
+
HELP
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ============================================================
|
|
103
|
+
# Cleanup trap
|
|
104
|
+
# ============================================================
|
|
105
|
+
|
|
106
|
+
cleanup() {
|
|
107
|
+
local exit_code=$?
|
|
108
|
+
# Kill any background processes
|
|
109
|
+
if [[ -n "${_SESSION_PID:-}" ]]; then
|
|
110
|
+
kill "$_SESSION_PID" 2>/dev/null || true
|
|
111
|
+
wait "$_SESSION_PID" 2>/dev/null || true
|
|
112
|
+
fi
|
|
113
|
+
# Stop heartbeat if running
|
|
114
|
+
stop_heartbeat "${_HEARTBEAT_PID:-}" 2>/dev/null || true
|
|
115
|
+
# Log recovery session location
|
|
116
|
+
if [[ -n "${_SESSION_DIR:-}" && -d "${_SESSION_DIR:-}" ]]; then
|
|
117
|
+
log_info "Session directory: $_SESSION_DIR"
|
|
118
|
+
if [[ -f "$_SESSION_DIR/logs/session.log" ]]; then
|
|
119
|
+
log_info "Session log: $_SESSION_DIR/logs/session.log"
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
122
|
+
if [[ $exit_code -ne 0 && $exit_code -ne 130 ]]; then
|
|
123
|
+
log_info "Re-run ./run-recovery.sh to try again."
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
126
|
+
trap cleanup EXIT
|
|
127
|
+
|
|
128
|
+
# ============================================================
|
|
129
|
+
# Detection
|
|
130
|
+
# ============================================================
|
|
131
|
+
|
|
132
|
+
run_detection() {
|
|
133
|
+
local detection_output
|
|
134
|
+
local detect_stderr_file
|
|
135
|
+
detect_stderr_file=$(mktemp)
|
|
136
|
+
detection_output=$(python3 "$RECOVERY_DETECT_SCRIPT" --project-root "$PROJECT_ROOT" 2>"$detect_stderr_file") || {
|
|
137
|
+
log_error "Detection script failed"
|
|
138
|
+
if [[ -s "$detect_stderr_file" ]]; then
|
|
139
|
+
log_error "Details: $(cat "$detect_stderr_file")"
|
|
140
|
+
fi
|
|
141
|
+
rm -f "$detect_stderr_file"
|
|
142
|
+
return 1
|
|
143
|
+
}
|
|
144
|
+
# Show migration warnings or other informational messages from detection
|
|
145
|
+
if [[ -s "$detect_stderr_file" ]]; then
|
|
146
|
+
log_warn "$(cat "$detect_stderr_file")"
|
|
147
|
+
fi
|
|
148
|
+
rm -f "$detect_stderr_file"
|
|
149
|
+
echo "$detection_output"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
display_report() {
|
|
153
|
+
local detection_json="$1"
|
|
154
|
+
|
|
155
|
+
local detected workflow_type branch phase phase_name reason remaining
|
|
156
|
+
detected=$(echo "$detection_json" | jq -r '.detected // false')
|
|
157
|
+
|
|
158
|
+
if [[ "$detected" != "true" ]]; then
|
|
159
|
+
echo ""
|
|
160
|
+
log_info "No interrupted workflow detected in this workspace."
|
|
161
|
+
echo ""
|
|
162
|
+
echo " To start a new workflow:"
|
|
163
|
+
echo " • /feature-workflow — build features from idea to code"
|
|
164
|
+
echo " • /bug-fix-workflow — fix a specific bug interactively"
|
|
165
|
+
echo " • /refactor-workflow — behavior-preserving code restructuring"
|
|
166
|
+
echo ""
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
workflow_type=$(echo "$detection_json" | jq -r '.workflow_type')
|
|
171
|
+
branch=$(echo "$detection_json" | jq -r '.git.current_branch // "unknown"')
|
|
172
|
+
phase=$(echo "$detection_json" | jq -r '.phase')
|
|
173
|
+
phase_name=$(echo "$detection_json" | jq -r '.phase_name')
|
|
174
|
+
reason=$(echo "$detection_json" | jq -r '.recovery.reason // ""')
|
|
175
|
+
remaining=$(echo "$detection_json" | jq -r '.recovery.remaining_work // ""')
|
|
176
|
+
|
|
177
|
+
local uncommitted staged commits_ahead
|
|
178
|
+
uncommitted=$(echo "$detection_json" | jq -r '.git.uncommitted_files // 0')
|
|
179
|
+
staged=$(echo "$detection_json" | jq -r '.git.staged_files // 0')
|
|
180
|
+
commits_ahead=$(echo "$detection_json" | jq -r '.git.commits_ahead_of_main // 0')
|
|
181
|
+
|
|
182
|
+
local has_changes files_modified files_added test_files
|
|
183
|
+
has_changes=$(echo "$detection_json" | jq -r '.code.has_changes // false')
|
|
184
|
+
files_modified=$(echo "$detection_json" | jq -r '.code.files_modified // 0')
|
|
185
|
+
files_added=$(echo "$detection_json" | jq -r '.code.files_added // 0')
|
|
186
|
+
test_files=$(echo "$detection_json" | jq -r '.code.test_files_touched // 0')
|
|
187
|
+
|
|
188
|
+
echo ""
|
|
189
|
+
echo "═══════════════════════════════════════════════════"
|
|
190
|
+
echo " Recovery Report"
|
|
191
|
+
echo "═══════════════════════════════════════════════════"
|
|
192
|
+
echo ""
|
|
193
|
+
echo " Workflow: $workflow_type"
|
|
194
|
+
echo " Branch: $branch"
|
|
195
|
+
echo " Phase: $phase — $phase_name"
|
|
196
|
+
echo ""
|
|
197
|
+
|
|
198
|
+
if [[ "$has_changes" == "true" ]]; then
|
|
199
|
+
echo " Code Changes:"
|
|
200
|
+
echo " Modified: $files_modified files"
|
|
201
|
+
echo " Added: $files_added files"
|
|
202
|
+
echo " Tests: $test_files test files touched"
|
|
203
|
+
echo ""
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
echo " Git State:"
|
|
207
|
+
echo " Uncommitted: $uncommitted files"
|
|
208
|
+
echo " Staged: $staged files"
|
|
209
|
+
echo " Ahead: $commits_ahead commits ahead of main"
|
|
210
|
+
echo ""
|
|
211
|
+
echo " Reason: $reason"
|
|
212
|
+
echo " Remaining: $remaining"
|
|
213
|
+
echo ""
|
|
214
|
+
|
|
215
|
+
# Show other interrupted workflows (if any)
|
|
216
|
+
local others
|
|
217
|
+
others=$(echo "$detection_json" | jq -r '.other_interrupted_workflows // [] | .[]' 2>/dev/null)
|
|
218
|
+
if [[ -n "$others" ]]; then
|
|
219
|
+
echo " Note: Other interrupted workflows also detected:"
|
|
220
|
+
echo "$others" | while IFS= read -r w; do
|
|
221
|
+
echo " • $w"
|
|
222
|
+
done
|
|
223
|
+
echo " (Run recovery again after this one completes)"
|
|
224
|
+
echo ""
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
echo "═══════════════════════════════════════════════════"
|
|
228
|
+
echo ""
|
|
229
|
+
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# ============================================================
|
|
234
|
+
# Session spawn (simplified from run-bugfix.sh)
|
|
235
|
+
# ============================================================
|
|
236
|
+
|
|
237
|
+
spawn_recovery_session() {
|
|
238
|
+
local bootstrap_prompt="$1"
|
|
239
|
+
local session_dir="$2"
|
|
240
|
+
local session_model="${3:-}"
|
|
241
|
+
|
|
242
|
+
local session_log="$session_dir/logs/session.log"
|
|
243
|
+
local progress_json="$session_dir/logs/progress.json"
|
|
244
|
+
|
|
245
|
+
local verbose_flag=""
|
|
246
|
+
if [[ "$VERBOSE" == "1" ]]; then
|
|
247
|
+
verbose_flag="--verbose"
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
local stream_json_flag=""
|
|
251
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
252
|
+
stream_json_flag="--output-format stream-json"
|
|
253
|
+
verbose_flag="--verbose"
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
local model_flag=""
|
|
257
|
+
local effective_model="${session_model:-$MODEL}"
|
|
258
|
+
if [[ -n "$effective_model" ]]; then
|
|
259
|
+
model_flag="--model $effective_model"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# Unset CLAUDECODE to prevent "nested session" error
|
|
263
|
+
unset CLAUDECODE 2>/dev/null || true
|
|
264
|
+
|
|
265
|
+
log_info "Spawning AI CLI session..."
|
|
266
|
+
log_info "CLI: $CLI_CMD"
|
|
267
|
+
[[ -n "$effective_model" ]] && log_info "Model: $effective_model"
|
|
268
|
+
log_info "Prompt: $bootstrap_prompt"
|
|
269
|
+
|
|
270
|
+
case "$CLI_CMD" in
|
|
271
|
+
*claude*)
|
|
272
|
+
"$CLI_CMD" \
|
|
273
|
+
-p "$(cat "$bootstrap_prompt")" \
|
|
274
|
+
--dangerously-skip-permissions \
|
|
275
|
+
$verbose_flag \
|
|
276
|
+
$stream_json_flag \
|
|
277
|
+
$model_flag \
|
|
278
|
+
> "$session_log" 2>&1 &
|
|
279
|
+
;;
|
|
280
|
+
*)
|
|
281
|
+
# CodeBuddy (cbc) and others: prompt via stdin
|
|
282
|
+
"$CLI_CMD" \
|
|
283
|
+
--print \
|
|
284
|
+
-y \
|
|
285
|
+
$verbose_flag \
|
|
286
|
+
$stream_json_flag \
|
|
287
|
+
$model_flag \
|
|
288
|
+
< "$bootstrap_prompt" \
|
|
289
|
+
> "$session_log" 2>&1 &
|
|
290
|
+
;;
|
|
291
|
+
esac
|
|
292
|
+
_SESSION_PID=$!
|
|
293
|
+
|
|
294
|
+
# Start progress parser
|
|
295
|
+
start_progress_parser "$session_log" "$progress_json" "$SCRIPTS_DIR"
|
|
296
|
+
local parser_pid="${_PARSER_PID:-}"
|
|
297
|
+
|
|
298
|
+
# Timeout watchdog
|
|
299
|
+
local watcher_pid=""
|
|
300
|
+
if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
301
|
+
( sleep "$SESSION_TIMEOUT" && kill -TERM "$_SESSION_PID" 2>/dev/null ) &
|
|
302
|
+
watcher_pid=$!
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
# Heartbeat monitor
|
|
306
|
+
start_heartbeat "$_SESSION_PID" "$session_log" "$progress_json" "$HEARTBEAT_INTERVAL" "$STALE_KILL_THRESHOLD"
|
|
307
|
+
_HEARTBEAT_PID="${_HEARTBEAT_PID:-}"
|
|
308
|
+
|
|
309
|
+
# Wait for session to finish
|
|
310
|
+
local exit_code=0
|
|
311
|
+
if wait "$_SESSION_PID" 2>/dev/null; then
|
|
312
|
+
exit_code=0
|
|
313
|
+
else
|
|
314
|
+
exit_code=$?
|
|
315
|
+
fi
|
|
316
|
+
_SESSION_PID=""
|
|
317
|
+
|
|
318
|
+
# Cleanup background processes
|
|
319
|
+
[[ -n "$watcher_pid" ]] && kill "$watcher_pid" 2>/dev/null || true
|
|
320
|
+
stop_heartbeat "${_HEARTBEAT_PID:-}"
|
|
321
|
+
_HEARTBEAT_PID=""
|
|
322
|
+
stop_progress_parser "$parser_pid"
|
|
323
|
+
[[ -n "$watcher_pid" ]] && wait "$watcher_pid" 2>/dev/null || true
|
|
324
|
+
|
|
325
|
+
# Map SIGTERM to timeout
|
|
326
|
+
if [[ $exit_code -eq 143 ]]; then
|
|
327
|
+
exit_code=124
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# Check for stale-kill
|
|
331
|
+
local stale_kill_marker="$session_dir/logs/stale-kill.json"
|
|
332
|
+
if [[ -f "$stale_kill_marker" ]]; then
|
|
333
|
+
log_warn "Session was stale-killed (no progress for too long)"
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# Log session summary
|
|
337
|
+
if [[ -f "$session_log" ]]; then
|
|
338
|
+
local final_size final_lines
|
|
339
|
+
final_size=$(wc -c < "$session_log" 2>/dev/null | tr -d ' ')
|
|
340
|
+
final_lines=$(wc -l < "$session_log" 2>/dev/null | tr -d ' ')
|
|
341
|
+
log_info "Session log: $final_lines lines, $((final_size / 1024))KB"
|
|
342
|
+
fi
|
|
343
|
+
log_info "Session exit code: $exit_code"
|
|
344
|
+
|
|
345
|
+
return $exit_code
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# ============================================================
|
|
349
|
+
# Post-session outcome detection
|
|
350
|
+
# ============================================================
|
|
351
|
+
|
|
352
|
+
detect_session_outcome() {
|
|
353
|
+
local exit_code="$1"
|
|
354
|
+
local session_dir="$2"
|
|
355
|
+
local workflow_type="$3"
|
|
356
|
+
local main_branch="${4:-main}"
|
|
357
|
+
|
|
358
|
+
local stale_kill_marker="$session_dir/logs/stale-kill.json"
|
|
359
|
+
local was_stale_killed=false
|
|
360
|
+
[[ -f "$stale_kill_marker" ]] && was_stale_killed=true
|
|
361
|
+
|
|
362
|
+
if [[ $exit_code -eq 124 ]]; then
|
|
363
|
+
log_warn "Session timed out"
|
|
364
|
+
echo "timed_out"
|
|
365
|
+
return
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
if [[ "$was_stale_killed" == true ]]; then
|
|
369
|
+
log_warn "Session stale-killed"
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# Check for commits
|
|
373
|
+
local has_commits=""
|
|
374
|
+
has_commits=$(git -C "$PROJECT_ROOT" log "${main_branch}..HEAD" --oneline 2>/dev/null | head -1 || true)
|
|
375
|
+
|
|
376
|
+
if [[ -n "$has_commits" ]]; then
|
|
377
|
+
echo "success"
|
|
378
|
+
return
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# Check for uncommitted changes
|
|
382
|
+
local uncommitted=""
|
|
383
|
+
uncommitted=$(git -C "$PROJECT_ROOT" status --porcelain 2>/dev/null | head -1 || true)
|
|
384
|
+
if [[ -n "$uncommitted" ]]; then
|
|
385
|
+
# Try auto-commit to preserve work (like run-bugfix.sh does)
|
|
386
|
+
log_info "Uncommitted changes found — attempting to preserve work..."
|
|
387
|
+
git -C "$PROJECT_ROOT" add -A 2>/dev/null || true
|
|
388
|
+
if git -C "$PROJECT_ROOT" commit --no-verify -m "wip(recovery): auto-save interrupted session work" 2>/dev/null; then
|
|
389
|
+
log_info "Auto-saved recovery work as WIP commit"
|
|
390
|
+
echo "success_wip"
|
|
391
|
+
return
|
|
392
|
+
fi
|
|
393
|
+
echo "partial"
|
|
394
|
+
return
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if [[ $exit_code -ne 0 ]]; then
|
|
398
|
+
echo "failed"
|
|
399
|
+
else
|
|
400
|
+
echo "no_changes"
|
|
401
|
+
fi
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# ============================================================
|
|
405
|
+
# Commands
|
|
406
|
+
# ============================================================
|
|
407
|
+
|
|
408
|
+
cmd_detect() {
|
|
409
|
+
prizm_check_common_dependencies
|
|
410
|
+
prizm_ensure_git_repo "$PROJECT_ROOT"
|
|
411
|
+
|
|
412
|
+
local detection_json
|
|
413
|
+
detection_json=$(run_detection) || {
|
|
414
|
+
log_error "Detection failed"
|
|
415
|
+
exit 1
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
display_report "$detection_json"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
cmd_run() {
|
|
422
|
+
local dry_run=false
|
|
423
|
+
local skip_confirm=false
|
|
424
|
+
local model_override=""
|
|
425
|
+
|
|
426
|
+
# Parse options
|
|
427
|
+
while [[ $# -gt 0 ]]; do
|
|
428
|
+
case "$1" in
|
|
429
|
+
--dry-run)
|
|
430
|
+
dry_run=true
|
|
431
|
+
shift
|
|
432
|
+
;;
|
|
433
|
+
--yes|-y)
|
|
434
|
+
skip_confirm=true
|
|
435
|
+
shift
|
|
436
|
+
;;
|
|
437
|
+
--model)
|
|
438
|
+
model_override="$2"
|
|
439
|
+
shift 2
|
|
440
|
+
;;
|
|
441
|
+
*)
|
|
442
|
+
log_error "Unknown option: $1"
|
|
443
|
+
show_help
|
|
444
|
+
exit 1
|
|
445
|
+
;;
|
|
446
|
+
esac
|
|
447
|
+
done
|
|
448
|
+
|
|
449
|
+
# Step 1: Check dependencies
|
|
450
|
+
prizm_check_common_dependencies
|
|
451
|
+
prizm_ensure_git_repo "$PROJECT_ROOT"
|
|
452
|
+
|
|
453
|
+
# Step 2: Run detection
|
|
454
|
+
log_info "Detecting interrupted workflow..."
|
|
455
|
+
local detection_json
|
|
456
|
+
detection_json=$(run_detection) || {
|
|
457
|
+
log_error "Detection failed"
|
|
458
|
+
exit 1
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# Step 3: Display report
|
|
462
|
+
if ! display_report "$detection_json"; then
|
|
463
|
+
exit 0 # No workflow detected, clean exit
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
local workflow_type phase phase_name main_branch
|
|
467
|
+
workflow_type=$(echo "$detection_json" | jq -r '.workflow_type')
|
|
468
|
+
phase=$(echo "$detection_json" | jq -r '.phase')
|
|
469
|
+
phase_name=$(echo "$detection_json" | jq -r '.phase_name')
|
|
470
|
+
|
|
471
|
+
# Detect main branch (used for post-session commit check)
|
|
472
|
+
main_branch="main"
|
|
473
|
+
if ! git -C "$PROJECT_ROOT" rev-parse --verify main >/dev/null 2>&1; then
|
|
474
|
+
if git -C "$PROJECT_ROOT" rev-parse --verify master >/dev/null 2>&1; then
|
|
475
|
+
main_branch="master"
|
|
476
|
+
fi
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
# Step 4: User confirmation
|
|
480
|
+
if [[ "$skip_confirm" != true ]]; then
|
|
481
|
+
echo -n "Ready to resume from Phase $phase ($phase_name). Continue? [y/N] "
|
|
482
|
+
read -r answer
|
|
483
|
+
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
|
484
|
+
log_info "Recovery cancelled by user."
|
|
485
|
+
exit 0
|
|
486
|
+
fi
|
|
487
|
+
else
|
|
488
|
+
log_info "Confirmation skipped (--yes flag)"
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
# Step 5: Create session directory
|
|
492
|
+
local timestamp session_id session_dir
|
|
493
|
+
timestamp=$(date +%Y%m%d-%H%M%S)
|
|
494
|
+
session_id="recovery-${workflow_type}-${timestamp}"
|
|
495
|
+
session_dir="$RECOVERY_STATE_DIR/$session_id"
|
|
496
|
+
_SESSION_DIR="$session_dir"
|
|
497
|
+
mkdir -p "$session_dir/logs"
|
|
498
|
+
|
|
499
|
+
# Save detection JSON for reference
|
|
500
|
+
local detection_file="$session_dir/detection.json"
|
|
501
|
+
echo "$detection_json" > "$detection_file"
|
|
502
|
+
|
|
503
|
+
# Step 6: Generate bootstrap prompt
|
|
504
|
+
log_info "Generating recovery bootstrap prompt..."
|
|
505
|
+
local bootstrap_prompt="$session_dir/bootstrap-prompt.md"
|
|
506
|
+
local gen_stderr_file="$session_dir/logs/prompt-gen-stderr.log"
|
|
507
|
+
local gen_output
|
|
508
|
+
gen_output=$(python3 "$SCRIPTS_DIR/generate-recovery-prompt.py" \
|
|
509
|
+
--detection-json "$detection_file" \
|
|
510
|
+
--output "$bootstrap_prompt" \
|
|
511
|
+
--project-root "$PROJECT_ROOT" \
|
|
512
|
+
--session-id "$session_id" \
|
|
513
|
+
2>"$gen_stderr_file") || {
|
|
514
|
+
log_error "Failed to generate recovery prompt"
|
|
515
|
+
if [[ -s "$gen_stderr_file" ]]; then
|
|
516
|
+
log_error "Details: $(cat "$gen_stderr_file")"
|
|
517
|
+
fi
|
|
518
|
+
exit 1
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
local gen_success
|
|
522
|
+
gen_success=$(echo "$gen_output" | jq -r '.success // false')
|
|
523
|
+
if [[ "$gen_success" != "true" ]]; then
|
|
524
|
+
local gen_error
|
|
525
|
+
gen_error=$(echo "$gen_output" | jq -r '.error // "unknown error"')
|
|
526
|
+
log_error "Prompt generation failed: $gen_error"
|
|
527
|
+
exit 1
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
local prompt_lines
|
|
531
|
+
prompt_lines=$(wc -l < "$bootstrap_prompt" | tr -d ' ')
|
|
532
|
+
log_success "Bootstrap prompt generated: $prompt_lines lines"
|
|
533
|
+
log_info "Prompt: $bootstrap_prompt"
|
|
534
|
+
|
|
535
|
+
# Step 7: Dry run exits here
|
|
536
|
+
if [[ "$dry_run" == true ]]; then
|
|
537
|
+
echo ""
|
|
538
|
+
log_success "Dry run complete. Inspect the prompt with:"
|
|
539
|
+
echo " cat $bootstrap_prompt"
|
|
540
|
+
echo ""
|
|
541
|
+
echo "To execute recovery:"
|
|
542
|
+
echo " ./run-recovery.sh run --yes"
|
|
543
|
+
exit 0
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
# Step 8: Record current branch
|
|
547
|
+
local current_branch
|
|
548
|
+
current_branch=$(git -C "$PROJECT_ROOT" branch --show-current 2>/dev/null || echo "unknown")
|
|
549
|
+
log_info "Current branch: $current_branch"
|
|
550
|
+
log_info "NOTE: Recovery continues on current branch (no new branch created)"
|
|
551
|
+
|
|
552
|
+
# Step 9: Spawn AI CLI session
|
|
553
|
+
echo ""
|
|
554
|
+
log_info "Starting recovery session..."
|
|
555
|
+
log_info "Workflow: $workflow_type"
|
|
556
|
+
log_info "Resuming from: Phase $phase ($phase_name)"
|
|
557
|
+
echo ""
|
|
558
|
+
|
|
559
|
+
local exit_code=0
|
|
560
|
+
spawn_recovery_session "$bootstrap_prompt" "$session_dir" "$model_override" || exit_code=$?
|
|
561
|
+
|
|
562
|
+
# Step 10: Detect outcome
|
|
563
|
+
local outcome
|
|
564
|
+
outcome=$(detect_session_outcome "$exit_code" "$session_dir" "$workflow_type" "$main_branch")
|
|
565
|
+
|
|
566
|
+
# Step 11: Post-session report
|
|
567
|
+
echo ""
|
|
568
|
+
echo "═══════════════════════════════════════════════════"
|
|
569
|
+
echo " Recovery Result"
|
|
570
|
+
echo "═══════════════════════════════════════════════════"
|
|
571
|
+
echo ""
|
|
572
|
+
echo " Workflow: $workflow_type"
|
|
573
|
+
echo " Recovered from: Phase $phase ($phase_name)"
|
|
574
|
+
echo " Outcome: $outcome"
|
|
575
|
+
echo " Session log: $session_dir/logs/session.log"
|
|
576
|
+
echo ""
|
|
577
|
+
|
|
578
|
+
case "$outcome" in
|
|
579
|
+
success)
|
|
580
|
+
log_success "Recovery completed successfully!"
|
|
581
|
+
# Auto-push if configured
|
|
582
|
+
if [[ "$AUTO_PUSH" == "1" ]]; then
|
|
583
|
+
log_info "Auto-pushing to remote (AUTO_PUSH=1)..."
|
|
584
|
+
if git -C "$PROJECT_ROOT" push 2>/dev/null; then
|
|
585
|
+
log_success "Pushed to remote"
|
|
586
|
+
else
|
|
587
|
+
log_warn "Push failed — push manually: git push"
|
|
588
|
+
fi
|
|
589
|
+
fi
|
|
590
|
+
echo ""
|
|
591
|
+
echo " Next steps:"
|
|
592
|
+
echo " • Review changes: git log --oneline -5"
|
|
593
|
+
echo " • Run tests: npm test"
|
|
594
|
+
if [[ "$workflow_type" == "bug-fix-workflow" && "$current_branch" == fix/* ]]; then
|
|
595
|
+
echo " • Merge to main: git checkout main && git merge $current_branch"
|
|
596
|
+
fi
|
|
597
|
+
if [[ "$AUTO_PUSH" != "1" ]]; then
|
|
598
|
+
echo " • Push when ready: git push"
|
|
599
|
+
fi
|
|
600
|
+
echo ""
|
|
601
|
+
;;
|
|
602
|
+
success_wip)
|
|
603
|
+
log_warn "Recovery session interrupted — work auto-saved as WIP commit."
|
|
604
|
+
echo ""
|
|
605
|
+
echo " The session was interrupted but uncommitted changes were saved."
|
|
606
|
+
echo " The WIP commit preserves your progress."
|
|
607
|
+
echo ""
|
|
608
|
+
echo " Next steps:"
|
|
609
|
+
echo " • Run recovery again to complete remaining phases: ./run-recovery.sh"
|
|
610
|
+
echo " • Or review the WIP commit: git log --oneline -1"
|
|
611
|
+
echo ""
|
|
612
|
+
;;
|
|
613
|
+
partial)
|
|
614
|
+
log_warn "Session completed but has uncommitted changes."
|
|
615
|
+
echo ""
|
|
616
|
+
echo " The session did work but didn't finish committing."
|
|
617
|
+
echo " Check uncommitted changes:"
|
|
618
|
+
echo " git status"
|
|
619
|
+
echo " git diff"
|
|
620
|
+
echo ""
|
|
621
|
+
echo " Options:"
|
|
622
|
+
echo " • Run recovery again: ./run-recovery.sh"
|
|
623
|
+
echo " • Commit manually: git add <files> && git commit"
|
|
624
|
+
echo ""
|
|
625
|
+
;;
|
|
626
|
+
timed_out)
|
|
627
|
+
log_warn "Session timed out before completing."
|
|
628
|
+
echo ""
|
|
629
|
+
echo " Options:"
|
|
630
|
+
echo " • Run recovery again: ./run-recovery.sh"
|
|
631
|
+
echo " • Increase timeout: SESSION_TIMEOUT=3600 ./run-recovery.sh"
|
|
632
|
+
echo ""
|
|
633
|
+
;;
|
|
634
|
+
no_changes)
|
|
635
|
+
log_warn "Session completed but produced no changes."
|
|
636
|
+
echo ""
|
|
637
|
+
echo " The session may have already been fully recovered,"
|
|
638
|
+
echo " or the AI couldn't make progress."
|
|
639
|
+
echo " Check the session log for details:"
|
|
640
|
+
echo " cat $session_dir/logs/session.log"
|
|
641
|
+
echo ""
|
|
642
|
+
;;
|
|
643
|
+
*)
|
|
644
|
+
log_error "Recovery session failed."
|
|
645
|
+
echo ""
|
|
646
|
+
echo " Check session log for errors:"
|
|
647
|
+
echo " cat $session_dir/logs/session.log"
|
|
648
|
+
echo ""
|
|
649
|
+
echo " Options:"
|
|
650
|
+
echo " • Run recovery again: ./run-recovery.sh"
|
|
651
|
+
echo " • Start fresh: /<workflow> command"
|
|
652
|
+
echo ""
|
|
653
|
+
;;
|
|
654
|
+
esac
|
|
655
|
+
|
|
656
|
+
echo "═══════════════════════════════════════════════════"
|
|
657
|
+
|
|
658
|
+
if [[ "$outcome" == "success" ]]; then
|
|
659
|
+
exit 0
|
|
660
|
+
else
|
|
661
|
+
exit 1
|
|
662
|
+
fi
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
# ============================================================
|
|
666
|
+
# Main
|
|
667
|
+
# ============================================================
|
|
668
|
+
|
|
669
|
+
main() {
|
|
670
|
+
local command="${1:-run}"
|
|
671
|
+
shift || true
|
|
672
|
+
|
|
673
|
+
case "$command" in
|
|
674
|
+
run)
|
|
675
|
+
cmd_run "$@"
|
|
676
|
+
;;
|
|
677
|
+
detect)
|
|
678
|
+
cmd_detect
|
|
679
|
+
;;
|
|
680
|
+
help|--help|-h)
|
|
681
|
+
show_help
|
|
682
|
+
;;
|
|
683
|
+
*)
|
|
684
|
+
log_error "Unknown command: $command"
|
|
685
|
+
show_help
|
|
686
|
+
exit 1
|
|
687
|
+
;;
|
|
688
|
+
esac
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
main "$@"
|