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.
- 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/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/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
# STALE_KILL_THRESHOLD=900 # Auto-kill after N seconds without parent log progress (0 = disabled)
|
|
45
45
|
# CODEX_WAIT_STALE_KILL_THRESHOLD=3600 # Longer no-log window while Codex waits on subagents
|
|
46
46
|
# CODEX_SUBAGENT_TIMEOUT_SECONDS=3300 # Codex subagent max runtime; defaults to wait threshold - 300
|
|
47
|
+
# CB_SUBAGENT_STALE_KILL_THRESHOLD=3600 # Longer no-log window while CodeBuddy sub-agents are running
|
|
47
48
|
# LOG_CLEANUP_ENABLED=1 # Periodic log cleanup (1=on, 0=off)
|
|
48
49
|
# LOG_RETENTION_DAYS=14 # Delete logs older than N days
|
|
49
50
|
# LOG_MAX_TOTAL_MB=1024 # Keep total logs under N MB via oldest-first cleanup
|
|
@@ -338,6 +338,41 @@ prizm_detect_cli_and_platform() {
|
|
|
338
338
|
export PRIZMKIT_PLATFORM="$PLATFORM"
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
# Validate PRIZMKIT_EFFORT against the detected platform's supported values.
|
|
342
|
+
# Exits with a friendly error if the value is unsupported.
|
|
343
|
+
prizm_validate_effort() {
|
|
344
|
+
[[ -n "${PRIZMKIT_EFFORT:-}" ]] || return 0
|
|
345
|
+
|
|
346
|
+
local session_platform
|
|
347
|
+
session_platform="$(_prizm_session_platform)"
|
|
348
|
+
|
|
349
|
+
local allowed_values
|
|
350
|
+
case "$session_platform" in
|
|
351
|
+
claude) allowed_values="low, medium, high, xhigh, max" ;;
|
|
352
|
+
codex) allowed_values="low, medium, high, xhigh" ;;
|
|
353
|
+
*) allowed_values="low, medium, high, xhigh" ;;
|
|
354
|
+
esac
|
|
355
|
+
|
|
356
|
+
local valid=0
|
|
357
|
+
local val
|
|
358
|
+
for val in $allowed_values; do
|
|
359
|
+
val="${val%,}"
|
|
360
|
+
[[ "$val" == "$PRIZMKIT_EFFORT" ]] && valid=1 && break
|
|
361
|
+
done
|
|
362
|
+
|
|
363
|
+
if [[ "$valid" -ne 1 ]]; then
|
|
364
|
+
local platform_label="$session_platform"
|
|
365
|
+
[[ "$session_platform" == "claude" ]] && platform_label="Claude Code"
|
|
366
|
+
[[ "$session_platform" == "codebuddy" ]] && platform_label="CodeBuddy"
|
|
367
|
+
[[ "$session_platform" == "codex" ]] && platform_label="Codex"
|
|
368
|
+
|
|
369
|
+
log_error "PRIZMKIT_EFFORT='$PRIZMKIT_EFFORT' is not supported by the detected CLI ($platform_label)."
|
|
370
|
+
log_error " Supported values for $platform_label: $allowed_values"
|
|
371
|
+
log_error " Set PRIZMKIT_EFFORT to one of the above, or unset it to use the CLI default."
|
|
372
|
+
exit 1
|
|
373
|
+
fi
|
|
374
|
+
}
|
|
375
|
+
|
|
341
376
|
# Start an AI CLI session in the background.
|
|
342
377
|
# Usage: prizm_start_ai_session <prompt_path> <log_path> <model>
|
|
343
378
|
# Sets PRIZM_AI_PID to the spawned process PID. Do not call this function via
|
|
@@ -380,6 +415,9 @@ prizm_start_ai_session() {
|
|
|
380
415
|
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
381
416
|
claude_args+=(--output-format stream-json)
|
|
382
417
|
fi
|
|
418
|
+
if [[ -n "${PRIZMKIT_EFFORT:-}" ]]; then
|
|
419
|
+
claude_args+=(--effort "$PRIZMKIT_EFFORT")
|
|
420
|
+
fi
|
|
383
421
|
if [[ -n "$model" ]]; then
|
|
384
422
|
claude_args+=(--model "$model")
|
|
385
423
|
fi
|
|
@@ -399,6 +437,9 @@ prizm_start_ai_session() {
|
|
|
399
437
|
if [[ -n "$model" ]]; then
|
|
400
438
|
codex_args+=(--model "$model")
|
|
401
439
|
fi
|
|
440
|
+
if [[ -n "${PRIZMKIT_EFFORT:-}" ]]; then
|
|
441
|
+
codex_args+=(--config "model_reasoning_effort=$PRIZMKIT_EFFORT")
|
|
442
|
+
fi
|
|
402
443
|
"$CLI_CMD" "${codex_args[@]}" - < "$prompt_path" > "$log_path" 2>&1 &
|
|
403
444
|
;;
|
|
404
445
|
*)
|
|
@@ -409,6 +450,9 @@ prizm_start_ai_session() {
|
|
|
409
450
|
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
410
451
|
cb_args+=(--output-format stream-json)
|
|
411
452
|
fi
|
|
453
|
+
if [[ -n "${PRIZMKIT_EFFORT:-}" ]]; then
|
|
454
|
+
cb_args+=(--effort "$PRIZMKIT_EFFORT")
|
|
455
|
+
fi
|
|
412
456
|
if [[ -n "$model" ]]; then
|
|
413
457
|
cb_args+=(--model "$model")
|
|
414
458
|
fi
|
|
@@ -900,6 +944,9 @@ prizm_run_ai_session() {
|
|
|
900
944
|
case "$session_platform" in
|
|
901
945
|
claude)
|
|
902
946
|
local claude_args=(-p "$(cat "$prompt_path")" --dangerously-skip-permissions)
|
|
947
|
+
if [[ -n "${PRIZMKIT_EFFORT:-}" ]]; then
|
|
948
|
+
claude_args+=(--effort "$PRIZMKIT_EFFORT")
|
|
949
|
+
fi
|
|
903
950
|
if [[ -n "$model" ]]; then
|
|
904
951
|
claude_args+=(--model "$model")
|
|
905
952
|
fi
|
|
@@ -919,10 +966,16 @@ prizm_run_ai_session() {
|
|
|
919
966
|
if [[ -n "$model" ]]; then
|
|
920
967
|
codex_args+=(--model "$model")
|
|
921
968
|
fi
|
|
969
|
+
if [[ -n "${PRIZMKIT_EFFORT:-}" ]]; then
|
|
970
|
+
codex_args+=(--config "model_reasoning_effort=$PRIZMKIT_EFFORT")
|
|
971
|
+
fi
|
|
922
972
|
"$CLI_CMD" "${codex_args[@]}" - < "$prompt_path" > "$log_path" 2>&1
|
|
923
973
|
;;
|
|
924
974
|
*)
|
|
925
975
|
local cb_args=(--print -y)
|
|
976
|
+
if [[ -n "${PRIZMKIT_EFFORT:-}" ]]; then
|
|
977
|
+
cb_args+=(--effort "$PRIZMKIT_EFFORT")
|
|
978
|
+
fi
|
|
926
979
|
if [[ -n "$model" ]]; then
|
|
927
980
|
cb_args+=(--model "$model")
|
|
928
981
|
fi
|
|
@@ -92,8 +92,8 @@ PY
|
|
|
92
92
|
|
|
93
93
|
local effective_stale_kill_threshold="$stale_kill_threshold"
|
|
94
94
|
if [[ $stale_kill_threshold -gt 0 && -f "$progress_json" ]]; then
|
|
95
|
-
local
|
|
96
|
-
|
|
95
|
+
local extended_threshold
|
|
96
|
+
extended_threshold=$(python3 - "$progress_json" "$stale_kill_threshold" <<'PY' 2>/dev/null || true
|
|
97
97
|
import json
|
|
98
98
|
import os
|
|
99
99
|
import sys
|
|
@@ -106,14 +106,21 @@ with open(progress_path, "r", encoding="utf-8") as fh:
|
|
|
106
106
|
|
|
107
107
|
spawn_count = 0
|
|
108
108
|
for tool in progress.get("tool_calls", []):
|
|
109
|
-
if isinstance(tool, dict) and tool.get("name")
|
|
109
|
+
if isinstance(tool, dict) and tool.get("name") in ("spawn_agent", "Agent", "TaskCreate"):
|
|
110
110
|
try:
|
|
111
111
|
spawn_count += int(tool.get("count", 0))
|
|
112
112
|
except (TypeError, ValueError):
|
|
113
113
|
pass
|
|
114
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
|
|
115
122
|
if (
|
|
116
|
-
|
|
123
|
+
fmt == "codex-json"
|
|
117
124
|
and progress.get("current_tool") == "wait"
|
|
118
125
|
and spawn_count > 0
|
|
119
126
|
):
|
|
@@ -124,10 +131,26 @@ if (
|
|
|
124
131
|
wait_threshold = max(base_threshold * 4, 3600)
|
|
125
132
|
if wait_threshold > base_threshold:
|
|
126
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)
|
|
127
150
|
PY
|
|
128
151
|
)
|
|
129
|
-
if [[ "$
|
|
130
|
-
effective_stale_kill_threshold="$
|
|
152
|
+
if [[ "$extended_threshold" =~ ^[0-9]+$ && "$extended_threshold" -gt "$stale_kill_threshold" ]]; then
|
|
153
|
+
effective_stale_kill_threshold="$extended_threshold"
|
|
131
154
|
fi
|
|
132
155
|
fi
|
|
133
156
|
|
|
@@ -51,6 +51,7 @@ LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
|
|
|
51
51
|
LOG_MAX_TOTAL_MB=${LOG_MAX_TOTAL_MB:-1024}
|
|
52
52
|
VERBOSE=${VERBOSE:-0}
|
|
53
53
|
MODEL=${MODEL:-""}
|
|
54
|
+
PRIZMKIT_EFFORT=${PRIZMKIT_EFFORT:-""}
|
|
54
55
|
DEV_BRANCH=${DEV_BRANCH:-""}
|
|
55
56
|
AUTO_PUSH=${AUTO_PUSH:-0}
|
|
56
57
|
STOP_ON_FAILURE=${STOP_ON_FAILURE:-0}
|
|
@@ -59,6 +60,9 @@ ENABLE_DEPLOY=${ENABLE_DEPLOY:-0}
|
|
|
59
60
|
# Source shared common helpers (CLI/platform detection + logs + deps)
|
|
60
61
|
prizm_detect_cli_and_platform
|
|
61
62
|
|
|
63
|
+
# Validate PRIZMKIT_EFFORT early (fail-fast before any sessions are spawned)
|
|
64
|
+
prizm_validate_effort
|
|
65
|
+
|
|
62
66
|
# Source shared heartbeat library
|
|
63
67
|
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
64
68
|
|
|
@@ -1197,6 +1201,7 @@ show_help() {
|
|
|
1197
1201
|
echo " MAX_RETRIES Max retries per bug (default: 3)"
|
|
1198
1202
|
echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
|
|
1199
1203
|
echo " MODEL Default AI model (overridden by per-bug model in bug list)"
|
|
1204
|
+
echo " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
|
|
1200
1205
|
echo " PIPELINE_MODE Default pipeline mode: lite|standard|full (overridden by --mode)"
|
|
1201
1206
|
echo " ENABLE_CRITIC Enable/disable critic: true|false|1|0 (overridden by --critic/--no-critic)"
|
|
1202
1207
|
echo " AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)"
|
|
@@ -54,6 +54,7 @@ LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
|
|
|
54
54
|
LOG_MAX_TOTAL_MB=${LOG_MAX_TOTAL_MB:-1024}
|
|
55
55
|
VERBOSE=${VERBOSE:-0}
|
|
56
56
|
MODEL=${MODEL:-""}
|
|
57
|
+
PRIZMKIT_EFFORT=${PRIZMKIT_EFFORT:-""}
|
|
57
58
|
DEV_BRANCH=${DEV_BRANCH:-""}
|
|
58
59
|
AUTO_PUSH=${AUTO_PUSH:-0}
|
|
59
60
|
STOP_ON_FAILURE=${STOP_ON_FAILURE:-0}
|
|
@@ -62,6 +63,9 @@ ENABLE_DEPLOY=${ENABLE_DEPLOY:-0}
|
|
|
62
63
|
# Source shared common helpers (CLI/platform detection + logs + deps)
|
|
63
64
|
prizm_detect_cli_and_platform
|
|
64
65
|
|
|
66
|
+
# Validate PRIZMKIT_EFFORT early (fail-fast before any sessions are spawned)
|
|
67
|
+
prizm_validate_effort
|
|
68
|
+
|
|
65
69
|
# Source shared heartbeat library
|
|
66
70
|
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
67
71
|
|
|
@@ -1483,6 +1487,7 @@ show_help() {
|
|
|
1483
1487
|
echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
|
|
1484
1488
|
echo " AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)"
|
|
1485
1489
|
echo " MODEL AI model ID (e.g. claude-opus-4.6, claude-sonnet-4.6, claude-haiku-4.5)"
|
|
1490
|
+
echo " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
|
|
1486
1491
|
echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
|
|
1487
1492
|
echo " STALE_KILL_THRESHOLD Auto-kill session after N seconds of no progress (default: 900)"
|
|
1488
1493
|
echo " HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)"
|
|
@@ -67,11 +67,15 @@ HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
|
|
|
67
67
|
STALE_KILL_THRESHOLD=${STALE_KILL_THRESHOLD:-900}
|
|
68
68
|
VERBOSE=${VERBOSE:-0}
|
|
69
69
|
MODEL=${MODEL:-""}
|
|
70
|
+
PRIZMKIT_EFFORT=${PRIZMKIT_EFFORT:-""}
|
|
70
71
|
AUTO_PUSH=${AUTO_PUSH:-0}
|
|
71
72
|
|
|
72
73
|
# Source shared common helpers (CLI/platform detection + logs + deps)
|
|
73
74
|
prizm_detect_cli_and_platform
|
|
74
75
|
|
|
76
|
+
# Validate PRIZMKIT_EFFORT early (fail-fast before any sessions are spawned)
|
|
77
|
+
prizm_validate_effort
|
|
78
|
+
|
|
75
79
|
# Source shared heartbeat library
|
|
76
80
|
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
77
81
|
|
|
@@ -106,6 +110,7 @@ Options (for 'run' command):
|
|
|
106
110
|
Environment Variables:
|
|
107
111
|
SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
|
|
108
112
|
MODEL AI model to use
|
|
113
|
+
PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)
|
|
109
114
|
VERBOSE Set to 1 for verbose AI CLI output
|
|
110
115
|
HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
|
|
111
116
|
STALE_KILL_THRESHOLD Auto-kill after N seconds no progress (default: 900)
|
|
@@ -117,6 +122,7 @@ Examples:
|
|
|
117
122
|
./run-recovery.sh run --dry-run # Generate prompt, don't execute
|
|
118
123
|
./run-recovery.sh run --yes # Skip confirmation prompt
|
|
119
124
|
./run-recovery.sh run --model claude-opus-4.6
|
|
125
|
+
PRIZMKIT_EFFORT=high ./run-recovery.sh # Use high reasoning effort
|
|
120
126
|
HELP
|
|
121
127
|
}
|
|
122
128
|
|
|
@@ -52,6 +52,7 @@ LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
|
|
|
52
52
|
LOG_MAX_TOTAL_MB=${LOG_MAX_TOTAL_MB:-1024}
|
|
53
53
|
VERBOSE=${VERBOSE:-0}
|
|
54
54
|
MODEL=${MODEL:-""}
|
|
55
|
+
PRIZMKIT_EFFORT=${PRIZMKIT_EFFORT:-""}
|
|
55
56
|
DEV_BRANCH=${DEV_BRANCH:-""}
|
|
56
57
|
AUTO_PUSH=${AUTO_PUSH:-0}
|
|
57
58
|
STOP_ON_FAILURE=${STOP_ON_FAILURE:-0}
|
|
@@ -61,6 +62,9 @@ ENABLE_DEPLOY=${ENABLE_DEPLOY:-0}
|
|
|
61
62
|
# Source shared common helpers (CLI/platform detection + logs + deps)
|
|
62
63
|
prizm_detect_cli_and_platform
|
|
63
64
|
|
|
65
|
+
# Validate PRIZMKIT_EFFORT early (fail-fast before any sessions are spawned)
|
|
66
|
+
prizm_validate_effort
|
|
67
|
+
|
|
64
68
|
# Source shared heartbeat library
|
|
65
69
|
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
66
70
|
|
|
@@ -1232,6 +1236,7 @@ show_help() {
|
|
|
1232
1236
|
echo " MAX_RETRIES Max retries per refactor (default: 3)"
|
|
1233
1237
|
echo " SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)"
|
|
1234
1238
|
echo " MODEL Default AI model (overridden by per-refactor model in refactor list)"
|
|
1239
|
+
echo " PRIZMKIT_EFFORT AI reasoning effort (low|medium|high|xhigh|max; max is Claude Code only)"
|
|
1235
1240
|
echo " PIPELINE_MODE Default pipeline mode: lite|standard|full (overridden by --mode)"
|
|
1236
1241
|
echo " ENABLE_CRITIC Enable/disable critic: true|false|1|0 (overridden by --critic/--no-critic)"
|
|
1237
1242
|
echo " AI_CLI AI CLI command name (auto-detected: cbc, claude, or codex)"
|
|
@@ -137,7 +137,10 @@ class ProgressTracker:
|
|
|
137
137
|
self.event_format = ""
|
|
138
138
|
self.active_subagent_count = 0
|
|
139
139
|
self.subagent_status_counts = Counter()
|
|
140
|
+
self._subagent_spawn_count = 0
|
|
140
141
|
self.codex_child_thread_ids = set()
|
|
142
|
+
self.cb_session_id = ""
|
|
143
|
+
self.cb_cwd = ""
|
|
141
144
|
self.claude_session_id = ""
|
|
142
145
|
self.claude_cwd = ""
|
|
143
146
|
self.claude_task_states = {}
|
|
@@ -147,6 +150,7 @@ class ProgressTracker:
|
|
|
147
150
|
self.last_child_activity_at = ""
|
|
148
151
|
self._codex_child_session_paths = {}
|
|
149
152
|
self._claude_child_session_files = []
|
|
153
|
+
self._cb_child_session_files = []
|
|
150
154
|
self._last_child_scan_at = 0.0
|
|
151
155
|
self._last_claude_fallback_scan_at = 0.0
|
|
152
156
|
self._last_claude_fallback_scan_key = ""
|
|
@@ -158,10 +162,13 @@ class ProgressTracker:
|
|
|
158
162
|
def process_event(self, event):
|
|
159
163
|
"""Process a single stream-json event and update state.
|
|
160
164
|
|
|
161
|
-
Supports
|
|
165
|
+
Supports three formats:
|
|
162
166
|
1. Claude API raw stream: message_start, content_block_start, content_block_delta, etc.
|
|
163
167
|
2. Claude Code verbose stream-json: assistant, user, tool_result, system, etc.
|
|
164
168
|
(produced by claude/claude-internal --verbose --output-format stream-json)
|
|
169
|
+
3. CodeBuddy stream-json: message/function_call/function_call_result events
|
|
170
|
+
(produced by cbc --print -y --output-format stream-json — message type
|
|
171
|
+
with sessionId/cwd metadata, function_call for tool invocations).
|
|
165
172
|
"""
|
|
166
173
|
event_type = event.get("type", "")
|
|
167
174
|
|
|
@@ -233,6 +240,93 @@ class ProgressTracker:
|
|
|
233
240
|
|
|
234
241
|
return
|
|
235
242
|
|
|
243
|
+
# ── CodeBuddy stream-json format ──────────────────────────────
|
|
244
|
+
# cbc --print -y --output-format stream-json emits:
|
|
245
|
+
# message (role=user/assistant), function_call, function_call_result,
|
|
246
|
+
# file-history-snapshot, error. The first user message carries
|
|
247
|
+
# sessionId and cwd metadata; later function_call items carry the
|
|
248
|
+
# same fields.
|
|
249
|
+
# NOTE: cbc emits tool invocations as EITHER tool_use blocks inside a
|
|
250
|
+
# message (role=assistant) event OR as standalone function_call events,
|
|
251
|
+
# but not both for the same invocation. The two handlers are
|
|
252
|
+
# complementary rather than redundant, so no deduplication is needed.
|
|
253
|
+
# Detect via event_type=="message" with a "sessionId" field.
|
|
254
|
+
# Must run before the Claude Code assistant / tool_result branches
|
|
255
|
+
# because those also accept those types but use different internals.
|
|
256
|
+
if event_type == "message" and isinstance(event.get("sessionId"), str):
|
|
257
|
+
self.event_format = self.event_format or "stream-json"
|
|
258
|
+
role = event.get("role", "")
|
|
259
|
+
sid = event.get("sessionId", "")
|
|
260
|
+
cwd = event.get("cwd", "")
|
|
261
|
+
if sid and not self.cb_session_id:
|
|
262
|
+
self.cb_session_id = sid.strip()
|
|
263
|
+
if cwd and not self.cb_cwd:
|
|
264
|
+
self.cb_cwd = cwd.strip()
|
|
265
|
+
if role == "assistant":
|
|
266
|
+
self.message_count += 1
|
|
267
|
+
self.is_active = True
|
|
268
|
+
content = event.get("content", [])
|
|
269
|
+
if isinstance(content, list):
|
|
270
|
+
for block in content:
|
|
271
|
+
block_type = block.get("type", "")
|
|
272
|
+
if block_type == "tool_use":
|
|
273
|
+
tool_name = block.get("name", "unknown")
|
|
274
|
+
self.current_tool = tool_name
|
|
275
|
+
self.tool_call_counts[tool_name] += 1
|
|
276
|
+
self.total_tool_calls += 1
|
|
277
|
+
tool_input = block.get("input", {})
|
|
278
|
+
if isinstance(tool_input, dict):
|
|
279
|
+
self._extract_tool_summary_from_dict(tool_input)
|
|
280
|
+
self._detect_phase(
|
|
281
|
+
json.dumps(tool_input, ensure_ascii=False)[:500]
|
|
282
|
+
)
|
|
283
|
+
# Track CodeBuddy Agent/Task tool invocations as
|
|
284
|
+
# potential sub-agent spawns (Gap 3 fix).
|
|
285
|
+
self._record_cb_agent_tool_call(tool_name, tool_input)
|
|
286
|
+
elif block_type == "text":
|
|
287
|
+
text = block.get("text", "")
|
|
288
|
+
if text.strip():
|
|
289
|
+
self.last_text_snippet = text.strip()[:120]
|
|
290
|
+
self._detect_phase(text)
|
|
291
|
+
self._detect_terminal_error(
|
|
292
|
+
text, require_error_context=True
|
|
293
|
+
)
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# CodeBuddy function_call / function_call_result in stream-json
|
|
297
|
+
if event_type in ("function_call", "function_call_result"):
|
|
298
|
+
self.event_format = self.event_format or "stream-json"
|
|
299
|
+
sid = event.get("sessionId", "")
|
|
300
|
+
cwd = event.get("cwd", "")
|
|
301
|
+
if sid and not self.cb_session_id:
|
|
302
|
+
self.cb_session_id = sid.strip()
|
|
303
|
+
if cwd and not self.cb_cwd:
|
|
304
|
+
self.cb_cwd = cwd.strip()
|
|
305
|
+
if event_type == "function_call":
|
|
306
|
+
tool_name = event.get("name", "unknown")
|
|
307
|
+
self.current_tool = tool_name
|
|
308
|
+
self.tool_call_counts[tool_name] += 1
|
|
309
|
+
self.total_tool_calls += 1
|
|
310
|
+
self.is_active = True
|
|
311
|
+
# Extract summary from arguments
|
|
312
|
+
raw_args = event.get("arguments", "")
|
|
313
|
+
if isinstance(raw_args, str) and raw_args.strip():
|
|
314
|
+
self._extract_tool_summary(raw_args)
|
|
315
|
+
self._detect_phase(raw_args[:500])
|
|
316
|
+
# Track Agent/Task tool invocations as sub-agent spawns
|
|
317
|
+
self._record_cb_agent_tool_call(tool_name, raw_args)
|
|
318
|
+
elif event_type == "function_call_result":
|
|
319
|
+
self.current_tool = None
|
|
320
|
+
# Check result text for phase / error hints
|
|
321
|
+
result_text = event.get("output") or ""
|
|
322
|
+
if result_text and isinstance(result_text, str):
|
|
323
|
+
stripped = result_text.strip()
|
|
324
|
+
if stripped:
|
|
325
|
+
self.last_text_snippet = stripped[:120]
|
|
326
|
+
self._detect_phase(stripped)
|
|
327
|
+
self._detect_terminal_error(stripped, require_error_context=True)
|
|
328
|
+
return
|
|
329
|
+
|
|
236
330
|
# ── Claude Code verbose format ──────────────────────────────
|
|
237
331
|
if event_type == "assistant":
|
|
238
332
|
self.event_format = self.event_format or "stream-json"
|
|
@@ -259,7 +353,10 @@ class ProgressTracker:
|
|
|
259
353
|
self._detect_phase(text)
|
|
260
354
|
self._detect_terminal_error(text, require_error_context=True)
|
|
261
355
|
|
|
262
|
-
elif event_type == "tool_result" or
|
|
356
|
+
elif event_type == "tool_result" or (
|
|
357
|
+
event_type == "user"
|
|
358
|
+
and not isinstance(event.get("sessionId"), str)
|
|
359
|
+
):
|
|
263
360
|
# tool_result contains output from tool execution
|
|
264
361
|
self.event_format = self.event_format or "stream-json"
|
|
265
362
|
self.is_active = True
|
|
@@ -269,11 +366,11 @@ class ProgressTracker:
|
|
|
269
366
|
# B) Nested user events: event["message"]["content"][] has type=="tool_result" items
|
|
270
367
|
content_candidates = []
|
|
271
368
|
|
|
272
|
-
# Format A: top-level tool_result
|
|
369
|
+
# Format A: top-level tool_result (Claude Code)
|
|
273
370
|
if event_type == "tool_result":
|
|
274
371
|
content_candidates.append(str(event.get("content", "")))
|
|
275
372
|
|
|
276
|
-
# Format B: nested inside user event
|
|
373
|
+
# Format B: nested inside user event (Claude Code, NOT CodeBuddy)
|
|
277
374
|
if event_type == "user":
|
|
278
375
|
message = event.get("message", {})
|
|
279
376
|
content_list = message.get("content", [])
|
|
@@ -465,15 +562,60 @@ class ProgressTracker:
|
|
|
465
562
|
self._current_tool_input_parts = []
|
|
466
563
|
|
|
467
564
|
elif event_type == "error":
|
|
565
|
+
self.event_format = self.event_format or "stream-json"
|
|
468
566
|
error_msg = event.get("error", {}).get("message", "Unknown error")
|
|
567
|
+
errors = event.get("errors") or []
|
|
568
|
+
if isinstance(errors, list) and errors:
|
|
569
|
+
error_msg = "; ".join(str(e) for e in errors[:3])
|
|
469
570
|
self.errors.append(error_msg)
|
|
470
571
|
self._detect_terminal_error(str(error_msg))
|
|
471
572
|
|
|
573
|
+
# ── CodeBuddy file-history-snapshot (ignore) ─────────────────
|
|
574
|
+
elif event_type == "file-history-snapshot":
|
|
575
|
+
return
|
|
576
|
+
|
|
472
577
|
# Check for subagent indicator
|
|
473
578
|
if event.get("parent_tool_use_id"):
|
|
474
579
|
# This is a sub-agent event; tool name is still tracked normally
|
|
475
580
|
pass
|
|
476
581
|
|
|
582
|
+
def _record_cb_agent_tool_call(self, tool_name, raw_args):
|
|
583
|
+
"""Record a CodeBuddy Agent/Task* tool invocation for sub-agent tracking.
|
|
584
|
+
|
|
585
|
+
CodeBuddy spawns sub-agents via:
|
|
586
|
+
- Agent(prompt=..., run_in_background=True/False) — synchronous or bg fork
|
|
587
|
+
- TaskCreate(subject=..., description=...) — scheduled task
|
|
588
|
+
|
|
589
|
+
(TaskUpdate/TaskOutput exist but are lifecycle-only — they track
|
|
590
|
+
existing tasks rather than creating new sub-agents, so we don't
|
|
591
|
+
count them as spawns.)
|
|
592
|
+
|
|
593
|
+
We don't get child session ids from these tool calls in the stream,
|
|
594
|
+
but recording the count lets the heartbeat code in heartbeat.sh apply
|
|
595
|
+
an extended stale-kill window just as it does for Codex spawn_agent.
|
|
596
|
+
"""
|
|
597
|
+
if tool_name not in ("Agent", "TaskCreate"):
|
|
598
|
+
return
|
|
599
|
+
# Both dict (from message events) and str/JSON (from function_call events)
|
|
600
|
+
# are supported.
|
|
601
|
+
if isinstance(raw_args, dict):
|
|
602
|
+
args = raw_args
|
|
603
|
+
elif isinstance(raw_args, str):
|
|
604
|
+
try:
|
|
605
|
+
args = json.loads(raw_args)
|
|
606
|
+
except (json.JSONDecodeError, TypeError):
|
|
607
|
+
return
|
|
608
|
+
else:
|
|
609
|
+
return
|
|
610
|
+
# For Agent tool, subagent_type or run_in_background hints at delegation
|
|
611
|
+
if tool_name == "Agent":
|
|
612
|
+
subagent_type = args.get("subagent_type", "")
|
|
613
|
+
prompt = args.get("prompt", "")
|
|
614
|
+
if subagent_type or prompt:
|
|
615
|
+
self._subagent_spawn_count += 1
|
|
616
|
+
elif tool_name == "TaskCreate":
|
|
617
|
+
self._subagent_spawn_count += 1
|
|
618
|
+
|
|
477
619
|
def _record_terminal_result(self, text="", is_error=False, api_error_status=None, api_error_code=""):
|
|
478
620
|
"""Record a Claude Code terminal result event."""
|
|
479
621
|
terminal_text = str(text or "")
|
|
@@ -757,17 +899,85 @@ class ProgressTracker:
|
|
|
757
899
|
return []
|
|
758
900
|
return matches
|
|
759
901
|
|
|
902
|
+
def _cb_projects_dir(self):
|
|
903
|
+
"""Return the CodeBuddy projects directory for transcript lookup.
|
|
904
|
+
|
|
905
|
+
CodeBuddy stores per-project session transcripts and sub-agent data
|
|
906
|
+
under ~/.codebuddy/projects/{projectDir}/{sessionId}/.
|
|
907
|
+
"""
|
|
908
|
+
cb_config = os.environ.get("CODEBUDDY_CONFIG_DIR")
|
|
909
|
+
if cb_config:
|
|
910
|
+
return Path(cb_config).expanduser() / "projects"
|
|
911
|
+
cb_home = os.environ.get("CODEBUDDY_HOME")
|
|
912
|
+
if cb_home:
|
|
913
|
+
return Path(cb_home).expanduser() / "projects"
|
|
914
|
+
return Path.home() / ".codebuddy" / "projects"
|
|
915
|
+
|
|
916
|
+
def _cb_project_key(self):
|
|
917
|
+
"""Encode cwd the same way CodeBuddy stores project subdirs.
|
|
918
|
+
|
|
919
|
+
CodeBuddy uses the same sanitisation as Claude Code (\\, /, : → -)
|
|
920
|
+
but strips the leading '-' so "/Users/test/MyProject" becomes
|
|
921
|
+
"Users-test-MyProject".
|
|
922
|
+
"""
|
|
923
|
+
cwd = self.cb_cwd
|
|
924
|
+
if not cwd:
|
|
925
|
+
return ""
|
|
926
|
+
return cwd.replace("\\", "-").replace("/", "-").replace(":", "").lstrip("-")
|
|
927
|
+
|
|
928
|
+
def _find_cb_child_session_files(self):
|
|
929
|
+
"""Find CodeBuddy subagent transcript data for this parent session.
|
|
930
|
+
|
|
931
|
+
CodeBuddy sub-agents store tool-results/{callId}.txt files; conversation
|
|
932
|
+
transcripts are not (as of 2026) written as agent-*.jsonl files in the
|
|
933
|
+
subagents/ directory. We track the tool-results .txt files as a proxy
|
|
934
|
+
for child activity so the heartbeat monitor can extend the stale-kill
|
|
935
|
+
window while sub-agents are running.
|
|
936
|
+
"""
|
|
937
|
+
if not self.cb_session_id:
|
|
938
|
+
return []
|
|
939
|
+
|
|
940
|
+
projects_dir = self._cb_projects_dir()
|
|
941
|
+
if not projects_dir.exists():
|
|
942
|
+
return []
|
|
943
|
+
|
|
944
|
+
project_key = self._cb_project_key()
|
|
945
|
+
if not project_key:
|
|
946
|
+
return []
|
|
947
|
+
|
|
948
|
+
subagents_dir = (
|
|
949
|
+
projects_dir / project_key / self.cb_session_id / "subagents"
|
|
950
|
+
)
|
|
951
|
+
if not subagents_dir.exists():
|
|
952
|
+
return []
|
|
953
|
+
|
|
954
|
+
# Collect all files under each agent-{id} subdirectory. These are
|
|
955
|
+
# currently tool-results/{callId}.txt files, but the glob also picks
|
|
956
|
+
# up future agent-*.jsonl transcripts should CodeBuddy add them.
|
|
957
|
+
try:
|
|
958
|
+
return sorted(
|
|
959
|
+
p for p in subagents_dir.glob("**/*")
|
|
960
|
+
if p.is_file()
|
|
961
|
+
)
|
|
962
|
+
except OSError:
|
|
963
|
+
return []
|
|
964
|
+
|
|
760
965
|
def refresh_child_session_activity(self, force=False):
|
|
761
966
|
"""Refresh child transcript file stats.
|
|
762
967
|
|
|
763
968
|
The heartbeat monitor uses this activity signature to treat subagent
|
|
764
969
|
transcript growth as real progress while the parent session is blocked
|
|
765
|
-
waiting for a child agent/tool result. Supports Codex child threads
|
|
766
|
-
Claude Code in-process teammate transcripts
|
|
970
|
+
waiting for a child agent/tool result. Supports Codex child threads,
|
|
971
|
+
Claude Code in-process teammate transcripts, and CodeBuddy sub-agent
|
|
972
|
+
file activity under subagents/.
|
|
767
973
|
"""
|
|
768
974
|
previous_signature = self.child_activity_signature
|
|
769
975
|
|
|
770
|
-
if
|
|
976
|
+
if (
|
|
977
|
+
not self.codex_child_thread_ids
|
|
978
|
+
and not self.claude_session_id
|
|
979
|
+
and not self.cb_session_id
|
|
980
|
+
):
|
|
771
981
|
self.child_session_files = []
|
|
772
982
|
self.child_total_bytes = 0
|
|
773
983
|
self.child_activity_signature = ""
|
|
@@ -788,6 +998,7 @@ class ProgressTracker:
|
|
|
788
998
|
if found:
|
|
789
999
|
self._codex_child_session_paths[thread_id] = found
|
|
790
1000
|
self._claude_child_session_files = self._find_claude_child_session_files()
|
|
1001
|
+
self._cb_child_session_files = self._find_cb_child_session_files()
|
|
791
1002
|
self._last_child_scan_at = now
|
|
792
1003
|
|
|
793
1004
|
files = []
|
|
@@ -826,6 +1037,14 @@ class ProgressTracker:
|
|
|
826
1037
|
for path in self._claude_child_session_files:
|
|
827
1038
|
add_file("claude", path.stem, path)
|
|
828
1039
|
|
|
1040
|
+
for path in self._cb_child_session_files:
|
|
1041
|
+
# Identifier for CodeBuddy sub-agent files: use the parent
|
|
1042
|
+
# directory name (agent-{id}) so heartbeat can distinguish
|
|
1043
|
+
# activity across different sub-agent instances.
|
|
1044
|
+
parent_name = path.parent.name if hasattr(path, "parent") else ""
|
|
1045
|
+
identifier = parent_name if parent_name.startswith("agent-") else path.name
|
|
1046
|
+
add_file("codebuddy", identifier, path)
|
|
1047
|
+
|
|
829
1048
|
self.child_session_files = files
|
|
830
1049
|
self.child_total_bytes = total_bytes
|
|
831
1050
|
self.child_activity_signature = "|".join(signature_parts)
|
|
@@ -861,11 +1080,14 @@ class ProgressTracker:
|
|
|
861
1080
|
"total_tool_calls": self.total_tool_calls,
|
|
862
1081
|
"active_subagent_count": self.active_subagent_count,
|
|
863
1082
|
"subagent_states": subagent_states,
|
|
1083
|
+
"subagent_spawn_count": self._subagent_spawn_count,
|
|
864
1084
|
"child_thread_ids": sorted(self.codex_child_thread_ids),
|
|
865
1085
|
"child_session_files": self.child_session_files,
|
|
866
1086
|
"child_total_bytes": self.child_total_bytes,
|
|
867
1087
|
"child_activity_signature": self.child_activity_signature,
|
|
868
1088
|
"last_child_activity_at": self.last_child_activity_at,
|
|
1089
|
+
"cb_session_id": self.cb_session_id,
|
|
1090
|
+
"cb_cwd": self.cb_cwd,
|
|
869
1091
|
"last_text_snippet": self.last_text_snippet,
|
|
870
1092
|
"last_result_is_error": self.last_result_is_error,
|
|
871
1093
|
"api_error_status": self.api_error_status,
|