prizmkit 1.1.8 → 1.1.10
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 +3 -3
- package/bundled/adapters/codebuddy/skill-adapter.js +21 -7
- package/bundled/agents/prizm-dev-team-reviewer.md +53 -173
- package/bundled/dev-pipeline/.env.example +45 -0
- package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +535 -0
- package/bundled/dev-pipeline/assets/feature-list-example.json +0 -1
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +57 -12
- package/bundled/dev-pipeline/launch-feature-daemon.sh +3 -1
- package/bundled/dev-pipeline/launch-refactor-daemon.sh +57 -12
- package/bundled/dev-pipeline/lib/branch.sh +6 -1
- package/bundled/dev-pipeline/lib/common.sh +71 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +2 -2
- package/bundled/dev-pipeline/retry-bugfix.sh +60 -23
- package/bundled/dev-pipeline/retry-feature.sh +47 -12
- package/bundled/dev-pipeline/retry-refactor.sh +105 -23
- package/bundled/dev-pipeline/run-bugfix.sh +265 -44
- package/bundled/dev-pipeline/run-feature.sh +35 -1
- package/bundled/dev-pipeline/run-refactor.sh +376 -51
- package/bundled/dev-pipeline/scripts/check-session-status.py +24 -1
- package/bundled/dev-pipeline/scripts/detect-stuck.py +195 -85
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +31 -19
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +19 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +98 -11
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +30 -5
- package/bundled/dev-pipeline/scripts/init-pipeline.py +3 -3
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +15 -4
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -5
- package/bundled/dev-pipeline/scripts/patch-completion-notes.py +191 -0
- package/bundled/dev-pipeline/scripts/update-bug-status.py +159 -14
- package/bundled/dev-pipeline/scripts/update-feature-status.py +79 -37
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +343 -13
- package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +1 -1
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +7 -11
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +41 -7
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +27 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +43 -19
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +54 -26
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +5 -14
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +36 -25
- package/bundled/dev-pipeline/templates/feature-list-schema.json +23 -11
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +270 -0
- package/bundled/dev-pipeline/templates/refactor-list-schema.json +10 -2
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +3 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +1 -0
- package/bundled/dev-pipeline/templates/sections/feature-context.md +2 -0
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +29 -2
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +22 -0
- package/bundled/dev-pipeline/templates/sections/phase-deploy-verification.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +8 -6
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +7 -5
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
- package/bundled/skills/_metadata.json +5 -22
- package/bundled/skills/app-planner/SKILL.md +92 -66
- package/bundled/skills/app-planner/assets/app-design-guide.md +1 -1
- package/bundled/skills/app-planner/references/architecture-decisions.md +1 -1
- package/bundled/skills/app-planner/references/project-brief-guide.md +69 -66
- package/bundled/skills/bug-fix-workflow/SKILL.md +47 -4
- package/bundled/skills/bug-planner/SKILL.md +130 -188
- package/bundled/skills/bug-planner/assets/bug-confirmation-template.md +43 -0
- package/bundled/skills/bug-planner/references/critic-and-verification.md +44 -0
- package/bundled/skills/bug-planner/references/error-recovery.md +73 -0
- package/bundled/skills/bug-planner/references/input-formats.md +53 -0
- package/bundled/skills/bug-planner/references/schema-validation.md +25 -0
- package/bundled/skills/bug-planner/references/severity-rules.md +16 -0
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -5
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -10
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +16 -3
- package/bundled/skills/feature-planner/SKILL.md +33 -122
- package/bundled/skills/feature-planner/assets/evaluation-guide.md +1 -1
- package/bundled/skills/feature-planner/assets/planning-guide.md +21 -5
- package/bundled/skills/feature-planner/references/browser-interaction.md +2 -4
- package/bundled/skills/feature-planner/references/completeness-review.md +57 -0
- package/bundled/skills/feature-planner/references/error-recovery.md +15 -34
- package/bundled/skills/feature-planner/references/incremental-feature-planning.md +1 -1
- package/bundled/skills/feature-planner/references/new-project-planning.md +2 -2
- package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -2
- package/bundled/skills/feature-workflow/SKILL.md +3 -4
- package/bundled/skills/prizm-kit/SKILL.md +39 -49
- package/bundled/skills/prizmkit-code-review/SKILL.md +51 -64
- package/bundled/skills/prizmkit-code-review/rules/dimensions.md +85 -0
- package/bundled/skills/prizmkit-code-review/rules/fix-strategy.md +11 -11
- package/bundled/skills/prizmkit-committer/SKILL.md +3 -31
- package/bundled/skills/prizmkit-deploy/SKILL.md +34 -31
- package/bundled/skills/prizmkit-deploy/assets/deploy-template.md +1 -1
- package/bundled/skills/prizmkit-implement/SKILL.md +35 -68
- package/bundled/skills/prizmkit-init/SKILL.md +112 -65
- package/bundled/skills/prizmkit-init/assets/project-brief-template.md +82 -0
- package/bundled/skills/prizmkit-plan/SKILL.md +120 -79
- package/bundled/skills/prizmkit-plan/assets/plan-template.md +28 -18
- package/bundled/skills/prizmkit-plan/assets/spec-template.md +28 -11
- package/bundled/skills/prizmkit-plan/references/clarify-guide.md +3 -3
- package/bundled/skills/prizmkit-plan/references/verification-checklist.md +60 -0
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +10 -81
- package/bundled/skills/prizmkit-prizm-docs/assets/{PRIZM-SPEC.md → prizm-docs-format.md} +41 -526
- package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +46 -0
- package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +16 -0
- package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +14 -0
- package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +19 -0
- package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +17 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +27 -65
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -4
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +7 -25
- package/bundled/skills/recovery-workflow/SKILL.md +8 -8
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +17 -9
- package/bundled/skills/refactor-planner/SKILL.md +23 -41
- package/bundled/skills/refactor-workflow/SKILL.md +1 -2
- package/bundled/team/prizm-dev-team.json +1 -1
- package/bundled/{skills/prizm-kit/assets → templates}/project-memory-template.md +1 -1
- package/package.json +1 -1
- package/src/clean.js +0 -1
- package/src/gitignore-template.js +0 -1
- package/src/scaffold.js +10 -3
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-analyze.md +0 -5
- package/bundled/dev-pipeline/templates/sections/phase-analyze-agent.md +0 -19
- package/bundled/dev-pipeline/templates/sections/phase-analyze-full.md +0 -19
- package/bundled/skills/app-planner/references/project-conventions.md +0 -93
- package/bundled/skills/prizmkit-analyze/SKILL.md +0 -207
- package/bundled/skills/prizmkit-code-review/rules/dimensions-bugfix.md +0 -25
- package/bundled/skills/prizmkit-code-review/rules/dimensions-feature.md +0 -43
- package/bundled/skills/prizmkit-code-review/rules/dimensions-refactor.md +0 -25
- package/bundled/skills/prizmkit-implement/references/deploy-guide-protocol.md +0 -69
- package/bundled/skills/prizmkit-verify/SKILL.md +0 -281
- package/bundled/skills/prizmkit-verify/scripts/verify-light.py +0 -402
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Core state machine for updating bug status in the bug-fix pipeline.
|
|
3
3
|
|
|
4
|
-
Handles
|
|
4
|
+
Handles eight actions:
|
|
5
5
|
- get_next: Find the next bug to process based on priority and severity
|
|
6
|
+
- start: Mark a bug as fixing when a session starts
|
|
6
7
|
- update: Update a bug's status based on session outcome
|
|
7
8
|
- status: Print a formatted overview of all bugs
|
|
8
9
|
- pause: Save pipeline state for graceful shutdown
|
|
9
10
|
- reset: Reset a bug to pending (status + retry count)
|
|
10
11
|
- clean: Reset + delete session history + delete bugfix artifacts
|
|
12
|
+
- unskip: Reset skipped bugs back to pending
|
|
11
13
|
|
|
12
14
|
Usage:
|
|
13
15
|
python3 update-bug-status.py \
|
|
14
16
|
--bug-list <path> --state-dir <path> \
|
|
15
|
-
--action <get_next|update|status|pause|reset|clean> \
|
|
17
|
+
--action <get_next|start|update|status|pause|reset|clean|unskip> \
|
|
16
18
|
[--bug-id <id>] [--session-status <status>] \
|
|
17
19
|
[--session-id <id>] [--max-retries <n>]
|
|
18
20
|
"""
|
|
@@ -39,12 +41,14 @@ SESSION_STATUS_VALUES = [
|
|
|
39
41
|
"failed",
|
|
40
42
|
"crashed",
|
|
41
43
|
"timed_out",
|
|
44
|
+
"commit_missing",
|
|
45
|
+
"docs_missing",
|
|
42
46
|
"merge_conflict",
|
|
43
47
|
]
|
|
44
48
|
|
|
45
49
|
TERMINAL_STATUSES = {"completed", "failed", "skipped", "needs_info"}
|
|
46
50
|
|
|
47
|
-
#
|
|
51
|
+
# Severity priority (lower value = higher priority)
|
|
48
52
|
SEVERITY_PRIORITY = {
|
|
49
53
|
"critical": 0,
|
|
50
54
|
"high": 1,
|
|
@@ -61,7 +65,7 @@ def parse_args():
|
|
|
61
65
|
parser.add_argument("--state-dir", required=True, help="Path to the state directory (default: .prizmkit/state/bugfix)")
|
|
62
66
|
parser.add_argument(
|
|
63
67
|
"--action", required=True,
|
|
64
|
-
choices=["get_next", "update", "status", "pause", "reset", "clean"],
|
|
68
|
+
choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "unskip", "complete"],
|
|
65
69
|
help="Action to perform",
|
|
66
70
|
)
|
|
67
71
|
parser.add_argument("--bug-id", default=None, help="Bug ID (required for 'update'/'reset'/'clean' actions)")
|
|
@@ -183,12 +187,12 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
183
187
|
elif bstatus == "pending":
|
|
184
188
|
pending_bugs.append(bug)
|
|
185
189
|
|
|
186
|
-
_PRIORITY_ORDER = {"
|
|
190
|
+
_PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
|
|
187
191
|
|
|
188
192
|
def sort_key(b):
|
|
189
193
|
severity = b.get("severity", "medium")
|
|
190
194
|
sev_order = SEVERITY_PRIORITY.get(severity, 2)
|
|
191
|
-
priority = _PRIORITY_ORDER.get(b.get("priority", "low"),
|
|
195
|
+
priority = _PRIORITY_ORDER.get(b.get("priority", "low"), 2)
|
|
192
196
|
return (sev_order, priority)
|
|
193
197
|
|
|
194
198
|
if in_progress_bugs:
|
|
@@ -196,7 +200,7 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
196
200
|
elif pending_bugs:
|
|
197
201
|
candidates = sorted(pending_bugs, key=sort_key)
|
|
198
202
|
else:
|
|
199
|
-
#
|
|
203
|
+
# All remaining bugs are in non-terminal but also non-pending/in_progress states
|
|
200
204
|
print("PIPELINE_BLOCKED")
|
|
201
205
|
return
|
|
202
206
|
|
|
@@ -240,17 +244,23 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
240
244
|
if err:
|
|
241
245
|
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
242
246
|
return
|
|
243
|
-
elif session_status
|
|
244
|
-
# Degraded outcome: keep artifacts for retry (branch preserves work)
|
|
247
|
+
elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
248
|
+
# Degraded outcome: keep artifacts for retry (branch preserves work).
|
|
249
|
+
# Write schema-valid status to bug-fix-list.json ("pending" for retry,
|
|
250
|
+
# "failed" if max retries exceeded). Store the granular degraded reason
|
|
251
|
+
# in status.json only (internal pipeline state, not schema-bound).
|
|
245
252
|
bs["retry_count"] = bs.get("retry_count", 0) + 1
|
|
246
253
|
|
|
247
254
|
if bs["retry_count"] >= max_retries:
|
|
248
255
|
bs["status"] = "failed"
|
|
249
256
|
target_status = "failed"
|
|
250
257
|
else:
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
# status.json keeps the granular degraded reason for diagnostics
|
|
259
|
+
bs["status"] = session_status
|
|
260
|
+
# bug-fix-list.json gets schema-valid "pending" (will be retried)
|
|
261
|
+
target_status = "pending"
|
|
253
262
|
|
|
263
|
+
bs["degraded_reason"] = session_status
|
|
254
264
|
bs["resume_from_phase"] = None
|
|
255
265
|
bs["sessions"] = []
|
|
256
266
|
bs["last_session_id"] = None
|
|
@@ -307,8 +317,8 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
307
317
|
"resume_from_phase": bs.get("resume_from_phase"),
|
|
308
318
|
"updated_at": bs["updated_at"],
|
|
309
319
|
}
|
|
310
|
-
if session_status
|
|
311
|
-
summary["degraded_reason"] =
|
|
320
|
+
if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
321
|
+
summary["degraded_reason"] = session_status
|
|
312
322
|
summary["restart_policy"] = "finalization_retry"
|
|
313
323
|
elif session_status != "success":
|
|
314
324
|
summary["restart_policy"] = "full_restart"
|
|
@@ -661,6 +671,133 @@ def action_pause(state_dir):
|
|
|
661
671
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
662
672
|
|
|
663
673
|
|
|
674
|
+
# ---------------------------------------------------------------------------
|
|
675
|
+
# Action: start
|
|
676
|
+
# ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
def action_start(args, bug_list_path, state_dir):
|
|
679
|
+
"""Mark a bug as in_progress when a session starts.
|
|
680
|
+
|
|
681
|
+
This keeps bug-fix-list.json/state status in sync during execution,
|
|
682
|
+
instead of only updating after session end.
|
|
683
|
+
"""
|
|
684
|
+
bug_id = args.bug_id
|
|
685
|
+
if not bug_id:
|
|
686
|
+
error_out("--bug-id is required for 'start' action")
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
bs = load_bug_status(state_dir, bug_id)
|
|
690
|
+
old_status = bs.get("status", "pending")
|
|
691
|
+
|
|
692
|
+
bs["status"] = "in_progress"
|
|
693
|
+
bs["updated_at"] = now_iso()
|
|
694
|
+
|
|
695
|
+
err = save_bug_status(state_dir, bug_id, bs)
|
|
696
|
+
if err:
|
|
697
|
+
error_out("Failed to save bug status: {}".format(err))
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
err = update_bug_in_list(bug_list_path, bug_id, "in_progress")
|
|
701
|
+
if err:
|
|
702
|
+
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
result = {
|
|
706
|
+
"action": "start",
|
|
707
|
+
"bug_id": bug_id,
|
|
708
|
+
"old_status": old_status,
|
|
709
|
+
"new_status": "in_progress",
|
|
710
|
+
"updated_at": bs["updated_at"],
|
|
711
|
+
}
|
|
712
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# ---------------------------------------------------------------------------
|
|
716
|
+
# Action: unskip
|
|
717
|
+
# ---------------------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
def action_unskip(args, bug_list_path, state_dir):
|
|
720
|
+
"""Reset skipped bugs back to pending.
|
|
721
|
+
|
|
722
|
+
Two modes:
|
|
723
|
+
- --bug-id B-001: Reset the specified skipped bug to pending.
|
|
724
|
+
- No --bug-id: Reset ALL skipped bugs to pending.
|
|
725
|
+
"""
|
|
726
|
+
bug_id = args.bug_id
|
|
727
|
+
|
|
728
|
+
data, err = load_json_file(bug_list_path)
|
|
729
|
+
if err:
|
|
730
|
+
error_out("Cannot load bug fix list: {}".format(err))
|
|
731
|
+
return
|
|
732
|
+
bugs = data.get("bugs", [])
|
|
733
|
+
|
|
734
|
+
to_reset = set()
|
|
735
|
+
|
|
736
|
+
if bug_id:
|
|
737
|
+
# Find the target bug
|
|
738
|
+
target = None
|
|
739
|
+
for b in bugs:
|
|
740
|
+
if isinstance(b, dict) and b.get("id") == bug_id:
|
|
741
|
+
target = b
|
|
742
|
+
break
|
|
743
|
+
if not target:
|
|
744
|
+
error_out("Bug '{}' not found in .prizmkit/plans/bug-fix-list.json".format(bug_id))
|
|
745
|
+
return
|
|
746
|
+
if target.get("status") not in ("failed", "skipped", "needs_info"):
|
|
747
|
+
error_out(
|
|
748
|
+
"Bug '{}' has status '{}', expected 'failed', 'skipped', or 'needs_info'".format(
|
|
749
|
+
bug_id, target.get("status", "unknown")
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
return
|
|
753
|
+
to_reset.add(bug_id)
|
|
754
|
+
else:
|
|
755
|
+
# No bug-id: reset ALL skipped/failed/needs_info bugs
|
|
756
|
+
for b in bugs:
|
|
757
|
+
if isinstance(b, dict) and b.get("id"):
|
|
758
|
+
if b.get("status") in ("failed", "skipped", "needs_info"):
|
|
759
|
+
to_reset.add(b["id"])
|
|
760
|
+
|
|
761
|
+
if not to_reset:
|
|
762
|
+
error_out("No bugs to unskip")
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
# Reset all collected bugs in bug-fix-list.json
|
|
766
|
+
reset_details = []
|
|
767
|
+
for b in bugs:
|
|
768
|
+
if isinstance(b, dict) and b.get("id") in to_reset:
|
|
769
|
+
old_status = b.get("status", "unknown")
|
|
770
|
+
b["status"] = "pending"
|
|
771
|
+
reset_details.append({
|
|
772
|
+
"bug_id": b["id"],
|
|
773
|
+
"title": b.get("title", ""),
|
|
774
|
+
"old_status": old_status,
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
err = write_json_file(bug_list_path, data)
|
|
778
|
+
if err:
|
|
779
|
+
error_out("Failed to write .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
780
|
+
return
|
|
781
|
+
|
|
782
|
+
# Reset status.json for each bug
|
|
783
|
+
for bid in to_reset:
|
|
784
|
+
bs = load_bug_status(state_dir, bid)
|
|
785
|
+
bs["status"] = "pending"
|
|
786
|
+
bs["retry_count"] = 0
|
|
787
|
+
bs["sessions"] = []
|
|
788
|
+
bs["last_session_id"] = None
|
|
789
|
+
bs["resume_from_phase"] = None
|
|
790
|
+
bs["updated_at"] = now_iso()
|
|
791
|
+
save_bug_status(state_dir, bid, bs)
|
|
792
|
+
|
|
793
|
+
result = {
|
|
794
|
+
"action": "unskip",
|
|
795
|
+
"reset_count": len(to_reset),
|
|
796
|
+
"bugs": reset_details,
|
|
797
|
+
}
|
|
798
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
799
|
+
|
|
800
|
+
|
|
664
801
|
# ---------------------------------------------------------------------------
|
|
665
802
|
# Main
|
|
666
803
|
# ---------------------------------------------------------------------------
|
|
@@ -673,7 +810,7 @@ def main():
|
|
|
673
810
|
error_out("--bug-id is required for 'update' action")
|
|
674
811
|
if not args.session_status:
|
|
675
812
|
error_out("--session-status is required for 'update' action")
|
|
676
|
-
if args.action in ("reset", "clean"):
|
|
813
|
+
if args.action in ("start", "reset", "clean", "complete"):
|
|
677
814
|
if not args.bug_id:
|
|
678
815
|
error_out("--bug-id is required for '{}' action".format(args.action))
|
|
679
816
|
if args.action == "clean":
|
|
@@ -686,6 +823,8 @@ def main():
|
|
|
686
823
|
|
|
687
824
|
if args.action == "get_next":
|
|
688
825
|
action_get_next(bug_list_data, args.state_dir)
|
|
826
|
+
elif args.action == "start":
|
|
827
|
+
action_start(args, args.bug_list, args.state_dir)
|
|
689
828
|
elif args.action == "update":
|
|
690
829
|
action_update(args, args.bug_list, args.state_dir)
|
|
691
830
|
elif args.action == "status":
|
|
@@ -696,6 +835,12 @@ def main():
|
|
|
696
835
|
action_clean(args, args.bug_list, args.state_dir)
|
|
697
836
|
elif args.action == "pause":
|
|
698
837
|
action_pause(args.state_dir)
|
|
838
|
+
elif args.action == "unskip":
|
|
839
|
+
action_unskip(args, args.bug_list, args.state_dir)
|
|
840
|
+
elif args.action == "complete":
|
|
841
|
+
# Shortcut: 'complete' is equivalent to 'update --session-status success'
|
|
842
|
+
args.session_status = "success"
|
|
843
|
+
action_update(args, args.bug_list, args.state_dir)
|
|
699
844
|
|
|
700
845
|
|
|
701
846
|
if __name__ == "__main__":
|
|
@@ -608,16 +608,21 @@ def action_update(args, feature_list_path, state_dir):
|
|
|
608
608
|
error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
|
|
609
609
|
return
|
|
610
610
|
elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
611
|
-
# Degraded outcome: keep artifacts for retry
|
|
611
|
+
# Degraded outcome: keep artifacts for retry.
|
|
612
|
+
# Store granular reason in status.json (internal state),
|
|
613
|
+
# but write only schema-valid status to feature-list.json.
|
|
612
614
|
fs["retry_count"] = fs.get("retry_count", 0) + 1
|
|
613
615
|
|
|
614
616
|
if fs["retry_count"] >= max_retries:
|
|
615
617
|
fs["status"] = "failed"
|
|
616
618
|
target_status = "failed"
|
|
617
619
|
else:
|
|
620
|
+
# status.json keeps the granular degraded reason for diagnostics
|
|
618
621
|
fs["status"] = session_status
|
|
619
|
-
|
|
622
|
+
# feature-list.json gets schema-valid "pending" (will be retried)
|
|
623
|
+
target_status = "pending"
|
|
620
624
|
|
|
625
|
+
fs["degraded_reason"] = session_status
|
|
621
626
|
fs["resume_from_phase"] = None
|
|
622
627
|
fs["sessions"] = []
|
|
623
628
|
fs["last_session_id"] = None
|
|
@@ -707,11 +712,12 @@ BOX_WIDTH = 68
|
|
|
707
712
|
|
|
708
713
|
|
|
709
714
|
def _calc_feature_duration(state_dir, feature_id):
|
|
710
|
-
"""
|
|
715
|
+
"""Calculate the duration (in seconds) of a completed feature.
|
|
711
716
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
Computes duration from status.json's created_at and updated_at fields.
|
|
718
|
+
If session records exist, attempts to use the first session's started_at
|
|
719
|
+
to the last update time for the calculation.
|
|
720
|
+
Returns None if the duration cannot be calculated.
|
|
715
721
|
"""
|
|
716
722
|
fs_path = os.path.join(state_dir, "features", feature_id, "status.json")
|
|
717
723
|
if not os.path.isfile(fs_path):
|
|
@@ -730,7 +736,7 @@ def _calc_feature_duration(state_dir, feature_id):
|
|
|
730
736
|
t_start = datetime.strptime(created_at, fmt)
|
|
731
737
|
t_end = datetime.strptime(updated_at, fmt)
|
|
732
738
|
delta = (t_end - t_start).total_seconds()
|
|
733
|
-
#
|
|
739
|
+
# Filter outliers: ignore durations less than 10s or more than 24h
|
|
734
740
|
if delta < 10 or delta > 86400:
|
|
735
741
|
return None
|
|
736
742
|
return delta
|
|
@@ -739,7 +745,7 @@ def _calc_feature_duration(state_dir, feature_id):
|
|
|
739
745
|
|
|
740
746
|
|
|
741
747
|
def _format_duration(seconds):
|
|
742
|
-
"""
|
|
748
|
+
"""Format seconds into a human-readable duration string."""
|
|
743
749
|
if seconds is None:
|
|
744
750
|
return "N/A"
|
|
745
751
|
seconds = int(seconds)
|
|
@@ -756,17 +762,18 @@ def _format_duration(seconds):
|
|
|
756
762
|
|
|
757
763
|
|
|
758
764
|
def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None):
|
|
759
|
-
"""
|
|
765
|
+
"""Estimate remaining time based on completed feature durations, weighted by complexity.
|
|
760
766
|
|
|
761
|
-
|
|
762
|
-
1.
|
|
763
|
-
2.
|
|
764
|
-
|
|
767
|
+
Strategy:
|
|
768
|
+
1. Collect durations of all completed features, grouped by complexity
|
|
769
|
+
2. For remaining pending/in_progress features, estimate using the average duration
|
|
770
|
+
of the corresponding complexity level
|
|
771
|
+
3. If no historical data exists for a complexity level, fall back to the global average
|
|
765
772
|
|
|
766
|
-
|
|
767
|
-
confidence: "high" (>=50%
|
|
773
|
+
Returns an (estimated_seconds, confidence) tuple.
|
|
774
|
+
confidence: "high" (>=50% completed), "medium" (>=25%), "low" (<25%)
|
|
768
775
|
"""
|
|
769
|
-
#
|
|
776
|
+
# Complexity weights (used for estimation when no historical data is available)
|
|
770
777
|
COMPLEXITY_WEIGHT = {"low": 1.0, "medium": 2.0, "high": 4.0}
|
|
771
778
|
|
|
772
779
|
# Build feature-list status map for terminal status override
|
|
@@ -776,7 +783,7 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
|
|
|
776
783
|
if isinstance(f, dict) and f.get("id"):
|
|
777
784
|
fl_status_map[f["id"]] = f.get("status")
|
|
778
785
|
|
|
779
|
-
#
|
|
786
|
+
# Collect completed feature durations grouped by complexity
|
|
780
787
|
duration_by_complexity = {} # complexity -> [duration_seconds]
|
|
781
788
|
feature_complexity_map = {} # feature_id -> complexity
|
|
782
789
|
|
|
@@ -811,13 +818,13 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
|
|
|
811
818
|
if not all_durations:
|
|
812
819
|
return None, "low"
|
|
813
820
|
|
|
814
|
-
#
|
|
821
|
+
# Calculate average duration per complexity level
|
|
815
822
|
avg_by_complexity = {}
|
|
816
823
|
for c, durations in duration_by_complexity.items():
|
|
817
824
|
avg_by_complexity[c] = sum(durations) / len(durations)
|
|
818
825
|
global_avg = sum(all_durations) / len(all_durations)
|
|
819
826
|
|
|
820
|
-
#
|
|
827
|
+
# Estimate duration for remaining features
|
|
821
828
|
remaining_seconds = 0.0
|
|
822
829
|
remaining_count = 0
|
|
823
830
|
for feature in features:
|
|
@@ -835,12 +842,12 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
|
|
|
835
842
|
if complexity in avg_by_complexity:
|
|
836
843
|
remaining_seconds += avg_by_complexity[complexity]
|
|
837
844
|
else:
|
|
838
|
-
#
|
|
845
|
+
# No historical data for this complexity; estimate using global avg × weight ratio
|
|
839
846
|
weight = COMPLEXITY_WEIGHT.get(complexity, 2.0)
|
|
840
847
|
base_weight = COMPLEXITY_WEIGHT.get("medium", 2.0)
|
|
841
848
|
remaining_seconds += global_avg * (weight / base_weight)
|
|
842
849
|
|
|
843
|
-
#
|
|
850
|
+
# Calculate confidence level
|
|
844
851
|
total = len([f for f in features if isinstance(f, dict) and f.get("id")])
|
|
845
852
|
completed = counts.get("completed", 0)
|
|
846
853
|
if total > 0:
|
|
@@ -882,8 +889,6 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
882
889
|
"pending": 0,
|
|
883
890
|
"skipped": 0,
|
|
884
891
|
"auto_skipped": 0,
|
|
885
|
-
"commit_missing": 0,
|
|
886
|
-
"docs_missing": 0,
|
|
887
892
|
}
|
|
888
893
|
feature_lines = []
|
|
889
894
|
|
|
@@ -897,6 +902,19 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
897
902
|
continue
|
|
898
903
|
status_map[fid] = feature.get("status", "pending")
|
|
899
904
|
|
|
905
|
+
# Build degraded_reason map from status.json (internal pipeline state)
|
|
906
|
+
degraded_reason_map = {}
|
|
907
|
+
for feature in features:
|
|
908
|
+
if not isinstance(feature, dict):
|
|
909
|
+
continue
|
|
910
|
+
fid = feature.get("id")
|
|
911
|
+
if not fid:
|
|
912
|
+
continue
|
|
913
|
+
fs = load_feature_status(state_dir, fid, feature.get("status"))
|
|
914
|
+
dr = fs.get("degraded_reason")
|
|
915
|
+
if dr:
|
|
916
|
+
degraded_reason_map[fid] = dr
|
|
917
|
+
|
|
900
918
|
for feature in features:
|
|
901
919
|
if not isinstance(feature, dict):
|
|
902
920
|
continue
|
|
@@ -906,6 +924,7 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
906
924
|
continue
|
|
907
925
|
|
|
908
926
|
fstatus = feature.get("status", "pending")
|
|
927
|
+
degraded_reason = degraded_reason_map.get(fid)
|
|
909
928
|
|
|
910
929
|
# Count statuses
|
|
911
930
|
if fstatus in counts:
|
|
@@ -914,7 +933,14 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
914
933
|
counts["pending"] += 1
|
|
915
934
|
|
|
916
935
|
# Build status indicator and color
|
|
917
|
-
|
|
936
|
+
# Show degraded reason via icon when a pending feature has one
|
|
937
|
+
if fstatus == "pending" and degraded_reason == "commit_missing":
|
|
938
|
+
icon = COLOR_RED + "[↑]" + COLOR_RESET
|
|
939
|
+
elif fstatus == "pending" and degraded_reason == "docs_missing":
|
|
940
|
+
icon = COLOR_RED + "[D]" + COLOR_RESET
|
|
941
|
+
elif fstatus == "pending" and degraded_reason == "merge_conflict":
|
|
942
|
+
icon = COLOR_RED + "[⚡]" + COLOR_RESET
|
|
943
|
+
elif fstatus == "completed":
|
|
918
944
|
icon = COLOR_GREEN + "[✓]" + COLOR_RESET
|
|
919
945
|
elif fstatus == "in_progress":
|
|
920
946
|
icon = COLOR_YELLOW + "[→]" + COLOR_RESET
|
|
@@ -924,16 +950,22 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
924
950
|
icon = COLOR_GRAY + "[—]" + COLOR_RESET
|
|
925
951
|
elif fstatus == "auto_skipped":
|
|
926
952
|
icon = COLOR_GRAY + "[⊘]" + COLOR_RESET
|
|
927
|
-
elif fstatus == "commit_missing":
|
|
928
|
-
icon = COLOR_RED + "[↑]" + COLOR_RESET
|
|
929
|
-
elif fstatus == "docs_missing":
|
|
930
|
-
icon = COLOR_RED + "[D]" + COLOR_RESET
|
|
931
953
|
else:
|
|
932
954
|
icon = COLOR_GRAY + "[ ]" + COLOR_RESET
|
|
933
955
|
|
|
934
956
|
# Build detail suffix
|
|
935
957
|
detail = ""
|
|
936
|
-
if fstatus == "pending":
|
|
958
|
+
if fstatus == "pending" and degraded_reason:
|
|
959
|
+
detail = " ({}, retrying)".format(degraded_reason)
|
|
960
|
+
# Also check if blocked by dependencies
|
|
961
|
+
deps = feature.get("dependencies", [])
|
|
962
|
+
blocking = [
|
|
963
|
+
d for d in deps
|
|
964
|
+
if status_map.get(d, "pending") != "completed"
|
|
965
|
+
]
|
|
966
|
+
if blocking:
|
|
967
|
+
detail = " ({}, blocked by {})".format(degraded_reason, ", ".join(blocking))
|
|
968
|
+
elif fstatus == "pending":
|
|
937
969
|
# Check if blocked by dependencies
|
|
938
970
|
deps = feature.get("dependencies", [])
|
|
939
971
|
blocking = [
|
|
@@ -950,6 +982,8 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
950
982
|
]
|
|
951
983
|
if blockers:
|
|
952
984
|
detail = " (auto-skipped: blocked by {})".format(", ".join(blockers))
|
|
985
|
+
elif fstatus == "failed" and degraded_reason:
|
|
986
|
+
detail = " (last failure: {})".format(degraded_reason)
|
|
953
987
|
|
|
954
988
|
# Apply color to the whole line content
|
|
955
989
|
if fstatus == "completed":
|
|
@@ -964,7 +998,7 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
964
998
|
line_content = "{} {} {}{}".format(
|
|
965
999
|
fid, icon, COLOR_RED + title + COLOR_RESET, detail
|
|
966
1000
|
)
|
|
967
|
-
elif
|
|
1001
|
+
elif degraded_reason:
|
|
968
1002
|
line_content = "{} {} {}{}".format(
|
|
969
1003
|
fid, icon, COLOR_RED + title + COLOR_RESET, detail
|
|
970
1004
|
)
|
|
@@ -978,16 +1012,16 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
978
1012
|
total = len(features)
|
|
979
1013
|
completed = counts["completed"]
|
|
980
1014
|
|
|
981
|
-
#
|
|
1015
|
+
# Calculate percentage
|
|
982
1016
|
if total > 0:
|
|
983
1017
|
percent = round(completed / total * 100, 1)
|
|
984
1018
|
else:
|
|
985
1019
|
percent = 0.0
|
|
986
1020
|
|
|
987
|
-
#
|
|
1021
|
+
# Generate progress bar
|
|
988
1022
|
progress_bar = _build_progress_bar(percent, width=24)
|
|
989
1023
|
|
|
990
|
-
#
|
|
1024
|
+
# Estimate remaining time
|
|
991
1025
|
est_remaining, confidence = _estimate_remaining_time(
|
|
992
1026
|
features, state_dir, counts, feature_list_data
|
|
993
1027
|
)
|
|
@@ -998,11 +1032,18 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
998
1032
|
summary_line2 = "Failed: {} | Pending: {} | Skipped: {} | Auto-skipped: {}".format(
|
|
999
1033
|
counts["failed"], counts["pending"], counts["skipped"], counts["auto_skipped"]
|
|
1000
1034
|
)
|
|
1001
|
-
|
|
1002
|
-
|
|
1035
|
+
|
|
1036
|
+
# Count degraded features (pending with a degraded_reason from status.json)
|
|
1037
|
+
degraded_count = sum(
|
|
1038
|
+
1 for fid, dr in degraded_reason_map.items()
|
|
1039
|
+
if status_map.get(fid) == "pending" and dr
|
|
1003
1040
|
)
|
|
1041
|
+
if degraded_count > 0:
|
|
1042
|
+
summary_line3 = "Degraded (retrying): {}".format(degraded_count)
|
|
1043
|
+
else:
|
|
1044
|
+
summary_line3 = None
|
|
1004
1045
|
|
|
1005
|
-
#
|
|
1046
|
+
# Build estimated remaining time line
|
|
1006
1047
|
CONFIDENCE_ICONS = {"high": "●", "medium": "◐", "low": "○"}
|
|
1007
1048
|
if est_remaining is not None:
|
|
1008
1049
|
eta_str = _format_duration(est_remaining)
|
|
@@ -1021,7 +1062,8 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
|
|
|
1021
1062
|
print("║" + pad_right(" Project: {}".format(app_name), inner) + " ║")
|
|
1022
1063
|
print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
|
|
1023
1064
|
print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
|
|
1024
|
-
|
|
1065
|
+
if summary_line3:
|
|
1066
|
+
print("║" + pad_right(" {}".format(summary_line3), inner) + " ║")
|
|
1025
1067
|
print("╠" + "─" * BOX_WIDTH + "╣")
|
|
1026
1068
|
print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
|
|
1027
1069
|
print("║" + pad_right(" {}".format(eta_line), inner) + " ║")
|