prizmkit 1.0.13 → 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 (77) 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/run-bugfix.sh +57 -57
  14. package/bundled/dev-pipeline/run.sh +75 -59
  15. package/bundled/dev-pipeline/scripts/check-session-status.py +47 -2
  16. package/bundled/dev-pipeline/scripts/cleanup-logs.py +192 -0
  17. package/bundled/dev-pipeline/scripts/detect-stuck.py +15 -3
  18. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +32 -27
  19. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +23 -23
  20. package/bundled/dev-pipeline/scripts/update-feature-status.py +50 -2
  21. package/bundled/dev-pipeline/scripts/utils.py +22 -0
  22. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +18 -1
  23. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +19 -1
  24. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +18 -2
  25. package/bundled/dev-pipeline/templates/session-status-schema.json +7 -1
  26. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  27. package/bundled/dev-pipeline/tests/conftest.py +133 -0
  28. package/bundled/dev-pipeline/tests/test_check_session.py +127 -0
  29. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +119 -0
  30. package/bundled/dev-pipeline/tests/test_detect_stuck.py +207 -0
  31. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +181 -0
  32. package/bundled/dev-pipeline/tests/test_generate_prompt.py +190 -0
  33. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +153 -0
  34. package/bundled/dev-pipeline/tests/test_init_pipeline.py +241 -0
  35. package/bundled/dev-pipeline/tests/test_update_bug_status.py +142 -0
  36. package/bundled/dev-pipeline/tests/test_update_feature_status.py +277 -0
  37. package/bundled/dev-pipeline/tests/test_utils.py +141 -0
  38. package/bundled/rules/USAGE.md +153 -0
  39. package/bundled/rules/_rules-metadata.json +43 -0
  40. package/bundled/rules/general/prefer-linux-commands.md +9 -0
  41. package/bundled/rules/prizm/prizm-commit-workflow.md +10 -0
  42. package/bundled/rules/prizm/prizm-documentation.md +19 -0
  43. package/bundled/rules/prizm/prizm-progressive-loading.md +11 -0
  44. package/bundled/skills/_metadata.json +130 -67
  45. package/bundled/skills/app-planner/SKILL.md +252 -499
  46. package/bundled/skills/app-planner/assets/evaluation-guide.md +44 -0
  47. package/bundled/skills/app-planner/scripts/validate-and-generate.py +143 -4
  48. package/bundled/skills/bug-planner/SKILL.md +58 -13
  49. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -7
  50. package/bundled/skills/dev-pipeline-launcher/SKILL.md +16 -7
  51. package/bundled/skills/feature-workflow/SKILL.md +175 -234
  52. package/bundled/skills/prizm-kit/SKILL.md +17 -31
  53. package/bundled/skills/{prizmkit-adr-manager → prizmkit-tool-adr-manager}/SKILL.md +6 -7
  54. package/bundled/skills/{prizmkit-api-doc-generator → prizmkit-tool-api-doc-generator}/SKILL.md +4 -5
  55. package/bundled/skills/{prizmkit-bug-reproducer → prizmkit-tool-bug-reproducer}/SKILL.md +4 -5
  56. package/bundled/skills/{prizmkit-ci-cd-generator → prizmkit-tool-ci-cd-generator}/SKILL.md +4 -5
  57. package/bundled/skills/{prizmkit-db-migration → prizmkit-tool-db-migration}/SKILL.md +4 -5
  58. package/bundled/skills/{prizmkit-dependency-health → prizmkit-tool-dependency-health}/SKILL.md +3 -4
  59. package/bundled/skills/{prizmkit-deployment-strategy → prizmkit-tool-deployment-strategy}/SKILL.md +4 -5
  60. package/bundled/skills/{prizmkit-error-triage → prizmkit-tool-error-triage}/SKILL.md +4 -5
  61. package/bundled/skills/{prizmkit-log-analyzer → prizmkit-tool-log-analyzer}/SKILL.md +4 -5
  62. package/bundled/skills/{prizmkit-monitoring-setup → prizmkit-tool-monitoring-setup}/SKILL.md +4 -5
  63. package/bundled/skills/{prizmkit-onboarding-generator → prizmkit-tool-onboarding-generator}/SKILL.md +4 -5
  64. package/bundled/skills/{prizmkit-perf-profiler → prizmkit-tool-perf-profiler}/SKILL.md +4 -5
  65. package/bundled/skills/{prizmkit-security-audit → prizmkit-tool-security-audit}/SKILL.md +3 -4
  66. package/bundled/skills/{prizmkit-tech-debt-tracker → prizmkit-tool-tech-debt-tracker}/SKILL.md +3 -4
  67. package/bundled/skills/refactor-skill/SKILL.md +371 -0
  68. package/bundled/skills/refactor-workflow/SKILL.md +17 -119
  69. package/package.json +1 -1
  70. package/src/external-skills.js +71 -0
  71. package/src/index.js +62 -4
  72. package/src/metadata.js +36 -0
  73. package/src/scaffold.js +136 -32
  74. package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +0 -356
  75. package/bundled/templates/claude-md-template.md +0 -38
  76. package/bundled/templates/codebuddy-md-template.md +0 -35
  77. /package/bundled/skills/{prizmkit-adr-manager → prizmkit-tool-adr-manager}/assets/adr-template.md +0 -0
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ """Clean up pipeline session logs by age and total size.
3
+
4
+ Targets files under any `.../sessions/.../logs/` directory inside a state dir.
5
+
6
+ Policies:
7
+ 1) Remove files older than retention window.
8
+ 2) If total remaining size still exceeds max threshold, remove oldest files first
9
+ until within threshold.
10
+
11
+ Usage:
12
+ python3 cleanup-logs.py --state-dir dev-pipeline/state
13
+ python3 cleanup-logs.py --state-dir dev-pipeline/bugfix-state --retention-days 30 --max-total-mb 2048
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import time
20
+
21
+ from utils import error_out, setup_logging
22
+
23
+ LOGGER = setup_logging("cleanup-logs")
24
+
25
+
26
+ def parse_args():
27
+ parser = argparse.ArgumentParser(description="Cleanup pipeline logs by age and total size.")
28
+ parser.add_argument("--state-dir", required=True, help="State directory to scan")
29
+ parser.add_argument(
30
+ "--retention-days",
31
+ type=int,
32
+ default=14,
33
+ help="Delete logs older than this many days (default: 14)",
34
+ )
35
+ parser.add_argument(
36
+ "--max-total-mb",
37
+ type=int,
38
+ default=1024,
39
+ help="Target max total log size in MB after cleanup (default: 1024)",
40
+ )
41
+ parser.add_argument("--dry-run", action="store_true", help="Report actions without deleting")
42
+ return parser.parse_args()
43
+
44
+
45
+ def iter_log_files(state_dir):
46
+ """Yield absolute paths of files inside .../sessions/.../logs/ directories."""
47
+ for root, _dirs, files in os.walk(state_dir):
48
+ if os.path.basename(root) != "logs":
49
+ continue
50
+
51
+ normalized = root.replace("\\", "/")
52
+ if "/sessions/" not in normalized:
53
+ continue
54
+
55
+ for name in files:
56
+ if name == ".DS_Store":
57
+ continue
58
+ yield os.path.join(root, name)
59
+
60
+
61
+ def file_info(path):
62
+ """Return file metadata dict with path, size, and mtime."""
63
+ st = os.stat(path)
64
+ return {"path": path, "size": st.st_size, "mtime": st.st_mtime}
65
+
66
+
67
+ def remove_file(path, dry_run=False):
68
+ if dry_run:
69
+ return True
70
+ try:
71
+ os.remove(path)
72
+ return True
73
+ except OSError:
74
+ return False
75
+
76
+
77
+ def cleanup_empty_dirs(state_dir, dry_run=False):
78
+ """Remove empty logs directories bottom-up."""
79
+ removed = 0
80
+ for root, dirs, _files in os.walk(state_dir, topdown=False):
81
+ for d in dirs:
82
+ full = os.path.join(root, d)
83
+ if os.path.basename(full) != "logs":
84
+ continue
85
+ try:
86
+ if not os.listdir(full):
87
+ if not dry_run:
88
+ os.rmdir(full)
89
+ removed += 1
90
+ except OSError:
91
+ continue
92
+ return removed
93
+
94
+
95
+ def main():
96
+ args = parse_args()
97
+ state_dir = os.path.abspath(args.state_dir)
98
+
99
+ if not os.path.isdir(state_dir):
100
+ error_out("State directory not found: {}".format(state_dir), code=2)
101
+
102
+ if args.retention_days < 0:
103
+ error_out("retention-days must be >= 0", code=2)
104
+ if args.max_total_mb < 0:
105
+ error_out("max-total-mb must be >= 0", code=2)
106
+
107
+ now = time.time()
108
+ retention_cutoff = now - (args.retention_days * 86400)
109
+ max_total_bytes = args.max_total_mb * 1024 * 1024
110
+
111
+ files = []
112
+ for path in iter_log_files(state_dir):
113
+ try:
114
+ files.append(file_info(path))
115
+ except OSError:
116
+ continue
117
+
118
+ initial_total = sum(f["size"] for f in files)
119
+
120
+ deleted_files = []
121
+ kept_files = []
122
+
123
+ # Step 1: age-based cleanup
124
+ for f in files:
125
+ if f["mtime"] < retention_cutoff:
126
+ if remove_file(f["path"], dry_run=args.dry_run):
127
+ deleted_files.append({**f, "reason": "retention"})
128
+ else:
129
+ kept_files.append(f)
130
+ else:
131
+ kept_files.append(f)
132
+
133
+ # Step 2: size-based cleanup (oldest first)
134
+ current_total = sum(f["size"] for f in kept_files)
135
+ if current_total > max_total_bytes:
136
+ kept_files.sort(key=lambda x: x["mtime"]) # oldest first
137
+ still_kept = []
138
+ for f in kept_files:
139
+ if current_total <= max_total_bytes:
140
+ still_kept.append(f)
141
+ continue
142
+
143
+ if remove_file(f["path"], dry_run=args.dry_run):
144
+ deleted_files.append({**f, "reason": "size"})
145
+ current_total -= f["size"]
146
+ else:
147
+ still_kept.append(f)
148
+
149
+ kept_files = still_kept
150
+
151
+ removed_empty_log_dirs = cleanup_empty_dirs(state_dir, dry_run=args.dry_run)
152
+
153
+ final_total = sum(f["size"] for f in kept_files)
154
+ reclaimed = initial_total - final_total
155
+
156
+ report = {
157
+ "success": True,
158
+ "state_dir": state_dir,
159
+ "dry_run": args.dry_run,
160
+ "retention_days": args.retention_days,
161
+ "max_total_mb": args.max_total_mb,
162
+ "initial_files": len(files),
163
+ "deleted_files": len(deleted_files),
164
+ "deleted_by_reason": {
165
+ "retention": sum(1 for f in deleted_files if f["reason"] == "retention"),
166
+ "size": sum(1 for f in deleted_files if f["reason"] == "size"),
167
+ },
168
+ "removed_empty_log_dirs": removed_empty_log_dirs,
169
+ "initial_total_bytes": initial_total,
170
+ "final_total_bytes": final_total,
171
+ "reclaimed_bytes": reclaimed,
172
+ }
173
+
174
+ LOGGER.info(
175
+ "cleanup complete: deleted=%s reclaimed=%sKB",
176
+ report["deleted_files"],
177
+ int(report["reclaimed_bytes"] / 1024),
178
+ )
179
+
180
+ print(json.dumps(report, indent=2, ensure_ascii=False))
181
+
182
+
183
+ if __name__ == "__main__":
184
+ try:
185
+ main()
186
+ except KeyboardInterrupt:
187
+ error_out("cleanup-logs interrupted", code=130)
188
+ except SystemExit:
189
+ raise
190
+ except Exception as exc:
191
+ LOGGER.exception("Unhandled exception in cleanup-logs")
192
+ error_out("cleanup-logs failed: {}".format(str(exc)), code=1)
@@ -21,6 +21,11 @@ import os
21
21
  import sys
22
22
  from datetime import datetime, timezone
23
23
 
24
+ from utils import error_out, setup_logging
25
+
26
+
27
+ LOGGER = setup_logging("detect-stuck")
28
+
24
29
 
25
30
  def parse_args():
26
31
  parser = argparse.ArgumentParser(
@@ -340,8 +345,7 @@ def main():
340
345
  state_dir = os.path.abspath(args.state_dir)
341
346
 
342
347
  if not os.path.isdir(state_dir):
343
- sys.stderr.write("Error: state directory not found: {}\n".format(state_dir))
344
- sys.exit(2)
348
+ error_out("State directory not found: {}".format(state_dir), code=2)
345
349
 
346
350
  # Determine which features to check
347
351
  if args.feature_id:
@@ -382,4 +386,12 @@ def main():
382
386
 
383
387
 
384
388
  if __name__ == "__main__":
385
- main()
389
+ try:
390
+ main()
391
+ except KeyboardInterrupt:
392
+ error_out("detect-stuck interrupted", code=130)
393
+ except SystemExit:
394
+ raise
395
+ except Exception as exc:
396
+ LOGGER.exception("Unhandled exception in detect-stuck")
397
+ error_out("detect-stuck failed: {}".format(str(exc)), code=1)
@@ -19,11 +19,13 @@ import os
19
19
  import re
20
20
  import sys
21
21
 
22
- from utils import load_json_file
22
+ from utils import load_json_file, setup_logging
23
23
 
24
24
 
25
25
  DEFAULT_MAX_RETRIES = 3
26
26
 
27
+ LOGGER = setup_logging("generate-bootstrap-prompt")
28
+
27
29
 
28
30
  def parse_args():
29
31
  parser = argparse.ArgumentParser(
@@ -359,10 +361,17 @@ def build_replacements(args, feature, features, global_context, script_dir):
359
361
 
360
362
  # Auto-detect platform if not set
361
363
  if not platform:
362
- if os.path.isdir(os.path.join(project_root, ".claude", "agents")):
364
+ has_claude = os.path.isdir(os.path.join(project_root, ".claude", "agents"))
365
+ has_codebuddy = os.path.isdir(os.path.join(project_root, ".codebuddy", "agents"))
366
+ if has_claude:
363
367
  platform = "claude"
364
- else:
368
+ elif has_codebuddy:
365
369
  platform = "codebuddy"
370
+ else:
371
+ raise RuntimeError(
372
+ "PrizmKit agents not found. Neither .claude/agents/ nor .codebuddy/agents/ exists. "
373
+ "Run `npx prizmkit install` first, or set PRIZMKIT_PLATFORM=claude|codebuddy explicitly."
374
+ )
366
375
 
367
376
  if platform == "claude":
368
377
  # Claude Code: agents in .claude/agents/, no native team config
@@ -498,6 +507,12 @@ def write_output(output_path, content):
498
507
  return None
499
508
 
500
509
 
510
+ def emit_failure(message):
511
+ """Emit standardized failure JSON and exit."""
512
+ print(json.dumps({"success": False, "error": message}, indent=2, ensure_ascii=False))
513
+ sys.exit(1)
514
+
515
+
501
516
  def main():
502
517
  args = parse_args()
503
518
 
@@ -538,38 +553,22 @@ def main():
538
553
  # Load template
539
554
  template_content, err = read_text_file(template_path)
540
555
  if err:
541
- output = {"success": False, "error": "Template error: {}".format(err)}
542
- print(json.dumps(output, indent=2, ensure_ascii=False))
543
- sys.exit(1)
556
+ emit_failure("Template error: {}".format(err))
544
557
 
545
558
  # Load feature list
546
559
  feature_list_data, err = load_json_file(args.feature_list)
547
560
  if err:
548
- output = {"success": False, "error": "Feature list error: {}".format(err)}
549
- print(json.dumps(output, indent=2, ensure_ascii=False))
550
- sys.exit(1)
561
+ emit_failure("Feature list error: {}".format(err))
551
562
 
552
563
  # Extract features array
553
564
  features = feature_list_data.get("features")
554
565
  if not isinstance(features, list):
555
- output = {
556
- "success": False,
557
- "error": "Feature list does not contain a 'features' array",
558
- }
559
- print(json.dumps(output, indent=2, ensure_ascii=False))
560
- sys.exit(1)
566
+ emit_failure("Feature list does not contain a 'features' array")
561
567
 
562
568
  # Find the target feature
563
569
  feature = find_feature(features, args.feature_id)
564
570
  if feature is None:
565
- output = {
566
- "success": False,
567
- "error": "Feature '{}' not found in feature list".format(
568
- args.feature_id
569
- ),
570
- }
571
- print(json.dumps(output, indent=2, ensure_ascii=False))
572
- sys.exit(1)
571
+ emit_failure("Feature '{}' not found in feature list".format(args.feature_id))
573
572
 
574
573
  # Extract global context
575
574
  global_context = feature_list_data.get("global_context", {})
@@ -592,9 +591,7 @@ def main():
592
591
  # Write the output
593
592
  err = write_output(args.output, rendered)
594
593
  if err:
595
- output = {"success": False, "error": err}
596
- print(json.dumps(output, indent=2, ensure_ascii=False))
597
- sys.exit(1)
594
+ emit_failure(err)
598
595
 
599
596
  # Success
600
597
  output = {
@@ -606,4 +603,12 @@ def main():
606
603
 
607
604
 
608
605
  if __name__ == "__main__":
609
- main()
606
+ try:
607
+ main()
608
+ except KeyboardInterrupt:
609
+ emit_failure("generate-bootstrap-prompt interrupted")
610
+ except SystemExit:
611
+ raise
612
+ except Exception as exc:
613
+ LOGGER.exception("Unhandled exception in generate-bootstrap-prompt")
614
+ emit_failure("Unexpected error: {}".format(str(exc)))
@@ -19,11 +19,13 @@ import os
19
19
  import re
20
20
  import sys
21
21
 
22
- from utils import load_json_file
22
+ from utils import load_json_file, setup_logging
23
23
 
24
24
 
25
25
  DEFAULT_MAX_RETRIES = 3
26
26
 
27
+ LOGGER = setup_logging("generate-bugfix-prompt")
28
+
27
29
 
28
30
  def parse_args():
29
31
  parser = argparse.ArgumentParser(
@@ -309,6 +311,12 @@ def write_output(output_path, content):
309
311
  return None
310
312
 
311
313
 
314
+ def emit_failure(message):
315
+ """Emit standardized failure JSON and exit."""
316
+ print(json.dumps({"success": False, "error": message}, indent=2, ensure_ascii=False))
317
+ sys.exit(1)
318
+
319
+
312
320
  def main():
313
321
  args = parse_args()
314
322
 
@@ -326,36 +334,22 @@ def main():
326
334
  # Load template
327
335
  template_content, err = read_text_file(template_path)
328
336
  if err:
329
- output = {"success": False, "error": "Template error: {}".format(err)}
330
- print(json.dumps(output, indent=2, ensure_ascii=False))
331
- sys.exit(1)
337
+ emit_failure("Template error: {}".format(err))
332
338
 
333
339
  # Load bug fix list
334
340
  bug_list_data, err = load_json_file(args.bug_list)
335
341
  if err:
336
- output = {"success": False, "error": "Bug list error: {}".format(err)}
337
- print(json.dumps(output, indent=2, ensure_ascii=False))
338
- sys.exit(1)
342
+ emit_failure("Bug list error: {}".format(err))
339
343
 
340
344
  # Extract bugs array
341
345
  bugs = bug_list_data.get("bugs")
342
346
  if not isinstance(bugs, list):
343
- output = {
344
- "success": False,
345
- "error": "Bug fix list does not contain a 'bugs' array",
346
- }
347
- print(json.dumps(output, indent=2, ensure_ascii=False))
348
- sys.exit(1)
347
+ emit_failure("Bug fix list does not contain a 'bugs' array")
349
348
 
350
349
  # Find the target bug
351
350
  bug = find_bug(bugs, args.bug_id)
352
351
  if bug is None:
353
- output = {
354
- "success": False,
355
- "error": "Bug '{}' not found in bug fix list".format(args.bug_id),
356
- }
357
- print(json.dumps(output, indent=2, ensure_ascii=False))
358
- sys.exit(1)
352
+ emit_failure("Bug '{}' not found in bug fix list".format(args.bug_id))
359
353
 
360
354
  # Extract global context
361
355
  global_context = bug_list_data.get("global_context", {})
@@ -371,9 +365,7 @@ def main():
371
365
  # Write the output
372
366
  err = write_output(args.output, rendered)
373
367
  if err:
374
- output = {"success": False, "error": err}
375
- print(json.dumps(output, indent=2, ensure_ascii=False))
376
- sys.exit(1)
368
+ emit_failure(err)
377
369
 
378
370
  # Success
379
371
  output = {
@@ -385,4 +377,12 @@ def main():
385
377
 
386
378
 
387
379
  if __name__ == "__main__":
388
- main()
380
+ try:
381
+ main()
382
+ except KeyboardInterrupt:
383
+ emit_failure("generate-bugfix-prompt interrupted")
384
+ except SystemExit:
385
+ raise
386
+ except Exception as exc:
387
+ LOGGER.exception("Unhandled exception in generate-bugfix-prompt")
388
+ emit_failure("Unexpected error: {}".format(str(exc)))
@@ -42,6 +42,8 @@ SESSION_STATUS_VALUES = [
42
42
  "failed",
43
43
  "crashed",
44
44
  "timed_out",
45
+ "commit_missing",
46
+ "docs_missing",
45
47
  ]
46
48
 
47
49
  TERMINAL_STATUSES = {"completed", "failed", "skipped"}
@@ -430,6 +432,25 @@ def action_update(args, feature_list_path, state_dir):
430
432
  if err:
431
433
  error_out("Failed to update feature-list.json: {}".format(err))
432
434
  return
435
+ elif session_status in ("commit_missing", "docs_missing"):
436
+ # Degraded outcome: keep artifacts for retry and expose specific status.
437
+ fs["retry_count"] = fs.get("retry_count", 0) + 1
438
+
439
+ if fs["retry_count"] >= max_retries:
440
+ fs["status"] = "failed"
441
+ target_status = "failed"
442
+ else:
443
+ fs["status"] = session_status
444
+ target_status = session_status
445
+
446
+ fs["resume_from_phase"] = None
447
+ fs["sessions"] = []
448
+ fs["last_session_id"] = None
449
+
450
+ err = update_feature_in_list(feature_list_path, feature_id, target_status)
451
+ if err:
452
+ error_out("Failed to update feature-list.json: {}".format(err))
453
+ return
433
454
  else:
434
455
  fs["retry_count"] = fs.get("retry_count", 0) + 1
435
456
 
@@ -479,7 +500,10 @@ def action_update(args, feature_list_path, state_dir):
479
500
  "resume_from_phase": fs.get("resume_from_phase"),
480
501
  "updated_at": fs["updated_at"],
481
502
  }
482
- if session_status != "success":
503
+ if session_status in ("commit_missing", "docs_missing"):
504
+ summary["degraded_reason"] = session_status
505
+ summary["restart_policy"] = "finalization_retry"
506
+ elif session_status != "success":
483
507
  summary["restart_policy"] = "full_restart"
484
508
  summary["cleanup_performed"] = cleaned
485
509
 
@@ -651,7 +675,15 @@ def action_status(feature_list_data, state_dir):
651
675
  app_name = feature_list_data.get("app_name", "Unknown")
652
676
 
653
677
  # Gather status info
654
- counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "skipped": 0}
678
+ counts = {
679
+ "completed": 0,
680
+ "in_progress": 0,
681
+ "failed": 0,
682
+ "pending": 0,
683
+ "skipped": 0,
684
+ "commit_missing": 0,
685
+ "docs_missing": 0,
686
+ }
655
687
  feature_lines = []
656
688
 
657
689
  # Build dependency info: feature_id -> list of dep_ids that are not completed
@@ -694,6 +726,10 @@ def action_status(feature_list_data, state_dir):
694
726
  icon = COLOR_RED + "[✗]" + COLOR_RESET
695
727
  elif fstatus == "skipped":
696
728
  icon = COLOR_GRAY + "[—]" + COLOR_RESET
729
+ elif fstatus == "commit_missing":
730
+ icon = COLOR_RED + "[↑]" + COLOR_RESET
731
+ elif fstatus == "docs_missing":
732
+ icon = COLOR_RED + "[D]" + COLOR_RESET
697
733
  else:
698
734
  icon = COLOR_GRAY + "[ ]" + COLOR_RESET
699
735
 
@@ -709,6 +745,10 @@ def action_status(feature_list_data, state_dir):
709
745
  detail = " ({})".format(", ".join(parts))
710
746
  elif fstatus == "failed":
711
747
  detail = " (failed after {} retries)".format(retry_count)
748
+ elif fstatus == "commit_missing":
749
+ detail = " (commit missing, retry {}/{})".format(retry_count, max_retries_val)
750
+ elif fstatus == "docs_missing":
751
+ detail = " (docs missing, retry {}/{})".format(retry_count, max_retries_val)
712
752
  elif fstatus == "pending":
713
753
  # Check if blocked by dependencies
714
754
  deps = feature.get("dependencies", [])
@@ -732,6 +772,10 @@ def action_status(feature_list_data, state_dir):
732
772
  line_content = "{} {} {}{}".format(
733
773
  fid, icon, COLOR_RED + title + COLOR_RESET, detail
734
774
  )
775
+ elif fstatus in ("commit_missing", "docs_missing"):
776
+ line_content = "{} {} {}{}".format(
777
+ fid, icon, COLOR_RED + title + COLOR_RESET, detail
778
+ )
735
779
  else:
736
780
  line_content = "{} {} {}{}".format(
737
781
  fid, icon, COLOR_GRAY + title + COLOR_RESET, detail
@@ -762,6 +806,9 @@ def action_status(feature_list_data, state_dir):
762
806
  summary_line2 = "Failed: {} | Pending: {} | Skipped: {}".format(
763
807
  counts["failed"], counts["pending"], counts["skipped"]
764
808
  )
809
+ summary_line3 = "Commit Missing: {} | Docs Missing: {}".format(
810
+ counts["commit_missing"], counts["docs_missing"]
811
+ )
765
812
 
766
813
  # 构建预估剩余时间行
767
814
  CONFIDENCE_ICONS = {"high": "●", "medium": "◐", "low": "○"}
@@ -782,6 +829,7 @@ def action_status(feature_list_data, state_dir):
782
829
  print("║" + pad_right(" App: {}".format(app_name), inner) + " ║")
783
830
  print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
784
831
  print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
832
+ print("║" + pad_right(" {}".format(summary_line3), inner) + " ║")
785
833
  print("╠" + "─" * BOX_WIDTH + "╣")
786
834
  print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
787
835
  print("║" + pad_right(" {}".format(eta_line), inner) + " ║")
@@ -6,6 +6,7 @@ to avoid duplication across pipeline scripts.
6
6
  """
7
7
 
8
8
  import json
9
+ import logging
9
10
  import os
10
11
  import sys
11
12
 
@@ -49,6 +50,27 @@ def write_json_file(path, data):
49
50
  return None
50
51
 
51
52
 
53
+ def setup_logging(name="prizmkit.dev_pipeline", level=None):
54
+ """Configure and return a standard logger for pipeline scripts.
55
+
56
+ Logs are written to stderr to avoid interfering with stdout JSON outputs.
57
+ """
58
+ resolved_level = (level or os.environ.get("PRIZMKIT_LOG_LEVEL", "INFO")).upper()
59
+ numeric_level = getattr(logging, resolved_level, logging.INFO)
60
+
61
+ root_logger = logging.getLogger()
62
+ if not root_logger.handlers:
63
+ logging.basicConfig(
64
+ level=numeric_level,
65
+ stream=sys.stderr,
66
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
67
+ )
68
+
69
+ logger = logging.getLogger(name)
70
+ logger.setLevel(numeric_level)
71
+ return logger
72
+
73
+
52
74
  def error_out(message, code=1):
53
75
  """Print an error JSON and exit with the given code."""
54
76
  output = {"error": message}
@@ -111,6 +111,21 @@ Key decisions: [list]
111
111
 
112
112
  **CP-2**: All acceptance criteria met, tests pass.
113
113
 
114
+ ### Phase 4.5: Prizm Doc Update (mandatory for feature sessions)
115
+
116
+ Run `prizmkit.doc.update` and sync project docs before commit:
117
+ 1. Use `git diff --cached --name-status` (fallback: `git diff --name-status`) to locate changed modules
118
+ 2. Update affected `.prizm-docs/` files (L1/L2, changelog.prizm)
119
+ 3. Stage documentation updates (`git add .prizm-docs/`) if changed
120
+
121
+ Doc maintenance pass condition (pipeline-enforced): `REGISTRY.md` **or** `.prizm-docs/` changed in the final commit.
122
+
123
+ ### Phase 4.7: Retrospective (feature sessions only, before commit)
124
+
125
+ If this session is a feature (not a bug-fix-only commit), run `prizmkit.retrospective` now — **before committing**.
126
+ Retrospective must update relevant `.prizm-docs/` sections (TRAPS/RULES/DECISIONS) when applicable, so those changes are included in the feature commit.
127
+ Stage any `.prizm-docs/` changes produced: `git add .prizm-docs/`
128
+
114
129
  ### Phase 5: Commit
115
130
 
116
131
  - Run `prizmkit.summarize` → archive to REGISTRY.md
@@ -136,7 +151,7 @@ Write to: `{{SESSION_STATUS_PATH}}`
136
151
  "feature_id": "{{FEATURE_ID}}",
137
152
  "feature_slug": "{{FEATURE_SLUG}}",
138
153
  "exec_tier": 1,
139
- "status": "<success|partial|failed>",
154
+ "status": "<success|partial|failed|commit_missing|docs_missing>",
140
155
  "completed_phases": [0, 1, 2, 3, 4, 5],
141
156
  "current_phase": 5,
142
157
  "checkpoint_reached": "CP-2",
@@ -145,6 +160,8 @@ Write to: `{{SESSION_STATUS_PATH}}`
145
160
  "errors": [],
146
161
  "can_resume": false,
147
162
  "resume_from_phase": null,
163
+ "docs_maintained": true,
164
+ "retrospective_done": true,
148
165
  "artifacts": {
149
166
  "context_snapshot_path": ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md",
150
167
  "plan_path": ".prizmkit/specs/{{FEATURE_SLUG}}/plan.md",
@@ -130,6 +130,7 @@ Prompt:
130
130
  > 2. Run prizmkit-code-review: verify all acceptance criteria, check code quality and correctness. Only read files mentioned in the Implementation Log.
131
131
  > 3. Run the test suite and report results.
132
132
  > 4. Append a 'Review Notes' section to `context-snapshot.md`: issues found (severity), test results, final verdict.
133
+ > 5. If review uncovers durable pitfalls or conventions, add corresponding TRAPS/RULES notes to relevant `.prizm-docs/` files.
133
134
  > Report verdict: PASS, PASS_WITH_WARNINGS, or NEEDS_FIXES."
134
135
 
135
136
  Wait for Reviewer to return.
@@ -137,6 +138,21 @@ Wait for Reviewer to return.
137
138
 
138
139
  **CP-2**: Tests pass, verdict is not NEEDS_FIXES.
139
140
 
141
+ ### Phase 4.5: Prizm Doc Update (mandatory for feature sessions)
142
+
143
+ Run `prizmkit.doc.update` and sync project docs before commit:
144
+ 1. Use `git diff --cached --name-status` (fallback: `git diff --name-status`) to locate changed modules
145
+ 2. Update affected `.prizm-docs/` files (L1/L2, changelog.prizm)
146
+ 3. Stage documentation updates (`git add .prizm-docs/`) if changed
147
+
148
+ Doc maintenance pass condition (pipeline-enforced): `REGISTRY.md` **or** `.prizm-docs/` changed in the final commit.
149
+
150
+ ### Phase 4.7: Retrospective (feature sessions only, before commit)
151
+
152
+ If this session is a feature (not a bug-fix-only commit), run `prizmkit.retrospective` now — **before committing**.
153
+ Retrospective must update relevant `.prizm-docs/` sections (TRAPS/RULES/DECISIONS) when applicable, so those changes are included in the feature commit.
154
+ Stage any `.prizm-docs/` changes produced: `git add .prizm-docs/`
155
+
140
156
  ### Phase 5: Commit
141
157
 
142
158
  - Run `prizmkit.summarize` → archive to REGISTRY.md
@@ -162,7 +178,7 @@ Write to: `{{SESSION_STATUS_PATH}}`
162
178
  "feature_id": "{{FEATURE_ID}}",
163
179
  "feature_slug": "{{FEATURE_SLUG}}",
164
180
  "exec_tier": 2,
165
- "status": "<success|partial|failed>",
181
+ "status": "<success|partial|failed|commit_missing|docs_missing>",
166
182
  "completed_phases": [0, 1, 2, 3, 4, 5],
167
183
  "current_phase": 5,
168
184
  "checkpoint_reached": "CP-2",
@@ -171,6 +187,8 @@ Write to: `{{SESSION_STATUS_PATH}}`
171
187
  "errors": [],
172
188
  "can_resume": false,
173
189
  "resume_from_phase": null,
190
+ "docs_maintained": true,
191
+ "retrospective_done": true,
174
192
  "artifacts": {
175
193
  "context_snapshot_path": ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md",
176
194
  "plan_path": ".prizmkit/specs/{{FEATURE_SLUG}}/plan.md",