prizmkit 1.0.0 → 1.0.1

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 (89) hide show
  1. package/bundled/VERSION.json +5 -0
  2. package/bundled/adapters/claude/agent-adapter.js +108 -0
  3. package/bundled/adapters/claude/command-adapter.js +104 -0
  4. package/bundled/adapters/claude/paths.js +35 -0
  5. package/bundled/adapters/claude/rules-adapter.js +77 -0
  6. package/bundled/adapters/claude/settings-adapter.js +73 -0
  7. package/bundled/adapters/claude/team-adapter.js +183 -0
  8. package/bundled/adapters/codebuddy/agent-adapter.js +43 -0
  9. package/bundled/adapters/codebuddy/paths.js +29 -0
  10. package/bundled/adapters/codebuddy/settings-adapter.js +47 -0
  11. package/bundled/adapters/codebuddy/skill-adapter.js +68 -0
  12. package/bundled/adapters/codebuddy/team-adapter.js +46 -0
  13. package/bundled/adapters/shared/frontmatter.js +77 -0
  14. package/bundled/agents/prizm-dev-team-coordinator.md +142 -0
  15. package/bundled/agents/prizm-dev-team-dev.md +99 -0
  16. package/bundled/agents/prizm-dev-team-pm.md +114 -0
  17. package/bundled/agents/prizm-dev-team-reviewer.md +119 -0
  18. package/bundled/dev-pipeline/README.md +482 -0
  19. package/bundled/dev-pipeline/assets/feature-list-example.json +147 -0
  20. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +138 -0
  21. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +425 -0
  22. package/bundled/dev-pipeline/launch-daemon.sh +549 -0
  23. package/bundled/dev-pipeline/reset-feature.sh +209 -0
  24. package/bundled/dev-pipeline/retry-bug.sh +344 -0
  25. package/bundled/dev-pipeline/retry-feature.sh +338 -0
  26. package/bundled/dev-pipeline/run-bugfix.sh +638 -0
  27. package/bundled/dev-pipeline/run.sh +845 -0
  28. package/bundled/dev-pipeline/scripts/check-session-status.py +158 -0
  29. package/bundled/dev-pipeline/scripts/detect-stuck.py +385 -0
  30. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +598 -0
  31. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +402 -0
  32. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +294 -0
  33. package/bundled/dev-pipeline/scripts/init-dev-team.py +134 -0
  34. package/bundled/dev-pipeline/scripts/init-pipeline.py +335 -0
  35. package/bundled/dev-pipeline/scripts/update-bug-status.py +748 -0
  36. package/bundled/dev-pipeline/scripts/update-feature-status.py +1076 -0
  37. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +262 -0
  38. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +159 -0
  39. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +291 -0
  40. package/bundled/dev-pipeline/templates/feature-list-schema.json +112 -0
  41. package/bundled/dev-pipeline/templates/session-status-schema.json +77 -0
  42. package/bundled/skills/_metadata.json +267 -0
  43. package/bundled/skills/app-planner/SKILL.md +580 -0
  44. package/bundled/skills/app-planner/assets/planning-guide.md +313 -0
  45. package/bundled/skills/app-planner/scripts/validate-and-generate.py +758 -0
  46. package/bundled/skills/bug-planner/SKILL.md +235 -0
  47. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +252 -0
  48. package/bundled/skills/dev-pipeline-launcher/SKILL.md +223 -0
  49. package/bundled/skills/prizm-kit/SKILL.md +151 -0
  50. package/bundled/skills/prizm-kit/assets/claude-md-template.md +38 -0
  51. package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +35 -0
  52. package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +15 -0
  53. package/bundled/skills/prizmkit-adr-manager/SKILL.md +68 -0
  54. package/bundled/skills/prizmkit-adr-manager/assets/adr-template.md +26 -0
  55. package/bundled/skills/prizmkit-analyze/SKILL.md +194 -0
  56. package/bundled/skills/prizmkit-api-doc-generator/SKILL.md +56 -0
  57. package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +351 -0
  58. package/bundled/skills/prizmkit-bug-reproducer/SKILL.md +62 -0
  59. package/bundled/skills/prizmkit-ci-cd-generator/SKILL.md +54 -0
  60. package/bundled/skills/prizmkit-clarify/SKILL.md +52 -0
  61. package/bundled/skills/prizmkit-code-review/SKILL.md +70 -0
  62. package/bundled/skills/prizmkit-committer/SKILL.md +117 -0
  63. package/bundled/skills/prizmkit-db-migration/SKILL.md +65 -0
  64. package/bundled/skills/prizmkit-dependency-health/SKILL.md +123 -0
  65. package/bundled/skills/prizmkit-deployment-strategy/SKILL.md +58 -0
  66. package/bundled/skills/prizmkit-error-triage/SKILL.md +55 -0
  67. package/bundled/skills/prizmkit-implement/SKILL.md +47 -0
  68. package/bundled/skills/prizmkit-init/SKILL.md +156 -0
  69. package/bundled/skills/prizmkit-log-analyzer/SKILL.md +55 -0
  70. package/bundled/skills/prizmkit-monitoring-setup/SKILL.md +75 -0
  71. package/bundled/skills/prizmkit-onboarding-generator/SKILL.md +70 -0
  72. package/bundled/skills/prizmkit-perf-profiler/SKILL.md +55 -0
  73. package/bundled/skills/prizmkit-plan/SKILL.md +54 -0
  74. package/bundled/skills/prizmkit-plan/assets/plan-template.md +37 -0
  75. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +140 -0
  76. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +943 -0
  77. package/bundled/skills/prizmkit-retrospective/SKILL.md +79 -0
  78. package/bundled/skills/prizmkit-security-audit/SKILL.md +130 -0
  79. package/bundled/skills/prizmkit-specify/SKILL.md +52 -0
  80. package/bundled/skills/prizmkit-specify/assets/spec-template.md +37 -0
  81. package/bundled/skills/prizmkit-summarize/SKILL.md +51 -0
  82. package/bundled/skills/prizmkit-summarize/assets/registry-template.md +18 -0
  83. package/bundled/skills/prizmkit-tasks/SKILL.md +50 -0
  84. package/bundled/skills/prizmkit-tasks/assets/tasks-template.md +21 -0
  85. package/bundled/skills/prizmkit-tech-debt-tracker/SKILL.md +139 -0
  86. package/bundled/team/prizm-dev-team.json +47 -0
  87. package/bundled/templates/claude-md-template.md +38 -0
  88. package/bundled/templates/codebuddy-md-template.md +35 -0
  89. package/package.json +2 -1
@@ -0,0 +1,748 @@
1
+ #!/usr/bin/env python3
2
+ """Core state machine for updating bug status in the bug-fix pipeline.
3
+
4
+ Handles six actions:
5
+ - get_next: Find the next bug to process based on priority and severity
6
+ - update: Update a bug's status based on session outcome
7
+ - status: Print a formatted overview of all bugs
8
+ - pause: Save pipeline state for graceful shutdown
9
+ - reset: Reset a bug to pending (status + retry count)
10
+ - clean: Reset + delete session history + delete bugfix artifacts
11
+
12
+ Usage:
13
+ python3 update-bug-status.py \
14
+ --bug-list <path> --state-dir <path> \
15
+ --action <get_next|update|status|pause|reset|clean> \
16
+ [--bug-id <id>] [--session-status <status>] \
17
+ [--session-id <id>] [--max-retries <n>]
18
+ """
19
+
20
+ import argparse
21
+ import json
22
+ import os
23
+ import shutil
24
+ import sys
25
+ from datetime import datetime, timezone
26
+
27
+
28
+ SESSION_STATUS_VALUES = [
29
+ "success",
30
+ "partial_resumable",
31
+ "partial_not_resumable",
32
+ "failed",
33
+ "crashed",
34
+ "timed_out",
35
+ ]
36
+
37
+ TERMINAL_STATUSES = {"completed", "failed", "skipped", "needs_info"}
38
+
39
+ # 严重度优先级(数值越小越优先)
40
+ SEVERITY_PRIORITY = {
41
+ "critical": 0,
42
+ "high": 1,
43
+ "medium": 2,
44
+ "low": 3,
45
+ }
46
+
47
+
48
+ def parse_args():
49
+ parser = argparse.ArgumentParser(
50
+ description="Core state machine for bug-fix pipeline bug status management."
51
+ )
52
+ parser.add_argument("--bug-list", required=True, help="Path to the bug-fix-list.json file")
53
+ parser.add_argument("--state-dir", required=True, help="Path to the bugfix-state/ directory")
54
+ parser.add_argument(
55
+ "--action", required=True,
56
+ choices=["get_next", "update", "status", "pause", "reset", "clean"],
57
+ help="Action to perform",
58
+ )
59
+ parser.add_argument("--bug-id", default=None, help="Bug ID (required for 'update'/'reset'/'clean' actions)")
60
+ parser.add_argument(
61
+ "--session-status", default=None, choices=SESSION_STATUS_VALUES,
62
+ help="Session outcome status (required for 'update' action)",
63
+ )
64
+ parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
65
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
66
+ parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
67
+ return parser.parse_args()
68
+
69
+
70
+ def now_iso():
71
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
72
+
73
+
74
+ def load_json_file(path):
75
+ abs_path = os.path.abspath(path)
76
+ if not os.path.isfile(abs_path):
77
+ return None, "File not found: {}".format(abs_path)
78
+ try:
79
+ with open(abs_path, "r", encoding="utf-8") as f:
80
+ data = json.load(f)
81
+ except json.JSONDecodeError as e:
82
+ return None, "Invalid JSON: {}".format(str(e))
83
+ except IOError as e:
84
+ return None, "Cannot read file: {}".format(str(e))
85
+ return data, None
86
+
87
+
88
+ def write_json_file(path, data):
89
+ abs_path = os.path.abspath(path)
90
+ parent = os.path.dirname(abs_path)
91
+ if parent and not os.path.isdir(parent):
92
+ try:
93
+ os.makedirs(parent, exist_ok=True)
94
+ except OSError as e:
95
+ return "Cannot create directory: {}".format(str(e))
96
+ try:
97
+ with open(abs_path, "w", encoding="utf-8") as f:
98
+ json.dump(data, f, indent=2, ensure_ascii=False)
99
+ f.write("\n")
100
+ except IOError as e:
101
+ return "Cannot write file: {}".format(str(e))
102
+ return None
103
+
104
+
105
+ def load_bug_status(state_dir, bug_id):
106
+ status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
107
+ if not os.path.isfile(status_path):
108
+ now = now_iso()
109
+ return {
110
+ "bug_id": bug_id,
111
+ "status": "pending",
112
+ "retry_count": 0,
113
+ "max_retries": 3,
114
+ "sessions": [],
115
+ "last_session_id": None,
116
+ "resume_from_phase": None,
117
+ "created_at": now,
118
+ "updated_at": now,
119
+ }
120
+ data, err = load_json_file(status_path)
121
+ if err:
122
+ now = now_iso()
123
+ return {
124
+ "bug_id": bug_id,
125
+ "status": "pending",
126
+ "retry_count": 0,
127
+ "max_retries": 3,
128
+ "sessions": [],
129
+ "last_session_id": None,
130
+ "resume_from_phase": None,
131
+ "created_at": now,
132
+ "updated_at": now,
133
+ }
134
+ return data
135
+
136
+
137
+ def save_bug_status(state_dir, bug_id, status_data):
138
+ status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
139
+ return write_json_file(status_path, status_data)
140
+
141
+
142
+ def update_bug_in_list(bug_list_path, bug_id, new_status):
143
+ data, err = load_json_file(bug_list_path)
144
+ if err:
145
+ return err
146
+ bugs = data.get("bugs", [])
147
+ found = False
148
+ for bug in bugs:
149
+ if isinstance(bug, dict) and bug.get("id") == bug_id:
150
+ bug["status"] = new_status
151
+ found = True
152
+ break
153
+ if not found:
154
+ return "Bug '{}' not found in bug-fix-list.json".format(bug_id)
155
+ return write_json_file(bug_list_path, data)
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Action: get_next
160
+ # ---------------------------------------------------------------------------
161
+
162
+ def action_get_next(bug_list_data, state_dir):
163
+ """Find the next bug to process.
164
+
165
+ Priority logic:
166
+ 1. Skip terminal statuses (completed, failed, skipped, needs_info)
167
+ 2. Prefer in_progress bugs (interrupted session resume) over pending
168
+ 3. Sort by: severity (critical > high > medium > low), then by priority field
169
+ """
170
+ bugs = bug_list_data.get("bugs", [])
171
+ if not bugs:
172
+ print("PIPELINE_COMPLETE")
173
+ return
174
+
175
+ # Build status map
176
+ status_map = {}
177
+ status_data_map = {}
178
+ for bug in bugs:
179
+ if not isinstance(bug, dict):
180
+ continue
181
+ bid = bug.get("id")
182
+ if not bid:
183
+ continue
184
+ bs = load_bug_status(state_dir, bid)
185
+ status_map[bid] = bs.get("status", "pending")
186
+ status_data_map[bid] = bs
187
+
188
+ # Check if all bugs are terminal
189
+ non_terminal = [
190
+ b for b in bugs
191
+ if isinstance(b, dict) and b.get("id")
192
+ and status_map.get(b["id"], "pending") not in TERMINAL_STATUSES
193
+ ]
194
+ if not non_terminal:
195
+ print("PIPELINE_COMPLETE")
196
+ return
197
+
198
+ # Separate in_progress from pending
199
+ in_progress_bugs = []
200
+ pending_bugs = []
201
+ for bug in non_terminal:
202
+ bid = bug.get("id")
203
+ bstatus = status_map.get(bid, "pending")
204
+ if bstatus == "in_progress":
205
+ in_progress_bugs.append(bug)
206
+ elif bstatus == "pending":
207
+ pending_bugs.append(bug)
208
+
209
+ def sort_key(b):
210
+ severity = b.get("severity", "medium")
211
+ sev_order = SEVERITY_PRIORITY.get(severity, 2)
212
+ priority = b.get("priority", 999)
213
+ return (sev_order, priority)
214
+
215
+ if in_progress_bugs:
216
+ candidates = sorted(in_progress_bugs, key=sort_key)
217
+ elif pending_bugs:
218
+ candidates = sorted(pending_bugs, key=sort_key)
219
+ else:
220
+ # 所有剩余的 bug 都处于非终端但也非 pending/in_progress 状态
221
+ print("PIPELINE_BLOCKED")
222
+ return
223
+
224
+ chosen = candidates[0]
225
+ chosen_id = chosen["id"]
226
+ chosen_status_data = status_data_map.get(chosen_id, {})
227
+
228
+ result = {
229
+ "bug_id": chosen_id,
230
+ "title": chosen.get("title", ""),
231
+ "severity": chosen.get("severity", "medium"),
232
+ "retry_count": chosen_status_data.get("retry_count", 0),
233
+ "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
234
+ }
235
+ print(json.dumps(result, indent=2, ensure_ascii=False))
236
+
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # Action: update
240
+ # ---------------------------------------------------------------------------
241
+
242
+ def action_update(args, bug_list_path, state_dir):
243
+ bug_id = args.bug_id
244
+ session_status = args.session_status
245
+ session_id = args.session_id
246
+ max_retries = args.max_retries
247
+
248
+ if not bug_id:
249
+ error_out("--bug-id is required for 'update' action")
250
+ return
251
+ if not session_status:
252
+ error_out("--session-status is required for 'update' action")
253
+ return
254
+
255
+ bs = load_bug_status(state_dir, bug_id)
256
+
257
+ if session_status == "success":
258
+ bs["status"] = "completed"
259
+ bs["resume_from_phase"] = None
260
+ err = update_bug_in_list(bug_list_path, bug_id, "completed")
261
+ if err:
262
+ error_out("Failed to update bug-fix-list.json: {}".format(err))
263
+ return
264
+ else:
265
+ bs["retry_count"] = bs.get("retry_count", 0) + 1
266
+
267
+ cleaned = cleanup_bug_artifacts(
268
+ state_dir=state_dir,
269
+ bug_id=bug_id,
270
+ project_root=args.project_root,
271
+ )
272
+
273
+ if bs["retry_count"] >= max_retries:
274
+ bs["status"] = "failed"
275
+ target_status = "failed"
276
+ else:
277
+ bs["status"] = "pending"
278
+ target_status = "pending"
279
+
280
+ bs["resume_from_phase"] = None
281
+ bs["sessions"] = []
282
+ bs["last_session_id"] = None
283
+
284
+ err = update_bug_in_list(bug_list_path, bug_id, target_status)
285
+ if err:
286
+ error_out("Failed to update bug-fix-list.json: {}".format(err))
287
+ return
288
+
289
+ if session_status == "success" and session_id:
290
+ sessions = bs.get("sessions", [])
291
+ if session_id not in sessions:
292
+ sessions.append(session_id)
293
+ bs["sessions"] = sessions
294
+ bs["last_session_id"] = session_id
295
+
296
+ bs["updated_at"] = now_iso()
297
+
298
+ err = save_bug_status(state_dir, bug_id, bs)
299
+ if err:
300
+ error_out("Failed to save bug status: {}".format(err))
301
+ return
302
+
303
+ summary = {
304
+ "action": "update",
305
+ "bug_id": bug_id,
306
+ "session_status": session_status,
307
+ "new_status": bs["status"],
308
+ "retry_count": bs["retry_count"],
309
+ "resume_from_phase": bs.get("resume_from_phase"),
310
+ "updated_at": bs["updated_at"],
311
+ }
312
+ if session_status != "success":
313
+ summary["restart_policy"] = "full_restart"
314
+ summary["cleanup_performed"] = cleaned
315
+
316
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
317
+
318
+
319
+ def _default_project_root():
320
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
321
+
322
+
323
+ def cleanup_bug_artifacts(state_dir, bug_id, project_root=None):
324
+ """Delete intermediate artifacts for a failed bug run."""
325
+ if not project_root:
326
+ project_root = _default_project_root()
327
+
328
+ cleaned = []
329
+
330
+ # 1) Remove all session history
331
+ sessions_dir = os.path.join(state_dir, "bugs", bug_id, "sessions")
332
+ sessions_deleted = 0
333
+ if os.path.isdir(sessions_dir):
334
+ for entry in os.listdir(sessions_dir):
335
+ entry_path = os.path.join(sessions_dir, entry)
336
+ if os.path.isdir(entry_path):
337
+ shutil.rmtree(entry_path)
338
+ sessions_deleted += 1
339
+ cleaned.append("Deleted {} session(s) from {}".format(sessions_deleted, sessions_dir))
340
+
341
+ # 2) Remove transient files under bug dir (keep status.json)
342
+ bug_dir = os.path.join(state_dir, "bugs", bug_id)
343
+ if os.path.isdir(bug_dir):
344
+ for entry in os.listdir(bug_dir):
345
+ if entry == "status.json" or entry == "sessions":
346
+ continue
347
+ entry_path = os.path.join(bug_dir, entry)
348
+ if os.path.isdir(entry_path):
349
+ shutil.rmtree(entry_path)
350
+ cleaned.append("Deleted directory {}".format(entry_path))
351
+ elif os.path.isfile(entry_path):
352
+ os.remove(entry_path)
353
+ cleaned.append("Deleted file {}".format(entry_path))
354
+
355
+ # 3) Remove bugfix artifacts
356
+ bugfix_dir = os.path.join(project_root, ".prizmkit", "bugfix", bug_id)
357
+ if os.path.isdir(bugfix_dir):
358
+ file_count = sum(len(files) for _, _, files in os.walk(bugfix_dir))
359
+ shutil.rmtree(bugfix_dir)
360
+ cleaned.append("Deleted {} ({} files)".format(bugfix_dir, file_count))
361
+
362
+ # 4) Remove shared dev-team workspace
363
+ dev_team_dir = os.path.join(project_root, ".dev-team")
364
+ if os.path.isdir(dev_team_dir):
365
+ file_count = sum(len(files) for _, _, files in os.walk(dev_team_dir))
366
+ shutil.rmtree(dev_team_dir)
367
+ cleaned.append("Deleted {} ({} files)".format(dev_team_dir, file_count))
368
+
369
+ # 5) Clear current-session pointer if it points to this bug
370
+ current_session_path = os.path.join(state_dir, "current-session.json")
371
+ if os.path.isfile(current_session_path):
372
+ current_session, _ = load_json_file(current_session_path)
373
+ if current_session and current_session.get("bug_id") == bug_id:
374
+ os.remove(current_session_path)
375
+ cleaned.append("Deleted {}".format(current_session_path))
376
+
377
+ return cleaned
378
+
379
+
380
+ def load_session_status(state_dir, bug_id, session_id):
381
+ session_status_path = os.path.join(
382
+ state_dir, "bugs", bug_id, "sessions",
383
+ session_id, "session-status.json"
384
+ )
385
+ data, err = load_json_file(session_status_path)
386
+ if err:
387
+ return None, err
388
+ return data, None
389
+
390
+
391
+ # ---------------------------------------------------------------------------
392
+ # Action: status
393
+ # ---------------------------------------------------------------------------
394
+
395
+ COLOR_GREEN = "\033[92m"
396
+ COLOR_YELLOW = "\033[93m"
397
+ COLOR_RED = "\033[91m"
398
+ COLOR_GRAY = "\033[90m"
399
+ COLOR_MAGENTA = "\033[95m"
400
+ COLOR_BOLD = "\033[1m"
401
+ COLOR_RESET = "\033[0m"
402
+
403
+ BOX_WIDTH = 68
404
+
405
+
406
+ def pad_right(text, width):
407
+ visible = text
408
+ i = 0
409
+ visible_len = 0
410
+ while i < len(text):
411
+ if text[i] == "\033":
412
+ while i < len(text) and text[i] != "m":
413
+ i += 1
414
+ i += 1
415
+ else:
416
+ visible_len += 1
417
+ i += 1
418
+ padding = width - visible_len
419
+ if padding > 0:
420
+ return text + " " * padding
421
+ return text
422
+
423
+
424
+ def _build_progress_bar(percent, width=20):
425
+ filled = int(width * percent / 100)
426
+ empty = width - filled
427
+ bar = "█" * filled + "░" * empty
428
+ return "{} {:>3}%".format(bar, int(percent))
429
+
430
+
431
+ SEVERITY_ICONS = {
432
+ "critical": COLOR_RED + "🔴" + COLOR_RESET,
433
+ "high": COLOR_MAGENTA + "🟠" + COLOR_RESET,
434
+ "medium": COLOR_YELLOW + "🟡" + COLOR_RESET,
435
+ "low": COLOR_GRAY + "🟢" + COLOR_RESET,
436
+ }
437
+
438
+
439
+ def action_status(bug_list_data, state_dir):
440
+ bugs = bug_list_data.get("bugs", [])
441
+ project_name = bug_list_data.get("project_name", "Unknown")
442
+
443
+ counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "needs_info": 0, "skipped": 0}
444
+ bug_lines = []
445
+
446
+ for bug in bugs:
447
+ if not isinstance(bug, dict):
448
+ continue
449
+ bid = bug.get("id")
450
+ title = bug.get("title", "Untitled")
451
+ severity = bug.get("severity", "medium")
452
+ if not bid:
453
+ continue
454
+
455
+ bs = load_bug_status(state_dir, bid)
456
+ bstatus = bs.get("status", "pending")
457
+ retry_count = bs.get("retry_count", 0)
458
+ max_retries_val = bs.get("max_retries", 3)
459
+ resume_phase = bs.get("resume_from_phase")
460
+
461
+ if bstatus in counts:
462
+ counts[bstatus] += 1
463
+ else:
464
+ counts["pending"] += 1
465
+
466
+ # Status icon
467
+ if bstatus == "completed":
468
+ icon = COLOR_GREEN + "[✓]" + COLOR_RESET
469
+ elif bstatus == "in_progress":
470
+ icon = COLOR_YELLOW + "[→]" + COLOR_RESET
471
+ elif bstatus == "failed":
472
+ icon = COLOR_RED + "[✗]" + COLOR_RESET
473
+ elif bstatus == "needs_info":
474
+ icon = COLOR_MAGENTA + "[?]" + COLOR_RESET
475
+ elif bstatus == "skipped":
476
+ icon = COLOR_GRAY + "[—]" + COLOR_RESET
477
+ else:
478
+ icon = COLOR_GRAY + "[ ]" + COLOR_RESET
479
+
480
+ # Severity badge
481
+ sev_badge = "[{}]".format(severity[:4].upper())
482
+
483
+ # Detail
484
+ detail = ""
485
+ if bstatus == "in_progress":
486
+ parts = []
487
+ if retry_count > 0:
488
+ parts.append("retry {}/{}".format(retry_count, max_retries_val))
489
+ if resume_phase is not None:
490
+ parts.append("CP-BF-{}".format(resume_phase))
491
+ if parts:
492
+ detail = " ({})".format(", ".join(parts))
493
+ elif bstatus == "failed":
494
+ detail = " (failed after {} retries)".format(retry_count)
495
+ elif bstatus == "needs_info":
496
+ detail = " (needs more info)"
497
+
498
+ # Colorize
499
+ if bstatus == "completed":
500
+ line_content = "{} {} {} {} {}{}".format(
501
+ bid, icon, sev_badge, COLOR_GREEN + title + COLOR_RESET, "", detail
502
+ )
503
+ elif bstatus == "in_progress":
504
+ line_content = "{} {} {} {} {}{}".format(
505
+ bid, icon, sev_badge, COLOR_YELLOW + title + COLOR_RESET, "", detail
506
+ )
507
+ elif bstatus == "failed":
508
+ line_content = "{} {} {} {} {}{}".format(
509
+ bid, icon, sev_badge, COLOR_RED + title + COLOR_RESET, "", detail
510
+ )
511
+ elif bstatus == "needs_info":
512
+ line_content = "{} {} {} {} {}{}".format(
513
+ bid, icon, sev_badge, COLOR_MAGENTA + title + COLOR_RESET, "", detail
514
+ )
515
+ else:
516
+ line_content = "{} {} {} {} {}{}".format(
517
+ bid, icon, sev_badge, COLOR_GRAY + title + COLOR_RESET, "", detail
518
+ )
519
+
520
+ bug_lines.append(line_content)
521
+
522
+ total = len(bugs)
523
+ completed = counts["completed"]
524
+ percent = round(completed / total * 100, 1) if total > 0 else 0.0
525
+ progress_bar = _build_progress_bar(percent, width=24)
526
+
527
+ summary_line = "Total: {} bugs | Completed: {} | In Progress: {}".format(
528
+ total, completed, counts["in_progress"]
529
+ )
530
+ summary_line2 = "Failed: {} | Pending: {} | Needs Info: {} | Skipped: {}".format(
531
+ counts["failed"], counts["pending"], counts["needs_info"], counts["skipped"]
532
+ )
533
+
534
+ inner = BOX_WIDTH - 2
535
+ print("╔" + "═" * BOX_WIDTH + "╗")
536
+ print("║" + pad_right(COLOR_BOLD + " Bug-Fix Pipeline Status" + COLOR_RESET, inner) + " ║")
537
+ print("╠" + "═" * BOX_WIDTH + "╣")
538
+ print("║" + pad_right(" Project: {}".format(project_name), inner) + " ║")
539
+ print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
540
+ print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
541
+ print("╠" + "─" * BOX_WIDTH + "╣")
542
+ print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
543
+ print("╠" + "═" * BOX_WIDTH + "╣")
544
+ for line in bug_lines:
545
+ print("║" + pad_right(" {}".format(line), inner) + " ║")
546
+ print("╚" + "═" * BOX_WIDTH + "╝")
547
+
548
+
549
+ # ---------------------------------------------------------------------------
550
+ # Action: reset
551
+ # ---------------------------------------------------------------------------
552
+
553
+ def action_reset(args, bug_list_path, state_dir):
554
+ bug_id = args.bug_id
555
+ if not bug_id:
556
+ error_out("--bug-id is required for 'reset' action")
557
+ return
558
+
559
+ bs = load_bug_status(state_dir, bug_id)
560
+ old_status = bs.get("status", "unknown")
561
+ old_retry = bs.get("retry_count", 0)
562
+
563
+ bs["status"] = "pending"
564
+ bs["retry_count"] = 0
565
+ bs["sessions"] = []
566
+ bs["last_session_id"] = None
567
+ bs["resume_from_phase"] = None
568
+ bs["updated_at"] = now_iso()
569
+
570
+ err = save_bug_status(state_dir, bug_id, bs)
571
+ if err:
572
+ error_out("Failed to save bug status: {}".format(err))
573
+ return
574
+
575
+ err = update_bug_in_list(bug_list_path, bug_id, "pending")
576
+ if err:
577
+ error_out("Failed to update bug-fix-list.json: {}".format(err))
578
+ return
579
+
580
+ result = {
581
+ "action": "reset",
582
+ "bug_id": bug_id,
583
+ "old_status": old_status,
584
+ "old_retry_count": old_retry,
585
+ "new_status": "pending",
586
+ }
587
+ print(json.dumps(result, indent=2, ensure_ascii=False))
588
+
589
+
590
+ # ---------------------------------------------------------------------------
591
+ # Action: clean
592
+ # ---------------------------------------------------------------------------
593
+
594
+ def action_clean(args, bug_list_path, state_dir):
595
+ bug_id = args.bug_id
596
+ project_root = args.project_root
597
+
598
+ if not bug_id:
599
+ error_out("--bug-id is required for 'clean' action")
600
+ return
601
+ if not project_root:
602
+ error_out("--project-root is required for 'clean' action")
603
+ return
604
+
605
+ cleaned = []
606
+
607
+ # 1. Delete session history
608
+ sessions_dir = os.path.join(state_dir, "bugs", bug_id, "sessions")
609
+ sessions_deleted = 0
610
+ if os.path.isdir(sessions_dir):
611
+ for entry in os.listdir(sessions_dir):
612
+ entry_path = os.path.join(sessions_dir, entry)
613
+ if os.path.isdir(entry_path):
614
+ shutil.rmtree(entry_path)
615
+ sessions_deleted += 1
616
+ cleaned.append("Deleted {} session(s) from {}".format(sessions_deleted, sessions_dir))
617
+
618
+ # 2. Delete bugfix artifacts for this bug
619
+ bugfix_dir = os.path.join(project_root, ".prizmkit", "bugfix", bug_id)
620
+ if os.path.isdir(bugfix_dir):
621
+ file_count = sum(len(files) for _, _, files in os.walk(bugfix_dir))
622
+ shutil.rmtree(bugfix_dir)
623
+ cleaned.append("Deleted {} ({} files)".format(bugfix_dir, file_count))
624
+
625
+ # 3. Delete shared dev-team workspace
626
+ dev_team_dir = os.path.join(project_root, ".dev-team")
627
+ if os.path.isdir(dev_team_dir):
628
+ file_count = sum(len(files) for _, _, files in os.walk(dev_team_dir))
629
+ shutil.rmtree(dev_team_dir)
630
+ cleaned.append("Deleted {} ({} files)".format(dev_team_dir, file_count))
631
+
632
+ # 4. Delete current-session pointer if it points to this bug
633
+ current_session_path = os.path.join(state_dir, "current-session.json")
634
+ if os.path.isfile(current_session_path):
635
+ current_session, _ = load_json_file(current_session_path)
636
+ if current_session and current_session.get("bug_id") == bug_id:
637
+ os.remove(current_session_path)
638
+ cleaned.append("Deleted {}".format(current_session_path))
639
+
640
+ # 5. Reset status
641
+ bs = load_bug_status(state_dir, bug_id)
642
+ old_status = bs.get("status", "unknown")
643
+ old_retry = bs.get("retry_count", 0)
644
+
645
+ bs["status"] = "pending"
646
+ bs["retry_count"] = 0
647
+ bs["sessions"] = []
648
+ bs["last_session_id"] = None
649
+ bs["resume_from_phase"] = None
650
+ bs["updated_at"] = now_iso()
651
+
652
+ err = save_bug_status(state_dir, bug_id, bs)
653
+ if err:
654
+ error_out("Failed to save bug status: {}".format(err))
655
+ return
656
+
657
+ err = update_bug_in_list(bug_list_path, bug_id, "pending")
658
+ if err:
659
+ error_out("Failed to update bug-fix-list.json: {}".format(err))
660
+ return
661
+
662
+ result = {
663
+ "action": "clean",
664
+ "bug_id": bug_id,
665
+ "old_status": old_status,
666
+ "old_retry_count": old_retry,
667
+ "new_status": "pending",
668
+ "sessions_deleted": sessions_deleted,
669
+ "cleaned": cleaned,
670
+ }
671
+ print(json.dumps(result, indent=2, ensure_ascii=False))
672
+
673
+
674
+ # ---------------------------------------------------------------------------
675
+ # Action: pause
676
+ # ---------------------------------------------------------------------------
677
+
678
+ def action_pause(state_dir):
679
+ pipeline_path = os.path.join(state_dir, "pipeline.json")
680
+ data, err = load_json_file(pipeline_path)
681
+ if err:
682
+ data = {"status": "paused", "paused_at": now_iso()}
683
+ else:
684
+ data["status"] = "paused"
685
+ data["paused_at"] = now_iso()
686
+
687
+ err = write_json_file(pipeline_path, data)
688
+ if err:
689
+ error_out("Failed to write pipeline.json: {}".format(err))
690
+ return
691
+
692
+ result = {
693
+ "action": "pause",
694
+ "status": "paused",
695
+ "paused_at": data["paused_at"],
696
+ }
697
+ print(json.dumps(result, indent=2, ensure_ascii=False))
698
+
699
+
700
+ # ---------------------------------------------------------------------------
701
+ # Helpers
702
+ # ---------------------------------------------------------------------------
703
+
704
+ def error_out(message):
705
+ output = {"error": message}
706
+ print(json.dumps(output, indent=2, ensure_ascii=False))
707
+ sys.exit(1)
708
+
709
+
710
+ # ---------------------------------------------------------------------------
711
+ # Main
712
+ # ---------------------------------------------------------------------------
713
+
714
+ def main():
715
+ args = parse_args()
716
+
717
+ if args.action == "update":
718
+ if not args.bug_id:
719
+ error_out("--bug-id is required for 'update' action")
720
+ if not args.session_status:
721
+ error_out("--session-status is required for 'update' action")
722
+ if args.action in ("reset", "clean"):
723
+ if not args.bug_id:
724
+ error_out("--bug-id is required for '{}' action".format(args.action))
725
+ if args.action == "clean":
726
+ if not args.project_root:
727
+ error_out("--project-root is required for 'clean' action")
728
+
729
+ bug_list_data, err = load_json_file(args.bug_list)
730
+ if err:
731
+ error_out("Cannot load bug fix list: {}".format(err))
732
+
733
+ if args.action == "get_next":
734
+ action_get_next(bug_list_data, args.state_dir)
735
+ elif args.action == "update":
736
+ action_update(args, args.bug_list, args.state_dir)
737
+ elif args.action == "status":
738
+ action_status(bug_list_data, args.state_dir)
739
+ elif args.action == "reset":
740
+ action_reset(args, args.bug_list, args.state_dir)
741
+ elif args.action == "clean":
742
+ action_clean(args, args.bug_list, args.state_dir)
743
+ elif args.action == "pause":
744
+ action_pause(args.state_dir)
745
+
746
+
747
+ if __name__ == "__main__":
748
+ main()