prizmkit 1.1.91 → 1.1.93

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.
Files changed (46) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +0 -4
  3. package/bundled/dev-pipeline/README.md +9 -39
  4. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +0 -4
  5. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +0 -2
  6. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -1
  7. package/bundled/dev-pipeline/launch-feature-daemon.sh +2 -3
  8. package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -1
  9. package/bundled/dev-pipeline/lib/branch.sh +8 -5
  10. package/bundled/dev-pipeline/lib/common.sh +122 -0
  11. package/bundled/dev-pipeline/lib/heartbeat.sh +13 -3
  12. package/bundled/dev-pipeline/run-bugfix.sh +149 -98
  13. package/bundled/dev-pipeline/run-feature.sh +90 -103
  14. package/bundled/dev-pipeline/run-recovery.sh +1 -32
  15. package/bundled/dev-pipeline/run-refactor.sh +148 -99
  16. package/bundled/dev-pipeline/scripts/update-bug-status.py +39 -4
  17. package/bundled/dev-pipeline/scripts/update-feature-status.py +43 -6
  18. package/bundled/dev-pipeline/scripts/update-refactor-status.py +36 -4
  19. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +3 -3
  20. package/bundled/dev-pipeline/tests/test_auto_skip.py +92 -3
  21. package/bundled/dev-pipeline-windows/.env.example +0 -4
  22. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -4
  23. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -2
  24. package/bundled/dev-pipeline-windows/lib/branch.ps1 +17 -4
  25. package/bundled/dev-pipeline-windows/lib/common.ps1 +9 -0
  26. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +89 -108
  27. package/bundled/dev-pipeline-windows/run-recovery.ps1 +4 -19
  28. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +39 -4
  29. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +43 -6
  30. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +36 -4
  31. package/bundled/skills/_metadata.json +1 -1
  32. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +1 -1
  33. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +5 -15
  34. package/bundled/skills/feature-pipeline-launcher/SKILL.md +4 -4
  35. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +3 -13
  36. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +3 -3
  37. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +4 -14
  38. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +1 -1
  39. package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +4 -14
  40. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +4 -4
  41. package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +2 -12
  42. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +3 -3
  43. package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +4 -14
  44. package/package.json +1 -1
  45. package/bundled/dev-pipeline/scripts/cleanup-logs.py +0 -192
  46. package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +0 -192
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.91",
3
- "bundledAt": "2026-07-01T17:51:47.362Z",
4
- "bundledFrom": "5dcb047"
2
+ "frameworkVersion": "1.1.93",
3
+ "bundledAt": "2026-07-02T17:03:22.377Z",
4
+ "bundledFrom": "dc86f56"
5
5
  }
@@ -23,7 +23,6 @@
23
23
 
24
24
  # ─── Pipeline Execution ──────────────────────────────────────────────
25
25
  # MAX_RETRIES=3 # Max retry attempts per task before marking failed
26
- # SESSION_TIMEOUT=0 # Session timeout in seconds (0 = no limit)
27
26
  # VERBOSE=1 # Verbose logging (1=on, 0=off)
28
27
 
29
28
  # ─── Feature Pipeline Only ────────────────────────────────────────────
@@ -45,6 +44,3 @@
45
44
  # CODEX_WAIT_STALE_KILL_THRESHOLD=3600 # Longer no-log window while Codex waits on subagents
46
45
  # CODEX_SUBAGENT_TIMEOUT_SECONDS=3300 # Codex subagent max runtime; defaults to wait threshold - 300
47
46
  # CB_SUBAGENT_STALE_KILL_THRESHOLD=3600 # Longer no-log window while CodeBuddy sub-agents are running
48
- # LOG_CLEANUP_ENABLED=1 # Periodic log cleanup (1=on, 0=off)
49
- # LOG_RETENTION_DAYS=14 # Delete logs older than N days
50
- # LOG_MAX_TOTAL_MB=1024 # Keep total logs under N MB via oldest-first cleanup
@@ -53,7 +53,6 @@ Main entry point. Drives the full feature development pipeline.
53
53
  | Option | Description |
54
54
  |--------|-------------|
55
55
  | `--dry-run` | Generate bootstrap prompt only, don't spawn AI session |
56
- | `--timeout N` | Override `SESSION_TIMEOUT` for this run (seconds) |
57
56
  | `--resume-phase N` | Resume from specific phase number |
58
57
  | `--no-reset` | Don't reset feature artifacts before running |
59
58
 
@@ -71,9 +70,6 @@ Main entry point. Drives the full feature development pipeline.
71
70
 
72
71
  # Resume from Phase 6 (implementation)
73
72
  ./.prizmkit/dev-pipeline/run-feature.sh run .prizmkit/plans/feature-list.json --resume-phase 6
74
-
75
- # With timeout per session
76
- SESSION_TIMEOUT=7200 ./.prizmkit/dev-pipeline/run-feature.sh run .prizmkit/plans/feature-list.json
77
73
  ```
78
74
 
79
75
  If `.prizmkit/plans/feature-list.json` path is omitted, defaults to `.prizmkit/plans/feature-list.json` in the project root.
@@ -140,7 +136,7 @@ Manages `run-feature.sh` as a background daemon process with PID tracking and lo
140
136
  ```bash
141
137
  ./.prizmkit/dev-pipeline/launch-feature-daemon.sh start .prizmkit/plans/feature-list.json \
142
138
  --mode standard \
143
- --env "MAX_RETRIES=5 SESSION_TIMEOUT=3600 MODEL=claude-sonnet-4.6"
139
+ --env "MAX_RETRIES=5 MODEL=claude-sonnet-4.6"
144
140
  ```
145
141
 
146
142
  **Output (JSON on stdout for programmatic consumption):**
@@ -177,7 +173,7 @@ Equivalent to `run-feature.sh` but for the bug-fix pipeline.
177
173
  ./run-bugfix.sh help
178
174
  ```
179
175
 
180
- **Single-bug options:** `--dry-run`, `--timeout N` (same as `run-feature.sh`)
176
+ **Single-bug options:** `--dry-run` (same as `run-feature.sh`)
181
177
 
182
178
  Processes bugs by: **severity** (critical > high > medium > low) then **priority** (high > medium > low).
183
179
 
@@ -342,7 +338,7 @@ python3 scripts/update-feature-status.py \
342
338
  | `complete` | `--feature-id` | Shortcut for manually marking completed |
343
339
 
344
340
  **Session status values (for `--session-status`):**
345
- `success`, `partial_resumable`, `partial_not_resumable`, `failed`, `crashed`, `timed_out`, `commit_missing`, `docs_missing`, `merge_conflict`
341
+ `success`, `partial_resumable`, `partial_not_resumable`, `failed`, `crashed`, `commit_missing`, `docs_missing`, `merge_conflict`
346
342
 
347
343
  **Output (JSON to stdout):**
348
344
 
@@ -429,26 +425,6 @@ python3 scripts/detect-stuck.py \
429
425
 
430
426
  ---
431
427
 
432
- ### `scripts/cleanup-logs.py` — Log File Cleanup
433
-
434
- Manages log file size by age and total size thresholds.
435
-
436
- ```bash
437
- python3 scripts/cleanup-logs.py \
438
- --state-dir <path> \
439
- [--retention-days <n>] \
440
- [--max-total-mb <n>] \
441
- [--dry-run]
442
- ```
443
-
444
- | Option | Default | Description |
445
- |--------|---------|-------------|
446
- | `--retention-days` | 14 | Delete files older than N days |
447
- | `--max-total-mb` | 1024 | If total > N MB, remove oldest first |
448
- | `--dry-run` | false | Show what would be deleted without deleting |
449
-
450
- ---
451
-
452
428
  ### `scripts/parse-stream-progress.py` — Real-Time Progress Parser
453
429
 
454
430
  Reads AI CLI `stream-json` output and extracts progress metrics.
@@ -533,7 +509,6 @@ Also exports: `log_info`, `log_warn`, `log_error`, `log_success` (with timestamp
533
509
  | Variable | Default | Used By | Description |
534
510
  |----------|---------|---------|-------------|
535
511
  | `MAX_RETRIES` | `3` | run-feature.sh | Max retry attempts per feature before marking as failed |
536
- | `SESSION_TIMEOUT` | `0` (none) | run-feature.sh, run-bugfix.sh | Timeout in seconds per AI CLI session. 0 = no timeout |
537
512
  | `PIPELINE_MODE` | (auto) | run-feature.sh, launch-feature-daemon.sh | Override mode for all features: `lite\|standard\|full` |
538
513
  | `ENABLE_CRITIC` | `false` | run-feature.sh, launch-feature-daemon.sh | Enable adversarial critic review: `true\|false` |
539
514
  | `DEV_BRANCH` | auto-generated | run-feature.sh | Custom git branch name (default: `dev/{feature-id}-{timestamp}`) |
@@ -555,21 +530,18 @@ Also exports: `log_info`, `log_warn`, `log_error`, `log_success` (with timestamp
555
530
  |----------|---------|---------|-------------|
556
531
  | `HEARTBEAT_INTERVAL` | `30` | run-feature.sh | Seconds between heartbeat log output |
557
532
  | `HEARTBEAT_STALE_THRESHOLD` | `600` | run-feature.sh | Seconds before a session is considered stale/stuck |
558
- | `LOG_CLEANUP_ENABLED` | `1` | run-feature.sh | Run log cleanup before pipeline execution |
559
- | `LOG_RETENTION_DAYS` | `14` | run-feature.sh | Delete logs older than N days |
560
- | `LOG_MAX_TOTAL_MB` | `1024` | run-feature.sh | Keep total log size under N MB |
561
533
 
562
534
  **Examples:**
563
535
 
564
536
  ```bash
565
537
  # All env vars in one invocation
566
- MAX_RETRIES=5 SESSION_TIMEOUT=7200 MODEL=claude-sonnet-4.6 VERBOSE=1 \
538
+ MAX_RETRIES=5 MODEL=claude-sonnet-4.6 VERBOSE=1 \
567
539
  ./.prizmkit/dev-pipeline/run-feature.sh run .prizmkit/plans/feature-list.json
568
540
 
569
541
  # Via daemon with --env
570
542
  ./.prizmkit/dev-pipeline/launch-feature-daemon.sh start .prizmkit/plans/feature-list.json \
571
543
  --mode standard \
572
- --env "MAX_RETRIES=5 SESSION_TIMEOUT=3600 AUTO_PUSH=1"
544
+ --env "MAX_RETRIES=5 AUTO_PUSH=1"
573
545
  ```
574
546
 
575
547
  ---
@@ -721,11 +693,10 @@ Features are executed in dependency order via DAG:
721
693
 
722
694
  1. **Bootstrap prompt** generated from feature spec, context, and acceptance criteria
723
695
  2. AI CLI spawned as background process with optional `--model` flag
724
- 3. **Timeout watchdog** runs in parallel if `SESSION_TIMEOUT > 0`
725
- 4. **Heartbeat monitor** prints progress every `HEARTBEAT_INTERVAL` seconds
726
- 5. **Progress parser** (if stream-json supported) extracts phase/tool metrics
727
- 6. On completion, `session-status.json` determines success/failure
728
- 7. Feature status updated; on failure, retry count increments
696
+ 3. **Heartbeat monitor** prints progress every `HEARTBEAT_INTERVAL` seconds
697
+ 4. **Progress parser** (if stream-json supported) extracts phase/tool metrics
698
+ 5. On completion, `session-status.json` determines success/failure
699
+ 6. Feature status updated; on failure, retry count increments
729
700
 
730
701
  ### Heartbeat Output
731
702
 
@@ -882,7 +853,6 @@ run-bugfix.sh main loop
882
853
  | +-- update-feature-status.py # Feature state machine (8 actions)
883
854
  | +-- update-bug-status.py # Bug state machine (6 actions)
884
855
  | +-- detect-stuck.py # Detect stuck/stale features
885
- | +-- cleanup-logs.py # Age/size-based log cleanup
886
856
  | +-- parse-stream-progress.py # Real-time stream-json progress parser
887
857
  |
888
858
  +-- templates/
@@ -344,7 +344,6 @@ pending, in_progress, completed, failed, skipped
344
344
  | `AI_CLI` | string | auto-detect | claude or cbc |
345
345
  | `MODEL` | string | (unset) | AI model override (per-task overrides this) |
346
346
  | `MAX_RETRIES` | integer | (not specified) | Retry attempts per task |
347
- | `SESSION_TIMEOUT` | integer | 0 | 0 = no limit |
348
347
  | `VERBOSE` | integer | (not specified) | 1=on, 0=off |
349
348
  | `ENABLE_CRITIC` | boolean | false | Adversarial review enable |
350
349
  | `PIPELINE_MODE` | string | auto-detect | lite/standard/full override |
@@ -356,9 +355,6 @@ pending, in_progress, completed, failed, skipped
356
355
  | `STALE_KILL_THRESHOLD` | integer | 900 | Auto-kill after N seconds without parent log progress |
357
356
  | `CODEX_WAIT_STALE_KILL_THRESHOLD` | integer | 3600 | Longer no-log stale window while Codex waits on subagents |
358
357
  | `CODEX_SUBAGENT_TIMEOUT_SECONDS` | integer | 3300 | Codex subagent max runtime |
359
- | `LOG_CLEANUP_ENABLED` | integer | 1 | Periodic cleanup |
360
- | `LOG_RETENTION_DAYS` | integer | 14 | Delete logs older than N days |
361
- | `LOG_MAX_TOTAL_MB` | integer | 1024 | Max total logs (MB) |
362
358
 
363
359
  **Priority:** Command-line env vars > .env file > defaults
364
360
 
@@ -51,7 +51,6 @@ Located at `.prizmkit/dev-pipeline/scripts/`:
51
51
  | `check-session-status.py` | Read and validate session-status.json output |
52
52
  | `detect-stuck.py` | Detect stuck/hung pipeline sessions via heartbeat |
53
53
  | `parse-stream-progress.py` | Parse AI CLI output stream for progress tracking |
54
- | `cleanup-logs.py` | Clean up old pipeline logs and state files |
55
54
  | `utils.py` | Shared utility functions for pipeline scripts |
56
55
 
57
56
  ## Artifact Mapping
@@ -125,7 +124,6 @@ The agent MUST write `session-status.json` before exiting:
125
124
  | `status: "partial"`, `can_resume: false` | Retry from scratch |
126
125
  | `status: "failed"` | Retry (up to MAX_RETRIES) |
127
126
  | No status file (crash) | Treat as failed, retry |
128
- | Timeout (exit 124) | Treat as timed_out, retry |
129
127
 
130
128
  ## Team Naming Convention
131
129
 
@@ -482,7 +482,6 @@ Examples:
482
482
 
483
483
  Environment Variables (pass via --env):
484
484
  MAX_RETRIES Max retries per bug (default: 3)
485
- SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
486
485
  VERBOSE Set to 1 for verbose AI CLI output
487
486
  HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
488
487
  DEV_BRANCH Custom dev branch name (default: auto-generated)
@@ -106,7 +106,7 @@ cmd_start() {
106
106
  --env)
107
107
  shift
108
108
  if [[ $# -eq 0 ]]; then
109
- log_error "--env requires a value (e.g. --env \"MAX_RETRIES=5 SESSION_TIMEOUT=3600\")"
109
+ log_error "--env requires a value (e.g. --env \"MAX_RETRIES=5\")"
110
110
  exit 1
111
111
  fi
112
112
  env_overrides="$1"
@@ -609,7 +609,7 @@ Examples:
609
609
  ./launch-feature-daemon.sh start --features F-001,F-003,F-007 # Run specific features
610
610
  ./launch-feature-daemon.sh start --mode full # Full mode for complex features
611
611
  ./launch-feature-daemon.sh start --critic # Enable adversarial critic review
612
- ./launch-feature-daemon.sh start --env "MAX_RETRIES=5 SESSION_TIMEOUT=7200"
612
+ ./launch-feature-daemon.sh start --env "MAX_RETRIES=5"
613
613
  ./launch-feature-daemon.sh start .prizmkit/plans/feature-list.json --mode full --critic --env "VERBOSE=1"
614
614
  ./launch-feature-daemon.sh status # Check if running (JSON on stdout)
615
615
  ./launch-feature-daemon.sh logs --follow # Live log tailing
@@ -619,7 +619,6 @@ Examples:
619
619
 
620
620
  Environment Variables (pass via --env):
621
621
  MAX_RETRIES Max retries per feature (default: 3)
622
- SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
623
622
  VERBOSE Set to 1 for verbose AI CLI output
624
623
  HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
625
624
  DEV_BRANCH Custom dev branch name (default: auto-generated)
@@ -483,7 +483,6 @@ Examples:
483
483
 
484
484
  Environment Variables (pass via --env):
485
485
  MAX_RETRIES Max retries per refactor (default: 3)
486
- SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
487
486
  VERBOSE Set to 1 for verbose AI CLI output
488
487
  STRICT_BEHAVIOR_CHECK Force full test suite after each refactor (default: 1)
489
488
  HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
@@ -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, excluding gitignored)
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=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
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
- if ! git -C "$project_root" add -A 2>/dev/null; then
225
- log_warn "git add -A failed uncommitted work may be lost on branch switch"
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
  # ============================================================
@@ -46,6 +46,7 @@ start_heartbeat() {
46
46
  local prev_progress_signature=""
47
47
  local prev_error_loop_signature=""
48
48
  local stale_seconds=0
49
+ local stale_reason="stale_session"
49
50
  while kill -0 "$cli_pid" 2>/dev/null; do
50
51
  sleep "$heartbeat_interval"
51
52
  elapsed=$((elapsed + heartbeat_interval))
@@ -240,16 +241,20 @@ PY
240
241
  # child activity as the primary progress signals — the parent process is
241
242
  # still running and children may still be producing output.
242
243
  if [[ "$error_loop_detected" == "true" ]]; then
244
+ stale_reason="error_loop"
243
245
  stale_seconds=$effective_stale_kill_threshold
244
246
  elif [[ "$log_truncated" == "true" ]]; then
247
+ stale_reason="stale_session"
245
248
  if [[ $progress_advanced -gt 0 || $child_growth -gt 0 ]]; then
246
249
  stale_seconds=0
247
250
  else
248
251
  stale_seconds=$((stale_seconds + heartbeat_interval))
249
252
  fi
250
253
  elif [[ $growth -le 0 && $child_growth -eq 0 && $progress_advanced -eq 0 ]]; then
254
+ stale_reason="stale_session"
251
255
  stale_seconds=$((stale_seconds + heartbeat_interval))
252
256
  else
257
+ stale_reason="stale_session"
253
258
  stale_seconds=0
254
259
  fi
255
260
 
@@ -333,14 +338,19 @@ PY
333
338
  # stale window to surface stuck agents promptly.
334
339
  if [[ $effective_stale_kill_threshold -gt 0 && $stale_seconds -ge $effective_stale_kill_threshold ]]; then
335
340
  local stale_mins=$((stale_seconds / 60))
336
- 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}"
337
- echo -e " ${RED}[HEARTBEAT]${NC} Killing AI CLI process $cli_pid (stale session)..."
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
338
348
  # Write the marker before killing. Some CLIs exit quickly, and the
339
349
  # parent runner may stop this heartbeat process immediately after
340
350
  # wait(1) returns.
341
351
  local _marker_dir
342
352
  _marker_dir="$(dirname "$session_log")"
343
- echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"stale_session\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
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
344
354
  kill -TERM "$cli_pid" 2>/dev/null || true
345
355
  # Give process 10s to exit gracefully, then force kill
346
356
  local stale_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"