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