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.
Files changed (123) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codebuddy/skill-adapter.js +21 -7
  3. package/bundled/agents/prizm-dev-team-reviewer.md +53 -173
  4. package/bundled/dev-pipeline/.env.example +45 -0
  5. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +535 -0
  6. package/bundled/dev-pipeline/assets/feature-list-example.json +0 -1
  7. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +57 -12
  8. package/bundled/dev-pipeline/launch-feature-daemon.sh +3 -1
  9. package/bundled/dev-pipeline/launch-refactor-daemon.sh +57 -12
  10. package/bundled/dev-pipeline/lib/branch.sh +6 -1
  11. package/bundled/dev-pipeline/lib/common.sh +71 -0
  12. package/bundled/dev-pipeline/lib/heartbeat.sh +2 -2
  13. package/bundled/dev-pipeline/retry-bugfix.sh +60 -23
  14. package/bundled/dev-pipeline/retry-feature.sh +47 -12
  15. package/bundled/dev-pipeline/retry-refactor.sh +105 -23
  16. package/bundled/dev-pipeline/run-bugfix.sh +265 -44
  17. package/bundled/dev-pipeline/run-feature.sh +35 -1
  18. package/bundled/dev-pipeline/run-refactor.sh +376 -51
  19. package/bundled/dev-pipeline/scripts/check-session-status.py +24 -1
  20. package/bundled/dev-pipeline/scripts/detect-stuck.py +195 -85
  21. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +31 -19
  22. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +19 -3
  23. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +98 -11
  24. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +30 -5
  25. package/bundled/dev-pipeline/scripts/init-pipeline.py +3 -3
  26. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +15 -4
  27. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -5
  28. package/bundled/dev-pipeline/scripts/patch-completion-notes.py +191 -0
  29. package/bundled/dev-pipeline/scripts/update-bug-status.py +159 -14
  30. package/bundled/dev-pipeline/scripts/update-feature-status.py +79 -37
  31. package/bundled/dev-pipeline/scripts/update-refactor-status.py +343 -13
  32. package/bundled/dev-pipeline/templates/agent-prompts/dev-fix.md +1 -1
  33. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +7 -11
  34. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +41 -7
  35. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +27 -3
  36. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +43 -19
  37. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +54 -26
  38. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +5 -14
  39. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +36 -25
  40. package/bundled/dev-pipeline/templates/feature-list-schema.json +23 -11
  41. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +270 -0
  42. package/bundled/dev-pipeline/templates/refactor-list-schema.json +10 -2
  43. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +3 -1
  44. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +1 -0
  45. package/bundled/dev-pipeline/templates/sections/feature-context.md +2 -0
  46. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +29 -2
  47. package/bundled/dev-pipeline/templates/sections/phase-commit.md +22 -0
  48. package/bundled/dev-pipeline/templates/sections/phase-deploy-verification.md +2 -2
  49. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +8 -6
  50. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +7 -5
  51. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
  52. package/bundled/skills/_metadata.json +5 -22
  53. package/bundled/skills/app-planner/SKILL.md +92 -66
  54. package/bundled/skills/app-planner/assets/app-design-guide.md +1 -1
  55. package/bundled/skills/app-planner/references/architecture-decisions.md +1 -1
  56. package/bundled/skills/app-planner/references/project-brief-guide.md +69 -66
  57. package/bundled/skills/bug-fix-workflow/SKILL.md +47 -4
  58. package/bundled/skills/bug-planner/SKILL.md +130 -188
  59. package/bundled/skills/bug-planner/assets/bug-confirmation-template.md +43 -0
  60. package/bundled/skills/bug-planner/references/critic-and-verification.md +44 -0
  61. package/bundled/skills/bug-planner/references/error-recovery.md +73 -0
  62. package/bundled/skills/bug-planner/references/input-formats.md +53 -0
  63. package/bundled/skills/bug-planner/references/schema-validation.md +25 -0
  64. package/bundled/skills/bug-planner/references/severity-rules.md +16 -0
  65. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -5
  66. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -10
  67. package/bundled/skills/feature-pipeline-launcher/SKILL.md +16 -3
  68. package/bundled/skills/feature-planner/SKILL.md +33 -122
  69. package/bundled/skills/feature-planner/assets/evaluation-guide.md +1 -1
  70. package/bundled/skills/feature-planner/assets/planning-guide.md +21 -5
  71. package/bundled/skills/feature-planner/references/browser-interaction.md +2 -4
  72. package/bundled/skills/feature-planner/references/completeness-review.md +57 -0
  73. package/bundled/skills/feature-planner/references/error-recovery.md +15 -34
  74. package/bundled/skills/feature-planner/references/incremental-feature-planning.md +1 -1
  75. package/bundled/skills/feature-planner/references/new-project-planning.md +2 -2
  76. package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -2
  77. package/bundled/skills/feature-workflow/SKILL.md +3 -4
  78. package/bundled/skills/prizm-kit/SKILL.md +39 -49
  79. package/bundled/skills/prizmkit-code-review/SKILL.md +51 -64
  80. package/bundled/skills/prizmkit-code-review/rules/dimensions.md +85 -0
  81. package/bundled/skills/prizmkit-code-review/rules/fix-strategy.md +11 -11
  82. package/bundled/skills/prizmkit-committer/SKILL.md +3 -31
  83. package/bundled/skills/prizmkit-deploy/SKILL.md +34 -31
  84. package/bundled/skills/prizmkit-deploy/assets/deploy-template.md +1 -1
  85. package/bundled/skills/prizmkit-implement/SKILL.md +35 -68
  86. package/bundled/skills/prizmkit-init/SKILL.md +112 -65
  87. package/bundled/skills/prizmkit-init/assets/project-brief-template.md +82 -0
  88. package/bundled/skills/prizmkit-plan/SKILL.md +120 -79
  89. package/bundled/skills/prizmkit-plan/assets/plan-template.md +28 -18
  90. package/bundled/skills/prizmkit-plan/assets/spec-template.md +28 -11
  91. package/bundled/skills/prizmkit-plan/references/clarify-guide.md +3 -3
  92. package/bundled/skills/prizmkit-plan/references/verification-checklist.md +60 -0
  93. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +10 -81
  94. package/bundled/skills/prizmkit-prizm-docs/assets/{PRIZM-SPEC.md → prizm-docs-format.md} +41 -526
  95. package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +46 -0
  96. package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +16 -0
  97. package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +14 -0
  98. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +19 -0
  99. package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +17 -0
  100. package/bundled/skills/prizmkit-retrospective/SKILL.md +27 -65
  101. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -4
  102. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +7 -25
  103. package/bundled/skills/recovery-workflow/SKILL.md +8 -8
  104. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +17 -9
  105. package/bundled/skills/refactor-planner/SKILL.md +23 -41
  106. package/bundled/skills/refactor-workflow/SKILL.md +1 -2
  107. package/bundled/team/prizm-dev-team.json +1 -1
  108. package/bundled/{skills/prizm-kit/assets → templates}/project-memory-template.md +1 -1
  109. package/package.json +1 -1
  110. package/src/clean.js +0 -1
  111. package/src/gitignore-template.js +0 -1
  112. package/src/scaffold.js +10 -3
  113. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-analyze.md +0 -5
  114. package/bundled/dev-pipeline/templates/sections/phase-analyze-agent.md +0 -19
  115. package/bundled/dev-pipeline/templates/sections/phase-analyze-full.md +0 -19
  116. package/bundled/skills/app-planner/references/project-conventions.md +0 -93
  117. package/bundled/skills/prizmkit-analyze/SKILL.md +0 -207
  118. package/bundled/skills/prizmkit-code-review/rules/dimensions-bugfix.md +0 -25
  119. package/bundled/skills/prizmkit-code-review/rules/dimensions-feature.md +0 -43
  120. package/bundled/skills/prizmkit-code-review/rules/dimensions-refactor.md +0 -25
  121. package/bundled/skills/prizmkit-implement/references/deploy-guide-protocol.md +0 -69
  122. package/bundled/skills/prizmkit-verify/SKILL.md +0 -281
  123. 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 six actions:
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 = {"critical": 0, "high": 1, "medium": 2, "low": 3}
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"), 3)
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
- # 所有剩余的 bug 都处于非终端但也非 pending/in_progress 状态
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 == "merge_conflict":
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
- bs["status"] = "merge_conflict"
252
- target_status = "merge_conflict"
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 == "merge_conflict":
311
- summary["degraded_reason"] = "merge_conflict"
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 and expose specific status.
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
- target_status = session_status
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
- """计算已完成 Feature 的耗时(秒)。
715
+ """Calculate the duration (in seconds) of a completed feature.
711
716
 
712
- 通过 status.json created_at updated_at 来计算。
713
- 如果有 session 记录,尝试用第一个 session started_at 到最后更新时间来计算。
714
- 返回 None 如果无法计算。
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
- # 过滤异常值:少于 10 秒或超过 24 小时的忽略
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
- """基于已完成 Feature 的历史耗时,按 complexity 加权预估剩余时间。
765
+ """Estimate remaining time based on completed feature durations, weighted by complexity.
760
766
 
761
- 策略:
762
- 1. 收集所有已完成 Feature 的耗时,按 complexity 分组
763
- 2. 对剩余的 pending/in_progress Feature,用对应 complexity 的平均耗时估算
764
- 3. 如果某个 complexity 没有历史数据,用全局平均值兜底
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
- 返回 (estimated_seconds, confidence) 元组。
767
- confidence: "high" (>=50% 已完成), "medium" (>=25%), "low" (<25%)
773
+ Returns an (estimated_seconds, confidence) tuple.
774
+ confidence: "high" (>=50% completed), "medium" (>=25%), "low" (<25%)
768
775
  """
769
- # complexity 权重(用于没有历史数据时的估算)
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
- # complexity 分组收集已完成 Feature 的耗时
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
- # 计算各 complexity 的平均耗时
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
- # 估算剩余 Feature 的耗时
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
- # 没有该 complexity 的历史数据,用全局均值 × 权重比例估算
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
- if fstatus == "completed":
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 fstatus in ("commit_missing", "docs_missing"):
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
- summary_line3 = "Commit Missing: {} | Docs Missing: {}".format(
1002
- counts["commit_missing"], counts["docs_missing"]
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
- print("║" + pad_right(" {}".format(summary_line3), inner) + " ║")
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) + " ║")