prizmkit 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/retry-bug.sh +12 -0
- package/bundled/dev-pipeline/retry-feature.sh +12 -0
- package/bundled/dev-pipeline/run-bugfix.sh +71 -57
- package/bundled/dev-pipeline/run.sh +87 -57
- 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 +73 -2
- package/bundled/dev-pipeline/scripts/utils.py +22 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +38 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +39 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +39 -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"}
|
|
@@ -401,12 +403,54 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
401
403
|
fs = load_feature_status(state_dir, feature_id)
|
|
402
404
|
|
|
403
405
|
if session_status == "success":
|
|
406
|
+
# No-op guard: if this exact successful session was already recorded,
|
|
407
|
+
# avoid rewriting state files again (prevents post-commit dirty changes).
|
|
408
|
+
existing_sessions = fs.get("sessions", [])
|
|
409
|
+
already_completed = fs.get("status") == "completed" and fs.get("resume_from_phase") is None
|
|
410
|
+
same_session_already_recorded = (
|
|
411
|
+
session_id
|
|
412
|
+
and session_id in existing_sessions
|
|
413
|
+
and fs.get("last_session_id") == session_id
|
|
414
|
+
)
|
|
415
|
+
if already_completed and (same_session_already_recorded or not session_id):
|
|
416
|
+
summary = {
|
|
417
|
+
"action": "update",
|
|
418
|
+
"feature_id": feature_id,
|
|
419
|
+
"session_status": session_status,
|
|
420
|
+
"new_status": fs.get("status", "completed"),
|
|
421
|
+
"retry_count": fs.get("retry_count", 0),
|
|
422
|
+
"resume_from_phase": fs.get("resume_from_phase"),
|
|
423
|
+
"updated_at": fs.get("updated_at"),
|
|
424
|
+
"no_op": True,
|
|
425
|
+
}
|
|
426
|
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
427
|
+
return
|
|
428
|
+
|
|
404
429
|
fs["status"] = "completed"
|
|
405
430
|
fs["resume_from_phase"] = None
|
|
406
431
|
err = update_feature_in_list(feature_list_path, feature_id, "completed")
|
|
407
432
|
if err:
|
|
408
433
|
error_out("Failed to update feature-list.json: {}".format(err))
|
|
409
434
|
return
|
|
435
|
+
elif session_status in ("commit_missing", "docs_missing"):
|
|
436
|
+
# Degraded outcome: keep artifacts for retry and expose specific status.
|
|
437
|
+
fs["retry_count"] = fs.get("retry_count", 0) + 1
|
|
438
|
+
|
|
439
|
+
if fs["retry_count"] >= max_retries:
|
|
440
|
+
fs["status"] = "failed"
|
|
441
|
+
target_status = "failed"
|
|
442
|
+
else:
|
|
443
|
+
fs["status"] = session_status
|
|
444
|
+
target_status = session_status
|
|
445
|
+
|
|
446
|
+
fs["resume_from_phase"] = None
|
|
447
|
+
fs["sessions"] = []
|
|
448
|
+
fs["last_session_id"] = None
|
|
449
|
+
|
|
450
|
+
err = update_feature_in_list(feature_list_path, feature_id, target_status)
|
|
451
|
+
if err:
|
|
452
|
+
error_out("Failed to update feature-list.json: {}".format(err))
|
|
453
|
+
return
|
|
410
454
|
else:
|
|
411
455
|
fs["retry_count"] = fs.get("retry_count", 0) + 1
|
|
412
456
|
|
|
@@ -456,7 +500,10 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
456
500
|
"resume_from_phase": fs.get("resume_from_phase"),
|
|
457
501
|
"updated_at": fs["updated_at"],
|
|
458
502
|
}
|
|
459
|
-
if session_status
|
|
503
|
+
if session_status in ("commit_missing", "docs_missing"):
|
|
504
|
+
summary["degraded_reason"] = session_status
|
|
505
|
+
summary["restart_policy"] = "finalization_retry"
|
|
506
|
+
elif session_status != "success":
|
|
460
507
|
summary["restart_policy"] = "full_restart"
|
|
461
508
|
summary["cleanup_performed"] = cleaned
|
|
462
509
|
|
|
@@ -628,7 +675,15 @@ def action_status(feature_list_data, state_dir):
|
|
|
628
675
|
app_name = feature_list_data.get("app_name", "Unknown")
|
|
629
676
|
|
|
630
677
|
# Gather status info
|
|
631
|
-
counts = {
|
|
678
|
+
counts = {
|
|
679
|
+
"completed": 0,
|
|
680
|
+
"in_progress": 0,
|
|
681
|
+
"failed": 0,
|
|
682
|
+
"pending": 0,
|
|
683
|
+
"skipped": 0,
|
|
684
|
+
"commit_missing": 0,
|
|
685
|
+
"docs_missing": 0,
|
|
686
|
+
}
|
|
632
687
|
feature_lines = []
|
|
633
688
|
|
|
634
689
|
# Build dependency info: feature_id -> list of dep_ids that are not completed
|
|
@@ -671,6 +726,10 @@ def action_status(feature_list_data, state_dir):
|
|
|
671
726
|
icon = COLOR_RED + "[✗]" + COLOR_RESET
|
|
672
727
|
elif fstatus == "skipped":
|
|
673
728
|
icon = COLOR_GRAY + "[—]" + COLOR_RESET
|
|
729
|
+
elif fstatus == "commit_missing":
|
|
730
|
+
icon = COLOR_RED + "[↑]" + COLOR_RESET
|
|
731
|
+
elif fstatus == "docs_missing":
|
|
732
|
+
icon = COLOR_RED + "[D]" + COLOR_RESET
|
|
674
733
|
else:
|
|
675
734
|
icon = COLOR_GRAY + "[ ]" + COLOR_RESET
|
|
676
735
|
|
|
@@ -686,6 +745,10 @@ def action_status(feature_list_data, state_dir):
|
|
|
686
745
|
detail = " ({})".format(", ".join(parts))
|
|
687
746
|
elif fstatus == "failed":
|
|
688
747
|
detail = " (failed after {} retries)".format(retry_count)
|
|
748
|
+
elif fstatus == "commit_missing":
|
|
749
|
+
detail = " (commit missing, retry {}/{})".format(retry_count, max_retries_val)
|
|
750
|
+
elif fstatus == "docs_missing":
|
|
751
|
+
detail = " (docs missing, retry {}/{})".format(retry_count, max_retries_val)
|
|
689
752
|
elif fstatus == "pending":
|
|
690
753
|
# Check if blocked by dependencies
|
|
691
754
|
deps = feature.get("dependencies", [])
|
|
@@ -709,6 +772,10 @@ def action_status(feature_list_data, state_dir):
|
|
|
709
772
|
line_content = "{} {} {}{}".format(
|
|
710
773
|
fid, icon, COLOR_RED + title + COLOR_RESET, detail
|
|
711
774
|
)
|
|
775
|
+
elif fstatus in ("commit_missing", "docs_missing"):
|
|
776
|
+
line_content = "{} {} {}{}".format(
|
|
777
|
+
fid, icon, COLOR_RED + title + COLOR_RESET, detail
|
|
778
|
+
)
|
|
712
779
|
else:
|
|
713
780
|
line_content = "{} {} {}{}".format(
|
|
714
781
|
fid, icon, COLOR_GRAY + title + COLOR_RESET, detail
|
|
@@ -739,6 +806,9 @@ def action_status(feature_list_data, state_dir):
|
|
|
739
806
|
summary_line2 = "Failed: {} | Pending: {} | Skipped: {}".format(
|
|
740
807
|
counts["failed"], counts["pending"], counts["skipped"]
|
|
741
808
|
)
|
|
809
|
+
summary_line3 = "Commit Missing: {} | Docs Missing: {}".format(
|
|
810
|
+
counts["commit_missing"], counts["docs_missing"]
|
|
811
|
+
)
|
|
742
812
|
|
|
743
813
|
# 构建预估剩余时间行
|
|
744
814
|
CONFIDENCE_ICONS = {"high": "●", "medium": "◐", "low": "○"}
|
|
@@ -759,6 +829,7 @@ def action_status(feature_list_data, state_dir):
|
|
|
759
829
|
print("║" + pad_right(" App: {}".format(app_name), inner) + " ║")
|
|
760
830
|
print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
|
|
761
831
|
print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
|
|
832
|
+
print("║" + pad_right(" {}".format(summary_line3), inner) + " ║")
|
|
762
833
|
print("╠" + "─" * BOX_WIDTH + "╣")
|
|
763
834
|
print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
|
|
764
835
|
print("║" + pad_right(" {}".format(eta_line), inner) + " ║")
|
|
@@ -6,6 +6,7 @@ to avoid duplication across pipeline scripts.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import logging
|
|
9
10
|
import os
|
|
10
11
|
import sys
|
|
11
12
|
|
|
@@ -49,6 +50,27 @@ def write_json_file(path, data):
|
|
|
49
50
|
return None
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
def setup_logging(name="prizmkit.dev_pipeline", level=None):
|
|
54
|
+
"""Configure and return a standard logger for pipeline scripts.
|
|
55
|
+
|
|
56
|
+
Logs are written to stderr to avoid interfering with stdout JSON outputs.
|
|
57
|
+
"""
|
|
58
|
+
resolved_level = (level or os.environ.get("PRIZMKIT_LOG_LEVEL", "INFO")).upper()
|
|
59
|
+
numeric_level = getattr(logging, resolved_level, logging.INFO)
|
|
60
|
+
|
|
61
|
+
root_logger = logging.getLogger()
|
|
62
|
+
if not root_logger.handlers:
|
|
63
|
+
logging.basicConfig(
|
|
64
|
+
level=numeric_level,
|
|
65
|
+
stream=sys.stderr,
|
|
66
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(name)
|
|
70
|
+
logger.setLevel(numeric_level)
|
|
71
|
+
return logger
|
|
72
|
+
|
|
73
|
+
|
|
52
74
|
def error_out(message, code=1):
|
|
53
75
|
"""Print an error JSON and exit with the given code."""
|
|
54
76
|
output = {"error": message}
|
|
@@ -111,6 +111,21 @@ Key decisions: [list]
|
|
|
111
111
|
|
|
112
112
|
**CP-2**: All acceptance criteria met, tests pass.
|
|
113
113
|
|
|
114
|
+
### Phase 4.5: Prizm Doc Update (mandatory for feature sessions)
|
|
115
|
+
|
|
116
|
+
Run `prizmkit.doc.update` and sync project docs before commit:
|
|
117
|
+
1. Use `git diff --cached --name-status` (fallback: `git diff --name-status`) to locate changed modules
|
|
118
|
+
2. Update affected `.prizm-docs/` files (L1/L2, changelog.prizm)
|
|
119
|
+
3. Stage documentation updates (`git add .prizm-docs/`) if changed
|
|
120
|
+
|
|
121
|
+
Doc maintenance pass condition (pipeline-enforced): `REGISTRY.md` **or** `.prizm-docs/` changed in the final commit.
|
|
122
|
+
|
|
123
|
+
### Phase 4.7: Retrospective (feature sessions only, before commit)
|
|
124
|
+
|
|
125
|
+
If this session is a feature (not a bug-fix-only commit), run `prizmkit.retrospective` now — **before committing**.
|
|
126
|
+
Retrospective must update relevant `.prizm-docs/` sections (TRAPS/RULES/DECISIONS) when applicable, so those changes are included in the feature commit.
|
|
127
|
+
Stage any `.prizm-docs/` changes produced: `git add .prizm-docs/`
|
|
128
|
+
|
|
114
129
|
### Phase 5: Commit
|
|
115
130
|
|
|
116
131
|
- Run `prizmkit.summarize` → archive to REGISTRY.md
|
|
@@ -122,6 +137,7 @@ Key decisions: [list]
|
|
|
122
137
|
--feature-id "{{FEATURE_ID}}" --session-id "{{SESSION_ID}}" --action complete
|
|
123
138
|
```
|
|
124
139
|
- Run `prizmkit.committer` → `feat({{FEATURE_ID}}): {{FEATURE_TITLE}}`, do NOT push
|
|
140
|
+
- MANDATORY: commit must be done via `prizmkit.committer` skill. Do NOT run manual `git add`/`git commit` as a substitute.
|
|
125
141
|
|
|
126
142
|
---
|
|
127
143
|
|
|
@@ -135,7 +151,7 @@ Write to: `{{SESSION_STATUS_PATH}}`
|
|
|
135
151
|
"feature_id": "{{FEATURE_ID}}",
|
|
136
152
|
"feature_slug": "{{FEATURE_SLUG}}",
|
|
137
153
|
"exec_tier": 1,
|
|
138
|
-
"status": "<success|partial|failed>",
|
|
154
|
+
"status": "<success|partial|failed|commit_missing|docs_missing>",
|
|
139
155
|
"completed_phases": [0, 1, 2, 3, 4, 5],
|
|
140
156
|
"current_phase": 5,
|
|
141
157
|
"checkpoint_reached": "CP-2",
|
|
@@ -144,6 +160,8 @@ Write to: `{{SESSION_STATUS_PATH}}`
|
|
|
144
160
|
"errors": [],
|
|
145
161
|
"can_resume": false,
|
|
146
162
|
"resume_from_phase": null,
|
|
163
|
+
"docs_maintained": true,
|
|
164
|
+
"retrospective_done": true,
|
|
147
165
|
"artifacts": {
|
|
148
166
|
"context_snapshot_path": ".prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md",
|
|
149
167
|
"plan_path": ".prizmkit/specs/{{FEATURE_SLUG}}/plan.md",
|
|
@@ -154,6 +172,23 @@ Write to: `{{SESSION_STATUS_PATH}}`
|
|
|
154
172
|
}
|
|
155
173
|
```
|
|
156
174
|
|
|
175
|
+
### Step 3.1: Final Clean Check (before exit)
|
|
176
|
+
|
|
177
|
+
After writing `session-status.json`, verify repository is clean:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
git status --short
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If any files remain, include them in the last commit:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
git add -A
|
|
187
|
+
git commit --amend --no-edit
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Re-check `git status --short` and ensure it is empty before exiting.
|
|
191
|
+
|
|
157
192
|
## Critical Paths
|
|
158
193
|
|
|
159
194
|
| Resource | Path |
|
|
@@ -168,4 +203,5 @@ Write to: `{{SESSION_STATUS_PATH}}`
|
|
|
168
203
|
- Tier 1: you do everything — no subagents, no TeamCreate
|
|
169
204
|
- Build context-snapshot.md FIRST; use it throughout instead of re-reading files
|
|
170
205
|
- ALWAYS write session-status.json before exiting
|
|
171
|
-
- `prizmkit.committer` is mandatory — do NOT skip the commit phase
|
|
206
|
+
- `prizmkit.committer` is mandatory — do NOT skip the commit phase, and do NOT replace it with manual git commit commands
|
|
207
|
+
- Before exiting, `git status --short` must be empty
|