prizmkit 1.0.6 → 1.0.8
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/README.md +14 -12
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +4 -0
- package/bundled/dev-pipeline/launch-daemon.sh +4 -0
- package/bundled/dev-pipeline/retry-bug.sh +25 -47
- package/bundled/dev-pipeline/retry-feature.sh +25 -47
- package/bundled/dev-pipeline/run-bugfix.sh +22 -48
- package/bundled/dev-pipeline/run.sh +24 -50
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +296 -0
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +7 -1
- package/bundled/skills/dev-pipeline-launcher/SKILL.md +32 -2
- package/bundled/skills/feature-workflow/SKILL.md +17 -12
- package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +22 -17
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -468,15 +468,17 @@ dev-pipeline/bugfix-state/ # Runtime state (gitignored)
|
|
|
468
468
|
└── session.log # Full session output
|
|
469
469
|
```
|
|
470
470
|
|
|
471
|
-
### Differences
|
|
472
|
-
|
|
473
|
-
| Aspect | Feature Pipeline | Bug Fix Pipeline |
|
|
474
|
-
|
|
475
|
-
| Input file | `feature-list.json` | `bug-fix-list.json` |
|
|
476
|
-
| ID format | `F-NNN` | `B-NNN` |
|
|
477
|
-
| State dir | `state/` | `bugfix-state/` |
|
|
478
|
-
| Ordering | Dependencies DAG → priority | Severity → priority (no dependencies) |
|
|
479
|
-
| Phases | 10-phase (specify → plan → tasks → implement → review) | 5-phase (triage → reproduce → fix → verify → commit) |
|
|
480
|
-
| Agents | Coordinator + PM + Dev + Reviewer | Dev + Reviewer only |
|
|
481
|
-
| Artifacts | spec.md, plan.md, tasks.md, REGISTRY.md | fix-plan.md, fix-report.md only |
|
|
482
|
-
| Commit prefix | `feat(<scope>):` | `fix(<scope>):` |
|
|
471
|
+
### Differences Between Pipelines
|
|
472
|
+
|
|
473
|
+
| Aspect | Feature Pipeline | Refactor Workflow | Bug Fix Pipeline |
|
|
474
|
+
|--------|-----------------|-------------------|------------------|
|
|
475
|
+
| Input file | `feature-list.json` | N/A (conversation trigger) | `bug-fix-list.json` |
|
|
476
|
+
| ID format | `F-NNN` | `<refactor-slug>` | `B-NNN` |
|
|
477
|
+
| State dir | `state/` | N/A (in-session) | `bugfix-state/` |
|
|
478
|
+
| Ordering | Dependencies DAG → priority | N/A (single refactor per session) | Severity → priority (no dependencies) |
|
|
479
|
+
| Phases | 10-phase (specify → plan → tasks → implement → review) | 6-phase (analyze → plan → tasks → implement → review → commit) | 5-phase (triage → reproduce → fix → verify → commit) |
|
|
480
|
+
| Agents | Coordinator + PM + Dev + Reviewer | Dev + Reviewer only | Dev + Reviewer only |
|
|
481
|
+
| Artifacts | spec.md, plan.md, tasks.md, REGISTRY.md | refactor-analysis.md, plan.md, tasks.md | fix-plan.md, fix-report.md only |
|
|
482
|
+
| Commit prefix | `feat(<scope>):` | `refactor(<scope>):` | `fix(<scope>):` |
|
|
483
|
+
| Scope Guard | N/A | ✅ (behavior change → STOP) | N/A |
|
|
484
|
+
| Test Strategy | TDD per task | Full suite after EVERY task | Reproduction test |
|
|
@@ -14,6 +14,10 @@ set -euo pipefail
|
|
|
14
14
|
# ./launch-bugfix-daemon.sh logs [--lines N] [--follow]
|
|
15
15
|
# ./launch-bugfix-daemon.sh restart [bug-fix-list.json] [--env "KEY=VAL ..."]
|
|
16
16
|
#
|
|
17
|
+
# NOTE:
|
|
18
|
+
# In AI skill sessions, always use this daemon wrapper.
|
|
19
|
+
# Do NOT call `run-bugfix.sh run ...` directly, because foreground sessions may be killed by CLI timeout.
|
|
20
|
+
#
|
|
17
21
|
# Files managed:
|
|
18
22
|
# bugfix-state/.pipeline.pid - PID of the background run-bugfix.sh process
|
|
19
23
|
# bugfix-state/pipeline-daemon.log - Consolidated stdout+stderr
|
|
@@ -14,6 +14,10 @@ set -euo pipefail
|
|
|
14
14
|
# ./launch-daemon.sh logs [--lines N] [--follow]
|
|
15
15
|
# ./launch-daemon.sh restart [feature-list.json] [--env "KEY=VAL ..."]
|
|
16
16
|
#
|
|
17
|
+
# NOTE:
|
|
18
|
+
# In AI skill sessions, always use this daemon wrapper.
|
|
19
|
+
# Do NOT call `run.sh run ...` directly, because foreground sessions may be killed by CLI timeout.
|
|
20
|
+
#
|
|
17
21
|
# Files managed:
|
|
18
22
|
# state/.pipeline.pid - PID of the background run.sh process
|
|
19
23
|
# state/pipeline-daemon.log - Consolidated stdout+stderr from run.sh
|
|
@@ -48,6 +48,12 @@ else
|
|
|
48
48
|
fi
|
|
49
49
|
export PRIZMKIT_PLATFORM="$PLATFORM"
|
|
50
50
|
|
|
51
|
+
# Source shared heartbeat library
|
|
52
|
+
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
53
|
+
|
|
54
|
+
# Detect stream-json support
|
|
55
|
+
detect_stream_json_support "$CLI_CMD"
|
|
56
|
+
|
|
51
57
|
# Colors
|
|
52
58
|
RED='\033[0;31m'
|
|
53
59
|
GREEN='\033[0;32m'
|
|
@@ -194,6 +200,13 @@ echo -e "${BOLD}═════════════════════
|
|
|
194
200
|
echo ""
|
|
195
201
|
|
|
196
202
|
SESSION_LOG="$SESSION_DIR/logs/session.log"
|
|
203
|
+
PROGRESS_JSON="$SESSION_DIR/logs/progress.json"
|
|
204
|
+
|
|
205
|
+
# Build stream-json flag
|
|
206
|
+
STREAM_JSON_FLAG=""
|
|
207
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
208
|
+
STREAM_JSON_FLAG="--output-format stream-json"
|
|
209
|
+
fi
|
|
197
210
|
|
|
198
211
|
# Spawn AI CLI session
|
|
199
212
|
case "$CLI_CMD" in
|
|
@@ -202,18 +215,24 @@ case "$CLI_CMD" in
|
|
|
202
215
|
--print \
|
|
203
216
|
-p "$(cat "$BOOTSTRAP_PROMPT")" \
|
|
204
217
|
--yes \
|
|
218
|
+
$STREAM_JSON_FLAG \
|
|
205
219
|
> "$SESSION_LOG" 2>&1 &
|
|
206
220
|
;;
|
|
207
221
|
*)
|
|
208
222
|
"$CLI_CMD" \
|
|
209
223
|
--print \
|
|
210
224
|
-y \
|
|
225
|
+
$STREAM_JSON_FLAG \
|
|
211
226
|
< "$BOOTSTRAP_PROMPT" \
|
|
212
227
|
> "$SESSION_LOG" 2>&1 &
|
|
213
228
|
;;
|
|
214
229
|
esac
|
|
215
230
|
CLI_PID=$!
|
|
216
231
|
|
|
232
|
+
# Start progress parser (no-op if stream-json not supported)
|
|
233
|
+
start_progress_parser "$SESSION_LOG" "$PROGRESS_JSON" "$SCRIPTS_DIR"
|
|
234
|
+
PARSER_PID="${_PARSER_PID:-}"
|
|
235
|
+
|
|
217
236
|
# Timeout watchdog
|
|
218
237
|
WATCHER_PID=""
|
|
219
238
|
if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
@@ -222,49 +241,8 @@ if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
|
222
241
|
fi
|
|
223
242
|
|
|
224
243
|
# Heartbeat
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
prev_size=0
|
|
228
|
-
while kill -0 "$CLI_PID" 2>/dev/null; do
|
|
229
|
-
sleep "$HEARTBEAT_INTERVAL"
|
|
230
|
-
elapsed=$((elapsed + HEARTBEAT_INTERVAL))
|
|
231
|
-
kill -0 "$CLI_PID" 2>/dev/null || break
|
|
232
|
-
|
|
233
|
-
cur_size=0
|
|
234
|
-
if [[ -f "$SESSION_LOG" ]]; then
|
|
235
|
-
cur_size=$(wc -c < "$SESSION_LOG" 2>/dev/null || echo 0)
|
|
236
|
-
cur_size=$(echo "$cur_size" | tr -d ' ')
|
|
237
|
-
fi
|
|
238
|
-
|
|
239
|
-
growth=$((cur_size - prev_size))
|
|
240
|
-
prev_size=$cur_size
|
|
241
|
-
|
|
242
|
-
if [[ $cur_size -gt 1048576 ]]; then
|
|
243
|
-
size_display="$((cur_size / 1048576))MB"
|
|
244
|
-
elif [[ $cur_size -gt 1024 ]]; then
|
|
245
|
-
size_display="$((cur_size / 1024))KB"
|
|
246
|
-
else
|
|
247
|
-
size_display="${cur_size}B"
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
|
-
mins=$((elapsed / 60))
|
|
251
|
-
secs=$((elapsed % 60))
|
|
252
|
-
|
|
253
|
-
last_activity=""
|
|
254
|
-
if [[ -f "$SESSION_LOG" ]]; then
|
|
255
|
-
last_activity=$(tail -20 "$SESSION_LOG" 2>/dev/null | grep -v '^$' | tail -1 | cut -c1-80 || echo "")
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
if [[ $growth -gt 0 ]]; then
|
|
259
|
-
icon="${GREEN}▶${NC}"
|
|
260
|
-
else
|
|
261
|
-
icon="${YELLOW}⏸${NC}"
|
|
262
|
-
fi
|
|
263
|
-
|
|
264
|
-
echo -e " ${icon} ${BLUE}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} (+${growth}B) | ${last_activity}"
|
|
265
|
-
done
|
|
266
|
-
) &
|
|
267
|
-
HEARTBEAT_PID=$!
|
|
244
|
+
start_heartbeat "$CLI_PID" "$SESSION_LOG" "$PROGRESS_JSON" "$HEARTBEAT_INTERVAL"
|
|
245
|
+
HEARTBEAT_PID="${_HEARTBEAT_PID:-}"
|
|
268
246
|
|
|
269
247
|
# Ctrl+C cleanup
|
|
270
248
|
cleanup() {
|
|
@@ -272,10 +250,10 @@ cleanup() {
|
|
|
272
250
|
log_warn "Interrupted. Killing session..."
|
|
273
251
|
kill "$CLI_PID" 2>/dev/null || true
|
|
274
252
|
[[ -n "$WATCHER_PID" ]] && kill "$WATCHER_PID" 2>/dev/null || true
|
|
275
|
-
|
|
253
|
+
stop_heartbeat "$HEARTBEAT_PID"
|
|
254
|
+
stop_progress_parser "$PARSER_PID"
|
|
276
255
|
wait "$CLI_PID" 2>/dev/null || true
|
|
277
256
|
[[ -n "$WATCHER_PID" ]] && wait "$WATCHER_PID" 2>/dev/null || true
|
|
278
|
-
wait "$HEARTBEAT_PID" 2>/dev/null || true
|
|
279
257
|
log_info "Session log: $SESSION_LOG"
|
|
280
258
|
exit 130
|
|
281
259
|
}
|
|
@@ -291,9 +269,9 @@ fi
|
|
|
291
269
|
|
|
292
270
|
# Cleanup background processes
|
|
293
271
|
[[ -n "$WATCHER_PID" ]] && kill "$WATCHER_PID" 2>/dev/null || true
|
|
294
|
-
|
|
272
|
+
stop_heartbeat "$HEARTBEAT_PID"
|
|
273
|
+
stop_progress_parser "$PARSER_PID"
|
|
295
274
|
[[ -n "$WATCHER_PID" ]] && wait "$WATCHER_PID" 2>/dev/null || true
|
|
296
|
-
wait "$HEARTBEAT_PID" 2>/dev/null || true
|
|
297
275
|
|
|
298
276
|
[[ $EXIT_CODE -eq 143 ]] && EXIT_CODE=124
|
|
299
277
|
|
|
@@ -48,6 +48,12 @@ else
|
|
|
48
48
|
fi
|
|
49
49
|
export PRIZMKIT_PLATFORM="$PLATFORM"
|
|
50
50
|
|
|
51
|
+
# Source shared heartbeat library
|
|
52
|
+
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
53
|
+
|
|
54
|
+
# Detect stream-json support
|
|
55
|
+
detect_stream_json_support "$CLI_CMD"
|
|
56
|
+
|
|
51
57
|
# Colors
|
|
52
58
|
RED='\033[0;31m'
|
|
53
59
|
GREEN='\033[0;32m'
|
|
@@ -188,6 +194,13 @@ echo -e "${BOLD}═════════════════════
|
|
|
188
194
|
echo ""
|
|
189
195
|
|
|
190
196
|
SESSION_LOG="$SESSION_DIR/logs/session.log"
|
|
197
|
+
PROGRESS_JSON="$SESSION_DIR/logs/progress.json"
|
|
198
|
+
|
|
199
|
+
# Build stream-json flag
|
|
200
|
+
STREAM_JSON_FLAG=""
|
|
201
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
202
|
+
STREAM_JSON_FLAG="--output-format stream-json"
|
|
203
|
+
fi
|
|
191
204
|
|
|
192
205
|
# Spawn AI CLI session
|
|
193
206
|
case "$CLI_CMD" in
|
|
@@ -196,18 +209,24 @@ case "$CLI_CMD" in
|
|
|
196
209
|
--print \
|
|
197
210
|
-p "$(cat "$BOOTSTRAP_PROMPT")" \
|
|
198
211
|
--yes \
|
|
212
|
+
$STREAM_JSON_FLAG \
|
|
199
213
|
> "$SESSION_LOG" 2>&1 &
|
|
200
214
|
;;
|
|
201
215
|
*)
|
|
202
216
|
"$CLI_CMD" \
|
|
203
217
|
--print \
|
|
204
218
|
-y \
|
|
219
|
+
$STREAM_JSON_FLAG \
|
|
205
220
|
< "$BOOTSTRAP_PROMPT" \
|
|
206
221
|
> "$SESSION_LOG" 2>&1 &
|
|
207
222
|
;;
|
|
208
223
|
esac
|
|
209
224
|
CBC_PID=$!
|
|
210
225
|
|
|
226
|
+
# Start progress parser (no-op if stream-json not supported)
|
|
227
|
+
start_progress_parser "$SESSION_LOG" "$PROGRESS_JSON" "$SCRIPTS_DIR"
|
|
228
|
+
PARSER_PID="${_PARSER_PID:-}"
|
|
229
|
+
|
|
211
230
|
# Timeout watchdog (only if SESSION_TIMEOUT > 0)
|
|
212
231
|
WATCHER_PID=""
|
|
213
232
|
if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
@@ -216,49 +235,8 @@ if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
|
216
235
|
fi
|
|
217
236
|
|
|
218
237
|
# Heartbeat
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
prev_size=0
|
|
222
|
-
while kill -0 "$CBC_PID" 2>/dev/null; do
|
|
223
|
-
sleep "$HEARTBEAT_INTERVAL"
|
|
224
|
-
elapsed=$((elapsed + HEARTBEAT_INTERVAL))
|
|
225
|
-
kill -0 "$CBC_PID" 2>/dev/null || break
|
|
226
|
-
|
|
227
|
-
cur_size=0
|
|
228
|
-
if [[ -f "$SESSION_LOG" ]]; then
|
|
229
|
-
cur_size=$(wc -c < "$SESSION_LOG" 2>/dev/null || echo 0)
|
|
230
|
-
cur_size=$(echo "$cur_size" | tr -d ' ')
|
|
231
|
-
fi
|
|
232
|
-
|
|
233
|
-
growth=$((cur_size - prev_size))
|
|
234
|
-
prev_size=$cur_size
|
|
235
|
-
|
|
236
|
-
if [[ $cur_size -gt 1048576 ]]; then
|
|
237
|
-
size_display="$((cur_size / 1048576))MB"
|
|
238
|
-
elif [[ $cur_size -gt 1024 ]]; then
|
|
239
|
-
size_display="$((cur_size / 1024))KB"
|
|
240
|
-
else
|
|
241
|
-
size_display="${cur_size}B"
|
|
242
|
-
fi
|
|
243
|
-
|
|
244
|
-
mins=$((elapsed / 60))
|
|
245
|
-
secs=$((elapsed % 60))
|
|
246
|
-
|
|
247
|
-
last_activity=""
|
|
248
|
-
if [[ -f "$SESSION_LOG" ]]; then
|
|
249
|
-
last_activity=$(tail -20 "$SESSION_LOG" 2>/dev/null | grep -v '^$' | tail -1 | cut -c1-80 || echo "")
|
|
250
|
-
fi
|
|
251
|
-
|
|
252
|
-
if [[ $growth -gt 0 ]]; then
|
|
253
|
-
icon="${GREEN}▶${NC}"
|
|
254
|
-
else
|
|
255
|
-
icon="${YELLOW}⏸${NC}"
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
echo -e " ${icon} ${BLUE}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} (+${growth}B) | ${last_activity}"
|
|
259
|
-
done
|
|
260
|
-
) &
|
|
261
|
-
HEARTBEAT_PID=$!
|
|
238
|
+
start_heartbeat "$CBC_PID" "$SESSION_LOG" "$PROGRESS_JSON" "$HEARTBEAT_INTERVAL"
|
|
239
|
+
HEARTBEAT_PID="${_HEARTBEAT_PID:-}"
|
|
262
240
|
|
|
263
241
|
# Ctrl+C cleanup
|
|
264
242
|
cleanup() {
|
|
@@ -266,10 +244,10 @@ cleanup() {
|
|
|
266
244
|
log_warn "Interrupted. Killing session..."
|
|
267
245
|
kill "$CBC_PID" 2>/dev/null || true
|
|
268
246
|
[[ -n "$WATCHER_PID" ]] && kill "$WATCHER_PID" 2>/dev/null || true
|
|
269
|
-
|
|
247
|
+
stop_heartbeat "$HEARTBEAT_PID"
|
|
248
|
+
stop_progress_parser "$PARSER_PID"
|
|
270
249
|
wait "$CBC_PID" 2>/dev/null || true
|
|
271
250
|
[[ -n "$WATCHER_PID" ]] && wait "$WATCHER_PID" 2>/dev/null || true
|
|
272
|
-
wait "$HEARTBEAT_PID" 2>/dev/null || true
|
|
273
251
|
log_info "Session log: $SESSION_LOG"
|
|
274
252
|
exit 130
|
|
275
253
|
}
|
|
@@ -285,9 +263,9 @@ fi
|
|
|
285
263
|
|
|
286
264
|
# Cleanup background processes
|
|
287
265
|
[[ -n "$WATCHER_PID" ]] && kill "$WATCHER_PID" 2>/dev/null || true
|
|
288
|
-
|
|
266
|
+
stop_heartbeat "$HEARTBEAT_PID"
|
|
267
|
+
stop_progress_parser "$PARSER_PID"
|
|
289
268
|
[[ -n "$WATCHER_PID" ]] && wait "$WATCHER_PID" 2>/dev/null || true
|
|
290
|
-
wait "$HEARTBEAT_PID" 2>/dev/null || true
|
|
291
269
|
|
|
292
270
|
[[ $EXIT_CODE -eq 143 ]] && EXIT_CODE=124
|
|
293
271
|
|
|
@@ -57,6 +57,12 @@ else
|
|
|
57
57
|
fi
|
|
58
58
|
export PRIZMKIT_PLATFORM="$PLATFORM"
|
|
59
59
|
|
|
60
|
+
# Source shared heartbeat library
|
|
61
|
+
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
62
|
+
|
|
63
|
+
# Detect stream-json support
|
|
64
|
+
detect_stream_json_support "$CLI_CMD"
|
|
65
|
+
|
|
60
66
|
# Bug list path (set in main, used by cleanup trap)
|
|
61
67
|
BUG_LIST=""
|
|
62
68
|
|
|
@@ -87,12 +93,18 @@ spawn_and_wait_session() {
|
|
|
87
93
|
local max_retries="$6"
|
|
88
94
|
|
|
89
95
|
local session_log="$session_dir/logs/session.log"
|
|
96
|
+
local progress_json="$session_dir/logs/progress.json"
|
|
90
97
|
|
|
91
98
|
local verbose_flag=""
|
|
92
99
|
if [[ "$VERBOSE" == "1" ]]; then
|
|
93
100
|
verbose_flag="--verbose"
|
|
94
101
|
fi
|
|
95
102
|
|
|
103
|
+
local stream_json_flag=""
|
|
104
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
105
|
+
stream_json_flag="--output-format stream-json"
|
|
106
|
+
fi
|
|
107
|
+
|
|
96
108
|
case "$CLI_CMD" in
|
|
97
109
|
*claude*)
|
|
98
110
|
"$CLI_CMD" \
|
|
@@ -100,6 +112,7 @@ spawn_and_wait_session() {
|
|
|
100
112
|
-p "$(cat "$bootstrap_prompt")" \
|
|
101
113
|
--yes \
|
|
102
114
|
$verbose_flag \
|
|
115
|
+
$stream_json_flag \
|
|
103
116
|
> "$session_log" 2>&1 &
|
|
104
117
|
;;
|
|
105
118
|
*)
|
|
@@ -107,12 +120,17 @@ spawn_and_wait_session() {
|
|
|
107
120
|
--print \
|
|
108
121
|
-y \
|
|
109
122
|
$verbose_flag \
|
|
123
|
+
$stream_json_flag \
|
|
110
124
|
< "$bootstrap_prompt" \
|
|
111
125
|
> "$session_log" 2>&1 &
|
|
112
126
|
;;
|
|
113
127
|
esac
|
|
114
128
|
local cli_pid=$!
|
|
115
129
|
|
|
130
|
+
# Start progress parser (no-op if stream-json not supported)
|
|
131
|
+
start_progress_parser "$session_log" "$progress_json" "$SCRIPTS_DIR"
|
|
132
|
+
local parser_pid="${_PARSER_PID:-}"
|
|
133
|
+
|
|
116
134
|
# Timeout watchdog
|
|
117
135
|
local watcher_pid=""
|
|
118
136
|
if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
@@ -121,52 +139,8 @@ spawn_and_wait_session() {
|
|
|
121
139
|
fi
|
|
122
140
|
|
|
123
141
|
# Heartbeat monitor
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
local elapsed=0
|
|
127
|
-
local prev_size=0
|
|
128
|
-
while kill -0 "$cli_pid" 2>/dev/null; do
|
|
129
|
-
sleep "$heartbeat_interval"
|
|
130
|
-
elapsed=$((elapsed + heartbeat_interval))
|
|
131
|
-
kill -0 "$cli_pid" 2>/dev/null || break
|
|
132
|
-
|
|
133
|
-
local cur_size=0
|
|
134
|
-
if [[ -f "$session_log" ]]; then
|
|
135
|
-
cur_size=$(wc -c < "$session_log" 2>/dev/null || echo 0)
|
|
136
|
-
cur_size=$(echo "$cur_size" | tr -d ' ')
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
local growth=$((cur_size - prev_size))
|
|
140
|
-
prev_size=$cur_size
|
|
141
|
-
|
|
142
|
-
local size_display
|
|
143
|
-
if [[ $cur_size -gt 1048576 ]]; then
|
|
144
|
-
size_display="$((cur_size / 1048576))MB"
|
|
145
|
-
elif [[ $cur_size -gt 1024 ]]; then
|
|
146
|
-
size_display="$((cur_size / 1024))KB"
|
|
147
|
-
else
|
|
148
|
-
size_display="${cur_size}B"
|
|
149
|
-
fi
|
|
150
|
-
|
|
151
|
-
local mins=$((elapsed / 60))
|
|
152
|
-
local secs=$((elapsed % 60))
|
|
153
|
-
|
|
154
|
-
local last_activity=""
|
|
155
|
-
if [[ -f "$session_log" ]]; then
|
|
156
|
-
last_activity=$(tail -20 "$session_log" 2>/dev/null | grep -v '^$' | tail -1 | cut -c1-80 || echo "")
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
|
-
local status_icon
|
|
160
|
-
if [[ $growth -gt 0 ]]; then
|
|
161
|
-
status_icon="${GREEN}▶${NC}"
|
|
162
|
-
else
|
|
163
|
-
status_icon="${YELLOW}⏸${NC}"
|
|
164
|
-
fi
|
|
165
|
-
|
|
166
|
-
echo -e " ${status_icon} ${BLUE}[HEARTBEAT]${NC} ${mins}m${secs}s elapsed | log: ${size_display} (+${growth}B) | ${last_activity}"
|
|
167
|
-
done
|
|
168
|
-
) &
|
|
169
|
-
local heartbeat_pid=$!
|
|
142
|
+
start_heartbeat "$cli_pid" "$session_log" "$progress_json" "$HEARTBEAT_INTERVAL"
|
|
143
|
+
local heartbeat_pid="${_HEARTBEAT_PID:-}"
|
|
170
144
|
|
|
171
145
|
# Wait for AI CLI to finish
|
|
172
146
|
local exit_code=0
|
|
@@ -178,9 +152,9 @@ spawn_and_wait_session() {
|
|
|
178
152
|
|
|
179
153
|
# Cleanup
|
|
180
154
|
[[ -n "$watcher_pid" ]] && kill "$watcher_pid" 2>/dev/null || true
|
|
181
|
-
|
|
155
|
+
stop_heartbeat "$heartbeat_pid"
|
|
156
|
+
stop_progress_parser "$parser_pid"
|
|
182
157
|
[[ -n "$watcher_pid" ]] && wait "$watcher_pid" 2>/dev/null || true
|
|
183
|
-
wait "$heartbeat_pid" 2>/dev/null || true
|
|
184
158
|
|
|
185
159
|
[[ $exit_code -eq 143 ]] && exit_code=124
|
|
186
160
|
|
|
@@ -61,6 +61,12 @@ fi
|
|
|
61
61
|
|
|
62
62
|
export PRIZMKIT_PLATFORM="$PLATFORM"
|
|
63
63
|
|
|
64
|
+
# Source shared heartbeat library
|
|
65
|
+
source "$SCRIPT_DIR/lib/heartbeat.sh"
|
|
66
|
+
|
|
67
|
+
# Detect stream-json support
|
|
68
|
+
detect_stream_json_support "$CLI_CMD"
|
|
69
|
+
|
|
64
70
|
# Feature list path (set in main, used by cleanup trap)
|
|
65
71
|
FEATURE_LIST=""
|
|
66
72
|
|
|
@@ -100,6 +106,7 @@ spawn_and_wait_session() {
|
|
|
100
106
|
local max_retries="$6"
|
|
101
107
|
|
|
102
108
|
local session_log="$session_dir/logs/session.log"
|
|
109
|
+
local progress_json="$session_dir/logs/progress.json"
|
|
103
110
|
|
|
104
111
|
# Spawn AI CLI session
|
|
105
112
|
local verbose_flag=""
|
|
@@ -107,6 +114,11 @@ spawn_and_wait_session() {
|
|
|
107
114
|
verbose_flag="--verbose"
|
|
108
115
|
fi
|
|
109
116
|
|
|
117
|
+
local stream_json_flag=""
|
|
118
|
+
if [[ "$USE_STREAM_JSON" == "true" ]]; then
|
|
119
|
+
stream_json_flag="--output-format stream-json"
|
|
120
|
+
fi
|
|
121
|
+
|
|
110
122
|
case "$CLI_CMD" in
|
|
111
123
|
*claude*)
|
|
112
124
|
# Claude Code: prompt via -p argument, --yes for auto-accept
|
|
@@ -115,6 +127,7 @@ spawn_and_wait_session() {
|
|
|
115
127
|
-p "$(cat "$bootstrap_prompt")" \
|
|
116
128
|
--yes \
|
|
117
129
|
$verbose_flag \
|
|
130
|
+
$stream_json_flag \
|
|
118
131
|
> "$session_log" 2>&1 &
|
|
119
132
|
;;
|
|
120
133
|
*)
|
|
@@ -123,12 +136,17 @@ spawn_and_wait_session() {
|
|
|
123
136
|
--print \
|
|
124
137
|
-y \
|
|
125
138
|
$verbose_flag \
|
|
139
|
+
$stream_json_flag \
|
|
126
140
|
< "$bootstrap_prompt" \
|
|
127
141
|
> "$session_log" 2>&1 &
|
|
128
142
|
;;
|
|
129
143
|
esac
|
|
130
144
|
local cbc_pid=$!
|
|
131
145
|
|
|
146
|
+
# Start progress parser (no-op if stream-json not supported)
|
|
147
|
+
start_progress_parser "$session_log" "$progress_json" "$SCRIPTS_DIR"
|
|
148
|
+
local parser_pid="${_PARSER_PID:-}"
|
|
149
|
+
|
|
132
150
|
# Timeout watchdog (only if SESSION_TIMEOUT > 0)
|
|
133
151
|
local watcher_pid=""
|
|
134
152
|
if [[ $SESSION_TIMEOUT -gt 0 ]]; then
|
|
@@ -136,53 +154,9 @@ spawn_and_wait_session() {
|
|
|
136
154
|
watcher_pid=$!
|
|
137
155
|
fi
|
|
138
156
|
|
|
139
|
-
# Heartbeat monitor
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
local elapsed=0
|
|
143
|
-
local prev_size=0
|
|
144
|
-
while kill -0 "$cbc_pid" 2>/dev/null; do
|
|
145
|
-
sleep "$heartbeat_interval"
|
|
146
|
-
elapsed=$((elapsed + heartbeat_interval))
|
|
147
|
-
kill -0 "$cbc_pid" 2>/dev/null || break
|
|
148
|
-
|
|
149
|
-
local cur_size=0
|
|
150
|
-
if [[ -f "$session_log" ]]; then
|
|
151
|
-
cur_size=$(wc -c < "$session_log" 2>/dev/null || echo 0)
|
|
152
|
-
cur_size=$(echo "$cur_size" | tr -d ' ')
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
local growth=$((cur_size - prev_size))
|
|
156
|
-
prev_size=$cur_size
|
|
157
|
-
|
|
158
|
-
local size_display
|
|
159
|
-
if [[ $cur_size -gt 1048576 ]]; then
|
|
160
|
-
size_display="$((cur_size / 1048576))MB"
|
|
161
|
-
elif [[ $cur_size -gt 1024 ]]; then
|
|
162
|
-
size_display="$((cur_size / 1024))KB"
|
|
163
|
-
else
|
|
164
|
-
size_display="${cur_size}B"
|
|
165
|
-
fi
|
|
166
|
-
|
|
167
|
-
local mins=$((elapsed / 60))
|
|
168
|
-
local secs=$((elapsed % 60))
|
|
169
|
-
|
|
170
|
-
local last_activity=""
|
|
171
|
-
if [[ -f "$session_log" ]]; then
|
|
172
|
-
last_activity=$(tail -20 "$session_log" 2>/dev/null | grep -v '^$' | tail -1 | cut -c1-80 || echo "")
|
|
173
|
-
fi
|
|
174
|
-
|
|
175
|
-
local status_icon
|
|
176
|
-
if [[ $growth -gt 0 ]]; then
|
|
177
|
-
status_icon="${GREEN}▶${NC}"
|
|
178
|
-
else
|
|
179
|
-
status_icon="${YELLOW}⏸${NC}"
|
|
180
|
-
fi
|
|
181
|
-
|
|
182
|
-
echo -e " ${status_icon} ${BLUE}[HEARTBEAT]${NC} ${mins}m${secs}s elapsed | log: ${size_display} (+${growth}B) | ${last_activity}"
|
|
183
|
-
done
|
|
184
|
-
) &
|
|
185
|
-
local heartbeat_pid=$!
|
|
157
|
+
# Heartbeat monitor (reads progress.json when available, falls back to tail)
|
|
158
|
+
start_heartbeat "$cbc_pid" "$session_log" "$progress_json" "$HEARTBEAT_INTERVAL"
|
|
159
|
+
local heartbeat_pid="${_HEARTBEAT_PID:-}"
|
|
186
160
|
|
|
187
161
|
# Wait for AI CLI to finish
|
|
188
162
|
local exit_code=0
|
|
@@ -192,11 +166,11 @@ spawn_and_wait_session() {
|
|
|
192
166
|
exit_code=$?
|
|
193
167
|
fi
|
|
194
168
|
|
|
195
|
-
# Clean up watcher and
|
|
169
|
+
# Clean up watcher, heartbeat, and parser
|
|
196
170
|
[[ -n "$watcher_pid" ]] && kill "$watcher_pid" 2>/dev/null || true
|
|
197
|
-
|
|
171
|
+
stop_heartbeat "$heartbeat_pid"
|
|
172
|
+
stop_progress_parser "$parser_pid"
|
|
198
173
|
[[ -n "$watcher_pid" ]] && wait "$watcher_pid" 2>/dev/null || true
|
|
199
|
-
wait "$heartbeat_pid" 2>/dev/null || true
|
|
200
174
|
|
|
201
175
|
# Map SIGTERM (143) to timeout code 124
|
|
202
176
|
if [[ $exit_code -eq 143 ]]; then
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
parse-stream-progress.py - Real-time stream-json progress parser
|
|
4
|
+
|
|
5
|
+
Continuously reads an AI CLI session log (JSONL from --output-format stream-json),
|
|
6
|
+
extracts tool calls, phase changes, and activity metrics, and writes structured
|
|
7
|
+
progress to a progress.json file for heartbeat monitoring.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 parse-stream-progress.py --session-log <path> --progress-file <path>
|
|
11
|
+
|
|
12
|
+
The script runs until:
|
|
13
|
+
- The session log stops growing and the CLI process exits
|
|
14
|
+
- It receives SIGTERM/SIGINT
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import signal
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import time
|
|
24
|
+
from collections import Counter
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Phase keywords for detection
|
|
29
|
+
PHASE_KEYWORDS = {
|
|
30
|
+
"specify": ["prizmkit-specify", "spec.md", "specification", "gathering requirements"],
|
|
31
|
+
"plan": ["prizmkit-plan", "plan.md", "architecture", "design plan"],
|
|
32
|
+
"tasks": ["prizmkit-tasks", "tasks.md", "task checklist", "task breakdown"],
|
|
33
|
+
"analyze": ["prizmkit-analyze", "cross-check", "consistency", "analyzing"],
|
|
34
|
+
"implement": ["prizmkit-implement", "implement", "TDD", "coding", "writing code"],
|
|
35
|
+
"code-review": ["prizmkit-code-review", "code review", "review verdict", "reviewing"],
|
|
36
|
+
"summarize": ["prizmkit-summarize", "REGISTRY.md", "summarize", "summary"],
|
|
37
|
+
"commit": ["prizmkit-committer", "git commit", "feat(", "fix(", "committing"],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProgressTracker:
|
|
42
|
+
"""Tracks progress state from stream-json events."""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.message_count = 0
|
|
46
|
+
self.current_tool = None
|
|
47
|
+
self.current_tool_input_summary = ""
|
|
48
|
+
self.current_phase = None
|
|
49
|
+
self.detected_phases = []
|
|
50
|
+
self.tool_call_counts = Counter()
|
|
51
|
+
self.total_tool_calls = 0
|
|
52
|
+
self.last_text_snippet = ""
|
|
53
|
+
self.is_active = True
|
|
54
|
+
self.errors = []
|
|
55
|
+
self._text_buffer = ""
|
|
56
|
+
self._in_tool_use = False
|
|
57
|
+
self._current_tool_input_parts = []
|
|
58
|
+
|
|
59
|
+
def process_event(self, event):
|
|
60
|
+
"""Process a single stream-json event and update state."""
|
|
61
|
+
event_type = event.get("type", "")
|
|
62
|
+
|
|
63
|
+
if event_type == "message_start":
|
|
64
|
+
self.message_count += 1
|
|
65
|
+
self.is_active = True
|
|
66
|
+
|
|
67
|
+
elif event_type == "message_stop":
|
|
68
|
+
self.current_tool = None
|
|
69
|
+
self.current_tool_input_summary = ""
|
|
70
|
+
self._in_tool_use = False
|
|
71
|
+
self._current_tool_input_parts = []
|
|
72
|
+
|
|
73
|
+
elif event_type == "content_block_start":
|
|
74
|
+
content_block = event.get("content_block", {})
|
|
75
|
+
block_type = content_block.get("type", "")
|
|
76
|
+
|
|
77
|
+
if block_type == "tool_use":
|
|
78
|
+
tool_name = content_block.get("name", "unknown")
|
|
79
|
+
self.current_tool = tool_name
|
|
80
|
+
self.current_tool_input_summary = ""
|
|
81
|
+
self.tool_call_counts[tool_name] += 1
|
|
82
|
+
self.total_tool_calls += 1
|
|
83
|
+
self._in_tool_use = True
|
|
84
|
+
self._current_tool_input_parts = []
|
|
85
|
+
|
|
86
|
+
elif block_type == "text":
|
|
87
|
+
self._text_buffer = ""
|
|
88
|
+
self._in_tool_use = False
|
|
89
|
+
|
|
90
|
+
elif event_type == "content_block_delta":
|
|
91
|
+
delta = event.get("delta", {})
|
|
92
|
+
delta_type = delta.get("type", "")
|
|
93
|
+
|
|
94
|
+
if delta_type == "text_delta":
|
|
95
|
+
text = delta.get("text", "")
|
|
96
|
+
self._text_buffer += text
|
|
97
|
+
# Keep last meaningful snippet
|
|
98
|
+
stripped = text.strip()
|
|
99
|
+
if stripped:
|
|
100
|
+
self.last_text_snippet = stripped[:120]
|
|
101
|
+
# Try to detect phase from text
|
|
102
|
+
self._detect_phase(text)
|
|
103
|
+
|
|
104
|
+
elif delta_type == "input_json_delta":
|
|
105
|
+
partial = delta.get("partial_json", "")
|
|
106
|
+
self._current_tool_input_parts.append(partial)
|
|
107
|
+
# Build a summary from accumulated input
|
|
108
|
+
accumulated = "".join(self._current_tool_input_parts)
|
|
109
|
+
self.current_tool_input_summary = accumulated[:150]
|
|
110
|
+
|
|
111
|
+
elif event_type == "content_block_stop":
|
|
112
|
+
if self._in_tool_use:
|
|
113
|
+
# Try to extract a better summary from complete tool input
|
|
114
|
+
full_input = "".join(self._current_tool_input_parts)
|
|
115
|
+
self._extract_tool_summary(full_input)
|
|
116
|
+
self._detect_phase(full_input)
|
|
117
|
+
else:
|
|
118
|
+
# Text block finished - detect phase from accumulated text
|
|
119
|
+
if self._text_buffer:
|
|
120
|
+
self._detect_phase(self._text_buffer)
|
|
121
|
+
self._in_tool_use = False
|
|
122
|
+
self._current_tool_input_parts = []
|
|
123
|
+
|
|
124
|
+
elif event_type == "error":
|
|
125
|
+
error_msg = event.get("error", {}).get("message", "Unknown error")
|
|
126
|
+
self.errors.append(error_msg)
|
|
127
|
+
|
|
128
|
+
# Check for subagent indicator
|
|
129
|
+
if event.get("parent_tool_use_id"):
|
|
130
|
+
# This is a sub-agent event; tool name is still tracked normally
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def _detect_phase(self, text):
|
|
134
|
+
"""Detect pipeline phase from text content."""
|
|
135
|
+
text_lower = text.lower()
|
|
136
|
+
for phase, keywords in PHASE_KEYWORDS.items():
|
|
137
|
+
for keyword in keywords:
|
|
138
|
+
if keyword.lower() in text_lower:
|
|
139
|
+
self.current_phase = phase
|
|
140
|
+
if phase not in self.detected_phases:
|
|
141
|
+
self.detected_phases.append(phase)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
def _extract_tool_summary(self, raw_input):
|
|
145
|
+
"""Extract a human-readable summary from tool input JSON."""
|
|
146
|
+
try:
|
|
147
|
+
data = json.loads(raw_input)
|
|
148
|
+
# Common patterns in tool inputs
|
|
149
|
+
if isinstance(data, dict):
|
|
150
|
+
# Agent tool - look for description or prompt
|
|
151
|
+
if "description" in data:
|
|
152
|
+
self.current_tool_input_summary = str(data["description"])[:100]
|
|
153
|
+
elif "command" in data:
|
|
154
|
+
self.current_tool_input_summary = str(data["command"])[:100]
|
|
155
|
+
elif "file_path" in data:
|
|
156
|
+
self.current_tool_input_summary = str(data["file_path"])[:100]
|
|
157
|
+
elif "pattern" in data:
|
|
158
|
+
self.current_tool_input_summary = str(data["pattern"])[:100]
|
|
159
|
+
elif "query" in data:
|
|
160
|
+
self.current_tool_input_summary = str(data["query"])[:100]
|
|
161
|
+
elif "prompt" in data:
|
|
162
|
+
self.current_tool_input_summary = str(data["prompt"])[:100]
|
|
163
|
+
except (json.JSONDecodeError, TypeError):
|
|
164
|
+
# Keep whatever partial summary we had
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def to_dict(self):
|
|
168
|
+
"""Export current state as a dictionary for JSON serialization."""
|
|
169
|
+
tool_calls = [
|
|
170
|
+
{"name": name, "count": count}
|
|
171
|
+
for name, count in self.tool_call_counts.most_common()
|
|
172
|
+
]
|
|
173
|
+
return {
|
|
174
|
+
"updated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
175
|
+
"message_count": self.message_count,
|
|
176
|
+
"current_tool": self.current_tool,
|
|
177
|
+
"current_tool_input_summary": self.current_tool_input_summary,
|
|
178
|
+
"current_phase": self.current_phase,
|
|
179
|
+
"detected_phases": self.detected_phases,
|
|
180
|
+
"tool_calls": tool_calls,
|
|
181
|
+
"total_tool_calls": self.total_tool_calls,
|
|
182
|
+
"last_text_snippet": self.last_text_snippet,
|
|
183
|
+
"is_active": self.is_active,
|
|
184
|
+
"errors": self.errors[-10:], # Keep last 10 errors
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def atomic_write_json(data, filepath):
|
|
189
|
+
"""Write JSON data atomically using tmp file + rename."""
|
|
190
|
+
dir_path = os.path.dirname(filepath)
|
|
191
|
+
tmp_path = None
|
|
192
|
+
try:
|
|
193
|
+
fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")
|
|
194
|
+
with os.fdopen(fd, "w") as f:
|
|
195
|
+
json.dump(data, f, indent=2)
|
|
196
|
+
f.write("\n")
|
|
197
|
+
os.rename(tmp_path, filepath)
|
|
198
|
+
except OSError:
|
|
199
|
+
# Best effort - remove tmp file if rename failed
|
|
200
|
+
if tmp_path:
|
|
201
|
+
try:
|
|
202
|
+
os.unlink(tmp_path)
|
|
203
|
+
except OSError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def tail_and_parse(session_log, progress_file, poll_interval=0.5):
|
|
208
|
+
"""Tail session log and parse stream-json events."""
|
|
209
|
+
tracker = ProgressTracker()
|
|
210
|
+
last_write_state = None
|
|
211
|
+
|
|
212
|
+
# Wait for log file to appear
|
|
213
|
+
wait_count = 0
|
|
214
|
+
while not os.path.exists(session_log):
|
|
215
|
+
time.sleep(poll_interval)
|
|
216
|
+
wait_count += 1
|
|
217
|
+
if wait_count > 120: # 60 seconds max wait
|
|
218
|
+
sys.stderr.write(f"Timeout waiting for {session_log}\n")
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
with open(session_log, "r") as f:
|
|
222
|
+
idle_count = 0
|
|
223
|
+
while True:
|
|
224
|
+
line = f.readline()
|
|
225
|
+
if line:
|
|
226
|
+
idle_count = 0
|
|
227
|
+
line = line.strip()
|
|
228
|
+
if not line:
|
|
229
|
+
continue
|
|
230
|
+
try:
|
|
231
|
+
event = json.loads(line)
|
|
232
|
+
tracker.process_event(event)
|
|
233
|
+
except json.JSONDecodeError:
|
|
234
|
+
# Not a JSON line (could be stderr mixed in)
|
|
235
|
+
# Use it as a text snippet if meaningful
|
|
236
|
+
stripped = line.strip()
|
|
237
|
+
if stripped and len(stripped) > 5:
|
|
238
|
+
tracker.last_text_snippet = stripped[:120]
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# Write progress if state changed
|
|
242
|
+
current_state = tracker.to_dict()
|
|
243
|
+
state_key = (
|
|
244
|
+
current_state["message_count"],
|
|
245
|
+
current_state["current_tool"],
|
|
246
|
+
current_state["current_phase"],
|
|
247
|
+
current_state["total_tool_calls"],
|
|
248
|
+
)
|
|
249
|
+
if state_key != last_write_state:
|
|
250
|
+
atomic_write_json(current_state, progress_file)
|
|
251
|
+
last_write_state = state_key
|
|
252
|
+
else:
|
|
253
|
+
idle_count += 1
|
|
254
|
+
# After 2 seconds of no new data, write current state anyway
|
|
255
|
+
# (ensures progress.json stays fresh)
|
|
256
|
+
if idle_count == 4:
|
|
257
|
+
current_state = tracker.to_dict()
|
|
258
|
+
atomic_write_json(current_state, progress_file)
|
|
259
|
+
|
|
260
|
+
# After 3600 idle cycles (30 min), mark inactive and exit
|
|
261
|
+
if idle_count > 3600:
|
|
262
|
+
tracker.is_active = False
|
|
263
|
+
atomic_write_json(tracker.to_dict(), progress_file)
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
time.sleep(poll_interval)
|
|
267
|
+
|
|
268
|
+
# Final write
|
|
269
|
+
tracker.is_active = False
|
|
270
|
+
atomic_write_json(tracker.to_dict(), progress_file)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main():
|
|
274
|
+
parser = argparse.ArgumentParser(description="Parse stream-json progress")
|
|
275
|
+
parser.add_argument("--session-log", required=True, help="Path to session.log (JSONL)")
|
|
276
|
+
parser.add_argument("--progress-file", required=True, help="Path to write progress.json")
|
|
277
|
+
args = parser.parse_args()
|
|
278
|
+
|
|
279
|
+
# Handle graceful shutdown
|
|
280
|
+
def handle_signal(signum, frame):
|
|
281
|
+
sys.exit(0)
|
|
282
|
+
|
|
283
|
+
signal.signal(signal.SIGTERM, handle_signal)
|
|
284
|
+
signal.signal(signal.SIGINT, handle_signal)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
tail_and_parse(args.session_log, args.progress_file)
|
|
288
|
+
except SystemExit:
|
|
289
|
+
pass
|
|
290
|
+
except Exception as e:
|
|
291
|
+
sys.stderr.write(f"parse-stream-progress.py error: {e}\n")
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
|
@@ -7,6 +7,12 @@ description: "Launch and manage the bugfix pipeline from within a cbc session. S
|
|
|
7
7
|
|
|
8
8
|
Launch the autonomous bug fix pipeline from within a cbc conversation. The pipeline runs as a fully detached background process -- closing the cbc session does NOT stop the pipeline.
|
|
9
9
|
|
|
10
|
+
### Mandatory Execution Mode (MUST)
|
|
11
|
+
|
|
12
|
+
- Always use daemon mode via `dev-pipeline/launch-bugfix-daemon.sh` for start/stop/status/log actions.
|
|
13
|
+
- NEVER run `dev-pipeline/run-bugfix.sh run ...` directly from this skill.
|
|
14
|
+
- Reason: foreground `run-bugfix.sh` can be terminated by AI CLI command timeout (e.g. cbc 120s), while daemon mode survives session timeout.
|
|
15
|
+
|
|
10
16
|
### When to Use
|
|
11
17
|
|
|
12
18
|
**Start bugfix pipeline** -- User says:
|
|
@@ -14,7 +20,7 @@ Launch the autonomous bug fix pipeline from within a cbc conversation. The pipel
|
|
|
14
20
|
- "start bug fix", "run bug fix", "execute bug list", "begin fixing"
|
|
15
21
|
- "启动 bug 修复", "开始修复 bug", "运行 bug 修复流水线", "开始修 bug"
|
|
16
22
|
- "修复所有 bug", "批量修复", "启动修复流水线"
|
|
17
|
-
- After bug-planner completes: "
|
|
23
|
+
- After bug-planner completes: "fix them", "开始修复"
|
|
18
24
|
|
|
19
25
|
**Check status** -- User says:
|
|
20
26
|
- "bugfix status", "check bug fixes", "how's the fixing going", "bug fix progress"
|
|
@@ -7,6 +7,12 @@ description: "Launch and manage the dev-pipeline from within a cbc session. Star
|
|
|
7
7
|
|
|
8
8
|
Launch the autonomous development pipeline from within a cbc conversation. The pipeline runs as a fully detached background process -- closing the cbc session does NOT stop the pipeline.
|
|
9
9
|
|
|
10
|
+
### Mandatory Execution Mode (MUST)
|
|
11
|
+
|
|
12
|
+
- Always use daemon mode via `dev-pipeline/launch-daemon.sh` for start/stop/status/log actions.
|
|
13
|
+
- NEVER run `dev-pipeline/run.sh run ...` directly from this skill.
|
|
14
|
+
- Reason: foreground `run.sh` can be terminated by AI CLI command timeout (e.g. cbc 120s), while daemon mode survives session timeout.
|
|
15
|
+
|
|
10
16
|
### When to Use
|
|
11
17
|
|
|
12
18
|
**Start pipeline** -- User says:
|
|
@@ -14,7 +20,7 @@ Launch the autonomous development pipeline from within a cbc conversation. The p
|
|
|
14
20
|
- "run the features", "execute feature list", "start implementing"
|
|
15
21
|
- "启动流水线", "开始实现", "运行流水线", "开始自动开发"
|
|
16
22
|
- "实现接下来的步骤", "执行 feature list", "开始构建"
|
|
17
|
-
- After app-planner completes: "
|
|
23
|
+
- After app-planner completes: "build it", "按 feature list 开始开发"
|
|
18
24
|
|
|
19
25
|
**Check status** -- User says:
|
|
20
26
|
- "pipeline status", "check pipeline", "how's it going", "progress"
|
|
@@ -28,6 +34,10 @@ Launch the autonomous development pipeline from within a cbc conversation. The p
|
|
|
28
34
|
- "show logs", "pipeline logs", "tail logs", "what's happening"
|
|
29
35
|
- "查看日志", "流水线日志", "看看日志"
|
|
30
36
|
|
|
37
|
+
**Retry single feature node** -- User says:
|
|
38
|
+
- "retry F-003", "retry this feature", "retry this node"
|
|
39
|
+
- "重试 F-003", "重试这个节点", "重跑这个 feature"
|
|
40
|
+
|
|
31
41
|
**Do NOT use this skill when:**
|
|
32
42
|
- User wants to plan features (use `app-planner` instead)
|
|
33
43
|
- User wants to implement a single feature manually within current session (use `prizmkit-implement`)
|
|
@@ -201,6 +211,26 @@ Pass via `--env`:
|
|
|
201
211
|
dev-pipeline/launch-daemon.sh start feature-list.json --env "SESSION_TIMEOUT=7200 MAX_RETRIES=5 VERBOSE=1"
|
|
202
212
|
```
|
|
203
213
|
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
#### Intent F: Retry Single Feature Node
|
|
217
|
+
|
|
218
|
+
When user says "retry F-003" or "重试 F-003":
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
dev-pipeline/retry-feature.sh F-003 feature-list.json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
When user says "从头重试 F-003" or "clean retry F-003":
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
dev-pipeline/reset-feature.sh F-003 --clean --run feature-list.json
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Notes:
|
|
231
|
+
- `retry-feature.sh` runs exactly one feature session and exits.
|
|
232
|
+
- Keep pipeline daemon mode for main run management (`launch-daemon.sh`).
|
|
233
|
+
|
|
204
234
|
### Error Handling
|
|
205
235
|
|
|
206
236
|
| Error | Action |
|
|
@@ -211,7 +241,7 @@ dev-pipeline/launch-daemon.sh start feature-list.json --env "SESSION_TIMEOUT=720
|
|
|
211
241
|
| Pipeline already running | Show status, ask if user wants to stop and restart |
|
|
212
242
|
| PID file stale (process dead) | `launch-daemon.sh` auto-cleans, retry start |
|
|
213
243
|
| Launch failed (process died immediately) | Show last 20 lines of log: `tail -20 dev-pipeline/state/pipeline-daemon.log` |
|
|
214
|
-
| All features blocked/failed | Show status, suggest
|
|
244
|
+
| All features blocked/failed | Show status, suggest daemon-safe recovery: `dev-pipeline/reset-feature.sh <F-XXX> --clean --run feature-list.json` |
|
|
215
245
|
| Permission denied on script | Run `chmod +x dev-pipeline/launch-daemon.sh dev-pipeline/run.sh` |
|
|
216
246
|
|
|
217
247
|
### Integration Notes
|
|
@@ -283,22 +283,27 @@ The pipeline supports resuming from the last completed phase by detecting existi
|
|
|
283
283
|
| `prizmkit-summarize` | Phase 7: archive to REGISTRY |
|
|
284
284
|
| `prizmkit-retrospective` | Optional: post-feature lessons learned |
|
|
285
285
|
| `prizmkit-bug-fix-workflow` | NOT used (separate pipeline for bugs) |
|
|
286
|
+
| `refactor-workflow` | NOT used (separate pipeline for refactoring) |
|
|
286
287
|
| `app-planner` | Pre-pipeline: interactive feature planning |
|
|
287
288
|
|
|
288
289
|
---
|
|
289
290
|
|
|
290
|
-
## Comparison with Bug Fix
|
|
291
|
-
|
|
292
|
-
| Dimension | Feature Workflow | Bug Fix Pipeline |
|
|
293
|
-
|
|
294
|
-
| Input | Natural language requirement | Bug description / stack trace |
|
|
295
|
-
| Pipeline Phases | 7 (Fast Path: 5) | 5 (Fast Path: 3) |
|
|
296
|
-
|
|
|
297
|
-
| Artifact
|
|
298
|
-
|
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
291
|
+
## Comparison with Refactor and Bug Fix Pipelines
|
|
292
|
+
|
|
293
|
+
| Dimension | Feature Workflow | Refactor Workflow | Bug Fix Pipeline |
|
|
294
|
+
|-----------|-----------------|-------------------|------------------|
|
|
295
|
+
| Input | Natural language requirement | Module/code target | Bug description / stack trace |
|
|
296
|
+
| Pipeline Phases | 7 (Fast Path: 5) | 6 (Fast Path: 4) | 5 (Fast Path: 3) |
|
|
297
|
+
| Phase 1 | Specify (spec.md) | Analyze (refactor-analysis.md) | Triage (fix-plan.md) |
|
|
298
|
+
| Artifact Docs | 3: spec.md + plan.md + tasks.md | 3: refactor-analysis.md + plan.md + tasks.md | 2: fix-plan.md + fix-report.md |
|
|
299
|
+
| Artifact Path | `.prizmkit/specs/<feature-slug>/` | `.prizmkit/refactor/<slug>/` | `.prizmkit/bugfix/<bug-id>/` |
|
|
300
|
+
| Skills Chain | specify → plan → tasks → analyze → implement → review → commit + summarize | tech-debt-tracker → plan → tasks → implement → review → commit | error-triage → bug-reproduce → implement → code-review → commit |
|
|
301
|
+
| Commit Prefix | `feat(<scope>):` | `refactor(<scope>):` | `fix(<scope>):` |
|
|
302
|
+
| REGISTRY Update | ✅ via summarize | ❌ not applicable | ❌ not applicable |
|
|
303
|
+
| Test Strategy | TDD per task | Full suite after EVERY task | Reproduction test |
|
|
304
|
+
| Scope Guard | N/A | ✅ (enforced) | N/A |
|
|
305
|
+
| Behavior Change | ✅ Expected | ❌ Forbidden | ✅ Fix behavior |
|
|
306
|
+
| Resume Support | ✅ artifact-based detection | ✅ artifact-based detection | ❌ |
|
|
302
307
|
|
|
303
308
|
## Path References
|
|
304
309
|
|
|
@@ -315,26 +315,31 @@ When `affected_feature` is non-empty:
|
|
|
315
315
|
| `prizmkit-specify` | NOT used (no spec.md for bugs) |
|
|
316
316
|
| `prizmkit-plan` | NOT used (no plan.md for bugs) |
|
|
317
317
|
| `prizmkit-tasks` | NOT used (no tasks.md for bugs) |
|
|
318
|
+
| `refactor-workflow` | NOT used (separate pipeline for refactoring) |
|
|
318
319
|
|
|
319
320
|
---
|
|
320
321
|
|
|
321
|
-
## Comparison with Feature
|
|
322
|
-
|
|
323
|
-
| Dimension | Feature Pipeline | Bug Fix Pipeline |
|
|
324
|
-
|
|
325
|
-
| Input Skill | `app-planner` (7-phase interactive) | `bug-planner` (multi-format parser) |
|
|
326
|
-
| Input File | `feature-list.json` (F-NNN) | `bug-fix-list.json` (B-NNN) |
|
|
327
|
-
| Schema Version | `dev-pipeline-feature-list-v1` | `dev-pipeline-bug-fix-list-v1` |
|
|
328
|
-
| Pipeline Phases | 7 Phase (Fast Path: 5) | 5 Phase (Fast Path: 3) |
|
|
329
|
-
|
|
|
330
|
-
| Artifact
|
|
331
|
-
|
|
|
332
|
-
|
|
|
333
|
-
|
|
|
334
|
-
|
|
|
335
|
-
|
|
|
336
|
-
|
|
|
337
|
-
|
|
|
322
|
+
## Comparison with Feature and Refactor Pipelines
|
|
323
|
+
|
|
324
|
+
| Dimension | Feature Pipeline | Refactor Workflow | Bug Fix Pipeline |
|
|
325
|
+
|-----------|-----------------|-------------------|------------------|
|
|
326
|
+
| Input Skill | `app-planner` (7-phase interactive) | Direct invocation (prizmkit.refactor) | `bug-planner` (multi-format parser) |
|
|
327
|
+
| Input File | `feature-list.json` (F-NNN) | N/A (conversation trigger) | `bug-fix-list.json` (B-NNN) |
|
|
328
|
+
| Schema Version | `dev-pipeline-feature-list-v1` | N/A | `dev-pipeline-bug-fix-list-v1` |
|
|
329
|
+
| Pipeline Phases | 7 Phase (Fast Path: 5) | 6 Phase (Fast Path: 4) | 5 Phase (Fast Path: 3) |
|
|
330
|
+
| Phase 1 | Specify (spec.md) | Analyze (refactor-analysis.md) | Triage (fix-plan.md) |
|
|
331
|
+
| Artifact Docs | 3: spec.md + plan.md + tasks.md | 3: refactor-analysis.md + plan.md + tasks.md | 2: fix-plan.md + fix-report.md |
|
|
332
|
+
| Artifact Path | `.prizmkit/specs/<feature-slug>/` | `.prizmkit/refactor/<slug>/` | `.prizmkit/bugfix/<bug-id>/` |
|
|
333
|
+
| Prompt Template | `bootstrap-prompt.md` | N/A (in-session) | `bugfix-bootstrap-prompt.md` |
|
|
334
|
+
| Skills Chain | specify → plan → tasks → implement → review → summarize → commit | tech-debt-tracker → plan → tasks → implement → review → commit | error-triage → bug-reproduce → implement → code-review → commit |
|
|
335
|
+
| Commit Prefix | `feat(<scope>):` | `refactor(<scope>):` | `fix(<scope>):` |
|
|
336
|
+
| REGISTRY Update | ✅ via summarize | ❌ not applicable | ❌ not applicable |
|
|
337
|
+
| TRAPS Update | Only in retrospective | Resolved TRAPS removed | ✅ automatic after every fix |
|
|
338
|
+
| Test Strategy | TDD per task | Full suite after EVERY task | Reproduction test |
|
|
339
|
+
| Scope Guard | N/A | ✅ (enforced) | N/A |
|
|
340
|
+
| Behavior Change | ✅ Expected | ❌ Forbidden | ✅ Fix behavior |
|
|
341
|
+
| Manual Verification | None | None | Supported (verification_type=manual/hybrid) |
|
|
342
|
+
| Agent Roles | PM → Dev → Reviewer → Doc | Dev → Reviewer (streamlined) | Dev → Reviewer (streamlined) |
|
|
338
343
|
|
|
339
344
|
## Path References
|
|
340
345
|
|