prizmkit 1.1.74 → 1.1.77
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/.env.example +1 -0
- package/bundled/dev-pipeline/lib/common.sh +53 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +29 -6
- package/bundled/dev-pipeline/run-bugfix.sh +5 -0
- package/bundled/dev-pipeline/run-feature.sh +5 -0
- package/bundled/dev-pipeline/run-recovery.sh +6 -0
- package/bundled/dev-pipeline/run-refactor.sh +5 -0
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +229 -7
- package/bundled/dev-pipeline-windows/lib/common.ps1 +23 -0
- package/bundled/dev-pipeline-windows/lib/heartbeat.sh +439 -0
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +7 -0
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +11 -0
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +229 -7
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +13 -10
- package/bundled/skills/app-planner/references/rules/backend/question-manifest.json +46 -0
- package/bundled/skills/app-planner/references/rules/database/question-manifest.json +39 -0
- package/bundled/skills/app-planner/references/rules/frontend/question-manifest.json +51 -0
- package/bundled/skills/app-planner/references/rules/mobile/question-manifest.json +47 -0
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +1 -0
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +1 -0
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +1 -0
- package/bundled/skills-windows/app-planner/SKILL.md +13 -10
- package/bundled/skills-windows/app-planner/references/rules/backend/question-manifest.json +46 -0
- package/bundled/skills-windows/app-planner/references/rules/database/question-manifest.json +39 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-manifest.json +51 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-manifest.json +47 -0
- package/package.json +1 -1
|
@@ -531,6 +531,26 @@ function Test-PrizmStreamJsonSupport {
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
function Test-PrizmEffort {
|
|
535
|
+
param([string]$CliCommand)
|
|
536
|
+
if ([string]::IsNullOrEmpty($env:PRIZMKIT_EFFORT)) { return }
|
|
537
|
+
|
|
538
|
+
$sessionPlatform = Get-PrizmSessionPlatform $CliCommand
|
|
539
|
+
$valid = @{
|
|
540
|
+
claude = @('low', 'medium', 'high', 'xhigh', 'max')
|
|
541
|
+
codex = @('low', 'medium', 'high', 'xhigh')
|
|
542
|
+
codebuddy = @('low', 'medium', 'high', 'xhigh')
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
$allowed = $valid[$sessionPlatform]
|
|
546
|
+
if ($env:PRIZMKIT_EFFORT -notin $allowed) {
|
|
547
|
+
Write-Host "ERROR: PRIZMKIT_EFFORT='$env:PRIZMKIT_EFFORT' is not supported by the detected CLI ($sessionPlatform)." -ForegroundColor Red
|
|
548
|
+
Write-Host " Supported values for $($sessionPlatform): $($allowed -join ', ')" -ForegroundColor Red
|
|
549
|
+
Write-Host " Set `$env:PRIZMKIT_EFFORT to one of the above, or unset it to use the CLI default." -ForegroundColor Red
|
|
550
|
+
exit 1
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
534
554
|
function Start-PrizmProgressParser {
|
|
535
555
|
param(
|
|
536
556
|
[string[]]$PythonCommand,
|
|
@@ -656,6 +676,7 @@ function Invoke-PrizmAiSession {
|
|
|
656
676
|
$cliArgs += @('-p', '--dangerously-skip-permissions')
|
|
657
677
|
if ($env:VERBOSE -in @('1','true','yes','on') -or $useStreamJson) { $cliArgs += '--verbose' }
|
|
658
678
|
if ($useStreamJson) { $cliArgs += @('--output-format', 'stream-json') }
|
|
679
|
+
if ($env:PRIZMKIT_EFFORT) { $cliArgs += @('--effort', $env:PRIZMKIT_EFFORT) }
|
|
659
680
|
if ($Model) { $cliArgs += @('--model', $Model) }
|
|
660
681
|
} elseif ($sessionPlatform -eq 'codex') {
|
|
661
682
|
$cliArgs += @('--ask-for-approval', 'never', '--sandbox', 'danger-full-access')
|
|
@@ -666,11 +687,13 @@ function Invoke-PrizmAiSession {
|
|
|
666
687
|
$cliArgs += @('exec', '--cd', $ProjectRoot, '--skip-git-repo-check')
|
|
667
688
|
if ($useStreamJson) { $cliArgs += '--json' }
|
|
668
689
|
if ($Model) { $cliArgs += @('--model', $Model) }
|
|
690
|
+
if ($env:PRIZMKIT_EFFORT) { $cliArgs += @('--config', "model_reasoning_effort=$env:PRIZMKIT_EFFORT") }
|
|
669
691
|
$cliArgs += '-'
|
|
670
692
|
} else {
|
|
671
693
|
$cliArgs += @('--print', '-y')
|
|
672
694
|
if ($env:VERBOSE -in @('1','true','yes','on')) { $cliArgs += '--verbose' }
|
|
673
695
|
if ($useStreamJson) { $cliArgs += @('--output-format', 'stream-json') }
|
|
696
|
+
if ($env:PRIZMKIT_EFFORT) { $cliArgs += @('--effort', $env:PRIZMKIT_EFFORT) }
|
|
674
697
|
if ($Model) { $cliArgs += @('--model', $Model) }
|
|
675
698
|
}
|
|
676
699
|
$generatedArgs = (($cliArgs | ForEach-Object { ConvertTo-PrizmProcessArgument $_ }) -join ' ')
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# dev-pipeline/lib/heartbeat.sh - Shared heartbeat monitoring
|
|
4
|
+
#
|
|
5
|
+
# Provides start_heartbeat / stop_heartbeat functions that read
|
|
6
|
+
# structured progress from progress.json (written by
|
|
7
|
+
# parse-stream-progress.py) and fall back to tail-based monitoring.
|
|
8
|
+
#
|
|
9
|
+
# When stale_kill_threshold is set (>0), the heartbeat monitor will
|
|
10
|
+
# automatically kill the AI CLI process if it shows no progress for
|
|
11
|
+
# the specified duration. This prevents sessions from hanging forever
|
|
12
|
+
# when the AI CLI process doesn't exit after completing its work.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
16
|
+
# start_heartbeat "$cli_pid" "$session_log" "$progress_json" "$interval" ["$stale_kill_threshold"]
|
|
17
|
+
# # ... wait for CLI to finish ...
|
|
18
|
+
# stop_heartbeat "$_HEARTBEAT_PID"
|
|
19
|
+
#
|
|
20
|
+
# Requires: colors (GREEN, YELLOW, BLUE, NC) and log functions
|
|
21
|
+
# to be defined before sourcing.
|
|
22
|
+
# ============================================================
|
|
23
|
+
|
|
24
|
+
# Start a heartbeat monitor in the background.
|
|
25
|
+
# Sets _HEARTBEAT_PID to the background process PID.
|
|
26
|
+
#
|
|
27
|
+
# Arguments:
|
|
28
|
+
# $1 - cli_pid PID of the AI CLI process to monitor
|
|
29
|
+
# $2 - session_log Path to session.log
|
|
30
|
+
# $3 - progress_json Path to progress.json (may not exist if stream-json disabled)
|
|
31
|
+
# $4 - interval Heartbeat interval in seconds
|
|
32
|
+
# $5 - stale_kill_threshold (optional) Seconds of no progress before auto-killing the process.
|
|
33
|
+
# 0 = disabled (default). Recommended: 900 (15 minutes).
|
|
34
|
+
start_heartbeat() {
|
|
35
|
+
local cli_pid="$1"
|
|
36
|
+
local session_log="$2"
|
|
37
|
+
local progress_json="$3"
|
|
38
|
+
local heartbeat_interval="$4"
|
|
39
|
+
local stale_kill_threshold="${5:-0}"
|
|
40
|
+
|
|
41
|
+
(
|
|
42
|
+
local elapsed=0
|
|
43
|
+
local prev_size=0
|
|
44
|
+
local prev_child_activity_signature=""
|
|
45
|
+
local stale_seconds=0
|
|
46
|
+
while kill -0 "$cli_pid" 2>/dev/null; do
|
|
47
|
+
sleep "$heartbeat_interval"
|
|
48
|
+
elapsed=$((elapsed + heartbeat_interval))
|
|
49
|
+
kill -0 "$cli_pid" 2>/dev/null || break
|
|
50
|
+
|
|
51
|
+
# Get log file size
|
|
52
|
+
local cur_size=0
|
|
53
|
+
if [[ -f "$session_log" ]]; then
|
|
54
|
+
cur_size=$(wc -c < "$session_log" 2>/dev/null || echo 0)
|
|
55
|
+
cur_size=$(echo "$cur_size" | tr -d ' ')
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
local growth=$((cur_size - prev_size))
|
|
59
|
+
prev_size=$cur_size
|
|
60
|
+
|
|
61
|
+
local child_activity_signature=""
|
|
62
|
+
local child_total_bytes=0
|
|
63
|
+
local child_session_count=0
|
|
64
|
+
if [[ -f "$progress_json" ]]; then
|
|
65
|
+
local child_activity_data
|
|
66
|
+
child_activity_data=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
67
|
+
import json
|
|
68
|
+
import sys
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
72
|
+
progress = json.load(fh)
|
|
73
|
+
except Exception:
|
|
74
|
+
sys.exit(0)
|
|
75
|
+
|
|
76
|
+
signature = str(progress.get("child_activity_signature") or "")
|
|
77
|
+
total_bytes = int(progress.get("child_total_bytes") or 0)
|
|
78
|
+
session_count = len(progress.get("child_session_files") or [])
|
|
79
|
+
print(f"{signature}\t{total_bytes}\t{session_count}")
|
|
80
|
+
PY
|
|
81
|
+
)
|
|
82
|
+
if [[ -n "$child_activity_data" ]]; then
|
|
83
|
+
IFS=$'\t' read -r child_activity_signature child_total_bytes child_session_count <<< "$child_activity_data"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
local child_growth=0
|
|
88
|
+
if [[ -n "$child_activity_signature" && "$child_activity_signature" != "$prev_child_activity_signature" ]]; then
|
|
89
|
+
child_growth=1
|
|
90
|
+
fi
|
|
91
|
+
prev_child_activity_signature="$child_activity_signature"
|
|
92
|
+
|
|
93
|
+
local effective_stale_kill_threshold="$stale_kill_threshold"
|
|
94
|
+
if [[ $stale_kill_threshold -gt 0 && -f "$progress_json" ]]; then
|
|
95
|
+
local extended_threshold
|
|
96
|
+
extended_threshold=$(python3 - "$progress_json" "$stale_kill_threshold" <<'PY' 2>/dev/null || true
|
|
97
|
+
import json
|
|
98
|
+
import os
|
|
99
|
+
import sys
|
|
100
|
+
|
|
101
|
+
progress_path = sys.argv[1]
|
|
102
|
+
base_threshold = int(sys.argv[2])
|
|
103
|
+
|
|
104
|
+
with open(progress_path, "r", encoding="utf-8") as fh:
|
|
105
|
+
progress = json.load(fh)
|
|
106
|
+
|
|
107
|
+
spawn_count = 0
|
|
108
|
+
for tool in progress.get("tool_calls", []):
|
|
109
|
+
if isinstance(tool, dict) and tool.get("name") in ("spawn_agent", "Agent", "TaskCreate"):
|
|
110
|
+
try:
|
|
111
|
+
spawn_count += int(tool.get("count", 0))
|
|
112
|
+
except (TypeError, ValueError):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
# Also check the subagent_spawn_count field (set by _record_cb_agent_tool_call)
|
|
116
|
+
if not spawn_count:
|
|
117
|
+
spawn_count = int(progress.get("subagent_spawn_count", 0))
|
|
118
|
+
|
|
119
|
+
fmt = progress.get("event_format", "")
|
|
120
|
+
|
|
121
|
+
# Codex: current_tool == "wait" means parent is blocked on spawn_agent completion
|
|
122
|
+
if (
|
|
123
|
+
fmt == "codex-json"
|
|
124
|
+
and progress.get("current_tool") == "wait"
|
|
125
|
+
and spawn_count > 0
|
|
126
|
+
):
|
|
127
|
+
configured = os.environ.get("CODEX_WAIT_STALE_KILL_THRESHOLD", "")
|
|
128
|
+
try:
|
|
129
|
+
wait_threshold = int(configured)
|
|
130
|
+
except ValueError:
|
|
131
|
+
wait_threshold = max(base_threshold * 4, 3600)
|
|
132
|
+
if wait_threshold > base_threshold:
|
|
133
|
+
print(wait_threshold)
|
|
134
|
+
|
|
135
|
+
# CodeBuddy: Agent tool blocks synchronously; Task* tools imply bg agents.
|
|
136
|
+
# Extend the stale window when sub-agents have been spawned so the heartbeat
|
|
137
|
+
# doesn't kill the parent while children are still running.
|
|
138
|
+
if (
|
|
139
|
+
fmt == "stream-json"
|
|
140
|
+
and spawn_count > 0
|
|
141
|
+
and progress.get("cb_session_id", "")
|
|
142
|
+
):
|
|
143
|
+
configured = os.environ.get("CB_SUBAGENT_STALE_KILL_THRESHOLD", "")
|
|
144
|
+
try:
|
|
145
|
+
cb_threshold = int(configured)
|
|
146
|
+
except ValueError:
|
|
147
|
+
cb_threshold = max(base_threshold * 4, 3600)
|
|
148
|
+
if cb_threshold > base_threshold:
|
|
149
|
+
print(cb_threshold)
|
|
150
|
+
PY
|
|
151
|
+
)
|
|
152
|
+
if [[ "$extended_threshold" =~ ^[0-9]+$ && "$extended_threshold" -gt "$stale_kill_threshold" ]]; then
|
|
153
|
+
effective_stale_kill_threshold="$extended_threshold"
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Check for error-loop: agent is actively producing output but results are
|
|
158
|
+
# all read-offset errors or wasted calls. This is a stuck agent that appears
|
|
159
|
+
# "active" by log growth but is accomplishing nothing.
|
|
160
|
+
local error_loop_detected=false
|
|
161
|
+
if [[ $effective_stale_kill_threshold -gt 0 && $growth -gt 0 && -f "$progress_json" ]]; then
|
|
162
|
+
local error_loop_flag
|
|
163
|
+
error_loop_flag=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
164
|
+
import json, sys
|
|
165
|
+
try:
|
|
166
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
167
|
+
progress = json.load(fh)
|
|
168
|
+
except Exception:
|
|
169
|
+
raise SystemExit(0)
|
|
170
|
+
errors = progress.get("errors", [])
|
|
171
|
+
if isinstance(errors, list) and len(errors) >= 5:
|
|
172
|
+
recent = errors[-5:]
|
|
173
|
+
if all(isinstance(e, dict) and e.get("type") in ("read_offset_overflow", "wasted_call") for e in recent):
|
|
174
|
+
print("error_loop")
|
|
175
|
+
PY
|
|
176
|
+
)
|
|
177
|
+
if [[ "$error_loop_flag" == "error_loop" ]]; then
|
|
178
|
+
error_loop_detected=true
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Track progress staleness. Parent sessions can sit in a wait/polling
|
|
183
|
+
# tool while child transcripts keep growing, so child activity counts.
|
|
184
|
+
# Error loops bypass normal growth-as-progress because the log is only
|
|
185
|
+
# growing with repeated failed reads or wasted calls.
|
|
186
|
+
if [[ "$error_loop_detected" == "true" ]]; then
|
|
187
|
+
stale_seconds=$effective_stale_kill_threshold
|
|
188
|
+
elif [[ $growth -eq 0 && $child_growth -eq 0 ]]; then
|
|
189
|
+
stale_seconds=$((stale_seconds + heartbeat_interval))
|
|
190
|
+
else
|
|
191
|
+
stale_seconds=0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
local size_display
|
|
195
|
+
if [[ $cur_size -gt 1048576 ]]; then
|
|
196
|
+
size_display="$((cur_size / 1048576))MB"
|
|
197
|
+
elif [[ $cur_size -gt 1024 ]]; then
|
|
198
|
+
size_display="$((cur_size / 1024))KB"
|
|
199
|
+
else
|
|
200
|
+
size_display="${cur_size}B"
|
|
201
|
+
fi
|
|
202
|
+
local child_display=""
|
|
203
|
+
if [[ ${child_total_bytes:-0} -gt 0 ]]; then
|
|
204
|
+
local child_size_display
|
|
205
|
+
if [[ $child_total_bytes -gt 1048576 ]]; then
|
|
206
|
+
child_size_display="$((child_total_bytes / 1048576))MB"
|
|
207
|
+
elif [[ $child_total_bytes -gt 1024 ]]; then
|
|
208
|
+
child_size_display="$((child_total_bytes / 1024))KB"
|
|
209
|
+
else
|
|
210
|
+
child_size_display="${child_total_bytes}B"
|
|
211
|
+
fi
|
|
212
|
+
child_display=" | child: ${child_size_display}"
|
|
213
|
+
if [[ ${child_session_count:-0} -gt 1 ]]; then
|
|
214
|
+
child_display="${child_display}/${child_session_count}"
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
local mins=$((elapsed / 60))
|
|
219
|
+
local secs=$((elapsed % 60))
|
|
220
|
+
|
|
221
|
+
local status_icon
|
|
222
|
+
if [[ $growth -gt 0 || $child_growth -gt 0 ]]; then
|
|
223
|
+
status_icon="${GREEN}▶${NC}"
|
|
224
|
+
else
|
|
225
|
+
status_icon="${YELLOW}⏸${NC}"
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
# Fatal provider/runtime errors are terminal; do not wait for the
|
|
229
|
+
# stale window when progress.json already proves the model cannot
|
|
230
|
+
# continue (for example context_too_large).
|
|
231
|
+
if [[ -f "$progress_json" ]]; then
|
|
232
|
+
local fatal_error_code=""
|
|
233
|
+
fatal_error_code=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
234
|
+
import json
|
|
235
|
+
import sys
|
|
236
|
+
try:
|
|
237
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
238
|
+
progress = json.load(fh)
|
|
239
|
+
except Exception:
|
|
240
|
+
raise SystemExit(0)
|
|
241
|
+
code = progress.get("fatal_error_code") or ""
|
|
242
|
+
if code:
|
|
243
|
+
print(code)
|
|
244
|
+
PY
|
|
245
|
+
)
|
|
246
|
+
if [[ -n "$fatal_error_code" ]]; then
|
|
247
|
+
echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}FATAL: ${fatal_error_code}${NC}"
|
|
248
|
+
local _marker_dir
|
|
249
|
+
_marker_dir="$(dirname "$session_log")"
|
|
250
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"${fatal_error_code}\", \"fatal_error_code\": \"${fatal_error_code}\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/fatal-error.json" 2>/dev/null || true
|
|
251
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"${fatal_error_code}\", \"fatal_error_code\": \"${fatal_error_code}\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
|
|
252
|
+
kill -TERM "$cli_pid" 2>/dev/null || true
|
|
253
|
+
local fatal_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
|
|
254
|
+
if [[ $fatal_kill_grace_seconds -gt 0 ]]; then
|
|
255
|
+
sleep "$fatal_kill_grace_seconds"
|
|
256
|
+
fi
|
|
257
|
+
if kill -0 "$cli_pid" 2>/dev/null; then
|
|
258
|
+
kill -9 "$cli_pid" 2>/dev/null || true
|
|
259
|
+
fi
|
|
260
|
+
break
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# Stale-kill: auto-terminate process if no progress for too long.
|
|
265
|
+
# Parent sessions can wait on spawned work; child transcript growth
|
|
266
|
+
# counts as progress above, while silent waits still use the active
|
|
267
|
+
# stale window to surface stuck agents promptly.
|
|
268
|
+
if [[ $effective_stale_kill_threshold -gt 0 && $stale_seconds -ge $effective_stale_kill_threshold ]]; then
|
|
269
|
+
local stale_mins=$((stale_seconds / 60))
|
|
270
|
+
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}"
|
|
271
|
+
echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (stale session)..."
|
|
272
|
+
# Write the marker before killing. Some CLIs exit quickly, and the
|
|
273
|
+
# parent runner may stop this heartbeat process immediately after
|
|
274
|
+
# wait(1) returns.
|
|
275
|
+
local _marker_dir
|
|
276
|
+
_marker_dir="$(dirname "$session_log")"
|
|
277
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"stale_session\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
|
|
278
|
+
kill -TERM "$cli_pid" 2>/dev/null || true
|
|
279
|
+
# Give process 10s to exit gracefully, then force kill
|
|
280
|
+
local stale_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
|
|
281
|
+
if [[ $stale_kill_grace_seconds -gt 0 ]]; then
|
|
282
|
+
sleep "$stale_kill_grace_seconds"
|
|
283
|
+
fi
|
|
284
|
+
if kill -0 "$cli_pid" 2>/dev/null; then
|
|
285
|
+
echo -e " ${RED}[HEARTBEAT]${NC} Process still alive after SIGTERM, sending SIGKILL..."
|
|
286
|
+
kill -9 "$cli_pid" 2>/dev/null || true
|
|
287
|
+
fi
|
|
288
|
+
break
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Build staleness hint for display
|
|
292
|
+
local stale_hint=""
|
|
293
|
+
if [[ $effective_stale_kill_threshold -gt 0 && $stale_seconds -gt 0 ]]; then
|
|
294
|
+
local stale_mins=$((stale_seconds / 60))
|
|
295
|
+
local threshold_mins=$((effective_stale_kill_threshold / 60))
|
|
296
|
+
stale_hint=" | stale: ${stale_mins}m/${threshold_mins}m"
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# Try structured progress from progress.json
|
|
300
|
+
if [[ -f "$progress_json" ]]; then
|
|
301
|
+
local phase tool msgs tools_total
|
|
302
|
+
phase=$(python3 -c "
|
|
303
|
+
import json, sys
|
|
304
|
+
try:
|
|
305
|
+
with open(sys.argv[1]) as f:
|
|
306
|
+
d = json.load(f)
|
|
307
|
+
parts = []
|
|
308
|
+
if d.get('current_phase'):
|
|
309
|
+
parts.append('phase: ' + d['current_phase'])
|
|
310
|
+
if d.get('current_tool'):
|
|
311
|
+
parts.append('tool: ' + d['current_tool'])
|
|
312
|
+
parts.append('msgs: ' + str(d.get('message_count', 0)))
|
|
313
|
+
parts.append(str(d.get('total_tool_calls', 0)) + ' tool calls')
|
|
314
|
+
print(' | '.join(parts))
|
|
315
|
+
except Exception:
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
" "$progress_json" 2>/dev/null) && {
|
|
318
|
+
echo -e " ${status_icon} ${BLUE}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display}${child_display} | ${phase}${stale_hint}"
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
# Fallback: tail-based activity detection
|
|
324
|
+
local last_activity=""
|
|
325
|
+
if [[ -f "$session_log" ]]; then
|
|
326
|
+
last_activity=$(tail -20 "$session_log" 2>/dev/null | grep -v '^$' | tail -1 | cut -c1-80 || echo "")
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
echo -e " ${status_icon} ${BLUE}[HEARTBEAT]${NC} ${mins}m${secs}s elapsed | log: ${size_display}${child_display} (+${growth}B) | ${last_activity}${stale_hint}"
|
|
330
|
+
done
|
|
331
|
+
) &
|
|
332
|
+
_HEARTBEAT_PID=$!
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Stop a heartbeat monitor process.
|
|
336
|
+
#
|
|
337
|
+
# Arguments:
|
|
338
|
+
# $1 - heartbeat_pid PID returned by start_heartbeat
|
|
339
|
+
stop_heartbeat() {
|
|
340
|
+
local heartbeat_pid="$1"
|
|
341
|
+
if [[ -n "$heartbeat_pid" ]]; then
|
|
342
|
+
kill "$heartbeat_pid" 2>/dev/null || true
|
|
343
|
+
wait "$heartbeat_pid" 2>/dev/null || true
|
|
344
|
+
fi
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Start the stream-json progress parser as a background process.
|
|
348
|
+
# Sets _PARSER_PID to the background process PID.
|
|
349
|
+
# No-op if USE_STREAM_JSON is not "true".
|
|
350
|
+
#
|
|
351
|
+
# Arguments:
|
|
352
|
+
# $1 - session_log Path to session.log
|
|
353
|
+
# $2 - progress_json Path to write progress.json
|
|
354
|
+
# $3 - scripts_dir Path to scripts/ directory
|
|
355
|
+
start_progress_parser() {
|
|
356
|
+
local session_log="$1"
|
|
357
|
+
local progress_json="$2"
|
|
358
|
+
local scripts_dir="$3"
|
|
359
|
+
|
|
360
|
+
_PARSER_PID=""
|
|
361
|
+
|
|
362
|
+
if [[ "${USE_STREAM_JSON:-}" != "true" ]]; then
|
|
363
|
+
return 0
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
local parser_script="$scripts_dir/parse-stream-progress.py"
|
|
367
|
+
if [[ ! -f "$parser_script" ]]; then
|
|
368
|
+
return 0
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
python3 "$parser_script" \
|
|
372
|
+
--session-log "$session_log" \
|
|
373
|
+
--progress-file "$progress_json" &
|
|
374
|
+
_PARSER_PID=$!
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Stop the progress parser process.
|
|
378
|
+
#
|
|
379
|
+
# Arguments:
|
|
380
|
+
# $1 - parser_pid PID returned by start_progress_parser
|
|
381
|
+
stop_progress_parser() {
|
|
382
|
+
local parser_pid="$1"
|
|
383
|
+
if [[ -n "$parser_pid" ]]; then
|
|
384
|
+
kill "$parser_pid" 2>/dev/null || true
|
|
385
|
+
wait "$parser_pid" 2>/dev/null || true
|
|
386
|
+
fi
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Detect whether the AI CLI supports structured JSON progress.
|
|
390
|
+
# Sets USE_STREAM_JSON to "true" or "false".
|
|
391
|
+
#
|
|
392
|
+
# Arguments:
|
|
393
|
+
# $1 - cli_cmd The AI CLI command
|
|
394
|
+
detect_stream_json_support() {
|
|
395
|
+
local cli_cmd="$1"
|
|
396
|
+
USE_STREAM_JSON="false"
|
|
397
|
+
STREAM_JSON_FORMAT=""
|
|
398
|
+
|
|
399
|
+
local session_platform=""
|
|
400
|
+
session_platform="$(_prizm_known_platform_from_cli "$cli_cmd" 2>/dev/null || true)"
|
|
401
|
+
if [[ -z "$session_platform" ]]; then
|
|
402
|
+
case "$(_prizm_normalize_platform "${PRIZMKIT_PLATFORM:-${PLATFORM:-}}")" in
|
|
403
|
+
codex|claude|codebuddy)
|
|
404
|
+
session_platform="$(_prizm_normalize_platform "${PRIZMKIT_PLATFORM:-${PLATFORM:-}}")"
|
|
405
|
+
;;
|
|
406
|
+
esac
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# CodeBuddy (cbc) always supports stream-json
|
|
410
|
+
if [[ "$session_platform" == "codebuddy" || "$cli_cmd" == "cbc" ]]; then
|
|
411
|
+
USE_STREAM_JSON="true"
|
|
412
|
+
STREAM_JSON_FORMAT="stream-json"
|
|
413
|
+
export USE_STREAM_JSON STREAM_JSON_FORMAT
|
|
414
|
+
return 0
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# Codex uses `codex exec --json` JSONL, not `--output-format stream-json`.
|
|
418
|
+
if [[ "$session_platform" == "codex" ]]; then
|
|
419
|
+
local codex_help
|
|
420
|
+
codex_help=$("$cli_cmd" exec --help 2>&1) || true
|
|
421
|
+
if echo "$codex_help" | grep -q -- "--json"; then
|
|
422
|
+
USE_STREAM_JSON="true"
|
|
423
|
+
STREAM_JSON_FORMAT="codex-json"
|
|
424
|
+
fi
|
|
425
|
+
export USE_STREAM_JSON STREAM_JSON_FORMAT
|
|
426
|
+
return 0
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
# For other CLIs, try to detect support via --help output
|
|
430
|
+
# Use explicit file descriptor to avoid issues in background processes
|
|
431
|
+
local help_output
|
|
432
|
+
help_output=$("$cli_cmd" --help 2>&1) || true
|
|
433
|
+
|
|
434
|
+
if echo "$help_output" | grep -q "stream-json"; then
|
|
435
|
+
USE_STREAM_JSON="true"
|
|
436
|
+
STREAM_JSON_FORMAT="stream-json"
|
|
437
|
+
fi
|
|
438
|
+
export USE_STREAM_JSON STREAM_JSON_FORMAT
|
|
439
|
+
}
|
|
@@ -38,6 +38,9 @@ function Invoke-PrizmPipeline {
|
|
|
38
38
|
Write-Host "Usage: .\run-$Kind.ps1 run [item-id] [list-path] [--dry-run] [--mode lite|standard|full] [--critic] [--max-retries N] [--timeout seconds]"
|
|
39
39
|
Write-Host " .\run-$Kind.ps1 status [list-path]"
|
|
40
40
|
Write-Host " .\run-$Kind.ps1 reset"
|
|
41
|
+
Write-Host ""
|
|
42
|
+
Write-Host "Environment Variables:"
|
|
43
|
+
Write-Host " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
|
|
41
44
|
$global:PRIZM_EXIT_CODE = 0
|
|
42
45
|
return
|
|
43
46
|
}
|
|
@@ -182,6 +185,10 @@ function Invoke-PrizmPipeline {
|
|
|
182
185
|
Write-PrizmInfo "Effective options: mode=$modeLabel critic=$criticLabel maxRetries=$retryLabel timeoutSeconds=$timeoutSeconds staleKillThreshold=$staleKillThreshold dryRun=$dryRun"
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
# Validate PRIZMKIT_EFFORT early (fail-fast before any sessions are spawned)
|
|
189
|
+
$cli = Resolve-PrizmAiCli $paths.ProjectRoot $paths.PrizmkitDir
|
|
190
|
+
Test-PrizmEffort $cli
|
|
191
|
+
|
|
185
192
|
if (-not (Test-Path $listPath)) { throw "List file not found: $listPath" }
|
|
186
193
|
$pipelineStatePath = Join-Path $stateDir 'pipeline.json'
|
|
187
194
|
if (-not $dryRun) {
|
|
@@ -19,6 +19,16 @@ if ($command -in @('help','--help','-h')) {
|
|
|
19
19
|
Write-Host ' --dry-run Generate and print the recovery prompt without starting AI'
|
|
20
20
|
Write-Host ' --yes Accepted for scripted use; no confirmation prompt is shown'
|
|
21
21
|
Write-Host ' --model Override MODEL for this recovery session'
|
|
22
|
+
Write-Host ''
|
|
23
|
+
Write-Host 'Environment Variables:'
|
|
24
|
+
Write-Host ' PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)'
|
|
25
|
+
Write-Host ''
|
|
26
|
+
Write-Host 'Examples:'
|
|
27
|
+
Write-Host ' .\run-recovery.ps1 # Auto-detect and recover'
|
|
28
|
+
Write-Host ' .\run-recovery.ps1 detect # Show what would be recovered'
|
|
29
|
+
Write-Host ' .\run-recovery.ps1 run --dry-run # Generate prompt, don''t execute'
|
|
30
|
+
Write-Host ' .\run-recovery.ps1 run --yes # Skip confirmation prompt'
|
|
31
|
+
Write-Host ' $env:PRIZMKIT_EFFORT=''high''; .\run-recovery.ps1 # Use high reasoning effort'
|
|
22
32
|
exit 0
|
|
23
33
|
}
|
|
24
34
|
|
|
@@ -96,6 +106,7 @@ Invoke-PrizmPythonText $python @((Join-Path $paths.ScriptsDir 'generate-recovery
|
|
|
96
106
|
if ($dryRun) { Get-Content $promptPath; exit 0 }
|
|
97
107
|
$cli = Resolve-PrizmAiCli $paths.ProjectRoot $paths.PrizmkitDir
|
|
98
108
|
$env:PRIZMKIT_PLATFORM = Get-PrizmPlatformFromProject $paths.ProjectRoot $paths.PrizmkitDir $cli
|
|
109
|
+
Test-PrizmEffort $cli
|
|
99
110
|
Write-PrizmInfo "Starting recovery session: $sessionId"
|
|
100
111
|
Write-PrizmInfo "Prompt: $promptPath"
|
|
101
112
|
Write-PrizmInfo "Log: $logPath"
|