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.
- package/bin/create-prizmkit.js +8 -6
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/codex/agent-adapter.js +38 -0
- package/bundled/adapters/codex/paths.js +27 -0
- package/bundled/adapters/codex/rules-adapter.js +30 -0
- package/bundled/adapters/codex/settings-adapter.js +27 -0
- package/bundled/adapters/codex/skill-adapter.js +65 -0
- package/bundled/adapters/codex/team-adapter.js +37 -0
- package/bundled/dev-pipeline/.env.example +2 -1
- package/bundled/dev-pipeline/README.md +10 -7
- package/bundled/dev-pipeline/lib/common.sh +278 -37
- package/bundled/dev-pipeline/run-bugfix.sh +10 -61
- package/bundled/dev-pipeline/run-feature.sh +10 -78
- package/bundled/dev-pipeline/run-recovery.sh +10 -46
- package/bundled/dev-pipeline/run-refactor.sh +10 -61
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -7
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +9 -3
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +9 -3
- package/bundled/dev-pipeline/scripts/utils.py +6 -4
- package/bundled/dev-pipeline-windows/.env.example +28 -0
- package/bundled/dev-pipeline-windows/README.md +30 -0
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +525 -0
- package/bundled/dev-pipeline-windows/assets/feature-list-example.json +146 -0
- package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +138 -0
- package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +9 -0
- package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +9 -0
- package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +9 -0
- package/bundled/dev-pipeline-windows/lib/common.ps1 +432 -0
- package/bundled/dev-pipeline-windows/lib/daemon.ps1 +140 -0
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +446 -0
- package/bundled/dev-pipeline-windows/lib/reset.ps1 +87 -0
- package/bundled/dev-pipeline-windows/reset-bug.ps1 +9 -0
- package/bundled/dev-pipeline-windows/reset-feature.ps1 +9 -0
- package/bundled/dev-pipeline-windows/reset-refactor.ps1 +9 -0
- package/bundled/dev-pipeline-windows/run-bugfix.ps1 +9 -0
- package/bundled/dev-pipeline-windows/run-feature.ps1 +9 -0
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +76 -0
- package/bundled/dev-pipeline-windows/run-refactor.ps1 +9 -0
- package/bundled/dev-pipeline-windows/scripts/check-session-status.py +228 -0
- package/bundled/dev-pipeline-windows/scripts/cleanup-logs.py +192 -0
- package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +530 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +1737 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +685 -0
- package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +805 -0
- package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +763 -0
- package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +316 -0
- package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +134 -0
- package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +380 -0
- package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +399 -0
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +388 -0
- package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +191 -0
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +864 -0
- package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +173 -0
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +1501 -0
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +1073 -0
- package/bundled/dev-pipeline-windows/scripts/utils.py +542 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +7 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +7 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +30 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +5 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +7 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +46 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +43 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +43 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +43 -0
- package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +263 -0
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +320 -0
- package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +237 -0
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +331 -0
- package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +270 -0
- package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +13 -0
- package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +91 -0
- package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +33 -0
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +10 -0
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +12 -0
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +7 -0
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +8 -0
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +9 -0
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +6 -0
- package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +21 -0
- package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +31 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +72 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +63 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +62 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +71 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +64 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +23 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +24 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +12 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +53 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +32 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +37 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +50 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +52 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +27 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +27 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +27 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +29 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +77 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +13 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +23 -0
- package/bundled/dev-pipeline-windows/templates/sections/session-context.md +5 -0
- package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +6 -0
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +67 -0
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +58 -0
- package/bundled/dev-pipeline-windows/templates/session-status-schema.json +83 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +26 -18
- package/bundled/skills/app-planner/references/architecture-decisions.md +9 -5
- package/bundled/skills/app-planner/references/frontend-design-guide.md +1 -1
- package/bundled/skills/feature-planner/SKILL.md +9 -2
- package/bundled/skills/prizmkit-init/SKILL.md +7 -6
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +2 -0
- package/bundled/skills-windows/app-planner/SKILL.md +639 -0
- package/bundled/skills-windows/app-planner/assets/app-design-guide.md +101 -0
- package/bundled/skills-windows/app-planner/references/architecture-decisions.md +52 -0
- package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +101 -0
- package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +71 -0
- package/bundled/skills-windows/app-planner/references/project-brief-guide.md +82 -0
- package/bundled/skills-windows/app-planner/references/red-team-checklist.md +40 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +609 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +285 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +249 -0
- package/bundled/skills-windows/app-planner/references/rules/backend/template.md +173 -0
- package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +373 -0
- package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +211 -0
- package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +184 -0
- package/bundled/skills-windows/app-planner/references/rules/database/template.md +158 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +810 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +188 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +302 -0
- package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +320 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +639 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +290 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +232 -0
- package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +175 -0
- package/bundled/skills-windows/bug-fix-workflow/SKILL.md +415 -0
- package/bundled/skills-windows/bug-planner/SKILL.md +395 -0
- package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +43 -0
- package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +44 -0
- package/bundled/skills-windows/bug-planner/references/error-recovery.md +73 -0
- package/bundled/skills-windows/bug-planner/references/input-formats.md +53 -0
- package/bundled/skills-windows/bug-planner/references/schema-validation.md +25 -0
- package/bundled/skills-windows/bug-planner/references/severity-rules.md +16 -0
- package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +322 -0
- package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +380 -0
- package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +441 -0
- package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +462 -0
- package/bundled/skills-windows/feature-planner/SKILL.md +401 -0
- package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +64 -0
- package/bundled/skills-windows/feature-planner/assets/planning-guide.md +214 -0
- package/bundled/skills-windows/feature-planner/references/browser-interaction.md +59 -0
- package/bundled/skills-windows/feature-planner/references/completeness-review.md +57 -0
- package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +75 -0
- package/bundled/skills-windows/feature-planner/references/error-recovery.md +90 -0
- package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +112 -0
- package/bundled/skills-windows/feature-planner/references/new-project-planning.md +85 -0
- package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +1029 -0
- package/bundled/skills-windows/feature-workflow/SKILL.md +531 -0
- package/bundled/skills-windows/prizmkit-init/SKILL.md +356 -0
- package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +82 -0
- package/bundled/skills-windows/prizmkit-init/references/config-schema.md +68 -0
- package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +41 -0
- package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +13 -0
- package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +9 -0
- package/bundled/skills-windows/recovery-workflow/SKILL.md +456 -0
- package/bundled/skills-windows/recovery-workflow/evals/evals.json +46 -0
- package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +544 -0
- package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +406 -0
- package/bundled/skills-windows/refactor-planner/SKILL.md +540 -0
- package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +292 -0
- package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +301 -0
- package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +221 -0
- package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +858 -0
- package/bundled/skills-windows/refactor-workflow/SKILL.md +503 -0
- package/package.json +3 -2
- package/src/clean.js +73 -2
- package/src/config.js +159 -50
- package/src/detect-platform.js +16 -8
- package/src/external-skills.js +26 -19
- package/src/index.js +31 -9
- package/src/manifest.js +6 -2
- package/src/metadata.js +43 -5
- package/src/platforms.js +36 -0
- package/src/prompts.js +31 -6
- package/src/runtimes.js +20 -0
- package/src/scaffold.js +314 -110
- 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)
|