prizmkit 1.1.57 → 1.1.60

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 (188) hide show
  1. package/bin/create-prizmkit.js +8 -6
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/adapters/codex/agent-adapter.js +38 -0
  4. package/bundled/adapters/codex/paths.js +27 -0
  5. package/bundled/adapters/codex/rules-adapter.js +30 -0
  6. package/bundled/adapters/codex/settings-adapter.js +27 -0
  7. package/bundled/adapters/codex/skill-adapter.js +65 -0
  8. package/bundled/adapters/codex/team-adapter.js +37 -0
  9. package/bundled/dev-pipeline/.env.example +2 -1
  10. package/bundled/dev-pipeline/README.md +10 -7
  11. package/bundled/dev-pipeline/lib/common.sh +278 -37
  12. package/bundled/dev-pipeline/run-bugfix.sh +10 -61
  13. package/bundled/dev-pipeline/run-feature.sh +10 -78
  14. package/bundled/dev-pipeline/run-recovery.sh +10 -46
  15. package/bundled/dev-pipeline/run-refactor.sh +10 -61
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -7
  17. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +9 -3
  18. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +9 -3
  19. package/bundled/dev-pipeline/scripts/utils.py +6 -4
  20. package/bundled/dev-pipeline-windows/.env.example +28 -0
  21. package/bundled/dev-pipeline-windows/README.md +30 -0
  22. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +525 -0
  23. package/bundled/dev-pipeline-windows/assets/feature-list-example.json +146 -0
  24. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +138 -0
  25. package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +9 -0
  26. package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +9 -0
  27. package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +9 -0
  28. package/bundled/dev-pipeline-windows/lib/common.ps1 +432 -0
  29. package/bundled/dev-pipeline-windows/lib/daemon.ps1 +140 -0
  30. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +446 -0
  31. package/bundled/dev-pipeline-windows/lib/reset.ps1 +87 -0
  32. package/bundled/dev-pipeline-windows/reset-bug.ps1 +9 -0
  33. package/bundled/dev-pipeline-windows/reset-feature.ps1 +9 -0
  34. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +9 -0
  35. package/bundled/dev-pipeline-windows/run-bugfix.ps1 +9 -0
  36. package/bundled/dev-pipeline-windows/run-feature.ps1 +9 -0
  37. package/bundled/dev-pipeline-windows/run-recovery.ps1 +76 -0
  38. package/bundled/dev-pipeline-windows/run-refactor.ps1 +9 -0
  39. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +228 -0
  40. package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +192 -0
  41. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +530 -0
  42. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +1737 -0
  43. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +685 -0
  44. package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +805 -0
  45. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +763 -0
  46. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +316 -0
  47. package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +134 -0
  48. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +380 -0
  49. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +399 -0
  50. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +388 -0
  51. package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +191 -0
  52. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +864 -0
  53. package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +173 -0
  54. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +1501 -0
  55. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +1073 -0
  56. package/bundled/dev-pipeline-windows/scripts/utils.py +542 -0
  57. package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +7 -0
  58. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +7 -0
  59. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +30 -0
  60. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +5 -0
  61. package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +7 -0
  62. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +46 -0
  63. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +43 -0
  64. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +43 -0
  65. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +43 -0
  66. package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +263 -0
  67. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +320 -0
  68. package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +237 -0
  69. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +331 -0
  70. package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +270 -0
  71. package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +13 -0
  72. package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +91 -0
  73. package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +33 -0
  74. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +10 -0
  75. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +12 -0
  76. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +7 -0
  77. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +8 -0
  78. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +9 -0
  79. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +6 -0
  80. package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +21 -0
  81. package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +31 -0
  82. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +72 -0
  83. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +63 -0
  84. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +62 -0
  85. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +71 -0
  86. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +64 -0
  87. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +23 -0
  88. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +24 -0
  89. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +12 -0
  90. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +53 -0
  91. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +32 -0
  92. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +37 -0
  93. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +50 -0
  94. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +52 -0
  95. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +27 -0
  96. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +27 -0
  97. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +27 -0
  98. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +29 -0
  99. package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +77 -0
  100. package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +13 -0
  101. package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +23 -0
  102. package/bundled/dev-pipeline-windows/templates/sections/session-context.md +5 -0
  103. package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +6 -0
  104. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +67 -0
  105. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +58 -0
  106. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +83 -0
  107. package/bundled/skills/_metadata.json +1 -1
  108. package/bundled/skills/app-planner/SKILL.md +26 -18
  109. package/bundled/skills/app-planner/references/architecture-decisions.md +9 -5
  110. package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
  111. package/bundled/skills/feature-planner/SKILL.md +9 -2
  112. package/bundled/skills/prizmkit-init/SKILL.md +7 -6
  113. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +2 -0
  114. package/bundled/skills-windows/app-planner/SKILL.md +639 -0
  115. package/bundled/skills-windows/app-planner/assets/app-design-guide.md +101 -0
  116. package/bundled/skills-windows/app-planner/references/architecture-decisions.md +52 -0
  117. package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +101 -0
  118. package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +71 -0
  119. package/bundled/skills-windows/app-planner/references/project-brief-guide.md +82 -0
  120. package/bundled/skills-windows/app-planner/references/red-team-checklist.md +40 -0
  121. package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +609 -0
  122. package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +285 -0
  123. package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +249 -0
  124. package/bundled/skills-windows/app-planner/references/rules/backend/template.md +173 -0
  125. package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +373 -0
  126. package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +211 -0
  127. package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +184 -0
  128. package/bundled/skills-windows/app-planner/references/rules/database/template.md +158 -0
  129. package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +810 -0
  130. package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +188 -0
  131. package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +302 -0
  132. package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +320 -0
  133. package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +639 -0
  134. package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +290 -0
  135. package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +232 -0
  136. package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +175 -0
  137. package/bundled/skills-windows/bug-fix-workflow/SKILL.md +415 -0
  138. package/bundled/skills-windows/bug-planner/SKILL.md +395 -0
  139. package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +43 -0
  140. package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +44 -0
  141. package/bundled/skills-windows/bug-planner/references/error-recovery.md +73 -0
  142. package/bundled/skills-windows/bug-planner/references/input-formats.md +53 -0
  143. package/bundled/skills-windows/bug-planner/references/schema-validation.md +25 -0
  144. package/bundled/skills-windows/bug-planner/references/severity-rules.md +16 -0
  145. package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +322 -0
  146. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +380 -0
  147. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +441 -0
  148. package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +462 -0
  149. package/bundled/skills-windows/feature-planner/SKILL.md +401 -0
  150. package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +64 -0
  151. package/bundled/skills-windows/feature-planner/assets/planning-guide.md +214 -0
  152. package/bundled/skills-windows/feature-planner/references/browser-interaction.md +59 -0
  153. package/bundled/skills-windows/feature-planner/references/completeness-review.md +57 -0
  154. package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +75 -0
  155. package/bundled/skills-windows/feature-planner/references/error-recovery.md +90 -0
  156. package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +112 -0
  157. package/bundled/skills-windows/feature-planner/references/new-project-planning.md +85 -0
  158. package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +1029 -0
  159. package/bundled/skills-windows/feature-workflow/SKILL.md +531 -0
  160. package/bundled/skills-windows/prizmkit-init/SKILL.md +356 -0
  161. package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +82 -0
  162. package/bundled/skills-windows/prizmkit-init/references/config-schema.md +68 -0
  163. package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +41 -0
  164. package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +13 -0
  165. package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +9 -0
  166. package/bundled/skills-windows/recovery-workflow/SKILL.md +456 -0
  167. package/bundled/skills-windows/recovery-workflow/evals/evals.json +46 -0
  168. package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +544 -0
  169. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +406 -0
  170. package/bundled/skills-windows/refactor-planner/SKILL.md +540 -0
  171. package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +292 -0
  172. package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +301 -0
  173. package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +221 -0
  174. package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +858 -0
  175. package/bundled/skills-windows/refactor-workflow/SKILL.md +503 -0
  176. package/package.json +3 -2
  177. package/src/clean.js +73 -2
  178. package/src/config.js +159 -50
  179. package/src/detect-platform.js +16 -8
  180. package/src/external-skills.js +26 -19
  181. package/src/index.js +31 -9
  182. package/src/manifest.js +6 -2
  183. package/src/metadata.js +43 -5
  184. package/src/platforms.js +36 -0
  185. package/src/prompts.js +31 -6
  186. package/src/runtimes.js +20 -0
  187. package/src/scaffold.js +314 -110
  188. package/src/upgrade.js +81 -41
@@ -0,0 +1,864 @@
1
+ #!/usr/bin/env python3
2
+ """Core state machine for updating bug status in the bug-fix pipeline.
3
+
4
+ Handles eight actions:
5
+ - get_next: Find the next bug to process based on priority and severity
6
+ - start: Mark a bug as fixing when a session starts
7
+ - update: Update a bug's status based on session outcome
8
+ - status: Print a formatted overview of all bugs
9
+ - pause: Save pipeline state for graceful shutdown
10
+ - reset: Reset a bug to pending (status + retry count)
11
+ - clean: Reset + delete session history + delete bugfix artifacts
12
+ - unskip: Reset skipped bugs back to pending
13
+
14
+ Usage:
15
+ python3 update-bug-status.py \
16
+ --bug-list <path> --state-dir <path> \
17
+ --action <get_next|start|update|status|pause|reset|clean|unskip> \
18
+ [--bug-id <id>] [--session-status <status>] \
19
+ [--session-id <id>] [--max-retries <n>]
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import shutil
26
+ from datetime import datetime, timezone
27
+
28
+ from utils import (
29
+ load_json_file,
30
+ write_json_file,
31
+ error_out,
32
+ pad_right,
33
+ _build_progress_bar,
34
+ )
35
+
36
+
37
+ SESSION_STATUS_VALUES = [
38
+ "success",
39
+ "partial_resumable",
40
+ "partial_not_resumable",
41
+ "failed",
42
+ "crashed",
43
+ "timed_out",
44
+ "commit_missing",
45
+ "docs_missing",
46
+ "merge_conflict",
47
+ ]
48
+
49
+ TERMINAL_STATUSES = {"completed", "failed", "skipped", "needs_info"}
50
+
51
+ # Severity priority (lower value = higher priority)
52
+ SEVERITY_PRIORITY = {
53
+ "critical": 0,
54
+ "high": 1,
55
+ "medium": 2,
56
+ "low": 3,
57
+ }
58
+
59
+
60
+ def parse_args():
61
+ parser = argparse.ArgumentParser(
62
+ description="Core state machine for bug-fix pipeline bug status management."
63
+ )
64
+ parser.add_argument("--bug-list", required=True, help="Path to the .prizmkit/plans/bug-fix-list.json file")
65
+ parser.add_argument("--state-dir", required=True, help="Path to the state directory (default: .prizmkit/state/bugfix)")
66
+ parser.add_argument(
67
+ "--action", required=True,
68
+ choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "unskip", "complete"],
69
+ help="Action to perform",
70
+ )
71
+ parser.add_argument("--bug-id", default=None, help="Bug ID (required for 'update'/'reset'/'clean' actions)")
72
+ parser.add_argument(
73
+ "--session-status", default=None, choices=SESSION_STATUS_VALUES,
74
+ help="Session outcome status (required for 'update' action)",
75
+ )
76
+ parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
77
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
78
+ parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
79
+ return parser.parse_args()
80
+
81
+
82
+ def now_iso():
83
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
84
+
85
+
86
+ def load_bug_status(state_dir, bug_id):
87
+ """Load runtime state from status.json for a bug.
88
+
89
+ Returns runtime fields only (retry_count, sessions, etc.).
90
+ The 'status' field is NOT included — status lives exclusively
91
+ in bug-fix-list.json.
92
+ """
93
+ status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
94
+ if not os.path.isfile(status_path):
95
+ now = now_iso()
96
+ return {
97
+ "bug_id": bug_id,
98
+ "retry_count": 0,
99
+ "max_retries": 3,
100
+ "sessions": [],
101
+ "last_session_id": None,
102
+ "resume_from_phase": None,
103
+ "created_at": now,
104
+ "updated_at": now,
105
+ }
106
+ data, err = load_json_file(status_path)
107
+ if err:
108
+ now = now_iso()
109
+ return {
110
+ "bug_id": bug_id,
111
+ "retry_count": 0,
112
+ "max_retries": 3,
113
+ "sessions": [],
114
+ "last_session_id": None,
115
+ "resume_from_phase": None,
116
+ "created_at": now,
117
+ "updated_at": now,
118
+ }
119
+ # Defensively remove status if present (legacy data)
120
+ data.pop("status", None)
121
+ return data
122
+
123
+
124
+ def save_bug_status(state_dir, bug_id, status_data):
125
+ """Write the status.json for a bug (runtime fields only)."""
126
+ # Defensively strip status — it belongs in bug-fix-list.json
127
+ status_data.pop("status", None)
128
+ status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
129
+ return write_json_file(status_path, status_data)
130
+
131
+
132
+ def get_bug_status_from_list(bug_list_path, bug_id):
133
+ """Read a single bug's status from bug-fix-list.json."""
134
+ data, err = load_json_file(bug_list_path)
135
+ if err:
136
+ return "pending"
137
+ for b in data.get("bugs", []):
138
+ if isinstance(b, dict) and b.get("id") == bug_id:
139
+ return b.get("status", "pending")
140
+ return "pending"
141
+
142
+
143
+ def update_bug_in_list(bug_list_path, bug_id, new_status):
144
+ data, err = load_json_file(bug_list_path)
145
+ if err:
146
+ return err
147
+ bugs = data.get("bugs", [])
148
+ found = False
149
+ for bug in bugs:
150
+ if isinstance(bug, dict) and bug.get("id") == bug_id:
151
+ bug["status"] = new_status
152
+ found = True
153
+ break
154
+ if not found:
155
+ return "Bug '{}' not found in .prizmkit/plans/bug-fix-list.json".format(bug_id)
156
+ return write_json_file(bug_list_path, data)
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Action: get_next
161
+ # ---------------------------------------------------------------------------
162
+
163
+ def action_get_next(bug_list_data, state_dir):
164
+ """Find the next bug to process.
165
+
166
+ Priority logic:
167
+ 1. Skip terminal statuses (completed, failed, skipped, needs_info)
168
+ 2. Prefer in_progress bugs (interrupted session resume) over pending
169
+ 3. Sort by: severity (critical > high > medium > low), then by priority (high > medium > low)
170
+ """
171
+ bugs = bug_list_data.get("bugs", [])
172
+ if not bugs:
173
+ print("PIPELINE_COMPLETE")
174
+ return
175
+
176
+ # Build status map from bug-fix-list.json (single source of truth)
177
+ status_map = {}
178
+ status_data_map = {}
179
+ for bug in bugs:
180
+ if not isinstance(bug, dict):
181
+ continue
182
+ bid = bug.get("id")
183
+ if not bid:
184
+ continue
185
+ status_map[bid] = bug.get("status", "pending")
186
+ bs = load_bug_status(state_dir, bid)
187
+ status_data_map[bid] = bs
188
+
189
+ # Check if all bugs are terminal
190
+ non_terminal = [
191
+ b for b in bugs
192
+ if isinstance(b, dict) and b.get("id")
193
+ and status_map.get(b["id"], "pending") not in TERMINAL_STATUSES
194
+ ]
195
+ if not non_terminal:
196
+ print("PIPELINE_COMPLETE")
197
+ return
198
+
199
+ # Separate in_progress from pending
200
+ in_progress_bugs = []
201
+ pending_bugs = []
202
+ for bug in non_terminal:
203
+ bid = bug.get("id")
204
+ bstatus = status_map.get(bid, "pending")
205
+ if bstatus == "in_progress":
206
+ in_progress_bugs.append(bug)
207
+ elif bstatus == "pending":
208
+ pending_bugs.append(bug)
209
+
210
+ _PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
211
+
212
+ def sort_key(b):
213
+ severity = b.get("severity", "medium")
214
+ sev_order = SEVERITY_PRIORITY.get(severity, 2)
215
+ priority = _PRIORITY_ORDER.get(b.get("priority", "low"), 2)
216
+ return (sev_order, priority)
217
+
218
+ if in_progress_bugs:
219
+ candidates = sorted(in_progress_bugs, key=sort_key)
220
+ elif pending_bugs:
221
+ candidates = sorted(pending_bugs, key=sort_key)
222
+ else:
223
+ # All remaining bugs are in non-terminal but also non-pending/in_progress states
224
+ print("PIPELINE_BLOCKED")
225
+ return
226
+
227
+ chosen = candidates[0]
228
+ chosen_id = chosen["id"]
229
+ chosen_status_data = status_data_map.get(chosen_id, {})
230
+
231
+ result = {
232
+ "bug_id": chosen_id,
233
+ "title": chosen.get("title", ""),
234
+ "severity": chosen.get("severity", "medium"),
235
+ "retry_count": chosen_status_data.get("retry_count", 0),
236
+ "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
237
+ }
238
+ print(json.dumps(result, indent=2, ensure_ascii=False))
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Action: update
243
+ # ---------------------------------------------------------------------------
244
+
245
+ def action_update(args, bug_list_path, state_dir):
246
+ bug_id = args.bug_id
247
+ session_status = args.session_status
248
+ session_id = args.session_id
249
+ max_retries = args.max_retries
250
+
251
+ if not bug_id:
252
+ error_out("--bug-id is required for 'update' action")
253
+ return
254
+ if not session_status:
255
+ error_out("--session-status is required for 'update' action")
256
+ return
257
+
258
+ bs = load_bug_status(state_dir, bug_id)
259
+
260
+ # Track what status we write to bug-fix-list.json
261
+ new_status = get_bug_status_from_list(bug_list_path, bug_id)
262
+
263
+ if session_status == "success":
264
+ new_status = "completed"
265
+ bs["resume_from_phase"] = None
266
+ err = update_bug_in_list(bug_list_path, bug_id, "completed")
267
+ if err:
268
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
269
+ return
270
+ elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
271
+ bs["retry_count"] = bs.get("retry_count", 0) + 1
272
+
273
+ if bs["retry_count"] >= max_retries:
274
+ new_status = "failed"
275
+ else:
276
+ new_status = "pending"
277
+
278
+ bs["degraded_reason"] = session_status
279
+ bs["resume_from_phase"] = None
280
+ bs["sessions"] = []
281
+ bs["last_session_id"] = None
282
+
283
+ err = update_bug_in_list(bug_list_path, bug_id, new_status)
284
+ if err:
285
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
286
+ return
287
+ else:
288
+ bs["retry_count"] = bs.get("retry_count", 0) + 1
289
+
290
+ cleaned = cleanup_bug_artifacts(
291
+ state_dir=state_dir,
292
+ bug_id=bug_id,
293
+ project_root=args.project_root,
294
+ )
295
+
296
+ if bs["retry_count"] >= max_retries:
297
+ new_status = "failed"
298
+ else:
299
+ new_status = "pending"
300
+
301
+ bs["resume_from_phase"] = None
302
+ bs["sessions"] = []
303
+ bs["last_session_id"] = None
304
+
305
+ err = update_bug_in_list(bug_list_path, bug_id, new_status)
306
+ if err:
307
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
308
+ return
309
+
310
+ if session_status == "success" and session_id:
311
+ sessions = bs.get("sessions", [])
312
+ if session_id not in sessions:
313
+ sessions.append(session_id)
314
+ bs["sessions"] = sessions
315
+ bs["last_session_id"] = session_id
316
+
317
+ bs["updated_at"] = now_iso()
318
+
319
+ err = save_bug_status(state_dir, bug_id, bs)
320
+ if err:
321
+ error_out("Failed to save bug status: {}".format(err))
322
+ return
323
+
324
+ summary = {
325
+ "action": "update",
326
+ "bug_id": bug_id,
327
+ "session_status": session_status,
328
+ "new_status": new_status,
329
+ "retry_count": bs["retry_count"],
330
+ "resume_from_phase": bs.get("resume_from_phase"),
331
+ "updated_at": bs["updated_at"],
332
+ }
333
+ if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
334
+ summary["degraded_reason"] = session_status
335
+ summary["restart_policy"] = "finalization_retry"
336
+ elif session_status != "success":
337
+ summary["restart_policy"] = "full_restart"
338
+ summary["cleanup_performed"] = cleaned
339
+
340
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
341
+
342
+
343
+ def _default_project_root():
344
+ env = os.environ.get("PROJECT_ROOT")
345
+ if env:
346
+ return os.path.abspath(env)
347
+ script_dir = os.path.dirname(os.path.abspath(__file__))
348
+ pipeline_dir = os.path.dirname(script_dir)
349
+ pipeline_parent = os.path.dirname(pipeline_dir)
350
+ if os.path.basename(pipeline_parent) == ".prizmkit":
351
+ return os.path.dirname(pipeline_parent)
352
+ return pipeline_parent
353
+
354
+
355
+ def cleanup_bug_artifacts(state_dir, bug_id, project_root=None):
356
+ """Delete intermediate artifacts for a failed bug run."""
357
+ if not project_root:
358
+ project_root = _default_project_root()
359
+
360
+ cleaned = []
361
+
362
+ # 1) Remove all session history
363
+ sessions_dir = os.path.join(state_dir, "bugs", bug_id, "sessions")
364
+ sessions_deleted = 0
365
+ if os.path.isdir(sessions_dir):
366
+ for entry in os.listdir(sessions_dir):
367
+ entry_path = os.path.join(sessions_dir, entry)
368
+ if os.path.isdir(entry_path):
369
+ shutil.rmtree(entry_path)
370
+ sessions_deleted += 1
371
+ cleaned.append("Deleted {} session(s) from {}".format(sessions_deleted, sessions_dir))
372
+
373
+ # 2) Remove transient files under bug dir (keep status.json)
374
+ bug_dir = os.path.join(state_dir, "bugs", bug_id)
375
+ if os.path.isdir(bug_dir):
376
+ for entry in os.listdir(bug_dir):
377
+ if entry == "status.json" or entry == "sessions":
378
+ continue
379
+ entry_path = os.path.join(bug_dir, entry)
380
+ if os.path.isdir(entry_path):
381
+ shutil.rmtree(entry_path)
382
+ cleaned.append("Deleted directory {}".format(entry_path))
383
+ elif os.path.isfile(entry_path):
384
+ os.remove(entry_path)
385
+ cleaned.append("Deleted file {}".format(entry_path))
386
+
387
+ # 3) Remove bugfix artifacts
388
+ bugfix_dir = os.path.join(project_root, ".prizmkit", "bugfix", bug_id)
389
+ if os.path.isdir(bugfix_dir):
390
+ file_count = sum(len(files) for _, _, files in os.walk(bugfix_dir))
391
+ shutil.rmtree(bugfix_dir)
392
+ cleaned.append("Deleted {} ({} files)".format(bugfix_dir, file_count))
393
+
394
+ # 4) Remove shared dev-team workspace
395
+ dev_team_dir = os.path.join(project_root, ".dev-team")
396
+ if os.path.isdir(dev_team_dir):
397
+ file_count = sum(len(files) for _, _, files in os.walk(dev_team_dir))
398
+ shutil.rmtree(dev_team_dir)
399
+ cleaned.append("Deleted {} ({} files)".format(dev_team_dir, file_count))
400
+
401
+ # 5) (removed: current-session.json no longer used)
402
+
403
+ return cleaned
404
+
405
+
406
+ def load_session_status(state_dir, bug_id, session_id):
407
+ session_status_path = os.path.join(
408
+ state_dir, "bugs", bug_id, "sessions",
409
+ session_id, "session-status.json"
410
+ )
411
+ data, err = load_json_file(session_status_path)
412
+ if err:
413
+ return None, err
414
+ return data, None
415
+
416
+
417
+ # ---------------------------------------------------------------------------
418
+ # Action: status
419
+ # ---------------------------------------------------------------------------
420
+
421
+ COLOR_GREEN = "\033[92m"
422
+ COLOR_YELLOW = "\033[93m"
423
+ COLOR_RED = "\033[91m"
424
+ COLOR_GRAY = "\033[90m"
425
+ COLOR_MAGENTA = "\033[95m"
426
+ COLOR_BOLD = "\033[1m"
427
+ COLOR_RESET = "\033[0m"
428
+
429
+ BOX_WIDTH = 68
430
+
431
+
432
+ SEVERITY_ICONS = {
433
+ "critical": COLOR_RED + "🔴" + COLOR_RESET,
434
+ "high": COLOR_MAGENTA + "🟠" + COLOR_RESET,
435
+ "medium": COLOR_YELLOW + "🟡" + COLOR_RESET,
436
+ "low": COLOR_GRAY + "🟢" + COLOR_RESET,
437
+ }
438
+
439
+
440
+ def action_status(bug_list_data, state_dir):
441
+ bugs = bug_list_data.get("bugs", [])
442
+ project_name = bug_list_data.get("project_name", "Unknown")
443
+
444
+ counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "needs_info": 0, "skipped": 0}
445
+ bug_lines = []
446
+
447
+ for bug in bugs:
448
+ if not isinstance(bug, dict):
449
+ continue
450
+ bid = bug.get("id")
451
+ title = bug.get("title", "Untitled")
452
+ severity = bug.get("severity", "medium")
453
+ if not bid:
454
+ continue
455
+
456
+ bstatus = bug.get("status", "pending")
457
+ bs = load_bug_status(state_dir, bid)
458
+ retry_count = bs.get("retry_count", 0)
459
+ max_retries_val = bs.get("max_retries", 3)
460
+ resume_phase = bs.get("resume_from_phase")
461
+
462
+ if bstatus in counts:
463
+ counts[bstatus] += 1
464
+ else:
465
+ counts["pending"] += 1
466
+
467
+ # Status icon
468
+ if bstatus == "completed":
469
+ icon = COLOR_GREEN + "[✓]" + COLOR_RESET
470
+ elif bstatus == "in_progress":
471
+ icon = COLOR_YELLOW + "[→]" + COLOR_RESET
472
+ elif bstatus == "failed":
473
+ icon = COLOR_RED + "[✗]" + COLOR_RESET
474
+ elif bstatus == "needs_info":
475
+ icon = COLOR_MAGENTA + "[?]" + COLOR_RESET
476
+ elif bstatus == "skipped":
477
+ icon = COLOR_GRAY + "[—]" + COLOR_RESET
478
+ else:
479
+ icon = COLOR_GRAY + "[ ]" + COLOR_RESET
480
+
481
+ # Severity badge
482
+ sev_badge = "[{}]".format(severity[:4].upper())
483
+
484
+ # Detail
485
+ detail = ""
486
+ if bstatus == "in_progress":
487
+ parts = []
488
+ if retry_count > 0:
489
+ parts.append("retry {}/{}".format(retry_count, max_retries_val))
490
+ if resume_phase is not None:
491
+ parts.append("CP-BF-{}".format(resume_phase))
492
+ if parts:
493
+ detail = " ({})".format(", ".join(parts))
494
+ elif bstatus == "failed":
495
+ detail = " (failed after {} retries)".format(retry_count)
496
+ elif bstatus == "needs_info":
497
+ detail = " (needs more info)"
498
+
499
+ # Colorize
500
+ if bstatus == "completed":
501
+ line_content = "{} {} {} {} {}{}".format(
502
+ bid, icon, sev_badge, COLOR_GREEN + title + COLOR_RESET, "", detail
503
+ )
504
+ elif bstatus == "in_progress":
505
+ line_content = "{} {} {} {} {}{}".format(
506
+ bid, icon, sev_badge, COLOR_YELLOW + title + COLOR_RESET, "", detail
507
+ )
508
+ elif bstatus == "failed":
509
+ line_content = "{} {} {} {} {}{}".format(
510
+ bid, icon, sev_badge, COLOR_RED + title + COLOR_RESET, "", detail
511
+ )
512
+ elif bstatus == "needs_info":
513
+ line_content = "{} {} {} {} {}{}".format(
514
+ bid, icon, sev_badge, COLOR_MAGENTA + title + COLOR_RESET, "", detail
515
+ )
516
+ else:
517
+ line_content = "{} {} {} {} {}{}".format(
518
+ bid, icon, sev_badge, COLOR_GRAY + title + COLOR_RESET, "", detail
519
+ )
520
+
521
+ bug_lines.append(line_content)
522
+
523
+ total = len(bugs)
524
+ completed = counts["completed"]
525
+ percent = round(completed / total * 100, 1) if total > 0 else 0.0
526
+ progress_bar = _build_progress_bar(percent, width=24)
527
+
528
+ summary_line = "Total: {} bugs | Completed: {} | In Progress: {}".format(
529
+ total, completed, counts["in_progress"]
530
+ )
531
+ summary_line2 = "Failed: {} | Pending: {} | Needs Info: {} | Skipped: {}".format(
532
+ counts["failed"], counts["pending"], counts["needs_info"], counts["skipped"]
533
+ )
534
+
535
+ inner = BOX_WIDTH - 2
536
+ print("╔" + "═" * BOX_WIDTH + "╗")
537
+ print("║" + pad_right(COLOR_BOLD + " Bug-Fix Pipeline Status" + COLOR_RESET, inner) + " ║")
538
+ print("╠" + "═" * BOX_WIDTH + "╣")
539
+ print("║" + pad_right(" Project: {}".format(project_name), inner) + " ║")
540
+ print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
541
+ print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
542
+ print("╠" + "─" * BOX_WIDTH + "╣")
543
+ print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
544
+ print("╠" + "═" * BOX_WIDTH + "╣")
545
+ for line in bug_lines:
546
+ print("║" + pad_right(" {}".format(line), inner) + " ║")
547
+ print("╚" + "═" * BOX_WIDTH + "╝")
548
+
549
+
550
+ # ---------------------------------------------------------------------------
551
+ # Action: reset
552
+ # ---------------------------------------------------------------------------
553
+
554
+ def action_reset(args, bug_list_path, state_dir):
555
+ bug_id = args.bug_id
556
+ if not bug_id:
557
+ error_out("--bug-id is required for 'reset' action")
558
+ return
559
+
560
+ bs = load_bug_status(state_dir, bug_id)
561
+ old_status = get_bug_status_from_list(bug_list_path, bug_id)
562
+ old_retry = bs.get("retry_count", 0)
563
+
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 .prizmkit/plans/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. (removed: current-session.json no longer used)
633
+
634
+ # 5. Reset status
635
+ bs = load_bug_status(state_dir, bug_id)
636
+ old_status = get_bug_status_from_list(bug_list_path, bug_id)
637
+ old_retry = bs.get("retry_count", 0)
638
+
639
+ bs["retry_count"] = 0
640
+ bs["sessions"] = []
641
+ bs["last_session_id"] = None
642
+ bs["resume_from_phase"] = None
643
+ bs["updated_at"] = now_iso()
644
+
645
+ err = save_bug_status(state_dir, bug_id, bs)
646
+ if err:
647
+ error_out("Failed to save bug status: {}".format(err))
648
+ return
649
+
650
+ err = update_bug_in_list(bug_list_path, bug_id, "pending")
651
+ if err:
652
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
653
+ return
654
+
655
+ result = {
656
+ "action": "clean",
657
+ "bug_id": bug_id,
658
+ "old_status": old_status,
659
+ "old_retry_count": old_retry,
660
+ "new_status": "pending",
661
+ "sessions_deleted": sessions_deleted,
662
+ "cleaned": cleaned,
663
+ }
664
+ print(json.dumps(result, indent=2, ensure_ascii=False))
665
+
666
+
667
+ # ---------------------------------------------------------------------------
668
+ # Action: pause
669
+ # ---------------------------------------------------------------------------
670
+
671
+ def action_pause(state_dir):
672
+ pipeline_path = os.path.join(state_dir, "pipeline.json")
673
+ data, err = load_json_file(pipeline_path)
674
+ if err:
675
+ data = {"status": "paused", "paused_at": now_iso()}
676
+ else:
677
+ data["status"] = "paused"
678
+ data["paused_at"] = now_iso()
679
+
680
+ err = write_json_file(pipeline_path, data)
681
+ if err:
682
+ error_out("Failed to write pipeline.json: {}".format(err))
683
+ return
684
+
685
+ result = {
686
+ "action": "pause",
687
+ "status": "paused",
688
+ "paused_at": data["paused_at"],
689
+ }
690
+ print(json.dumps(result, indent=2, ensure_ascii=False))
691
+
692
+
693
+ # ---------------------------------------------------------------------------
694
+ # Action: start
695
+ # ---------------------------------------------------------------------------
696
+
697
+ def action_start(args, bug_list_path, state_dir):
698
+ """Mark a bug as in_progress when a session starts.
699
+
700
+ This keeps bug-fix-list.json/state status in sync during execution,
701
+ instead of only updating after session end.
702
+ """
703
+ bug_id = args.bug_id
704
+ if not bug_id:
705
+ error_out("--bug-id is required for 'start' action")
706
+ return
707
+
708
+ bs = load_bug_status(state_dir, bug_id)
709
+ old_status = get_bug_status_from_list(bug_list_path, bug_id)
710
+
711
+ bs["updated_at"] = now_iso()
712
+
713
+ err = save_bug_status(state_dir, bug_id, bs)
714
+ if err:
715
+ error_out("Failed to save bug status: {}".format(err))
716
+ return
717
+
718
+ err = update_bug_in_list(bug_list_path, bug_id, "in_progress")
719
+ if err:
720
+ error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
721
+ return
722
+
723
+ result = {
724
+ "action": "start",
725
+ "bug_id": bug_id,
726
+ "old_status": old_status,
727
+ "new_status": "in_progress",
728
+ "updated_at": bs["updated_at"],
729
+ }
730
+ print(json.dumps(result, indent=2, ensure_ascii=False))
731
+
732
+
733
+ # ---------------------------------------------------------------------------
734
+ # Action: unskip
735
+ # ---------------------------------------------------------------------------
736
+
737
+ def action_unskip(args, bug_list_path, state_dir):
738
+ """Reset skipped bugs back to pending.
739
+
740
+ Two modes:
741
+ - --bug-id B-001: Reset the specified skipped bug to pending.
742
+ - No --bug-id: Reset ALL skipped bugs to pending.
743
+ """
744
+ bug_id = args.bug_id
745
+
746
+ data, err = load_json_file(bug_list_path)
747
+ if err:
748
+ error_out("Cannot load bug fix list: {}".format(err))
749
+ return
750
+ bugs = data.get("bugs", [])
751
+
752
+ to_reset = set()
753
+
754
+ if bug_id:
755
+ # Find the target bug
756
+ target = None
757
+ for b in bugs:
758
+ if isinstance(b, dict) and b.get("id") == bug_id:
759
+ target = b
760
+ break
761
+ if not target:
762
+ error_out("Bug '{}' not found in .prizmkit/plans/bug-fix-list.json".format(bug_id))
763
+ return
764
+ if target.get("status") not in ("failed", "skipped", "needs_info"):
765
+ error_out(
766
+ "Bug '{}' has status '{}', expected 'failed', 'skipped', or 'needs_info'".format(
767
+ bug_id, target.get("status", "unknown")
768
+ )
769
+ )
770
+ return
771
+ to_reset.add(bug_id)
772
+ else:
773
+ # No bug-id: reset ALL skipped/failed/needs_info bugs
774
+ for b in bugs:
775
+ if isinstance(b, dict) and b.get("id"):
776
+ if b.get("status") in ("failed", "skipped", "needs_info"):
777
+ to_reset.add(b["id"])
778
+
779
+ if not to_reset:
780
+ error_out("No bugs to unskip")
781
+ return
782
+
783
+ # Reset all collected bugs in bug-fix-list.json
784
+ reset_details = []
785
+ for b in bugs:
786
+ if isinstance(b, dict) and b.get("id") in to_reset:
787
+ old_status = b.get("status", "unknown")
788
+ b["status"] = "pending"
789
+ reset_details.append({
790
+ "bug_id": b["id"],
791
+ "title": b.get("title", ""),
792
+ "old_status": old_status,
793
+ })
794
+
795
+ err = write_json_file(bug_list_path, data)
796
+ if err:
797
+ error_out("Failed to write .prizmkit/plans/bug-fix-list.json: {}".format(err))
798
+ return
799
+
800
+ # Reset runtime fields in status.json for each bug
801
+ for bid in to_reset:
802
+ bs = load_bug_status(state_dir, bid)
803
+ bs["retry_count"] = 0
804
+ bs["sessions"] = []
805
+ bs["last_session_id"] = None
806
+ bs["resume_from_phase"] = None
807
+ bs["updated_at"] = now_iso()
808
+ save_bug_status(state_dir, bid, bs)
809
+
810
+ result = {
811
+ "action": "unskip",
812
+ "reset_count": len(to_reset),
813
+ "bugs": reset_details,
814
+ }
815
+ print(json.dumps(result, indent=2, ensure_ascii=False))
816
+
817
+
818
+ # ---------------------------------------------------------------------------
819
+ # Main
820
+ # ---------------------------------------------------------------------------
821
+
822
+ def main():
823
+ args = parse_args()
824
+
825
+ if args.action == "update":
826
+ if not args.bug_id:
827
+ error_out("--bug-id is required for 'update' action")
828
+ if not args.session_status:
829
+ error_out("--session-status is required for 'update' action")
830
+ if args.action in ("start", "reset", "clean", "complete"):
831
+ if not args.bug_id:
832
+ error_out("--bug-id is required for '{}' action".format(args.action))
833
+ if args.action == "clean":
834
+ if not args.project_root:
835
+ error_out("--project-root is required for 'clean' action")
836
+
837
+ bug_list_data, err = load_json_file(args.bug_list)
838
+ if err:
839
+ error_out("Cannot load bug fix list: {}".format(err))
840
+
841
+ if args.action == "get_next":
842
+ action_get_next(bug_list_data, args.state_dir)
843
+ elif args.action == "start":
844
+ action_start(args, args.bug_list, args.state_dir)
845
+ elif args.action == "update":
846
+ action_update(args, args.bug_list, args.state_dir)
847
+ elif args.action == "status":
848
+ action_status(bug_list_data, args.state_dir)
849
+ elif args.action == "reset":
850
+ action_reset(args, args.bug_list, args.state_dir)
851
+ elif args.action == "clean":
852
+ action_clean(args, args.bug_list, args.state_dir)
853
+ elif args.action == "pause":
854
+ action_pause(args.state_dir)
855
+ elif args.action == "unskip":
856
+ action_unskip(args, args.bug_list, args.state_dir)
857
+ elif args.action == "complete":
858
+ # Shortcut: 'complete' is equivalent to 'update --session-status success'
859
+ args.session_status = "success"
860
+ action_update(args, args.bug_list, args.state_dir)
861
+
862
+
863
+ if __name__ == "__main__":
864
+ main()