prizmkit 1.1.57 → 1.1.60

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 (188) hide show
  1. package/bin/create-prizmkit.js +8 -6
  2. package/bundled/VERSION.json +3 -3
  3. package/bundled/adapters/codex/agent-adapter.js +38 -0
  4. package/bundled/adapters/codex/paths.js +27 -0
  5. package/bundled/adapters/codex/rules-adapter.js +30 -0
  6. package/bundled/adapters/codex/settings-adapter.js +27 -0
  7. package/bundled/adapters/codex/skill-adapter.js +65 -0
  8. package/bundled/adapters/codex/team-adapter.js +37 -0
  9. package/bundled/dev-pipeline/.env.example +2 -1
  10. package/bundled/dev-pipeline/README.md +10 -7
  11. package/bundled/dev-pipeline/lib/common.sh +278 -37
  12. package/bundled/dev-pipeline/run-bugfix.sh +10 -61
  13. package/bundled/dev-pipeline/run-feature.sh +10 -78
  14. package/bundled/dev-pipeline/run-recovery.sh +10 -46
  15. package/bundled/dev-pipeline/run-refactor.sh +10 -61
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -7
  17. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +9 -3
  18. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +9 -3
  19. package/bundled/dev-pipeline/scripts/utils.py +6 -4
  20. package/bundled/dev-pipeline-windows/.env.example +28 -0
  21. package/bundled/dev-pipeline-windows/README.md +30 -0
  22. package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +525 -0
  23. package/bundled/dev-pipeline-windows/assets/feature-list-example.json +146 -0
  24. package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +138 -0
  25. package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +9 -0
  26. package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +9 -0
  27. package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +9 -0
  28. package/bundled/dev-pipeline-windows/lib/common.ps1 +432 -0
  29. package/bundled/dev-pipeline-windows/lib/daemon.ps1 +140 -0
  30. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +446 -0
  31. package/bundled/dev-pipeline-windows/lib/reset.ps1 +87 -0
  32. package/bundled/dev-pipeline-windows/reset-bug.ps1 +9 -0
  33. package/bundled/dev-pipeline-windows/reset-feature.ps1 +9 -0
  34. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +9 -0
  35. package/bundled/dev-pipeline-windows/run-bugfix.ps1 +9 -0
  36. package/bundled/dev-pipeline-windows/run-feature.ps1 +9 -0
  37. package/bundled/dev-pipeline-windows/run-recovery.ps1 +76 -0
  38. package/bundled/dev-pipeline-windows/run-refactor.ps1 +9 -0
  39. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +228 -0
  40. package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +192 -0
  41. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +530 -0
  42. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +1737 -0
  43. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +685 -0
  44. package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +805 -0
  45. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +763 -0
  46. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +316 -0
  47. package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +134 -0
  48. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +380 -0
  49. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +399 -0
  50. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +388 -0
  51. package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +191 -0
  52. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +864 -0
  53. package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +173 -0
  54. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +1501 -0
  55. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +1073 -0
  56. package/bundled/dev-pipeline-windows/scripts/utils.py +542 -0
  57. package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +7 -0
  58. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +7 -0
  59. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +30 -0
  60. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +5 -0
  61. package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +7 -0
  62. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +46 -0
  63. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +43 -0
  64. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +43 -0
  65. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +43 -0
  66. package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +263 -0
  67. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +320 -0
  68. package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +237 -0
  69. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +331 -0
  70. package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +270 -0
  71. package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +13 -0
  72. package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +91 -0
  73. package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +33 -0
  74. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +10 -0
  75. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +12 -0
  76. package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +7 -0
  77. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +8 -0
  78. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +9 -0
  79. package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +6 -0
  80. package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +21 -0
  81. package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +31 -0
  82. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +72 -0
  83. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +63 -0
  84. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +62 -0
  85. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +71 -0
  86. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +64 -0
  87. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +23 -0
  88. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +24 -0
  89. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +12 -0
  90. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +53 -0
  91. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +32 -0
  92. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +37 -0
  93. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +50 -0
  94. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +52 -0
  95. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +27 -0
  96. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +27 -0
  97. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +27 -0
  98. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +29 -0
  99. package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +77 -0
  100. package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +13 -0
  101. package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +23 -0
  102. package/bundled/dev-pipeline-windows/templates/sections/session-context.md +5 -0
  103. package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +6 -0
  104. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +67 -0
  105. package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +58 -0
  106. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +83 -0
  107. package/bundled/skills/_metadata.json +1 -1
  108. package/bundled/skills/app-planner/SKILL.md +26 -18
  109. package/bundled/skills/app-planner/references/architecture-decisions.md +9 -5
  110. package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
  111. package/bundled/skills/feature-planner/SKILL.md +9 -2
  112. package/bundled/skills/prizmkit-init/SKILL.md +7 -6
  113. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +2 -0
  114. package/bundled/skills-windows/app-planner/SKILL.md +639 -0
  115. package/bundled/skills-windows/app-planner/assets/app-design-guide.md +101 -0
  116. package/bundled/skills-windows/app-planner/references/architecture-decisions.md +52 -0
  117. package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +101 -0
  118. package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +71 -0
  119. package/bundled/skills-windows/app-planner/references/project-brief-guide.md +82 -0
  120. package/bundled/skills-windows/app-planner/references/red-team-checklist.md +40 -0
  121. package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +609 -0
  122. package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +285 -0
  123. package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +249 -0
  124. package/bundled/skills-windows/app-planner/references/rules/backend/template.md +173 -0
  125. package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +373 -0
  126. package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +211 -0
  127. package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +184 -0
  128. package/bundled/skills-windows/app-planner/references/rules/database/template.md +158 -0
  129. package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +810 -0
  130. package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +188 -0
  131. package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +302 -0
  132. package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +320 -0
  133. package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +639 -0
  134. package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +290 -0
  135. package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +232 -0
  136. package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +175 -0
  137. package/bundled/skills-windows/bug-fix-workflow/SKILL.md +415 -0
  138. package/bundled/skills-windows/bug-planner/SKILL.md +395 -0
  139. package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +43 -0
  140. package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +44 -0
  141. package/bundled/skills-windows/bug-planner/references/error-recovery.md +73 -0
  142. package/bundled/skills-windows/bug-planner/references/input-formats.md +53 -0
  143. package/bundled/skills-windows/bug-planner/references/schema-validation.md +25 -0
  144. package/bundled/skills-windows/bug-planner/references/severity-rules.md +16 -0
  145. package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +322 -0
  146. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +380 -0
  147. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +441 -0
  148. package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +462 -0
  149. package/bundled/skills-windows/feature-planner/SKILL.md +401 -0
  150. package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +64 -0
  151. package/bundled/skills-windows/feature-planner/assets/planning-guide.md +214 -0
  152. package/bundled/skills-windows/feature-planner/references/browser-interaction.md +59 -0
  153. package/bundled/skills-windows/feature-planner/references/completeness-review.md +57 -0
  154. package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +75 -0
  155. package/bundled/skills-windows/feature-planner/references/error-recovery.md +90 -0
  156. package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +112 -0
  157. package/bundled/skills-windows/feature-planner/references/new-project-planning.md +85 -0
  158. package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +1029 -0
  159. package/bundled/skills-windows/feature-workflow/SKILL.md +531 -0
  160. package/bundled/skills-windows/prizmkit-init/SKILL.md +356 -0
  161. package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +82 -0
  162. package/bundled/skills-windows/prizmkit-init/references/config-schema.md +68 -0
  163. package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +41 -0
  164. package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +13 -0
  165. package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +9 -0
  166. package/bundled/skills-windows/recovery-workflow/SKILL.md +456 -0
  167. package/bundled/skills-windows/recovery-workflow/evals/evals.json +46 -0
  168. package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +544 -0
  169. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +406 -0
  170. package/bundled/skills-windows/refactor-planner/SKILL.md +540 -0
  171. package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +292 -0
  172. package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +301 -0
  173. package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +221 -0
  174. package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +858 -0
  175. package/bundled/skills-windows/refactor-workflow/SKILL.md +503 -0
  176. package/package.json +3 -2
  177. package/src/clean.js +73 -2
  178. package/src/config.js +159 -50
  179. package/src/detect-platform.js +16 -8
  180. package/src/external-skills.js +26 -19
  181. package/src/index.js +31 -9
  182. package/src/manifest.js +6 -2
  183. package/src/metadata.js +43 -5
  184. package/src/platforms.js +36 -0
  185. package/src/prompts.js +31 -6
  186. package/src/runtimes.js +20 -0
  187. package/src/scaffold.js +314 -110
  188. package/src/upgrade.js +81 -41
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env python3
2
+ """Detect stuck items in the dev-pipeline (features, bugs, or refactors).
3
+
4
+ Checks each item for conditions that indicate it is stuck:
5
+ 1. Max retries exceeded
6
+ 2. Same checkpoint for consecutive sessions
7
+ 3. Stale or missing heartbeat (for in_progress items)
8
+ 4. Dependency deadlock (depends on a failed item)
9
+
10
+ Outputs a JSON report to stdout and exits with code 1 if any stuck
11
+ items are found, 0 otherwise.
12
+
13
+ Usage:
14
+ python3 detect-stuck.py --state-dir <path> --pipeline-type feature [--item-id <id>]
15
+ [--max-retries <n>] [--stale-threshold <seconds>]
16
+ [--task-list <path>]
17
+
18
+ # Legacy feature-only args still supported:
19
+ python3 detect-stuck.py --state-dir <path> [--feature-id <id>]
20
+ [--feature-list <path>]
21
+ """
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import sys
27
+ from datetime import datetime, timezone
28
+
29
+ from utils import error_out, setup_logging
30
+
31
+
32
+ LOGGER = setup_logging("detect-stuck")
33
+
34
+
35
+ def parse_args():
36
+ parser = argparse.ArgumentParser(
37
+ description="Detect stuck items in the dev-pipeline."
38
+ )
39
+ parser.add_argument(
40
+ "--state-dir",
41
+ required=True,
42
+ help="Path to the state directory (e.g. .prizmkit/state/features)",
43
+ )
44
+ parser.add_argument(
45
+ "--pipeline-type",
46
+ choices=["feature", "bugfix", "refactor"],
47
+ default=None,
48
+ help="Pipeline type (auto-detected from --feature-id/--bug-id/--refactor-id if omitted)",
49
+ )
50
+ parser.add_argument(
51
+ "--item-id",
52
+ default=None,
53
+ help="Check a specific item ID, or check all if omitted",
54
+ )
55
+ # Legacy feature-only args (still supported for backward compat)
56
+ parser.add_argument(
57
+ "--feature-id",
58
+ default=None,
59
+ help="(Legacy) Feature ID — equivalent to --pipeline-type feature --item-id <id>",
60
+ )
61
+ parser.add_argument(
62
+ "--bug-id",
63
+ default=None,
64
+ help="Bug ID — equivalent to --pipeline-type bugfix --item-id <id>",
65
+ )
66
+ parser.add_argument(
67
+ "--refactor-id",
68
+ default=None,
69
+ help="Refactor ID — equivalent to --pipeline-type refactor --item-id <id>",
70
+ )
71
+ parser.add_argument(
72
+ "--max-retries",
73
+ type=int,
74
+ default=3,
75
+ help="Maximum allowed retries before an item is considered stuck (default: 3)",
76
+ )
77
+ parser.add_argument(
78
+ "--stale-threshold",
79
+ type=int,
80
+ default=600,
81
+ help="Heartbeat staleness threshold in seconds (default: 600)",
82
+ )
83
+ parser.add_argument(
84
+ "--feature-list",
85
+ default=None,
86
+ help="(Legacy) Path to feature-list.json — use --task-list instead",
87
+ )
88
+ parser.add_argument(
89
+ "--bug-list",
90
+ default=None,
91
+ help="Path to bug-fix-list.json",
92
+ )
93
+ parser.add_argument(
94
+ "--refactor-list",
95
+ default=None,
96
+ help="Path to refactor-list.json",
97
+ )
98
+ parser.add_argument(
99
+ "--task-list",
100
+ default=None,
101
+ help="Path to the task list JSON (feature-list, bug-fix-list, or refactor-list)",
102
+ )
103
+ return parser.parse_args()
104
+
105
+
106
+ def load_json(path):
107
+ """Load and return parsed JSON from a file. Returns None on any error."""
108
+ try:
109
+ with open(path, "r", encoding="utf-8") as f:
110
+ return json.load(f)
111
+ except (IOError, OSError, json.JSONDecodeError, ValueError):
112
+ return None
113
+
114
+
115
+ def discover_item_ids(state_dir, subdir):
116
+ """Return a sorted list of item IDs found in state/{subdir}/."""
117
+ items_dir = os.path.join(state_dir, subdir)
118
+ if not os.path.isdir(items_dir):
119
+ return []
120
+ ids = []
121
+ for name in os.listdir(items_dir):
122
+ item_path = os.path.join(items_dir, name)
123
+ if os.path.isdir(item_path):
124
+ ids.append(name)
125
+ return sorted(ids)
126
+
127
+
128
+ def get_session_statuses(item_dir):
129
+ """Return session-status.json data for all sessions of an item, sorted by session ID.
130
+
131
+ Returns a list of (session_id, data) tuples.
132
+ """
133
+ sessions_dir = os.path.join(item_dir, "sessions")
134
+ if not os.path.isdir(sessions_dir):
135
+ return []
136
+ results = []
137
+ for session_name in sorted(os.listdir(sessions_dir)):
138
+ session_path = os.path.join(sessions_dir, session_name)
139
+ if not os.path.isdir(session_path):
140
+ continue
141
+ status_path = os.path.join(session_path, "session-status.json")
142
+ data = load_json(status_path)
143
+ if data is not None:
144
+ results.append((session_name, data))
145
+ return results
146
+
147
+
148
+ def parse_iso_timestamp(ts_str):
149
+ """Parse an ISO 8601 timestamp string to a datetime object.
150
+
151
+ Handles formats with and without timezone info. Returns None on failure.
152
+ """
153
+ if not isinstance(ts_str, str):
154
+ return None
155
+ # Try parsing with timezone (Z suffix or +HH:MM offset)
156
+ formats = [
157
+ "%Y-%m-%dT%H:%M:%SZ",
158
+ "%Y-%m-%dT%H:%M:%S+00:00",
159
+ "%Y-%m-%dT%H:%M:%S.%fZ",
160
+ "%Y-%m-%dT%H:%M:%S.%f+00:00",
161
+ ]
162
+ for fmt in formats:
163
+ try:
164
+ dt = datetime.strptime(ts_str, fmt)
165
+ return dt.replace(tzinfo=timezone.utc)
166
+ except ValueError:
167
+ continue
168
+ # Fallback: try stripping and replacing
169
+ try:
170
+ clean = ts_str.replace("Z", "+00:00")
171
+ # Python 3.7+ fromisoformat
172
+ if hasattr(datetime, "fromisoformat"):
173
+ dt = datetime.fromisoformat(clean)
174
+ if dt.tzinfo is None:
175
+ dt = dt.replace(tzinfo=timezone.utc)
176
+ return dt
177
+ except (ValueError, AttributeError):
178
+ pass
179
+ return None
180
+
181
+
182
+ def check_max_retries(item_status, max_retries):
183
+ """Check 1: Has the item exceeded the maximum retry count?
184
+
185
+ Returns a stuck-report dict or None.
186
+ """
187
+ retry_count = item_status.get("retry_count", 0)
188
+ if not isinstance(retry_count, int):
189
+ return None
190
+ if retry_count >= max_retries:
191
+ return {
192
+ "reason": "max_retries_exceeded",
193
+ "details": "Retry count {} has reached or exceeded max retries {}".format(
194
+ retry_count, max_retries
195
+ ),
196
+ "suggestion": "Investigate recurring failures and consider resetting the item or adjusting the approach",
197
+ }
198
+ return None
199
+
200
+
201
+ def check_stuck_checkpoint(item_dir):
202
+ """Check 2: Is the item stuck at the same checkpoint for 3 consecutive sessions?
203
+
204
+ Returns a stuck-report dict or None.
205
+ """
206
+ session_statuses = get_session_statuses(item_dir)
207
+ if len(session_statuses) < 3:
208
+ return None
209
+
210
+ # Take the last 3 sessions
211
+ last_three = session_statuses[-3:]
212
+ checkpoints = []
213
+ for _sid, data in last_three:
214
+ cp = data.get("checkpoint_reached")
215
+ checkpoints.append(cp)
216
+
217
+ # All three must be non-None and identical
218
+ if checkpoints[0] is not None and all(cp == checkpoints[0] for cp in checkpoints):
219
+ return {
220
+ "reason": "stuck_at_checkpoint",
221
+ "details": "Stuck at {} for 3 consecutive sessions".format(checkpoints[0]),
222
+ "suggestion": "Review plan.md generation - checkpoint {} validation is repeatedly failing".format(
223
+ checkpoints[0]
224
+ ),
225
+ }
226
+ return None
227
+
228
+
229
+ def check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_threshold, task_list_status=None):
230
+ """Check 3: Is the heartbeat stale or missing for an in_progress item?
231
+
232
+ Only applies to items whose status indicates active work.
233
+ Status is read from task_list_status (task list JSON, single source of truth).
234
+ Uses last_session_id from the item's own status.json to find the active session.
235
+
236
+ Returns a stuck-report dict or None.
237
+ """
238
+ status = task_list_status
239
+ # All pipelines now use "in_progress" as the active status
240
+ in_progress_statuses = {"in_progress"}
241
+ if status not in in_progress_statuses:
242
+ return None
243
+
244
+ # Use last_session_id from the item's own status
245
+ session_id = item_status.get("last_session_id")
246
+ if not session_id:
247
+ return None
248
+
249
+ # Check heartbeat file
250
+ heartbeat_path = os.path.join(
251
+ state_dir, items_subdir, item_id, "sessions", session_id, "heartbeat.json"
252
+ )
253
+ heartbeat = load_json(heartbeat_path)
254
+
255
+ if heartbeat is None:
256
+ return {
257
+ "reason": "no_heartbeat",
258
+ "details": "Item is {} but no heartbeat.json found for session {}".format(
259
+ status, session_id
260
+ ),
261
+ "suggestion": "The agent session may have crashed without writing a heartbeat - check session logs",
262
+ }
263
+
264
+ # Parse heartbeat timestamp and check staleness
265
+ ts_str = heartbeat.get("timestamp")
266
+ heartbeat_time = parse_iso_timestamp(ts_str)
267
+ if heartbeat_time is None:
268
+ return {
269
+ "reason": "stale_heartbeat",
270
+ "details": "Heartbeat has invalid or unparseable timestamp: {}".format(ts_str),
271
+ "suggestion": "Check the agent session - heartbeat timestamp is malformed",
272
+ }
273
+
274
+ now = datetime.now(timezone.utc)
275
+ age_seconds = (now - heartbeat_time).total_seconds()
276
+ if age_seconds > stale_threshold:
277
+ return {
278
+ "reason": "stale_heartbeat",
279
+ "details": "Heartbeat is {:.0f}s old (threshold: {}s) for session {}".format(
280
+ age_seconds, stale_threshold, session_id
281
+ ),
282
+ "suggestion": "The agent may be hung or crashed - consider terminating and retrying the session",
283
+ }
284
+
285
+ return None
286
+
287
+
288
+ def check_dependency_deadlock(item_id, task_list_data, state_dir, items_subdir, items_key):
289
+ """Check 4: Does this item depend on a failed item?
290
+
291
+ Reads dependency status from task list JSON (single source of truth).
292
+
293
+ Returns a stuck-report dict or None.
294
+ """
295
+ if task_list_data is None:
296
+ return None
297
+
298
+ items = task_list_data.get(items_key, [])
299
+ if not isinstance(items, list):
300
+ return None
301
+
302
+ # Build status map from task list
303
+ status_map = {}
304
+ for item in items:
305
+ if isinstance(item, dict) and item.get("id"):
306
+ status_map[item["id"]] = item.get("status", "pending")
307
+
308
+ # Find this item in the list to get its dependencies
309
+ deps = None
310
+ for item in items:
311
+ if not isinstance(item, dict):
312
+ continue
313
+ if item.get("id") == item_id:
314
+ deps = item.get("dependencies", [])
315
+ break
316
+
317
+ if not deps or not isinstance(deps, list):
318
+ return None
319
+
320
+ # Check each dependency's status from the task list
321
+ for dep_id in deps:
322
+ dep_state = status_map.get(dep_id)
323
+ if dep_state == "failed":
324
+ return {
325
+ "reason": "dependency_failed",
326
+ "details": "Depends on {} which has status 'failed'".format(dep_id),
327
+ "suggestion": "Fix or skip {} to unblock {}".format(dep_id, item_id),
328
+ }
329
+
330
+ return None
331
+
332
+
333
+ def find_task_list(state_dir):
334
+ """Attempt to locate and load the task list JSON via pipeline.json reference.
335
+
336
+ Resolves the list path relative to state_dir when it is a relative path,
337
+ so that pipeline.json is portable across machines and directory structures.
338
+ """
339
+ pipeline_path = os.path.join(state_dir, "pipeline.json")
340
+ pipeline = load_json(pipeline_path)
341
+ if pipeline is None:
342
+ return None
343
+
344
+ # Try various path keys used by different pipeline types
345
+ fl_path = (
346
+ pipeline.get("feature_list_path")
347
+ or pipeline.get("bug_list_path")
348
+ or pipeline.get("refactor_list_path")
349
+ )
350
+ if not fl_path:
351
+ return None
352
+
353
+ # Resolve relative paths relative to state_dir (not process cwd)
354
+ if not os.path.isabs(fl_path):
355
+ fl_path = os.path.join(state_dir, fl_path)
356
+
357
+ fl_path = os.path.normpath(fl_path)
358
+ if os.path.isfile(fl_path):
359
+ return load_json(fl_path)
360
+
361
+ return None
362
+
363
+
364
+ # Pipeline type configurations
365
+ PIPELINE_CONFIG = {
366
+ "feature": {"subdir": "features", "items_key": "features", "id_label": "feature_id"},
367
+ "bugfix": {"subdir": "bugs", "items_key": "bugs", "id_label": "bug_id"},
368
+ "refactor": {"subdir": "refactors", "items_key": "refactors", "id_label": "refactor_id"},
369
+ }
370
+
371
+
372
+ def check_item(item_id, state_dir, items_subdir, items_key, task_list_data, max_retries, stale_threshold):
373
+ """Run all stuck-detection checks on a single item.
374
+
375
+ Returns a list of stuck-report dicts (may be empty if item is not stuck).
376
+ """
377
+ item_dir = os.path.join(state_dir, items_subdir, item_id)
378
+ status_path = os.path.join(item_dir, "status.json")
379
+ item_status = load_json(status_path)
380
+
381
+ if item_status is None:
382
+ # Create a minimal runtime dict so checks can proceed
383
+ item_status = {}
384
+
385
+ # Look up item status from task list (single source of truth)
386
+ task_list_status = None
387
+ if task_list_data:
388
+ for item in task_list_data.get(items_key, []):
389
+ if isinstance(item, dict) and item.get("id") == item_id:
390
+ task_list_status = item.get("status", "pending")
391
+ break
392
+
393
+ reports = []
394
+
395
+ # Check 1: Max retries exceeded
396
+ result = check_max_retries(item_status, max_retries)
397
+ if result is not None:
398
+ reports.append(result)
399
+
400
+ # Check 2: Stuck at same checkpoint
401
+ result = check_stuck_checkpoint(item_dir)
402
+ if result is not None:
403
+ reports.append(result)
404
+
405
+ # Check 3: Stale heartbeat
406
+ result = check_stale_heartbeat(item_id, item_status, state_dir, items_subdir, stale_threshold, task_list_status)
407
+ if result is not None:
408
+ reports.append(result)
409
+
410
+ # Check 4: Dependency deadlock
411
+ result = check_dependency_deadlock(item_id, task_list_data, state_dir, items_subdir, items_key)
412
+ if result is not None:
413
+ reports.append(result)
414
+
415
+ return reports
416
+
417
+
418
+ def resolve_pipeline_type(args):
419
+ """Resolve pipeline type, item ID, and task list path from args.
420
+
421
+ Supports both new generic args and legacy feature-only args.
422
+ Returns (pipeline_type, item_id, task_list_path).
423
+ """
424
+ # Explicit --pipeline-type takes precedence
425
+ if args.pipeline_type:
426
+ ptype = args.pipeline_type
427
+ item_id = args.item_id
428
+ task_list = args.task_list
429
+ # Legacy / shorthand: --feature-id, --bug-id, --refactor-id
430
+ elif args.feature_id:
431
+ ptype = "feature"
432
+ item_id = args.feature_id
433
+ task_list = args.feature_list or args.task_list
434
+ elif args.bug_id:
435
+ ptype = "bugfix"
436
+ item_id = args.bug_id
437
+ task_list = args.bug_list or args.task_list
438
+ elif args.refactor_id:
439
+ ptype = "refactor"
440
+ item_id = args.refactor_id
441
+ task_list = args.refactor_list or args.task_list
442
+ # Legacy: --feature-list without --feature-id means check all features
443
+ elif args.feature_list:
444
+ ptype = "feature"
445
+ item_id = None
446
+ task_list = args.feature_list
447
+ elif args.bug_list:
448
+ ptype = "bugfix"
449
+ item_id = None
450
+ task_list = args.bug_list
451
+ elif args.refactor_list:
452
+ ptype = "refactor"
453
+ item_id = None
454
+ task_list = args.refactor_list
455
+ else:
456
+ # Default to feature for backward compat
457
+ ptype = "feature"
458
+ item_id = None
459
+ task_list = args.task_list
460
+
461
+ return ptype, item_id, task_list
462
+
463
+
464
+ def main():
465
+ args = parse_args()
466
+ state_dir = os.path.abspath(args.state_dir)
467
+
468
+ if not os.path.isdir(state_dir):
469
+ error_out("State directory not found: {}".format(state_dir), code=2)
470
+
471
+ # Resolve pipeline type and parameters
472
+ ptype, item_id, task_list_path = resolve_pipeline_type(args)
473
+ config = PIPELINE_CONFIG[ptype]
474
+ items_subdir = config["subdir"]
475
+ items_key = config["items_key"]
476
+ id_label = config["id_label"]
477
+
478
+ # Determine which items to check
479
+ if item_id:
480
+ item_ids = [item_id]
481
+ else:
482
+ item_ids = discover_item_ids(state_dir, items_subdir)
483
+
484
+ # Load task list for dependency checks
485
+ if task_list_path:
486
+ task_list_data = load_json(os.path.abspath(task_list_path))
487
+ else:
488
+ task_list_data = find_task_list(state_dir)
489
+
490
+ stuck_items = []
491
+ for iid in item_ids:
492
+ reports = check_item(
493
+ iid, state_dir, items_subdir, items_key,
494
+ task_list_data, args.max_retries, args.stale_threshold
495
+ )
496
+ for report in reports:
497
+ stuck_items.append(
498
+ {
499
+ id_label: iid,
500
+ "reason": report["reason"],
501
+ "details": report["details"],
502
+ "suggestion": report["suggestion"],
503
+ }
504
+ )
505
+
506
+ output = {
507
+ "pipeline_type": ptype,
508
+ "stuck_items": stuck_items,
509
+ "total_checked": len(item_ids),
510
+ "stuck_count": len(stuck_items),
511
+ }
512
+
513
+ print(json.dumps(output, indent=2, ensure_ascii=False))
514
+
515
+ if stuck_items:
516
+ sys.exit(1)
517
+ else:
518
+ sys.exit(0)
519
+
520
+
521
+ if __name__ == "__main__":
522
+ try:
523
+ main()
524
+ except KeyboardInterrupt:
525
+ error_out("detect-stuck interrupted", code=130)
526
+ except SystemExit:
527
+ raise
528
+ except Exception as exc:
529
+ LOGGER.exception("Unhandled exception in detect-stuck")
530
+ error_out("detect-stuck failed: {}".format(str(exc)), code=1)