prizmkit 1.1.74 → 1.1.76

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.
@@ -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"