prizmkit 1.1.67 → 1.1.69
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/lib/common.sh +40 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +5 -5
- package/bundled/dev-pipeline/run-bugfix.sh +26 -5
- package/bundled/dev-pipeline/run-feature.sh +20 -3
- package/bundled/dev-pipeline/run-refactor.sh +26 -5
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +217 -18
- package/bundled/dev-pipeline/scripts/update-bug-status.py +15 -0
- package/bundled/dev-pipeline/scripts/update-feature-status.py +18 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +15 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +19 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +19 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +22 -1
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +10 -0
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +10 -0
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +12 -0
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +12 -0
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +5 -1
- package/bundled/dev-pipeline/tests/test_auto_skip.py +39 -0
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +1 -1
- package/bundled/dev-pipeline-windows/lib/common.ps1 +19 -0
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +19 -3
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +217 -18
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +15 -0
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +18 -0
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +15 -0
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +22 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +10 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +10 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +12 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +12 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +5 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +5 -1
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -418,6 +418,46 @@ prizm_start_ai_session() {
|
|
|
418
418
|
PRIZM_AI_PID=$!
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
# Detect AI CLI/provider infrastructure failures that are outside the
|
|
422
|
+
# generated code's control. These should be retried without consuming the
|
|
423
|
+
# item's code retry budget.
|
|
424
|
+
prizm_detect_infra_error() {
|
|
425
|
+
local session_log="${1:-}"
|
|
426
|
+
local progress_json="${2:-}"
|
|
427
|
+
|
|
428
|
+
local haystack=""
|
|
429
|
+
if [[ -n "$session_log" && -f "$session_log" ]]; then
|
|
430
|
+
haystack="$(tail -c 65536 "$session_log" 2>/dev/null || true)"
|
|
431
|
+
fi
|
|
432
|
+
if [[ -n "$progress_json" && -f "$progress_json" ]]; then
|
|
433
|
+
haystack+=$'\n'
|
|
434
|
+
haystack+="$(cat "$progress_json" 2>/dev/null || true)"
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
[[ -n "$haystack" ]] || return 1
|
|
438
|
+
|
|
439
|
+
if printf '%s' "$haystack" | grep -Eiq \
|
|
440
|
+
'auth_unavailable|no auth available|502 Bad Gateway|503 Service Unavailable|504 Gateway Timeout|gateway timeout|upstream (connect )?error|connection reset|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|rate limit|rate_limit|temporarily unavailable|overloaded'; then
|
|
441
|
+
return 0
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
return 1
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
prizm_extract_update_new_status() {
|
|
448
|
+
python3 -c "
|
|
449
|
+
import json, sys
|
|
450
|
+
raw = sys.stdin.read()
|
|
451
|
+
try:
|
|
452
|
+
data = json.loads(raw)
|
|
453
|
+
except Exception:
|
|
454
|
+
sys.exit(0)
|
|
455
|
+
value = data.get('new_status')
|
|
456
|
+
if value:
|
|
457
|
+
print(value)
|
|
458
|
+
"
|
|
459
|
+
}
|
|
460
|
+
|
|
421
461
|
# Run an AI CLI session synchronously.
|
|
422
462
|
# Usage: prizm_run_ai_session <prompt_path> <log_path> <model>
|
|
423
463
|
prizm_run_ai_session() {
|
|
@@ -90,8 +90,8 @@ PY
|
|
|
90
90
|
fi
|
|
91
91
|
prev_child_activity_signature="$child_activity_signature"
|
|
92
92
|
|
|
93
|
-
# Track progress staleness.
|
|
94
|
-
# while child transcripts keep growing, so child activity counts.
|
|
93
|
+
# Track progress staleness. Parent sessions can sit in a wait/polling
|
|
94
|
+
# tool while child transcripts keep growing, so child activity counts.
|
|
95
95
|
if [[ $growth -eq 0 && $child_growth -eq 0 ]]; then
|
|
96
96
|
stale_seconds=$((stale_seconds + heartbeat_interval))
|
|
97
97
|
else
|
|
@@ -174,9 +174,9 @@ PY
|
|
|
174
174
|
fi
|
|
175
175
|
|
|
176
176
|
# Stale-kill: auto-terminate process if no progress for too long.
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
# stale window
|
|
177
|
+
# Parent sessions can wait on spawned work; child transcript growth
|
|
178
|
+
# counts as progress above, while silent waits still use the active
|
|
179
|
+
# stale window to surface stuck agents promptly.
|
|
180
180
|
if [[ $effective_stale_kill_threshold -gt 0 && $stale_seconds -ge $effective_stale_kill_threshold ]]; then
|
|
181
181
|
local stale_mins=$((stale_seconds / 60))
|
|
182
182
|
echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}STALE-KILL: no progress for ${stale_mins}m (threshold: ${effective_stale_kill_threshold}s)${NC}"
|
|
@@ -145,6 +145,11 @@ spawn_and_wait_session() {
|
|
|
145
145
|
log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
|
|
146
146
|
fi
|
|
147
147
|
|
|
148
|
+
local was_infra_error=false
|
|
149
|
+
if [[ $exit_code -ne 0 ]] && prizm_detect_infra_error "$session_log" "$progress_json"; then
|
|
150
|
+
was_infra_error=true
|
|
151
|
+
fi
|
|
152
|
+
|
|
148
153
|
# Session summary
|
|
149
154
|
if [[ -f "$session_log" ]]; then
|
|
150
155
|
local final_size=$(wc -c < "$session_log" 2>/dev/null | tr -d ' ')
|
|
@@ -162,6 +167,10 @@ spawn_and_wait_session() {
|
|
|
162
167
|
if [[ $exit_code -eq 124 ]]; then
|
|
163
168
|
log_warn "Session timed out after ${SESSION_TIMEOUT}s"
|
|
164
169
|
session_status="timed_out"
|
|
170
|
+
elif [[ "$was_infra_error" == true ]]; then
|
|
171
|
+
log_warn "Session failed due to AI CLI/provider infrastructure error"
|
|
172
|
+
log_warn "Infrastructure errors are retried without consuming code retry budget"
|
|
173
|
+
session_status="infra_error"
|
|
165
174
|
elif [[ "$was_stale_killed" == true ]]; then
|
|
166
175
|
log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
|
|
167
176
|
log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
@@ -259,14 +268,20 @@ sys.exit(0)
|
|
|
259
268
|
prizm_detect_subagents "$session_log"
|
|
260
269
|
|
|
261
270
|
# Update bug status (do NOT commit on dev branch — commit happens after merge)
|
|
262
|
-
|
|
271
|
+
local update_output
|
|
272
|
+
update_output=$(python3 "$SCRIPTS_DIR/update-bug-status.py" \
|
|
263
273
|
--bug-list "$bug_list" \
|
|
264
274
|
--state-dir "$STATE_DIR" \
|
|
265
275
|
--bug-id "$bug_id" \
|
|
266
276
|
--session-status "$session_status" \
|
|
267
277
|
--session-id "$session_id" \
|
|
268
278
|
--max-retries "$max_retries" \
|
|
269
|
-
--action update
|
|
279
|
+
--action update 2>&1) || {
|
|
280
|
+
log_error "Failed to update bug status: $update_output"
|
|
281
|
+
update_output=""
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_SPAWN_ITEM_STATUS="$(printf '%s' "$update_output" | prizm_extract_update_new_status)"
|
|
270
285
|
|
|
271
286
|
_SPAWN_RESULT="$session_status"
|
|
272
287
|
}
|
|
@@ -693,6 +708,7 @@ else:
|
|
|
693
708
|
trap cleanup_single_bug SIGINT SIGTERM
|
|
694
709
|
|
|
695
710
|
_SPAWN_RESULT=""
|
|
711
|
+
_SPAWN_ITEM_STATUS=""
|
|
696
712
|
|
|
697
713
|
# Branch lifecycle: create and checkout bugfix branch
|
|
698
714
|
local _proj_root
|
|
@@ -1078,12 +1094,14 @@ DEPLOY_PROMPT_EOF
|
|
|
1078
1094
|
# Spawn session
|
|
1079
1095
|
log_info "Spawning AI CLI session: $session_id"
|
|
1080
1096
|
_SPAWN_RESULT=""
|
|
1097
|
+
_SPAWN_ITEM_STATUS=""
|
|
1081
1098
|
|
|
1082
1099
|
spawn_and_wait_session \
|
|
1083
1100
|
"$bug_id" "$bug_list" "$session_id" \
|
|
1084
1101
|
"$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$bug_model" "$_ORIGINAL_BRANCH"
|
|
1085
1102
|
|
|
1086
1103
|
local session_status="$_SPAWN_RESULT"
|
|
1104
|
+
local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
|
|
1087
1105
|
|
|
1088
1106
|
# Merge per-bug dev branch back to original on success
|
|
1089
1107
|
if [[ "$session_status" == "success" && -n "$_DEV_BRANCH_NAME" ]]; then
|
|
@@ -1112,15 +1130,18 @@ DEPLOY_PROMPT_EOF
|
|
|
1112
1130
|
session_count=$((session_count + 1))
|
|
1113
1131
|
total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
|
|
1114
1132
|
|
|
1115
|
-
# Stop-on-failure: abort
|
|
1116
|
-
|
|
1133
|
+
# Stop-on-failure: abort only after the task is actually marked failed.
|
|
1134
|
+
# Pending retry outcomes, including infrastructure errors, keep running.
|
|
1135
|
+
if [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" && "$item_status_after_session" == "failed" ]]; then
|
|
1117
1136
|
echo ""
|
|
1118
1137
|
log_error "════════════════════════════════════════════════════"
|
|
1119
|
-
log_error " STOP_ON_FAILURE: Pipeline halted after $bug_id
|
|
1138
|
+
log_error " STOP_ON_FAILURE: Pipeline halted after $bug_id exhausted retries."
|
|
1120
1139
|
log_error " Total sessions completed: $session_count"
|
|
1121
1140
|
log_error " Set STOP_ON_FAILURE=0 to continue past failures."
|
|
1122
1141
|
log_error "════════════════════════════════════════════════════"
|
|
1123
1142
|
break
|
|
1143
|
+
elif [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" ]]; then
|
|
1144
|
+
log_info "STOP_ON_FAILURE: $bug_id is ${item_status_after_session:-unknown}; retry budget not exhausted, continuing."
|
|
1124
1145
|
fi
|
|
1125
1146
|
|
|
1126
1147
|
# Stuck detection
|
|
@@ -153,6 +153,11 @@ spawn_and_wait_session() {
|
|
|
153
153
|
log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
|
|
154
154
|
fi
|
|
155
155
|
|
|
156
|
+
local was_infra_error=false
|
|
157
|
+
if [[ $exit_code -ne 0 ]] && prizm_detect_infra_error "$session_log" "$progress_json"; then
|
|
158
|
+
was_infra_error=true
|
|
159
|
+
fi
|
|
160
|
+
|
|
156
161
|
# Show final session summary
|
|
157
162
|
if [[ -f "$session_log" ]]; then
|
|
158
163
|
local final_size=$(wc -c < "$session_log" 2>/dev/null | tr -d ' ')
|
|
@@ -172,6 +177,10 @@ spawn_and_wait_session() {
|
|
|
172
177
|
if [[ $exit_code -eq 124 ]]; then
|
|
173
178
|
log_warn "Session timed out after ${SESSION_TIMEOUT}s"
|
|
174
179
|
session_status="timed_out"
|
|
180
|
+
elif [[ "$was_infra_error" == true ]]; then
|
|
181
|
+
log_warn "Session failed due to AI CLI/provider infrastructure error"
|
|
182
|
+
log_warn "Infrastructure errors are retried without consuming code retry budget"
|
|
183
|
+
session_status="infra_error"
|
|
175
184
|
elif [[ "$was_stale_killed" == true ]]; then
|
|
176
185
|
log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
|
|
177
186
|
log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
@@ -347,6 +356,8 @@ sys.exit(0)
|
|
|
347
356
|
log_error ".prizmkit/plans/feature-list.json may be out of sync. Manual intervention needed."
|
|
348
357
|
}
|
|
349
358
|
|
|
359
|
+
_SPAWN_ITEM_STATUS="$(printf '%s' "$update_output" | prizm_extract_update_new_status)"
|
|
360
|
+
|
|
350
361
|
# Return status via global variable (avoids $() swallowing stdout)
|
|
351
362
|
_SPAWN_RESULT="$session_status"
|
|
352
363
|
}
|
|
@@ -848,6 +859,7 @@ else:
|
|
|
848
859
|
trap cleanup_single_feature SIGINT SIGTERM
|
|
849
860
|
|
|
850
861
|
_SPAWN_RESULT=""
|
|
862
|
+
_SPAWN_ITEM_STATUS=""
|
|
851
863
|
|
|
852
864
|
# Branch lifecycle: create and checkout feature branch
|
|
853
865
|
local _proj_root
|
|
@@ -1300,11 +1312,13 @@ DEPLOY_PROMPT_EOF
|
|
|
1300
1312
|
log_info "Feature model: $feature_model"
|
|
1301
1313
|
fi
|
|
1302
1314
|
_SPAWN_RESULT=""
|
|
1315
|
+
_SPAWN_ITEM_STATUS=""
|
|
1303
1316
|
|
|
1304
1317
|
spawn_and_wait_session \
|
|
1305
1318
|
"$feature_id" "$feature_list" "$session_id" \
|
|
1306
1319
|
"$bootstrap_prompt" "$session_dir" "$MAX_RETRIES" "$feature_model" "$_ORIGINAL_BRANCH"
|
|
1307
1320
|
local session_status="$_SPAWN_RESULT"
|
|
1321
|
+
local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
|
|
1308
1322
|
|
|
1309
1323
|
# Merge per-feature dev branch back to original on success
|
|
1310
1324
|
if [[ "$session_status" == "success" && -n "$_DEV_BRANCH_NAME" ]]; then
|
|
@@ -1333,15 +1347,18 @@ DEPLOY_PROMPT_EOF
|
|
|
1333
1347
|
session_count=$((session_count + 1))
|
|
1334
1348
|
total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
|
|
1335
1349
|
|
|
1336
|
-
# Stop-on-failure: abort
|
|
1337
|
-
|
|
1350
|
+
# Stop-on-failure: abort only after the task is actually marked failed.
|
|
1351
|
+
# Pending retry outcomes, including infrastructure errors, keep running.
|
|
1352
|
+
if [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" && "$item_status_after_session" == "failed" ]]; then
|
|
1338
1353
|
echo ""
|
|
1339
1354
|
log_error "════════════════════════════════════════════════════"
|
|
1340
|
-
log_error " STOP_ON_FAILURE: Pipeline halted after $feature_id
|
|
1355
|
+
log_error " STOP_ON_FAILURE: Pipeline halted after $feature_id exhausted retries."
|
|
1341
1356
|
log_error " Total sessions completed: $session_count"
|
|
1342
1357
|
log_error " Set STOP_ON_FAILURE=0 to continue past failures."
|
|
1343
1358
|
log_error "════════════════════════════════════════════════════"
|
|
1344
1359
|
break
|
|
1360
|
+
elif [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" ]]; then
|
|
1361
|
+
log_info "STOP_ON_FAILURE: $feature_id is ${item_status_after_session:-unknown}; retry budget not exhausted, continuing."
|
|
1345
1362
|
fi
|
|
1346
1363
|
|
|
1347
1364
|
# Brief pause before next iteration
|
|
@@ -147,6 +147,11 @@ spawn_and_wait_session() {
|
|
|
147
147
|
log_warn "Session was stale-killed by heartbeat monitor (no progress for too long)"
|
|
148
148
|
fi
|
|
149
149
|
|
|
150
|
+
local was_infra_error=false
|
|
151
|
+
if [[ $exit_code -ne 0 ]] && prizm_detect_infra_error "$session_log" "$progress_json"; then
|
|
152
|
+
was_infra_error=true
|
|
153
|
+
fi
|
|
154
|
+
|
|
150
155
|
# Session summary
|
|
151
156
|
if [[ -f "$session_log" ]]; then
|
|
152
157
|
local final_size=$(wc -c < "$session_log" 2>/dev/null | tr -d ' ')
|
|
@@ -164,6 +169,10 @@ spawn_and_wait_session() {
|
|
|
164
169
|
if [[ $exit_code -eq 124 ]]; then
|
|
165
170
|
log_warn "Session timed out after ${SESSION_TIMEOUT}s"
|
|
166
171
|
session_status="timed_out"
|
|
172
|
+
elif [[ "$was_infra_error" == true ]]; then
|
|
173
|
+
log_warn "Session failed due to AI CLI/provider infrastructure error"
|
|
174
|
+
log_warn "Infrastructure errors are retried without consuming code retry budget"
|
|
175
|
+
session_status="infra_error"
|
|
167
176
|
elif [[ "$was_stale_killed" == true ]]; then
|
|
168
177
|
log_warn "Session stale-killed (no progress for ${STALE_KILL_THRESHOLD}s)"
|
|
169
178
|
log_warn "Stale-killed sessions are treated as failed; dev branch is preserved for inspection"
|
|
@@ -286,14 +295,20 @@ sys.exit(0)
|
|
|
286
295
|
fi
|
|
287
296
|
|
|
288
297
|
# Update refactor status (do NOT commit on dev branch — commit happens after merge)
|
|
289
|
-
|
|
298
|
+
local update_output
|
|
299
|
+
update_output=$(python3 "$SCRIPTS_DIR/update-refactor-status.py" \
|
|
290
300
|
--refactor-list "$refactor_list" \
|
|
291
301
|
--state-dir "$STATE_DIR" \
|
|
292
302
|
--refactor-id "$refactor_id" \
|
|
293
303
|
--session-status "$session_status" \
|
|
294
304
|
--session-id "$session_id" \
|
|
295
305
|
--max-retries "$max_retries" \
|
|
296
|
-
--action update
|
|
306
|
+
--action update 2>&1) || {
|
|
307
|
+
log_error "Failed to update refactor status: $update_output"
|
|
308
|
+
update_output=""
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
_SPAWN_ITEM_STATUS="$(printf '%s' "$update_output" | prizm_extract_update_new_status)"
|
|
297
312
|
|
|
298
313
|
_SPAWN_RESULT="$session_status"
|
|
299
314
|
}
|
|
@@ -723,6 +738,7 @@ else:
|
|
|
723
738
|
trap cleanup_single_refactor SIGINT SIGTERM
|
|
724
739
|
|
|
725
740
|
_SPAWN_RESULT=""
|
|
741
|
+
_SPAWN_ITEM_STATUS=""
|
|
726
742
|
|
|
727
743
|
# Branch lifecycle: create and checkout refactor branch
|
|
728
744
|
local _proj_root
|
|
@@ -1114,6 +1130,7 @@ DEPLOY_PROMPT_EOF
|
|
|
1114
1130
|
# Spawn session
|
|
1115
1131
|
log_info "Spawning AI CLI session: $session_id"
|
|
1116
1132
|
_SPAWN_RESULT=""
|
|
1133
|
+
_SPAWN_ITEM_STATUS=""
|
|
1117
1134
|
|
|
1118
1135
|
spawn_and_wait_session \
|
|
1119
1136
|
"$refactor_id" "$refactor_list" "$session_id" \
|
|
@@ -1130,6 +1147,7 @@ DEPLOY_PROMPT_EOF
|
|
|
1130
1147
|
fi
|
|
1131
1148
|
|
|
1132
1149
|
local session_status="$_SPAWN_RESULT"
|
|
1150
|
+
local item_status_after_session="${_SPAWN_ITEM_STATUS:-}"
|
|
1133
1151
|
|
|
1134
1152
|
# Merge per-refactor dev branch back to original on success
|
|
1135
1153
|
if [[ "$session_status" == "success" && -n "$_DEV_BRANCH_NAME" ]]; then
|
|
@@ -1168,15 +1186,18 @@ DEPLOY_PROMPT_EOF
|
|
|
1168
1186
|
session_count=$((session_count + 1))
|
|
1169
1187
|
total_subagent_calls=$((total_subagent_calls + _SUBAGENT_COUNT))
|
|
1170
1188
|
|
|
1171
|
-
# Stop-on-failure: abort
|
|
1172
|
-
|
|
1189
|
+
# Stop-on-failure: abort only after the task is actually marked failed.
|
|
1190
|
+
# Pending retry outcomes, including infrastructure errors, keep running.
|
|
1191
|
+
if [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" && "$item_status_after_session" == "failed" ]]; then
|
|
1173
1192
|
echo ""
|
|
1174
1193
|
log_error "════════════════════════════════════════════════════"
|
|
1175
|
-
log_error " STOP_ON_FAILURE: Pipeline halted after $refactor_id
|
|
1194
|
+
log_error " STOP_ON_FAILURE: Pipeline halted after $refactor_id exhausted retries."
|
|
1176
1195
|
log_error " Total sessions completed: $session_count"
|
|
1177
1196
|
log_error " Set STOP_ON_FAILURE=0 to continue past failures."
|
|
1178
1197
|
log_error "════════════════════════════════════════════════════"
|
|
1179
1198
|
break
|
|
1199
|
+
elif [[ "$session_status" != "success" && "$STOP_ON_FAILURE" == "1" ]]; then
|
|
1200
|
+
log_info "STOP_ON_FAILURE: $refactor_id is ${item_status_after_session:-unknown}; retry budget not exhausted, continuing."
|
|
1180
1201
|
fi
|
|
1181
1202
|
|
|
1182
1203
|
log_info "Pausing 5s before next refactor..."
|
|
@@ -63,7 +63,8 @@ PHASE_KEYWORDS = {
|
|
|
63
63
|
class ProgressTracker:
|
|
64
64
|
"""Tracks progress state from stream-json events."""
|
|
65
65
|
|
|
66
|
-
def __init__(self):
|
|
66
|
+
def __init__(self, session_log=None):
|
|
67
|
+
self.session_log_path = Path(session_log).expanduser() if session_log else None
|
|
67
68
|
self.message_count = 0
|
|
68
69
|
self.current_tool = None
|
|
69
70
|
self.current_tool_input_summary = ""
|
|
@@ -78,12 +79,19 @@ class ProgressTracker:
|
|
|
78
79
|
self.active_subagent_count = 0
|
|
79
80
|
self.subagent_status_counts = Counter()
|
|
80
81
|
self.codex_child_thread_ids = set()
|
|
82
|
+
self.claude_session_id = ""
|
|
83
|
+
self.claude_cwd = ""
|
|
84
|
+
self.claude_task_states = {}
|
|
81
85
|
self.child_session_files = []
|
|
82
86
|
self.child_total_bytes = 0
|
|
83
87
|
self.child_activity_signature = ""
|
|
84
88
|
self.last_child_activity_at = ""
|
|
85
89
|
self._codex_child_session_paths = {}
|
|
90
|
+
self._claude_child_session_files = []
|
|
86
91
|
self._last_child_scan_at = 0.0
|
|
92
|
+
self._last_claude_fallback_scan_at = 0.0
|
|
93
|
+
self._last_claude_fallback_scan_key = ""
|
|
94
|
+
self._claude_fallback_scan_interval_seconds = 10.0
|
|
87
95
|
self._text_buffer = ""
|
|
88
96
|
self._in_tool_use = False
|
|
89
97
|
self._current_tool_input_parts = []
|
|
@@ -195,11 +203,76 @@ class ProgressTracker:
|
|
|
195
203
|
self.is_active = True
|
|
196
204
|
|
|
197
205
|
elif event_type == "system":
|
|
198
|
-
# System events (hooks, init, etc.) — track but don't count as messages
|
|
206
|
+
# System events (hooks, init, task notifications, etc.) — track but don't count as messages.
|
|
199
207
|
self.event_format = self.event_format or "stream-json"
|
|
200
208
|
subtype = event.get("subtype", "")
|
|
201
209
|
if subtype == "init":
|
|
202
210
|
self.is_active = True
|
|
211
|
+
session_id = event.get("session_id")
|
|
212
|
+
if isinstance(session_id, str) and session_id.strip():
|
|
213
|
+
self.claude_session_id = session_id.strip()
|
|
214
|
+
cwd = event.get("cwd")
|
|
215
|
+
if isinstance(cwd, str) and cwd.strip():
|
|
216
|
+
self.claude_cwd = cwd.strip()
|
|
217
|
+
elif subtype == "task_started":
|
|
218
|
+
task_id = event.get("task_id")
|
|
219
|
+
if isinstance(task_id, str) and task_id.strip():
|
|
220
|
+
self.claude_task_states[task_id.strip()] = {
|
|
221
|
+
"status": "running",
|
|
222
|
+
"summary": str(event.get("description") or "")[:120],
|
|
223
|
+
"tool_use_id": str(event.get("tool_use_id") or ""),
|
|
224
|
+
"task_type": str(event.get("task_type") or ""),
|
|
225
|
+
"subagent_type": str(event.get("subagent_type") or ""),
|
|
226
|
+
}
|
|
227
|
+
self._update_claude_subagent_status_counts()
|
|
228
|
+
elif subtype in ("task_updated", "task_progress"):
|
|
229
|
+
task_id = event.get("task_id")
|
|
230
|
+
if isinstance(task_id, str) and task_id.strip():
|
|
231
|
+
state = self.claude_task_states.setdefault(task_id.strip(), {})
|
|
232
|
+
patch = event.get("patch") if isinstance(event.get("patch"), dict) else {}
|
|
233
|
+
status = patch.get("status") or event.get("status")
|
|
234
|
+
if status:
|
|
235
|
+
state["status"] = str(status)
|
|
236
|
+
summary = patch.get("summary") or patch.get("description") or event.get("summary") or event.get("description")
|
|
237
|
+
if summary:
|
|
238
|
+
state["summary"] = str(summary)[:120]
|
|
239
|
+
else:
|
|
240
|
+
state.setdefault("summary", "")
|
|
241
|
+
tool_use_id = patch.get("tool_use_id") or event.get("tool_use_id")
|
|
242
|
+
if tool_use_id:
|
|
243
|
+
state["tool_use_id"] = str(tool_use_id)
|
|
244
|
+
else:
|
|
245
|
+
state.setdefault("tool_use_id", "")
|
|
246
|
+
task_type = patch.get("task_type") or event.get("task_type")
|
|
247
|
+
if task_type:
|
|
248
|
+
state["task_type"] = str(task_type)
|
|
249
|
+
else:
|
|
250
|
+
state.setdefault("task_type", "")
|
|
251
|
+
subagent_type = patch.get("subagent_type") or event.get("subagent_type")
|
|
252
|
+
if subagent_type:
|
|
253
|
+
state["subagent_type"] = str(subagent_type)
|
|
254
|
+
else:
|
|
255
|
+
state.setdefault("subagent_type", "")
|
|
256
|
+
self._update_claude_subagent_status_counts()
|
|
257
|
+
elif subtype == "task_notification":
|
|
258
|
+
task_id = event.get("task_id")
|
|
259
|
+
if isinstance(task_id, str) and task_id.strip():
|
|
260
|
+
state = self.claude_task_states.setdefault(task_id.strip(), {})
|
|
261
|
+
status = event.get("status") or "completed"
|
|
262
|
+
state["status"] = str(status)
|
|
263
|
+
state["summary"] = str(event.get("summary") or state.get("summary") or "")[:120]
|
|
264
|
+
state.setdefault("tool_use_id", str(event.get("tool_use_id") or ""))
|
|
265
|
+
task_type = event.get("task_type")
|
|
266
|
+
if task_type:
|
|
267
|
+
state["task_type"] = str(task_type)
|
|
268
|
+
else:
|
|
269
|
+
state.setdefault("task_type", "")
|
|
270
|
+
subagent_type = event.get("subagent_type")
|
|
271
|
+
if subagent_type:
|
|
272
|
+
state["subagent_type"] = str(subagent_type)
|
|
273
|
+
else:
|
|
274
|
+
state.setdefault("subagent_type", "")
|
|
275
|
+
self._update_claude_subagent_status_counts()
|
|
203
276
|
|
|
204
277
|
# ── Claude API raw stream format ────────────────────────────
|
|
205
278
|
elif event_type == "message_start":
|
|
@@ -391,16 +464,135 @@ class ProgressTracker:
|
|
|
391
464
|
pass
|
|
392
465
|
return str(matches[0])
|
|
393
466
|
|
|
467
|
+
def _is_tracked_claude_subagent_state(self, state):
|
|
468
|
+
"""Return true for Claude Code task events representing in-process agents."""
|
|
469
|
+
if not isinstance(state, dict):
|
|
470
|
+
return False
|
|
471
|
+
task_type = str(state.get("task_type") or "")
|
|
472
|
+
task_type_lower = task_type.lower()
|
|
473
|
+
subagent_type = str(state.get("subagent_type") or "")
|
|
474
|
+
if task_type_lower == "local_bash":
|
|
475
|
+
return False
|
|
476
|
+
tracked_types = {"in_process_teammate", "subagent", "agent", "teammate"}
|
|
477
|
+
if task_type_lower in tracked_types:
|
|
478
|
+
return True
|
|
479
|
+
if task_type_lower == "local_agent" and subagent_type:
|
|
480
|
+
return True
|
|
481
|
+
summary = str(state.get("summary") or "")
|
|
482
|
+
return bool(
|
|
483
|
+
not task_type
|
|
484
|
+
and summary.lower().startswith(("dev:", "critic:", "reviewer:", "agent:"))
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def _has_tracked_claude_subagent_task(self):
|
|
488
|
+
"""Return true once a Claude Code local-agent/subagent task has been observed."""
|
|
489
|
+
return any(
|
|
490
|
+
self._is_tracked_claude_subagent_state(state)
|
|
491
|
+
for state in self.claude_task_states.values()
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def _update_claude_subagent_status_counts(self):
|
|
495
|
+
"""Track Claude Code in-process teammate task state counts."""
|
|
496
|
+
counts = Counter()
|
|
497
|
+
active = 0
|
|
498
|
+
inactive_statuses = {
|
|
499
|
+
"completed",
|
|
500
|
+
"failed",
|
|
501
|
+
"cancelled",
|
|
502
|
+
"canceled",
|
|
503
|
+
"killed",
|
|
504
|
+
"stopped",
|
|
505
|
+
"success",
|
|
506
|
+
"error",
|
|
507
|
+
}
|
|
508
|
+
for state in self.claude_task_states.values():
|
|
509
|
+
if not self._is_tracked_claude_subagent_state(state):
|
|
510
|
+
continue
|
|
511
|
+
status = str(state.get("status") or "unknown")
|
|
512
|
+
counts[status] += 1
|
|
513
|
+
if status.lower() not in inactive_statuses:
|
|
514
|
+
active += 1
|
|
515
|
+
summary = state.get("summary") or state.get("subagent_type")
|
|
516
|
+
if summary:
|
|
517
|
+
self.last_text_snippet = str(summary).strip()[:120]
|
|
518
|
+
self._detect_phase(str(summary))
|
|
519
|
+
self.subagent_status_counts = counts
|
|
520
|
+
self.active_subagent_count = active
|
|
521
|
+
|
|
522
|
+
def _claude_projects_dir(self):
|
|
523
|
+
"""Return the Claude Code projects directory for transcript lookup."""
|
|
524
|
+
projects_dir = os.environ.get("CLAUDE_PROJECTS_DIR")
|
|
525
|
+
if projects_dir:
|
|
526
|
+
return Path(projects_dir).expanduser()
|
|
527
|
+
claude_config_dir = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
528
|
+
if claude_config_dir:
|
|
529
|
+
return Path(claude_config_dir).expanduser() / "projects"
|
|
530
|
+
claude_home = os.environ.get("CLAUDE_HOME")
|
|
531
|
+
if claude_home:
|
|
532
|
+
return Path(claude_home).expanduser() / "projects"
|
|
533
|
+
return Path.home() / ".claude" / "projects"
|
|
534
|
+
|
|
535
|
+
def _claude_project_key(self):
|
|
536
|
+
"""Encode cwd the same way Claude Code stores project transcript dirs."""
|
|
537
|
+
cwd = self.claude_cwd
|
|
538
|
+
if not cwd:
|
|
539
|
+
return ""
|
|
540
|
+
return cwd.replace("\\", "-").replace("/", "-").replace(":", "")
|
|
541
|
+
|
|
542
|
+
def _find_claude_child_session_files(self):
|
|
543
|
+
"""Find Claude Code subagent transcripts for this parent session."""
|
|
544
|
+
if not self.claude_session_id:
|
|
545
|
+
return []
|
|
546
|
+
|
|
547
|
+
projects_dir = self._claude_projects_dir()
|
|
548
|
+
if not projects_dir.exists():
|
|
549
|
+
return []
|
|
550
|
+
|
|
551
|
+
candidates = []
|
|
552
|
+
project_key = self._claude_project_key()
|
|
553
|
+
if project_key:
|
|
554
|
+
candidates.append(
|
|
555
|
+
projects_dir / project_key / self.claude_session_id / "subagents"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
for candidate in candidates:
|
|
559
|
+
if candidate.exists():
|
|
560
|
+
try:
|
|
561
|
+
return sorted(candidate.glob("*.jsonl"))
|
|
562
|
+
except OSError:
|
|
563
|
+
return []
|
|
564
|
+
|
|
565
|
+
# Fallback for non-standard cwd encoding or custom Claude homes. Avoid
|
|
566
|
+
# repeatedly walking every stored transcript before any Agent task exists.
|
|
567
|
+
if not self._has_tracked_claude_subagent_task():
|
|
568
|
+
return []
|
|
569
|
+
|
|
570
|
+
fallback_scan_key = f"{projects_dir}:{self.claude_session_id}"
|
|
571
|
+
now = time.monotonic()
|
|
572
|
+
if (
|
|
573
|
+
self._last_claude_fallback_scan_key == fallback_scan_key
|
|
574
|
+
and now - self._last_claude_fallback_scan_at < self._claude_fallback_scan_interval_seconds
|
|
575
|
+
):
|
|
576
|
+
return self._claude_child_session_files
|
|
577
|
+
self._last_claude_fallback_scan_key = fallback_scan_key
|
|
578
|
+
self._last_claude_fallback_scan_at = now
|
|
579
|
+
try:
|
|
580
|
+
matches = sorted(projects_dir.rglob(f"{self.claude_session_id}/subagents/*.jsonl"))
|
|
581
|
+
except OSError:
|
|
582
|
+
return []
|
|
583
|
+
return matches
|
|
584
|
+
|
|
394
585
|
def refresh_child_session_activity(self, force=False):
|
|
395
|
-
"""Refresh
|
|
586
|
+
"""Refresh child transcript file stats.
|
|
396
587
|
|
|
397
588
|
The heartbeat monitor uses this activity signature to treat subagent
|
|
398
|
-
transcript growth as real progress while the parent
|
|
399
|
-
|
|
589
|
+
transcript growth as real progress while the parent session is blocked
|
|
590
|
+
waiting for a child agent/tool result. Supports Codex child threads and
|
|
591
|
+
Claude Code in-process teammate transcripts.
|
|
400
592
|
"""
|
|
401
593
|
previous_signature = self.child_activity_signature
|
|
402
594
|
|
|
403
|
-
if not self.codex_child_thread_ids:
|
|
595
|
+
if not self.codex_child_thread_ids and not self.claude_session_id:
|
|
404
596
|
self.child_session_files = []
|
|
405
597
|
self.child_total_bytes = 0
|
|
406
598
|
self.child_activity_signature = ""
|
|
@@ -420,6 +612,7 @@ class ProgressTracker:
|
|
|
420
612
|
found = self._find_codex_child_session_file(thread_id)
|
|
421
613
|
if found:
|
|
422
614
|
self._codex_child_session_paths[thread_id] = found
|
|
615
|
+
self._claude_child_session_files = self._find_claude_child_session_files()
|
|
423
616
|
self._last_child_scan_at = now
|
|
424
617
|
|
|
425
618
|
files = []
|
|
@@ -427,24 +620,22 @@ class ProgressTracker:
|
|
|
427
620
|
total_bytes = 0
|
|
428
621
|
max_mtime = 0.0
|
|
429
622
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if not path:
|
|
433
|
-
continue
|
|
623
|
+
def add_file(kind, identifier, path):
|
|
624
|
+
nonlocal total_bytes, max_mtime
|
|
434
625
|
try:
|
|
435
626
|
stat = os.stat(path)
|
|
436
627
|
except OSError:
|
|
437
|
-
|
|
438
|
-
|
|
628
|
+
return
|
|
629
|
+
path_str = str(path)
|
|
439
630
|
total_bytes += stat.st_size
|
|
440
631
|
max_mtime = max(max_mtime, stat.st_mtime)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
)
|
|
632
|
+
mtime_ns = getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000))
|
|
633
|
+
signature_parts.append(f"{kind}:{identifier}:{stat.st_size}:{mtime_ns}")
|
|
444
634
|
files.append(
|
|
445
635
|
{
|
|
446
|
-
"
|
|
447
|
-
"
|
|
636
|
+
"kind": kind,
|
|
637
|
+
"thread_id": identifier,
|
|
638
|
+
"path": path_str,
|
|
448
639
|
"size": stat.st_size,
|
|
449
640
|
"mtime": datetime.fromtimestamp(
|
|
450
641
|
stat.st_mtime, timezone.utc
|
|
@@ -452,6 +643,14 @@ class ProgressTracker:
|
|
|
452
643
|
}
|
|
453
644
|
)
|
|
454
645
|
|
|
646
|
+
for thread_id in sorted(self.codex_child_thread_ids):
|
|
647
|
+
path = self._codex_child_session_paths.get(thread_id)
|
|
648
|
+
if path:
|
|
649
|
+
add_file("codex", thread_id, path)
|
|
650
|
+
|
|
651
|
+
for path in self._claude_child_session_files:
|
|
652
|
+
add_file("claude", path.stem, path)
|
|
653
|
+
|
|
455
654
|
self.child_session_files = files
|
|
456
655
|
self.child_total_bytes = total_bytes
|
|
457
656
|
self.child_activity_signature = "|".join(signature_parts)
|
|
@@ -519,7 +718,7 @@ def atomic_write_json(data, filepath):
|
|
|
519
718
|
|
|
520
719
|
def tail_and_parse(session_log, progress_file, poll_interval=0.5):
|
|
521
720
|
"""Tail session log and parse stream-json events."""
|
|
522
|
-
tracker = ProgressTracker()
|
|
721
|
+
tracker = ProgressTracker(session_log)
|
|
523
722
|
last_write_state = None
|
|
524
723
|
|
|
525
724
|
def state_key(state):
|