prizmkit 1.1.90 → 1.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/lib/branch.sh +8 -5
- package/bundled/dev-pipeline/lib/common.sh +122 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +69 -25
- package/bundled/dev-pipeline/run-bugfix.sh +150 -39
- package/bundled/dev-pipeline/run-feature.sh +98 -33
- package/bundled/dev-pipeline/run-refactor.sh +150 -39
- package/bundled/dev-pipeline/scripts/update-bug-status.py +39 -4
- package/bundled/dev-pipeline/scripts/update-feature-status.py +43 -6
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +36 -4
- package/bundled/dev-pipeline/tests/test_auto_skip.py +92 -3
- package/bundled/dev-pipeline-windows/lib/branch.ps1 +17 -4
- package/bundled/dev-pipeline-windows/lib/common.ps1 +70 -13
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +104 -34
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +7 -3
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +39 -4
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +43 -6
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +36 -4
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -211,18 +211,21 @@ branch_save_wip() {
|
|
|
211
211
|
return 0
|
|
212
212
|
fi
|
|
213
213
|
|
|
214
|
-
# Check if there are any uncommitted changes (tracked or untracked,
|
|
214
|
+
# Check if there are any uncommitted changes (tracked or untracked,
|
|
215
|
+
# excluding gitignored and root-level hidden tool temporary worktrees)
|
|
215
216
|
local has_changes
|
|
216
|
-
has_changes=$(
|
|
217
|
+
has_changes=$(prizm_git_status_safe "$project_root" || true)
|
|
217
218
|
if [[ -z "$has_changes" ]]; then
|
|
218
219
|
return 0
|
|
219
220
|
fi
|
|
220
221
|
|
|
221
222
|
log_warn "Saving uncommitted work-in-progress on branch: $dev_branch"
|
|
222
223
|
|
|
223
|
-
# Stage all changes (tracked + untracked, respects .gitignore)
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
# Stage all changes (tracked + untracked, respects .gitignore), excluding
|
|
225
|
+
# root-level hidden tool temporary worktrees so nested agent repos are not
|
|
226
|
+
# recorded as accidental gitlinks in the user's project.
|
|
227
|
+
if ! prizm_git_add_all_safe "$project_root"; then
|
|
228
|
+
log_warn "git add failed — uncommitted work may be lost on branch switch"
|
|
226
229
|
return 0
|
|
227
230
|
fi
|
|
228
231
|
|
|
@@ -23,6 +23,128 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*
|
|
|
23
23
|
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
|
24
24
|
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
|
|
25
25
|
|
|
26
|
+
prizm_require_positive_int() {
|
|
27
|
+
local option_name="$1"
|
|
28
|
+
local option_value="$2"
|
|
29
|
+
if [[ ! "$option_value" =~ ^[1-9][0-9]*$ ]]; then
|
|
30
|
+
log_error "$option_name must be a positive integer: $option_value"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Ignore temporary git worktrees created under root-level hidden AI tool dirs
|
|
36
|
+
# matching .<tool>/worktree(s), without hiding normal source directories.
|
|
37
|
+
PRIZM_GIT_HIDDEN_TOOL_WORKTREE_EXCLUDES=(
|
|
38
|
+
':(top,exclude,glob).*/worktree'
|
|
39
|
+
':(top,exclude,glob).*/worktree/**'
|
|
40
|
+
':(top,exclude,glob).*/worktrees'
|
|
41
|
+
':(top,exclude,glob).*/worktrees/**'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
prizm_git_status_safe() {
|
|
45
|
+
local project_root="$1"
|
|
46
|
+
git -C "$project_root" status --porcelain -- . \
|
|
47
|
+
"${PRIZM_GIT_HIDDEN_TOOL_WORKTREE_EXCLUDES[@]}" 2>/dev/null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
prizm_git_add_all_safe() {
|
|
51
|
+
local project_root="$1"
|
|
52
|
+
git -C "$project_root" add -A -- . \
|
|
53
|
+
"${PRIZM_GIT_HIDDEN_TOOL_WORKTREE_EXCLUDES[@]}" 2>/dev/null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
prizm_git_relative_path() {
|
|
57
|
+
local project_root="$1"
|
|
58
|
+
local target_path="$2"
|
|
59
|
+
python3 - "$project_root" "$target_path" <<'PY' 2>/dev/null || true
|
|
60
|
+
import os
|
|
61
|
+
import sys
|
|
62
|
+
root = os.path.abspath(sys.argv[1])
|
|
63
|
+
target = os.path.abspath(sys.argv[2])
|
|
64
|
+
print(os.path.relpath(target, root).replace(os.sep, "/"))
|
|
65
|
+
PY
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
prizm_git_status_non_bookkeeping() {
|
|
69
|
+
local project_root="$1"
|
|
70
|
+
local state_dir="$2"
|
|
71
|
+
local list_path="$3"
|
|
72
|
+
|
|
73
|
+
local state_rel list_rel
|
|
74
|
+
state_rel="$(prizm_git_relative_path "$project_root" "$state_dir")"
|
|
75
|
+
list_rel="$(prizm_git_relative_path "$project_root" "$list_path")"
|
|
76
|
+
|
|
77
|
+
local line path
|
|
78
|
+
while IFS= read -r line; do
|
|
79
|
+
[[ -n "$line" ]] || continue
|
|
80
|
+
path="${line:3}"
|
|
81
|
+
if [[ "$path" == *" -> "* ]]; then
|
|
82
|
+
path="${path##* -> }"
|
|
83
|
+
fi
|
|
84
|
+
path="${path%\"}"
|
|
85
|
+
path="${path#\"}"
|
|
86
|
+
path="${path#./}"
|
|
87
|
+
if [[ -n "$list_rel" && "$path" == "$list_rel" ]]; then
|
|
88
|
+
continue
|
|
89
|
+
fi
|
|
90
|
+
if [[ -n "$state_rel" && ( "$path" == "$state_rel" || "$path" == "$state_rel/"* ) ]]; then
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
93
|
+
printf '%s\n' "$line"
|
|
94
|
+
done < <(prizm_git_status_safe "$project_root")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
prizm_git_status_bookkeeping() {
|
|
98
|
+
local project_root="$1"
|
|
99
|
+
local state_dir="$2"
|
|
100
|
+
local list_path="$3"
|
|
101
|
+
|
|
102
|
+
local state_rel list_rel
|
|
103
|
+
state_rel="$(prizm_git_relative_path "$project_root" "$state_dir")"
|
|
104
|
+
list_rel="$(prizm_git_relative_path "$project_root" "$list_path")"
|
|
105
|
+
|
|
106
|
+
local line path
|
|
107
|
+
while IFS= read -r line; do
|
|
108
|
+
[[ -n "$line" ]] || continue
|
|
109
|
+
path="${line:3}"
|
|
110
|
+
if [[ "$path" == *" -> "* ]]; then
|
|
111
|
+
path="${path##* -> }"
|
|
112
|
+
fi
|
|
113
|
+
path="${path%\"}"
|
|
114
|
+
path="${path#\"}"
|
|
115
|
+
path="${path#./}"
|
|
116
|
+
if [[ -n "$list_rel" && "$path" == "$list_rel" ]]; then
|
|
117
|
+
printf '%s\n' "$line"
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
if [[ -n "$state_rel" && ( "$path" == "$state_rel" || "$path" == "$state_rel/"* ) ]]; then
|
|
121
|
+
printf '%s\n' "$line"
|
|
122
|
+
fi
|
|
123
|
+
done < <(prizm_git_status_safe "$project_root")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
prizm_git_commit_paths() {
|
|
127
|
+
local project_root="$1"
|
|
128
|
+
local message="$2"
|
|
129
|
+
shift 2
|
|
130
|
+
|
|
131
|
+
local paths=()
|
|
132
|
+
local path rel_path
|
|
133
|
+
for path in "$@"; do
|
|
134
|
+
[[ -n "$path" && -e "$path" ]] || continue
|
|
135
|
+
rel_path="$(prizm_git_relative_path "$project_root" "$path")"
|
|
136
|
+
[[ -n "$rel_path" ]] || continue
|
|
137
|
+
paths+=("$rel_path")
|
|
138
|
+
done
|
|
139
|
+
[[ ${#paths[@]} -gt 0 ]] || return 0
|
|
140
|
+
|
|
141
|
+
git -C "$project_root" add -- "${paths[@]}" 2>/dev/null || true
|
|
142
|
+
if ! git -C "$project_root" diff --cached --quiet -- "${paths[@]}" 2>/dev/null; then
|
|
143
|
+
git -C "$project_root" commit --no-verify -m "$message" -- "${paths[@]}" 2>/dev/null || true
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
26
148
|
# ============================================================
|
|
27
149
|
# Path resolution (single source of truth)
|
|
28
150
|
# ============================================================
|
|
@@ -43,7 +43,10 @@ start_heartbeat() {
|
|
|
43
43
|
local prev_size=0
|
|
44
44
|
local prev_max_size=0
|
|
45
45
|
local prev_child_activity_signature=""
|
|
46
|
+
local prev_progress_signature=""
|
|
47
|
+
local prev_error_loop_signature=""
|
|
46
48
|
local stale_seconds=0
|
|
49
|
+
local stale_reason="stale_session"
|
|
47
50
|
while kill -0 "$cli_pid" 2>/dev/null; do
|
|
48
51
|
sleep "$heartbeat_interval"
|
|
49
52
|
elapsed=$((elapsed + heartbeat_interval))
|
|
@@ -77,9 +80,10 @@ start_heartbeat() {
|
|
|
77
80
|
local child_activity_signature=""
|
|
78
81
|
local child_total_bytes=0
|
|
79
82
|
local child_session_count=0
|
|
83
|
+
local progress_signature=""
|
|
80
84
|
if [[ -f "$progress_json" ]]; then
|
|
81
|
-
local
|
|
82
|
-
|
|
85
|
+
local progress_activity_data
|
|
86
|
+
progress_activity_data=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
83
87
|
import json
|
|
84
88
|
import sys
|
|
85
89
|
|
|
@@ -89,14 +93,31 @@ try:
|
|
|
89
93
|
except Exception:
|
|
90
94
|
sys.exit(0)
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
child_signature = str(progress.get("child_activity_signature") or "")
|
|
93
97
|
total_bytes = int(progress.get("child_total_bytes") or 0)
|
|
94
98
|
session_count = len(progress.get("child_session_files") or [])
|
|
95
|
-
|
|
99
|
+
progress_signature = json.dumps(
|
|
100
|
+
[
|
|
101
|
+
progress.get("current_phase") or "",
|
|
102
|
+
progress.get("current_tool") or "",
|
|
103
|
+
int(progress.get("message_count") or 0),
|
|
104
|
+
int(progress.get("total_tool_calls") or 0),
|
|
105
|
+
int(progress.get("active_subagent_count") or 0),
|
|
106
|
+
int(progress.get("subagent_spawn_count") or 0),
|
|
107
|
+
child_signature,
|
|
108
|
+
total_bytes,
|
|
109
|
+
session_count,
|
|
110
|
+
progress.get("fatal_error_code") or "",
|
|
111
|
+
progress.get("terminal_result_text") or "",
|
|
112
|
+
],
|
|
113
|
+
separators=(",", ":"),
|
|
114
|
+
)
|
|
115
|
+
sep = "\x1f"
|
|
116
|
+
print(sep.join([child_signature, str(total_bytes), str(session_count), progress_signature]))
|
|
96
117
|
PY
|
|
97
118
|
)
|
|
98
|
-
if [[ -n "$
|
|
99
|
-
IFS=$'\
|
|
119
|
+
if [[ -n "$progress_activity_data" ]]; then
|
|
120
|
+
IFS=$'\037' read -r child_activity_signature child_total_bytes child_session_count progress_signature <<< "$progress_activity_data"
|
|
100
121
|
fi
|
|
101
122
|
fi
|
|
102
123
|
|
|
@@ -106,6 +127,12 @@ PY
|
|
|
106
127
|
fi
|
|
107
128
|
prev_child_activity_signature="$child_activity_signature"
|
|
108
129
|
|
|
130
|
+
local progress_advanced=0
|
|
131
|
+
if [[ -n "$progress_signature" && "$progress_signature" != "$prev_progress_signature" ]]; then
|
|
132
|
+
progress_advanced=1
|
|
133
|
+
fi
|
|
134
|
+
prev_progress_signature="$progress_signature"
|
|
135
|
+
|
|
109
136
|
local effective_stale_kill_threshold="$stale_kill_threshold"
|
|
110
137
|
if [[ $stale_kill_threshold -gt 0 && -f "$progress_json" ]]; then
|
|
111
138
|
local extended_threshold
|
|
@@ -172,8 +199,11 @@ PY
|
|
|
172
199
|
|
|
173
200
|
# Check for error-loop: agent is actively producing output but results are
|
|
174
201
|
# all read-offset errors or wasted calls. This is a stuck agent that appears
|
|
175
|
-
# "active" by log growth but is accomplishing nothing.
|
|
202
|
+
# "active" by log growth but is accomplishing nothing. Only a newly advancing
|
|
203
|
+
# error signature counts; stale historical errors must not turn later normal
|
|
204
|
+
# output into a stale kill.
|
|
176
205
|
local error_loop_detected=false
|
|
206
|
+
local error_loop_signature=""
|
|
177
207
|
if [[ $effective_stale_kill_threshold -gt 0 && $growth -gt 0 && -f "$progress_json" ]]; then
|
|
178
208
|
local error_loop_flag
|
|
179
209
|
error_loop_flag=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
@@ -187,35 +217,44 @@ errors = progress.get("errors", [])
|
|
|
187
217
|
if isinstance(errors, list) and len(errors) >= 5:
|
|
188
218
|
recent = errors[-5:]
|
|
189
219
|
if all(isinstance(e, dict) and e.get("type") in ("read_offset_overflow", "wasted_call") for e in recent):
|
|
190
|
-
print("
|
|
220
|
+
print(json.dumps(recent, sort_keys=True, separators=(",", ":")))
|
|
191
221
|
PY
|
|
192
222
|
)
|
|
193
|
-
if [[ "$error_loop_flag"
|
|
194
|
-
|
|
223
|
+
if [[ -n "$error_loop_flag" ]]; then
|
|
224
|
+
error_loop_signature="$error_loop_flag"
|
|
225
|
+
if [[ -n "$prev_error_loop_signature" && "$error_loop_signature" != "$prev_error_loop_signature" ]]; then
|
|
226
|
+
error_loop_detected=true
|
|
227
|
+
fi
|
|
195
228
|
fi
|
|
196
229
|
fi
|
|
230
|
+
prev_error_loop_signature="$error_loop_signature"
|
|
197
231
|
|
|
198
232
|
# Track progress staleness. Parent sessions can sit in a wait/polling
|
|
199
|
-
# tool while child transcripts
|
|
200
|
-
#
|
|
201
|
-
#
|
|
233
|
+
# tool while child transcripts or structured progress counters keep
|
|
234
|
+
# advancing, so both child activity and progress.json state changes
|
|
235
|
+
# count as progress. Error loops bypass normal growth-as-progress
|
|
236
|
+
# because the log is only growing with repeated failed reads or wasted
|
|
237
|
+
# calls.
|
|
202
238
|
#
|
|
203
239
|
# When the main session log has been truncated (e.g. by subagent stdout
|
|
204
|
-
# redirection severing the file descriptor), use
|
|
205
|
-
# primary progress
|
|
206
|
-
#
|
|
240
|
+
# redirection severing the file descriptor), use structured progress and
|
|
241
|
+
# child activity as the primary progress signals — the parent process is
|
|
242
|
+
# still running and children may still be producing output.
|
|
207
243
|
if [[ "$error_loop_detected" == "true" ]]; then
|
|
244
|
+
stale_reason="error_loop"
|
|
208
245
|
stale_seconds=$effective_stale_kill_threshold
|
|
209
246
|
elif [[ "$log_truncated" == "true" ]]; then
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if [[ $child_growth -gt 0 ]]; then
|
|
247
|
+
stale_reason="stale_session"
|
|
248
|
+
if [[ $progress_advanced -gt 0 || $child_growth -gt 0 ]]; then
|
|
213
249
|
stale_seconds=0
|
|
250
|
+
else
|
|
251
|
+
stale_seconds=$((stale_seconds + heartbeat_interval))
|
|
214
252
|
fi
|
|
215
|
-
|
|
216
|
-
|
|
253
|
+
elif [[ $growth -le 0 && $child_growth -eq 0 && $progress_advanced -eq 0 ]]; then
|
|
254
|
+
stale_reason="stale_session"
|
|
217
255
|
stale_seconds=$((stale_seconds + heartbeat_interval))
|
|
218
256
|
else
|
|
257
|
+
stale_reason="stale_session"
|
|
219
258
|
stale_seconds=0
|
|
220
259
|
fi
|
|
221
260
|
|
|
@@ -251,7 +290,7 @@ PY
|
|
|
251
290
|
local status_icon
|
|
252
291
|
if $log_truncated; then
|
|
253
292
|
status_icon="${RED}⚠${NC}"
|
|
254
|
-
elif [[ $growth -gt 0 || $child_growth -gt 0 ]]; then
|
|
293
|
+
elif [[ $growth -gt 0 || $child_growth -gt 0 || $progress_advanced -gt 0 ]]; then
|
|
255
294
|
status_icon="${GREEN}▶${NC}"
|
|
256
295
|
else
|
|
257
296
|
status_icon="${YELLOW}⏸${NC}"
|
|
@@ -299,14 +338,19 @@ PY
|
|
|
299
338
|
# stale window to surface stuck agents promptly.
|
|
300
339
|
if [[ $effective_stale_kill_threshold -gt 0 && $stale_seconds -ge $effective_stale_kill_threshold ]]; then
|
|
301
340
|
local stale_mins=$((stale_seconds / 60))
|
|
302
|
-
|
|
303
|
-
|
|
341
|
+
if [[ "$stale_reason" == "error_loop" ]]; then
|
|
342
|
+
echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}STALE-KILL: repeated read-offset/wasted-call error loop (threshold: ${effective_stale_kill_threshold}s)${NC}"
|
|
343
|
+
echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (error loop)..."
|
|
344
|
+
else
|
|
345
|
+
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}"
|
|
346
|
+
echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (stale session)..."
|
|
347
|
+
fi
|
|
304
348
|
# Write the marker before killing. Some CLIs exit quickly, and the
|
|
305
349
|
# parent runner may stop this heartbeat process immediately after
|
|
306
350
|
# wait(1) returns.
|
|
307
351
|
local _marker_dir
|
|
308
352
|
_marker_dir="$(dirname "$session_log")"
|
|
309
|
-
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"
|
|
353
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"${stale_reason}\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
|
|
310
354
|
kill -TERM "$cli_pid" 2>/dev/null || true
|
|
311
355
|
# Give process 10s to exit gracefully, then force kill
|
|
312
356
|
local stale_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
|