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.
- package/bundled/VERSION.json +5 -0
- package/bundled/adapters/claude/agent-adapter.js +108 -0
- package/bundled/adapters/claude/command-adapter.js +104 -0
- package/bundled/adapters/claude/paths.js +35 -0
- package/bundled/adapters/claude/rules-adapter.js +77 -0
- package/bundled/adapters/claude/settings-adapter.js +73 -0
- package/bundled/adapters/claude/team-adapter.js +183 -0
- package/bundled/adapters/codebuddy/agent-adapter.js +43 -0
- package/bundled/adapters/codebuddy/paths.js +29 -0
- package/bundled/adapters/codebuddy/settings-adapter.js +47 -0
- package/bundled/adapters/codebuddy/skill-adapter.js +68 -0
- package/bundled/adapters/codebuddy/team-adapter.js +46 -0
- package/bundled/adapters/shared/frontmatter.js +77 -0
- package/bundled/agents/prizm-dev-team-coordinator.md +142 -0
- package/bundled/agents/prizm-dev-team-dev.md +99 -0
- package/bundled/agents/prizm-dev-team-pm.md +114 -0
- package/bundled/agents/prizm-dev-team-reviewer.md +119 -0
- package/bundled/dev-pipeline/README.md +482 -0
- package/bundled/dev-pipeline/assets/feature-list-example.json +147 -0
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +138 -0
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +425 -0
- package/bundled/dev-pipeline/launch-daemon.sh +549 -0
- package/bundled/dev-pipeline/reset-feature.sh +209 -0
- package/bundled/dev-pipeline/retry-bug.sh +344 -0
- package/bundled/dev-pipeline/retry-feature.sh +338 -0
- package/bundled/dev-pipeline/run-bugfix.sh +638 -0
- package/bundled/dev-pipeline/run.sh +845 -0
- package/bundled/dev-pipeline/scripts/check-session-status.py +158 -0
- package/bundled/dev-pipeline/scripts/detect-stuck.py +385 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +598 -0
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +402 -0
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +294 -0
- package/bundled/dev-pipeline/scripts/init-dev-team.py +134 -0
- package/bundled/dev-pipeline/scripts/init-pipeline.py +335 -0
- package/bundled/dev-pipeline/scripts/update-bug-status.py +748 -0
- package/bundled/dev-pipeline/scripts/update-feature-status.py +1076 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +262 -0
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +159 -0
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +291 -0
- package/bundled/dev-pipeline/templates/feature-list-schema.json +112 -0
- package/bundled/dev-pipeline/templates/session-status-schema.json +77 -0
- package/bundled/skills/_metadata.json +267 -0
- package/bundled/skills/app-planner/SKILL.md +580 -0
- package/bundled/skills/app-planner/assets/planning-guide.md +313 -0
- package/bundled/skills/app-planner/scripts/validate-and-generate.py +758 -0
- package/bundled/skills/bug-planner/SKILL.md +235 -0
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +252 -0
- package/bundled/skills/dev-pipeline-launcher/SKILL.md +223 -0
- package/bundled/skills/prizm-kit/SKILL.md +151 -0
- package/bundled/skills/prizm-kit/assets/claude-md-template.md +38 -0
- package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +35 -0
- package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +15 -0
- package/bundled/skills/prizmkit-adr-manager/SKILL.md +68 -0
- package/bundled/skills/prizmkit-adr-manager/assets/adr-template.md +26 -0
- package/bundled/skills/prizmkit-analyze/SKILL.md +194 -0
- package/bundled/skills/prizmkit-api-doc-generator/SKILL.md +56 -0
- package/bundled/skills/prizmkit-bug-fix-workflow/SKILL.md +351 -0
- package/bundled/skills/prizmkit-bug-reproducer/SKILL.md +62 -0
- package/bundled/skills/prizmkit-ci-cd-generator/SKILL.md +54 -0
- package/bundled/skills/prizmkit-clarify/SKILL.md +52 -0
- package/bundled/skills/prizmkit-code-review/SKILL.md +70 -0
- package/bundled/skills/prizmkit-committer/SKILL.md +117 -0
- package/bundled/skills/prizmkit-db-migration/SKILL.md +65 -0
- package/bundled/skills/prizmkit-dependency-health/SKILL.md +123 -0
- package/bundled/skills/prizmkit-deployment-strategy/SKILL.md +58 -0
- package/bundled/skills/prizmkit-error-triage/SKILL.md +55 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +47 -0
- package/bundled/skills/prizmkit-init/SKILL.md +156 -0
- package/bundled/skills/prizmkit-log-analyzer/SKILL.md +55 -0
- package/bundled/skills/prizmkit-monitoring-setup/SKILL.md +75 -0
- package/bundled/skills/prizmkit-onboarding-generator/SKILL.md +70 -0
- package/bundled/skills/prizmkit-perf-profiler/SKILL.md +55 -0
- package/bundled/skills/prizmkit-plan/SKILL.md +54 -0
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +37 -0
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +140 -0
- package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +943 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +79 -0
- package/bundled/skills/prizmkit-security-audit/SKILL.md +130 -0
- package/bundled/skills/prizmkit-specify/SKILL.md +52 -0
- package/bundled/skills/prizmkit-specify/assets/spec-template.md +37 -0
- package/bundled/skills/prizmkit-summarize/SKILL.md +51 -0
- package/bundled/skills/prizmkit-summarize/assets/registry-template.md +18 -0
- package/bundled/skills/prizmkit-tasks/SKILL.md +50 -0
- package/bundled/skills/prizmkit-tasks/assets/tasks-template.md +21 -0
- package/bundled/skills/prizmkit-tech-debt-tracker/SKILL.md +139 -0
- package/bundled/team/prizm-dev-team.json +47 -0
- package/bundled/templates/claude-md-template.md +38 -0
- package/bundled/templates/codebuddy-md-template.md +35 -0
- 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()
|