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
@@ -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"}
@@ -401,12 +403,54 @@ def action_update(args, feature_list_path, state_dir):
401
403
  fs = load_feature_status(state_dir, feature_id)
402
404
 
403
405
  if session_status == "success":
406
+ # No-op guard: if this exact successful session was already recorded,
407
+ # avoid rewriting state files again (prevents post-commit dirty changes).
408
+ existing_sessions = fs.get("sessions", [])
409
+ already_completed = fs.get("status") == "completed" and fs.get("resume_from_phase") is None
410
+ same_session_already_recorded = (
411
+ session_id
412
+ and session_id in existing_sessions
413
+ and fs.get("last_session_id") == session_id
414
+ )
415
+ if already_completed and (same_session_already_recorded or not session_id):
416
+ summary = {
417
+ "action": "update",
418
+ "feature_id": feature_id,
419
+ "session_status": session_status,
420
+ "new_status": fs.get("status", "completed"),
421
+ "retry_count": fs.get("retry_count", 0),
422
+ "resume_from_phase": fs.get("resume_from_phase"),
423
+ "updated_at": fs.get("updated_at"),
424
+ "no_op": True,
425
+ }
426
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
427
+ return
428
+
404
429
  fs["status"] = "completed"
405
430
  fs["resume_from_phase"] = None
406
431
  err = update_feature_in_list(feature_list_path, feature_id, "completed")
407
432
  if err:
408
433
  error_out("Failed to update feature-list.json: {}".format(err))
409
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
410
454
  else:
411
455
  fs["retry_count"] = fs.get("retry_count", 0) + 1
412
456
 
@@ -456,7 +500,10 @@ def action_update(args, feature_list_path, state_dir):
456
500
  "resume_from_phase": fs.get("resume_from_phase"),
457
501
  "updated_at": fs["updated_at"],
458
502
  }
459
- 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":
460
507
  summary["restart_policy"] = "full_restart"
461
508
  summary["cleanup_performed"] = cleaned
462
509
 
@@ -628,7 +675,15 @@ def action_status(feature_list_data, state_dir):
628
675
  app_name = feature_list_data.get("app_name", "Unknown")
629
676
 
630
677
  # Gather status info
631
- 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
+ }
632
687
  feature_lines = []
633
688
 
634
689
  # Build dependency info: feature_id -> list of dep_ids that are not completed
@@ -671,6 +726,10 @@ def action_status(feature_list_data, state_dir):
671
726
  icon = COLOR_RED + "[✗]" + COLOR_RESET
672
727
  elif fstatus == "skipped":
673
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
674
733
  else:
675
734
  icon = COLOR_GRAY + "[ ]" + COLOR_RESET
676
735
 
@@ -686,6 +745,10 @@ def action_status(feature_list_data, state_dir):
686
745
  detail = " ({})".format(", ".join(parts))
687
746
  elif fstatus == "failed":
688
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)
689
752
  elif fstatus == "pending":
690
753
  # Check if blocked by dependencies
691
754
  deps = feature.get("dependencies", [])
@@ -709,6 +772,10 @@ def action_status(feature_list_data, state_dir):
709
772
  line_content = "{} {} {}{}".format(
710
773
  fid, icon, COLOR_RED + title + COLOR_RESET, detail
711
774
  )
775
+ elif fstatus in ("commit_missing", "docs_missing"):
776
+ line_content = "{} {} {}{}".format(
777
+ fid, icon, COLOR_RED + title + COLOR_RESET, detail
778
+ )
712
779
  else:
713
780
  line_content = "{} {} {}{}".format(
714
781
  fid, icon, COLOR_GRAY + title + COLOR_RESET, detail
@@ -739,6 +806,9 @@ def action_status(feature_list_data, state_dir):
739
806
  summary_line2 = "Failed: {} | Pending: {} | Skipped: {}".format(
740
807
  counts["failed"], counts["pending"], counts["skipped"]
741
808
  )
809
+ summary_line3 = "Commit Missing: {} | Docs Missing: {}".format(
810
+ counts["commit_missing"], counts["docs_missing"]
811
+ )
742
812
 
743
813
  # 构建预估剩余时间行
744
814
  CONFIDENCE_ICONS = {"high": "●", "medium": "◐", "low": "○"}
@@ -759,6 +829,7 @@ def action_status(feature_list_data, state_dir):
759
829
  print("║" + pad_right(" App: {}".format(app_name), inner) + " ║")
760
830
  print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
761
831
  print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
832
+ print("║" + pad_right(" {}".format(summary_line3), inner) + " ║")
762
833
  print("╠" + "─" * BOX_WIDTH + "╣")
763
834
  print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
764
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
@@ -122,6 +137,7 @@ Key decisions: [list]
122
137
  --feature-id "{{FEATURE_ID}}" --session-id "{{SESSION_ID}}" --action complete
123
138
  ```
124
139
  - Run `prizmkit.committer` → `feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`, do NOT push
140
+ - MANDATORY: commit must be done via `prizmkit.committer` skill. Do NOT run manual `git add`/`git commit` as a substitute.
125
141
 
126
142
  ---
127
143
 
@@ -135,7 +151,7 @@ Write to: `{{SESSION_STATUS_PATH}}`
135
151
  "feature_id": "{{FEATURE_ID}}",
136
152
  "feature_slug": "{{FEATURE_SLUG}}",
137
153
  "exec_tier": 1,
138
- "status": "<success|partial|failed>",
154
+ "status": "<success|partial|failed|commit_missing|docs_missing>",
139
155
  "completed_phases": [0, 1, 2, 3, 4, 5],
140
156
  "current_phase": 5,
141
157
  "checkpoint_reached": "CP-2",
@@ -144,6 +160,8 @@ Write to: `{{SESSION_STATUS_PATH}}`
144
160
  "errors": [],
145
161
  "can_resume": false,
146
162
  "resume_from_phase": null,
163
+ "docs_maintained": true,
164
+ "retrospective_done": true,
147
165
  "artifacts": {
148
166
  "context_snapshot_path": ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md",
149
167
  "plan_path": ".prizmkit/specs/{{FEATURE_SLUG}}/plan.md",
@@ -154,6 +172,23 @@ Write to: `{{SESSION_STATUS_PATH}}`
154
172
  }
155
173
  ```
156
174
 
175
+ ### Step 3.1: Final Clean Check (before exit)
176
+
177
+ After writing `session-status.json`, verify repository is clean:
178
+
179
+ ```bash
180
+ git status --short
181
+ ```
182
+
183
+ If any files remain, include them in the last commit:
184
+
185
+ ```bash
186
+ git add -A
187
+ git commit --amend --no-edit
188
+ ```
189
+
190
+ Re-check `git status --short` and ensure it is empty before exiting.
191
+
157
192
  ## Critical Paths
158
193
 
159
194
  | Resource | Path |
@@ -168,4 +203,5 @@ Write to: `{{SESSION_STATUS_PATH}}`
168
203
  - Tier 1: you do everything — no subagents, no TeamCreate
169
204
  - Build context-snapshot.md FIRST; use it throughout instead of re-reading files
170
205
  - ALWAYS write session-status.json before exiting
171
- - `prizmkit.committer` is mandatory — do NOT skip the commit phase
206
+ - `prizmkit.committer` is mandatory — do NOT skip the commit phase, and do NOT replace it with manual git commit commands
207
+ - Before exiting, `git status --short` must be empty