prizmkit 1.0.12 → 1.0.14

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 (79) hide show
  1. package/bin/create-prizmkit.js +4 -1
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/adapters/claude/command-adapter.js +35 -4
  4. package/bundled/adapters/claude/rules-adapter.js +6 -58
  5. package/bundled/adapters/claude/team-adapter.js +2 -2
  6. package/bundled/adapters/codebuddy/agent-adapter.js +0 -1
  7. package/bundled/adapters/codebuddy/rules-adapter.js +30 -0
  8. package/bundled/adapters/shared/frontmatter.js +3 -1
  9. package/bundled/dev-pipeline/README.md +13 -3
  10. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +10 -0
  11. package/bundled/dev-pipeline/launch-daemon.sh +18 -4
  12. package/bundled/dev-pipeline/lib/common.sh +105 -0
  13. package/bundled/dev-pipeline/retry-bug.sh +12 -0
  14. package/bundled/dev-pipeline/retry-feature.sh +12 -0
  15. package/bundled/dev-pipeline/run-bugfix.sh +71 -57
  16. package/bundled/dev-pipeline/run.sh +87 -57
  17. package/bundled/dev-pipeline/scripts/check-session-status.py +47 -2
  18. package/bundled/dev-pipeline/scripts/cleanup-logs.py +192 -0
  19. package/bundled/dev-pipeline/scripts/detect-stuck.py +15 -3
  20. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +32 -27
  21. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +23 -23
  22. package/bundled/dev-pipeline/scripts/update-feature-status.py +73 -2
  23. package/bundled/dev-pipeline/scripts/utils.py +22 -0
  24. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +38 -2
  25. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +39 -2
  26. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +39 -2
  27. package/bundled/dev-pipeline/templates/session-status-schema.json +7 -1
  28. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  29. package/bundled/dev-pipeline/tests/conftest.py +133 -0
  30. package/bundled/dev-pipeline/tests/test_check_session.py +127 -0
  31. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +119 -0
  32. package/bundled/dev-pipeline/tests/test_detect_stuck.py +207 -0
  33. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +181 -0
  34. package/bundled/dev-pipeline/tests/test_generate_prompt.py +190 -0
  35. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +153 -0
  36. package/bundled/dev-pipeline/tests/test_init_pipeline.py +241 -0
  37. package/bundled/dev-pipeline/tests/test_update_bug_status.py +142 -0
  38. package/bundled/dev-pipeline/tests/test_update_feature_status.py +277 -0
  39. package/bundled/dev-pipeline/tests/test_utils.py +141 -0
  40. package/bundled/rules/USAGE.md +153 -0
  41. package/bundled/rules/_rules-metadata.json +43 -0
  42. package/bundled/rules/general/prefer-linux-commands.md +9 -0
  43. package/bundled/rules/prizm/prizm-commit-workflow.md +10 -0
  44. package/bundled/rules/prizm/prizm-documentation.md +19 -0
  45. package/bundled/rules/prizm/prizm-progressive-loading.md +11 -0
  46. package/bundled/skills/_metadata.json +130 -67
  47. package/bundled/skills/app-planner/SKILL.md +252 -499
  48. package/bundled/skills/app-planner/assets/evaluation-guide.md +44 -0
  49. package/bundled/skills/app-planner/scripts/validate-and-generate.py +143 -4
  50. package/bundled/skills/bug-planner/SKILL.md +58 -13
  51. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -7
  52. package/bundled/skills/dev-pipeline-launcher/SKILL.md +16 -7
  53. package/bundled/skills/feature-workflow/SKILL.md +175 -234
  54. package/bundled/skills/prizm-kit/SKILL.md +17 -31
  55. package/bundled/skills/{prizmkit-adr-manager → prizmkit-tool-adr-manager}/SKILL.md +6 -7
  56. package/bundled/skills/{prizmkit-api-doc-generator → prizmkit-tool-api-doc-generator}/SKILL.md +4 -5
  57. package/bundled/skills/{prizmkit-bug-reproducer → prizmkit-tool-bug-reproducer}/SKILL.md +4 -5
  58. package/bundled/skills/{prizmkit-ci-cd-generator → prizmkit-tool-ci-cd-generator}/SKILL.md +4 -5
  59. package/bundled/skills/{prizmkit-db-migration → prizmkit-tool-db-migration}/SKILL.md +4 -5
  60. package/bundled/skills/{prizmkit-dependency-health → prizmkit-tool-dependency-health}/SKILL.md +3 -4
  61. package/bundled/skills/{prizmkit-deployment-strategy → prizmkit-tool-deployment-strategy}/SKILL.md +4 -5
  62. package/bundled/skills/{prizmkit-error-triage → prizmkit-tool-error-triage}/SKILL.md +4 -5
  63. package/bundled/skills/{prizmkit-log-analyzer → prizmkit-tool-log-analyzer}/SKILL.md +4 -5
  64. package/bundled/skills/{prizmkit-monitoring-setup → prizmkit-tool-monitoring-setup}/SKILL.md +4 -5
  65. package/bundled/skills/{prizmkit-onboarding-generator → prizmkit-tool-onboarding-generator}/SKILL.md +4 -5
  66. package/bundled/skills/{prizmkit-perf-profiler → prizmkit-tool-perf-profiler}/SKILL.md +4 -5
  67. package/bundled/skills/{prizmkit-security-audit → prizmkit-tool-security-audit}/SKILL.md +3 -4
  68. package/bundled/skills/{prizmkit-tech-debt-tracker → prizmkit-tool-tech-debt-tracker}/SKILL.md +3 -4
  69. package/bundled/skills/refactor-skill/SKILL.md +371 -0
  70. package/bundled/skills/refactor-workflow/SKILL.md +17 -119
  71. package/package.json +1 -1
  72. package/src/external-skills.js +71 -0
  73. package/src/index.js +62 -4
  74. package/src/metadata.js +36 -0
  75. package/src/scaffold.js +136 -32
  76. package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +0 -356
  77. package/bundled/templates/claude-md-template.md +0 -38
  78. package/bundled/templates/codebuddy-md-template.md +0 -35
  79. /package/bundled/skills/{prizmkit-adr-manager → prizmkit-tool-adr-manager}/assets/adr-template.md +0 -0
@@ -17,9 +17,14 @@ set -euo pipefail
17
17
  # MAX_RETRIES Max retries per bug (default: 3)
18
18
  # SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
19
19
  # AI_CLI AI CLI command name (auto-detected: cbc or claude)
20
+ # CODEBUDDY_CLI Legacy alias for AI_CLI (deprecated, use AI_CLI instead)
21
+ # PRIZMKIT_PLATFORM Force platform: 'codebuddy' or 'claude' (auto-detected)
20
22
  # VERBOSE Set to 1 to enable --verbose on AI CLI
21
23
  # HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
22
24
  # HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)
25
+ # LOG_CLEANUP_ENABLED Run periodic log cleanup (default: 1)
26
+ # LOG_RETENTION_DAYS Delete logs older than N days (default: 14)
27
+ # LOG_MAX_TOTAL_MB Keep total logs under N MB via oldest-first cleanup (default: 1024)
23
28
  # ============================================================
24
29
 
25
30
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -31,31 +36,14 @@ MAX_RETRIES=${MAX_RETRIES:-3}
31
36
  SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
32
37
  HEARTBEAT_STALE_THRESHOLD=${HEARTBEAT_STALE_THRESHOLD:-600}
33
38
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
39
+ LOG_CLEANUP_ENABLED=${LOG_CLEANUP_ENABLED:-1}
40
+ LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
41
+ LOG_MAX_TOTAL_MB=${LOG_MAX_TOTAL_MB:-1024}
34
42
  VERBOSE=${VERBOSE:-0}
35
43
 
36
- # AI CLI detection
37
- if [[ -n "${AI_CLI:-}" ]]; then
38
- CLI_CMD="$AI_CLI"
39
- elif [[ -n "${CODEBUDDY_CLI:-}" ]]; then
40
- CLI_CMD="$CODEBUDDY_CLI"
41
- elif command -v cbc &>/dev/null; then
42
- CLI_CMD="cbc"
43
- elif command -v claude &>/dev/null; then
44
- CLI_CMD="claude"
45
- else
46
- echo "ERROR: No AI CLI found. Install CodeBuddy (cbc) or Claude Code (claude)." >&2
47
- exit 1
48
- fi
49
-
50
- # Platform detection
51
- if [[ -n "${PRIZMKIT_PLATFORM:-}" ]]; then
52
- PLATFORM="$PRIZMKIT_PLATFORM"
53
- elif [[ "$CLI_CMD" == *"claude"* ]]; then
54
- PLATFORM="claude"
55
- else
56
- PLATFORM="codebuddy"
57
- fi
58
- export PRIZMKIT_PLATFORM="$PLATFORM"
44
+ # Source shared common helpers (CLI/platform detection + logs + deps)
45
+ source "$SCRIPT_DIR/lib/common.sh"
46
+ prizm_detect_cli_and_platform
59
47
 
60
48
  # Source shared heartbeat library
61
49
  source "$SCRIPT_DIR/lib/heartbeat.sh"
@@ -66,20 +54,6 @@ detect_stream_json_support "$CLI_CMD"
66
54
  # Bug list path (set in main, used by cleanup trap)
67
55
  BUG_LIST=""
68
56
 
69
- # Colors
70
- RED='\033[0;31m'
71
- GREEN='\033[0;32m'
72
- YELLOW='\033[1;33m'
73
- BLUE='\033[0;34m'
74
- MAGENTA='\033[0;35m'
75
- BOLD='\033[1m'
76
- NC='\033[0m'
77
-
78
- log_info() { echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
79
- log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
80
- log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
81
- log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
82
-
83
57
  # ============================================================
84
58
  # Shared: Spawn AI CLI session and wait for result
85
59
  # ============================================================
@@ -180,6 +154,20 @@ spawn_and_wait_session() {
180
154
  session_status="crashed"
181
155
  fi
182
156
 
157
+ if [[ "$session_status" == "success" ]]; then
158
+ local project_root
159
+ project_root="$(cd "$SCRIPT_DIR/.." && pwd)"
160
+ if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
161
+ local dirty_files=""
162
+ dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
163
+ if [[ -n "$dirty_files" ]]; then
164
+ log_error "Session reported success but git working tree is not clean."
165
+ echo "$dirty_files" | sed 's/^/ - /'
166
+ session_status="failed"
167
+ fi
168
+ fi
169
+ fi
170
+
183
171
  log_info "Session result: $session_status"
184
172
 
185
173
  # Update bug status
@@ -220,13 +208,29 @@ trap cleanup SIGINT SIGTERM
220
208
  # ============================================================
221
209
 
222
210
  check_dependencies() {
223
- if ! command -v jq &>/dev/null; then
224
- log_error "jq is required but not installed. Install with: brew install jq"
225
- exit 1
211
+ prizm_check_common_dependencies "$CLI_CMD"
212
+ }
213
+
214
+ run_log_cleanup() {
215
+ if [[ "$LOG_CLEANUP_ENABLED" != "1" ]]; then
216
+ return 0
226
217
  fi
227
- if ! command -v python3 &>/dev/null; then
228
- log_error "python3 is required but not installed."
229
- exit 1
218
+
219
+ local cleanup_result
220
+ cleanup_result=$(python3 "$SCRIPTS_DIR/cleanup-logs.py" \
221
+ --state-dir "$STATE_DIR" \
222
+ --retention-days "$LOG_RETENTION_DAYS" \
223
+ --max-total-mb "$LOG_MAX_TOTAL_MB" 2>/dev/null) || {
224
+ log_warn "Log cleanup failed (continuing)"
225
+ return 0
226
+ }
227
+
228
+ local deleted reclaimed_kb
229
+ deleted=$(echo "$cleanup_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('deleted_files', 0))" 2>/dev/null || echo "0")
230
+ reclaimed_kb=$(echo "$cleanup_result" | python3 -c "import sys,json; print(int(json.load(sys.stdin).get('reclaimed_bytes', 0)/1024))" 2>/dev/null || echo "0")
231
+
232
+ if [[ "$deleted" -gt 0 ]]; then
233
+ log_info "Log cleanup: deleted $deleted files, reclaimed ${reclaimed_kb}KB"
230
234
  fi
231
235
  }
232
236
 
@@ -269,6 +273,7 @@ run_one() {
269
273
  fi
270
274
 
271
275
  check_dependencies
276
+ run_log_cleanup
272
277
 
273
278
  # Initialize state if needed
274
279
  if [[ ! -f "$STATE_DIR/pipeline.json" ]]; then
@@ -285,14 +290,14 @@ run_one() {
285
290
  local bug_title
286
291
  bug_title=$(python3 -c "
287
292
  import json, sys
288
- with open('$bug_list') as f:
293
+ with open(sys.argv[1]) as f:
289
294
  data = json.load(f)
290
295
  for bug in data.get('bugs', []):
291
- if bug.get('id') == '$bug_id':
296
+ if bug.get('id') == sys.argv[2]:
292
297
  print(bug.get('title', ''))
293
298
  sys.exit(0)
294
299
  sys.exit(1)
295
- " 2>/dev/null) || {
300
+ " "$bug_list" "$bug_id" 2>/dev/null) || {
296
301
  log_error "Bug $bug_id not found in $bug_list"
297
302
  exit 1
298
303
  }
@@ -300,14 +305,14 @@ sys.exit(1)
300
305
  local bug_severity
301
306
  bug_severity=$(python3 -c "
302
307
  import json, sys
303
- with open('$bug_list') as f:
308
+ with open(sys.argv[1]) as f:
304
309
  data = json.load(f)
305
310
  for bug in data.get('bugs', []):
306
- if bug.get('id') == '$bug_id':
311
+ if bug.get('id') == sys.argv[2]:
307
312
  print(bug.get('severity', 'medium'))
308
313
  sys.exit(0)
309
314
  sys.exit(1)
310
- " 2>/dev/null) || bug_severity="medium"
315
+ " "$bug_list" "$bug_id" 2>/dev/null) || bug_severity="medium"
311
316
 
312
317
  # Reset bug status
313
318
  python3 "$SCRIPTS_DIR/update-bug-status.py" \
@@ -362,14 +367,14 @@ sys.exit(1)
362
367
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
363
368
  echo ""
364
369
 
365
- cleanup() {
370
+ cleanup_single_bug() {
366
371
  echo ""
367
372
  log_warn "Interrupted. Killing session..."
368
373
  kill 0 2>/dev/null || true
369
374
  log_info "Session log: $session_dir/logs/session.log"
370
375
  exit 130
371
376
  }
372
- trap cleanup SIGINT SIGTERM
377
+ trap cleanup_single_bug SIGINT SIGTERM
373
378
 
374
379
  _SPAWN_RESULT=""
375
380
  spawn_and_wait_session \
@@ -410,6 +415,7 @@ main() {
410
415
  fi
411
416
 
412
417
  check_dependencies
418
+ run_log_cleanup
413
419
 
414
420
  # Initialize pipeline state if needed
415
421
  if [[ ! -f "$STATE_DIR/pipeline.json" ]]; then
@@ -468,6 +474,7 @@ main() {
468
474
  log_success " All bugs processed! Bug fix pipeline finished."
469
475
  log_success " Total sessions: $session_count"
470
476
  log_success "════════════════════════════════════════════════════"
477
+ rm -f "$STATE_DIR/current-session.json"
471
478
  break
472
479
  fi
473
480
 
@@ -515,18 +522,22 @@ main() {
515
522
  --state-dir "$STATE_DIR" \
516
523
  --output "$bootstrap_prompt" >/dev/null 2>&1
517
524
 
518
- # Track current session
525
+ # Track current session (atomic write via temp file)
519
526
  python3 -c "
520
- import json
527
+ import json, sys, os
521
528
  from datetime import datetime
529
+ bug_id, session_id, state_dir = sys.argv[1], sys.argv[2], sys.argv[3]
522
530
  data = {
523
- 'bug_id': '$bug_id',
524
- 'session_id': '$session_id',
531
+ 'bug_id': bug_id,
532
+ 'session_id': session_id,
525
533
  'started_at': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
526
534
  }
527
- with open('$STATE_DIR/current-session.json', 'w') as f:
535
+ target = os.path.join(state_dir, 'current-session.json')
536
+ tmp = target + '.tmp'
537
+ with open(tmp, 'w') as f:
528
538
  json.dump(data, f, indent=2)
529
- "
539
+ os.replace(tmp, target)
540
+ " "$bug_id" "$session_id" "$STATE_DIR"
530
541
 
531
542
  # Spawn session
532
543
  log_info "Spawning AI CLI session: $session_id"
@@ -566,6 +577,9 @@ show_help() {
566
577
  echo " AI_CLI AI CLI command name (auto-detected: cbc or claude)"
567
578
  echo " VERBOSE Set to 1 for verbose AI CLI output"
568
579
  echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
580
+ echo " LOG_CLEANUP_ENABLED Run log cleanup before execution (default: 1)"
581
+ echo " LOG_RETENTION_DAYS Delete logs older than N days (default: 14)"
582
+ echo " LOG_MAX_TOTAL_MB Keep total logs under N MB (default: 1024)"
569
583
  echo ""
570
584
  echo "Examples:"
571
585
  echo " ./run-bugfix.sh run # Run all bugs"
@@ -17,12 +17,15 @@ set -euo pipefail
17
17
  # Environment Variables:
18
18
  # MAX_RETRIES Max retries per feature (default: 3)
19
19
  # SESSION_TIMEOUT Session timeout in seconds (default: 0 = no limit)
20
- # AI_CLI AI CLI command name (auto-detected: cbc or claude)
20
+ # AI_CLI AI CLI command name (override; also readable from .prizmkit/config.json)
21
21
  # CODEBUDDY_CLI Legacy alias for AI_CLI (deprecated, use AI_CLI instead)
22
22
  # PRIZMKIT_PLATFORM Force platform: 'codebuddy' or 'claude' (auto-detected)
23
23
  # VERBOSE Set to 1 to enable --verbose on AI CLI (shows subagent output)
24
24
  # HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)
25
25
  # HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)
26
+ # LOG_CLEANUP_ENABLED Run periodic log cleanup (default: 1)
27
+ # LOG_RETENTION_DAYS Delete logs older than N days (default: 14)
28
+ # LOG_MAX_TOTAL_MB Keep total logs under N MB via oldest-first cleanup (default: 1024)
26
29
  # ============================================================
27
30
 
28
31
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -34,32 +37,14 @@ MAX_RETRIES=${MAX_RETRIES:-3}
34
37
  SESSION_TIMEOUT=${SESSION_TIMEOUT:-0}
35
38
  HEARTBEAT_STALE_THRESHOLD=${HEARTBEAT_STALE_THRESHOLD:-600}
36
39
  HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-30}
40
+ LOG_CLEANUP_ENABLED=${LOG_CLEANUP_ENABLED:-1}
41
+ LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
42
+ LOG_MAX_TOTAL_MB=${LOG_MAX_TOTAL_MB:-1024}
37
43
  VERBOSE=${VERBOSE:-0}
38
44
 
39
- # AI CLI detection: AI_CLI > CODEBUDDY_CLI > auto-detect > error
40
- if [[ -n "${AI_CLI:-}" ]]; then
41
- CLI_CMD="$AI_CLI"
42
- elif [[ -n "${CODEBUDDY_CLI:-}" ]]; then
43
- CLI_CMD="$CODEBUDDY_CLI"
44
- elif command -v cbc &>/dev/null; then
45
- CLI_CMD="cbc"
46
- elif command -v claude &>/dev/null; then
47
- CLI_CMD="claude"
48
- else
49
- echo "ERROR: No AI CLI found. Install CodeBuddy (cbc) or Claude Code (claude)." >&2
50
- exit 1
51
- fi
52
-
53
- # Platform detection
54
- if [[ -n "${PRIZMKIT_PLATFORM:-}" ]]; then
55
- PLATFORM="$PRIZMKIT_PLATFORM"
56
- elif [[ "$CLI_CMD" == *"claude"* ]]; then
57
- PLATFORM="claude"
58
- else
59
- PLATFORM="codebuddy"
60
- fi
61
-
62
- export PRIZMKIT_PLATFORM="$PLATFORM"
45
+ # Source shared common helpers (CLI/platform detection + logs + deps)
46
+ source "$SCRIPT_DIR/lib/common.sh"
47
+ prizm_detect_cli_and_platform
63
48
 
64
49
  # Source shared heartbeat library
65
50
  source "$SCRIPT_DIR/lib/heartbeat.sh"
@@ -70,19 +55,6 @@ detect_stream_json_support "$CLI_CMD"
70
55
  # Feature list path (set in main, used by cleanup trap)
71
56
  FEATURE_LIST=""
72
57
 
73
- # Colors for output
74
- RED='\033[0;31m'
75
- GREEN='\033[0;32m'
76
- YELLOW='\033[1;33m'
77
- BLUE='\033[0;34m'
78
- BOLD='\033[1m'
79
- NC='\033[0m'
80
-
81
- log_info() { echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
82
- log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
83
- log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
84
- log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
85
-
86
58
  # ============================================================
87
59
  # Shared: Spawn an AI CLI session and wait for result
88
60
  # ============================================================
@@ -199,6 +171,49 @@ spawn_and_wait_session() {
199
171
  session_status="crashed"
200
172
  fi
201
173
 
174
+ if [[ "$session_status" == "success" ]]; then
175
+ local project_root
176
+ project_root="$(cd "$SCRIPT_DIR/.." && pwd)"
177
+ if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
178
+ local dirty_files=""
179
+ dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
180
+ if [[ -n "$dirty_files" ]]; then
181
+ log_warn "Session reported success but git working tree is not clean."
182
+ echo "$dirty_files" | sed 's/^/ - /'
183
+ session_status="commit_missing"
184
+ else
185
+ # Bugfix commits (fix: / fix(scope):) are exempt from docs check.
186
+ local last_commit_subject=""
187
+ last_commit_subject=$(git -C "$project_root" log -1 --pretty=%s 2>/dev/null || true)
188
+ if [[ "$last_commit_subject" =~ ^fix(\(|:) ]]; then
189
+ log_info "Detected bugfix commit prefix; skipping docs check."
190
+ else
191
+ local docs_changed=""
192
+ docs_changed=$(git -C "$project_root" log --name-only --format="" -1 2>/dev/null \
193
+ | grep -E '(\.prizm-docs/|\.prizmkit/specs/REGISTRY\.md)' | head -1 || true)
194
+ if [[ -z "$docs_changed" ]]; then
195
+ log_warn "Session committed but no .prizm-docs or REGISTRY.md changes detected."
196
+ session_status="docs_missing"
197
+ fi
198
+ fi
199
+ fi
200
+ fi
201
+ fi
202
+
203
+ # Persist final pipeline-resolved status back to session-status.json
204
+ # only for post-check statuses introduced by the pipeline itself.
205
+ if [[ -f "$session_status_file" && ( "$session_status" == "commit_missing" || "$session_status" == "docs_missing" ) ]]; then
206
+ python3 -c "
207
+ import json, sys
208
+ p, st = sys.argv[1], sys.argv[2]
209
+ with open(p, 'r', encoding='utf-8') as f:
210
+ data = json.load(f)
211
+ data['status'] = st
212
+ with open(p, 'w', encoding='utf-8') as f:
213
+ json.dump(data, f, indent=2, ensure_ascii=False)
214
+ " "$session_status_file" "$session_status" >/dev/null 2>&1 || true
215
+ fi
216
+
202
217
  log_info "Session result: $session_status"
203
218
 
204
219
  # Update feature status
@@ -240,23 +255,29 @@ trap cleanup SIGINT SIGTERM
240
255
  # ============================================================
241
256
 
242
257
  check_dependencies() {
243
- # Check for jq
244
- if ! command -v jq &>/dev/null; then
245
- log_error "jq is required but not installed. Install with: brew install jq"
246
- exit 1
247
- fi
258
+ prizm_check_common_dependencies "$CLI_CMD"
259
+ }
248
260
 
249
- # Check for python3
250
- if ! command -v python3 &>/dev/null; then
251
- log_error "python3 is required but not installed."
252
- exit 1
261
+ run_log_cleanup() {
262
+ if [[ "$LOG_CLEANUP_ENABLED" != "1" ]]; then
263
+ return 0
253
264
  fi
254
265
 
255
- # Check for AI CLI
256
- if ! command -v "$CLI_CMD" &>/dev/null; then
257
- log_warn "AI CLI '$CLI_CMD' not found in PATH."
258
- log_warn "Set AI_CLI environment variable to the correct command."
259
- log_warn "Continuing anyway (will fail when spawning sessions)..."
266
+ local cleanup_result
267
+ cleanup_result=$(python3 "$SCRIPTS_DIR/cleanup-logs.py" \
268
+ --state-dir "$STATE_DIR" \
269
+ --retention-days "$LOG_RETENTION_DAYS" \
270
+ --max-total-mb "$LOG_MAX_TOTAL_MB" 2>/dev/null) || {
271
+ log_warn "Log cleanup failed (continuing)"
272
+ return 0
273
+ }
274
+
275
+ local deleted reclaimed_kb
276
+ deleted=$(echo "$cleanup_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('deleted_files', 0))" 2>/dev/null || echo "0")
277
+ reclaimed_kb=$(echo "$cleanup_result" | python3 -c "import sys,json; print(int(json.load(sys.stdin).get('reclaimed_bytes', 0)/1024))" 2>/dev/null || echo "0")
278
+
279
+ if [[ "$deleted" -gt 0 ]]; then
280
+ log_info "Log cleanup: deleted $deleted files, reclaimed ${reclaimed_kb}KB"
260
281
  fi
261
282
  }
262
283
 
@@ -371,6 +392,7 @@ run_one() {
371
392
  fi
372
393
 
373
394
  check_dependencies
395
+ run_log_cleanup
374
396
 
375
397
  # Verify feature exists
376
398
  local feature_title
@@ -536,8 +558,8 @@ sys.exit(1)
536
558
  echo -e "${BOLD}════════════════════════════════════════════════════${NC}"
537
559
  echo ""
538
560
 
539
- # Override cleanup trap for single-feature mode
540
- cleanup() {
561
+ # Override cleanup trap for single-feature mode (use distinct name to avoid overwriting global cleanup)
562
+ cleanup_single_feature() {
541
563
  echo ""
542
564
  log_warn "Interrupted. Killing session..."
543
565
  # Kill all child processes
@@ -545,7 +567,7 @@ sys.exit(1)
545
567
  log_info "Session log: $session_dir/logs/session.log"
546
568
  exit 130
547
569
  }
548
- trap cleanup SIGINT SIGTERM
570
+ trap cleanup_single_feature SIGINT SIGTERM
549
571
 
550
572
  _SPAWN_RESULT=""
551
573
  spawn_and_wait_session \
@@ -589,6 +611,7 @@ main() {
589
611
  fi
590
612
 
591
613
  check_dependencies
614
+ run_log_cleanup
592
615
 
593
616
  # Initialize pipeline state if needed
594
617
  if [[ ! -f "$STATE_DIR/pipeline.json" ]]; then
@@ -669,6 +692,7 @@ for f in data.get('stuck_features', []):
669
692
  log_success " All features completed! Pipeline finished."
670
693
  log_success " Total sessions: $session_count"
671
694
  log_success "════════════════════════════════════════════════════"
695
+ rm -f "$STATE_DIR/current-session.json"
672
696
  break
673
697
  fi
674
698
 
@@ -715,7 +739,7 @@ for f in data.get('stuck_features', []):
715
739
  --state-dir "$STATE_DIR" \
716
740
  --output "$bootstrap_prompt" >/dev/null 2>&1
717
741
 
718
- # Update current session tracking
742
+ # Update current session tracking (atomic write via temp file)
719
743
  python3 -c "
720
744
  import json, sys, os
721
745
  from datetime import datetime
@@ -725,8 +749,11 @@ data = {
725
749
  'session_id': session_id,
726
750
  'started_at': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
727
751
  }
728
- with open(os.path.join(state_dir, 'current-session.json'), 'w') as f:
752
+ target = os.path.join(state_dir, 'current-session.json')
753
+ tmp = target + '.tmp'
754
+ with open(tmp, 'w') as f:
729
755
  json.dump(data, f, indent=2)
756
+ os.replace(tmp, target)
730
757
  " "$feature_id" "$session_id" "$STATE_DIR"
731
758
 
732
759
  # Mark feature as in-progress before spawning session
@@ -780,6 +807,9 @@ show_help() {
780
807
  echo " AI_CLI AI CLI command name (auto-detected: cbc or claude)"
781
808
  echo " HEARTBEAT_INTERVAL Heartbeat log interval in seconds (default: 30)"
782
809
  echo " HEARTBEAT_STALE_THRESHOLD Heartbeat stale threshold in seconds (default: 600)"
810
+ echo " LOG_CLEANUP_ENABLED Run log cleanup before execution (default: 1)"
811
+ echo " LOG_RETENTION_DAYS Delete logs older than N days (default: 14)"
812
+ echo " LOG_MAX_TOTAL_MB Keep total logs under N MB (default: 1024)"
783
813
  echo ""
784
814
  echo "Examples:"
785
815
  echo " ./run.sh run # Run all features"
@@ -13,9 +13,13 @@ import argparse
13
13
  import json
14
14
  import sys
15
15
 
16
+ from utils import setup_logging
17
+
16
18
 
17
19
  REQUIRED_FIELDS = ["session_id", "feature_id", "status", "timestamp"]
18
20
 
21
+ LOGGER = setup_logging("check-session-status")
22
+
19
23
 
20
24
  def parse_args():
21
25
  parser = argparse.ArgumentParser(
@@ -61,7 +65,8 @@ def validate_required_fields(data):
61
65
  def determine_status(data):
62
66
  """Determine the single-line status string from the parsed data.
63
67
 
64
- Returns one of: success, partial_resumable, partial_not_resumable, failed.
68
+ Returns one of: success, partial_resumable, partial_not_resumable,
69
+ failed, commit_missing, docs_missing.
65
70
  """
66
71
  status = data.get("status", "")
67
72
 
@@ -75,6 +80,10 @@ def determine_status(data):
75
80
  return "partial_not_resumable"
76
81
  elif status == "failed":
77
82
  return "failed"
83
+ elif status == "commit_missing":
84
+ return "commit_missing"
85
+ elif status == "docs_missing":
86
+ return "docs_missing"
78
87
  else:
79
88
  # Unknown status value — treat as crashed
80
89
  return "crashed"
@@ -155,4 +164,40 @@ def main():
155
164
 
156
165
 
157
166
  if __name__ == "__main__":
158
- main()
167
+ try:
168
+ main()
169
+ except KeyboardInterrupt:
170
+ detail = {
171
+ "status": "crashed",
172
+ "feature_id": None,
173
+ "completed_phases": [],
174
+ "checkpoint_reached": None,
175
+ "tasks_completed": 0,
176
+ "tasks_total": 0,
177
+ "error_count": 1,
178
+ "can_resume": False,
179
+ "resume_from_phase": None,
180
+ "internal_error": "Interrupted",
181
+ }
182
+ sys.stderr.write(json.dumps(detail, indent=2, ensure_ascii=False) + "\n")
183
+ print("crashed")
184
+ sys.exit(0)
185
+ except SystemExit:
186
+ raise
187
+ except Exception as exc:
188
+ LOGGER.exception("Unhandled exception in check-session-status")
189
+ detail = {
190
+ "status": "crashed",
191
+ "feature_id": None,
192
+ "completed_phases": [],
193
+ "checkpoint_reached": None,
194
+ "tasks_completed": 0,
195
+ "tasks_total": 0,
196
+ "error_count": 1,
197
+ "can_resume": False,
198
+ "resume_from_phase": None,
199
+ "internal_error": str(exc),
200
+ }
201
+ sys.stderr.write(json.dumps(detail, indent=2, ensure_ascii=False) + "\n")
202
+ print("crashed")
203
+ sys.exit(0)