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,1073 @@
1
+ #!/usr/bin/env python3
2
+ """Core state machine for updating refactor status in the refactor pipeline.
3
+
4
+ Handles eight actions:
5
+ - get_next: Find the next refactor to process based on dependency order, priority, complexity
6
+ - start: Mark a refactor as in_progress when a session starts
7
+ - update: Update a refactor's status based on session outcome
8
+ - status: Print a formatted overview of all refactors
9
+ - pause: Save pipeline state for graceful shutdown
10
+ - reset: Reset a refactor to pending (status + retry count)
11
+ - clean: Reset + delete session history + delete refactor artifacts
12
+ - unskip: Reset skipped refactors and their downstream dependents back to pending
13
+
14
+ Usage:
15
+ python3 update-refactor-status.py \
16
+ --refactor-list <path> --state-dir <path> \
17
+ --action <get_next|start|update|status|pause|reset|clean|unskip> \
18
+ [--refactor-id <id>] [--session-status <status>] \
19
+ [--session-id <id>] [--max-retries <n>]
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import shutil
26
+ import sys
27
+ from datetime import datetime, timezone
28
+
29
+ from utils import (
30
+ load_json_file,
31
+ write_json_file,
32
+ error_out,
33
+ pad_right,
34
+ _build_progress_bar,
35
+ )
36
+
37
+
38
+ SESSION_STATUS_VALUES = [
39
+ "success",
40
+ "partial_resumable",
41
+ "partial_not_resumable",
42
+ "failed",
43
+ "crashed",
44
+ "timed_out",
45
+ "commit_missing",
46
+ "docs_missing",
47
+ "merge_conflict",
48
+ ]
49
+
50
+ TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped"}
51
+
52
+ # Artifact directory names (relative to project root)
53
+ REFACTOR_ARTIFACTS_REL = os.path.join(".prizmkit", "refactor")
54
+ DEV_TEAM_DIR_NAME = ".dev-team"
55
+
56
+ # Priority ordering (lower number = higher priority)
57
+ PRIORITY_ORDER = {
58
+ "critical": 0,
59
+ "high": 1,
60
+ "medium": 2,
61
+ "low": 3,
62
+ }
63
+
64
+ # Complexity ordering (lower number = simpler, processed first)
65
+ COMPLEXITY_ORDER = {
66
+ "low": 0,
67
+ "medium": 1,
68
+ "high": 2,
69
+ }
70
+
71
+
72
+ def parse_args():
73
+ parser = argparse.ArgumentParser(
74
+ description="Core state machine for refactor pipeline status management."
75
+ )
76
+ parser.add_argument("--refactor-list", required=True, help="Path to the .prizmkit/plans/refactor-list.json file")
77
+ parser.add_argument("--state-dir", required=True, help="Path to the state directory (default: .prizmkit/state/refactor)")
78
+ parser.add_argument(
79
+ "--action", required=True,
80
+ choices=["get_next", "start", "update", "status", "pause", "reset", "clean", "unskip", "complete"],
81
+ help="Action to perform",
82
+ )
83
+ parser.add_argument("--refactor-id", default=None, help="Refactor ID (required for 'update'/'reset'/'clean' actions)")
84
+ parser.add_argument(
85
+ "--session-status", default=None, choices=SESSION_STATUS_VALUES,
86
+ help="Session outcome status (required for 'update' action)",
87
+ )
88
+ parser.add_argument("--session-id", default=None, help="Session ID (optional, for 'update' action)")
89
+ parser.add_argument("--max-retries", type=int, default=3, help="Maximum retry count (default: 3)")
90
+ parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
91
+ return parser.parse_args()
92
+
93
+
94
+ def now_iso():
95
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
96
+
97
+
98
+ def _default_status(refactor_id):
99
+ """Create a default refactor runtime status object (no status field)."""
100
+ now = now_iso()
101
+ return {
102
+ "refactor_id": refactor_id,
103
+ "retry_count": 0,
104
+ "max_retries": 3,
105
+ "sessions": [],
106
+ "last_session_id": None,
107
+ "resume_from_phase": None,
108
+ "created_at": now,
109
+ "updated_at": now,
110
+ }
111
+
112
+
113
+ def load_refactor_status(state_dir, refactor_id):
114
+ """Load runtime state from status.json for a refactor.
115
+
116
+ Returns runtime fields only (retry_count, sessions, etc.).
117
+ The 'status' field is NOT included — status lives exclusively
118
+ in refactor-list.json.
119
+ """
120
+ status_path = os.path.join(state_dir, "refactors", refactor_id, "status.json")
121
+ if not os.path.isfile(status_path):
122
+ return _default_status(refactor_id)
123
+ data, err = load_json_file(status_path)
124
+ if err:
125
+ return _default_status(refactor_id)
126
+ # Defensively remove status if present (legacy data)
127
+ data.pop("status", None)
128
+ return data
129
+
130
+
131
+ def save_refactor_status(state_dir, refactor_id, status_data):
132
+ """Write the status.json for a refactor (runtime fields only)."""
133
+ # Defensively strip status — it belongs in refactor-list.json
134
+ status_data.pop("status", None)
135
+ status_path = os.path.join(state_dir, "refactors", refactor_id, "status.json")
136
+ return write_json_file(status_path, status_data)
137
+
138
+
139
+ def get_refactor_status_from_list(refactor_list_path, refactor_id):
140
+ """Read a single refactor's status from refactor-list.json."""
141
+ data, err = load_json_file(refactor_list_path)
142
+ if err:
143
+ return "pending"
144
+ for r in data.get("refactors", []):
145
+ if isinstance(r, dict) and r.get("id") == refactor_id:
146
+ return r.get("status", "pending")
147
+ return "pending"
148
+
149
+
150
+ def update_refactor_in_list(refactor_list_path, refactor_id, new_status):
151
+ data, err = load_json_file(refactor_list_path)
152
+ if err:
153
+ return err
154
+ refactors = data.get("refactors", [])
155
+ found = False
156
+ for refactor in refactors:
157
+ if isinstance(refactor, dict) and refactor.get("id") == refactor_id:
158
+ refactor["status"] = new_status
159
+ found = True
160
+ break
161
+ if not found:
162
+ return "Refactor '{}' not found in .prizmkit/plans/refactor-list.json".format(refactor_id)
163
+ return write_json_file(refactor_list_path, data)
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Action: get_next
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ def _dependencies_met(refactor, completed_set):
172
+ """Check if all dependencies for a refactor are in terminal (completed) status."""
173
+ deps = refactor.get("dependencies", [])
174
+ if not deps or not isinstance(deps, list):
175
+ return True
176
+ return all(dep in completed_set for dep in deps)
177
+
178
+
179
+ def _count_unmet_deps(refactor, completed_set):
180
+ """Count how many dependencies are not yet completed."""
181
+ deps = refactor.get("dependencies", [])
182
+ if not deps or not isinstance(deps, list):
183
+ return 0
184
+ return sum(1 for dep in deps if dep not in completed_set)
185
+
186
+
187
+ def action_get_next(refactor_list_data, state_dir):
188
+ """Find the next refactor to process.
189
+
190
+ Priority logic:
191
+ 1. Skip terminal statuses (completed, failed, skipped)
192
+ 2. Only consider refactors whose dependencies are all completed
193
+ 3. Prefer in_progress refactors (interrupted session resume) over pending
194
+ 4. Sort by: dependency order (no-dependency items first),
195
+ then priority (critical > high > medium > low),
196
+ then complexity (low first)
197
+ """
198
+ refactors = refactor_list_data.get("refactors", [])
199
+ if not refactors:
200
+ print("PIPELINE_COMPLETE")
201
+ return
202
+
203
+ # Build status map from refactor-list.json (single source of truth)
204
+ status_map = {}
205
+ status_data_map = {}
206
+ for r in refactors:
207
+ if not isinstance(r, dict):
208
+ continue
209
+ rid = r.get("id")
210
+ if not rid:
211
+ continue
212
+ status_map[rid] = r.get("status", "pending")
213
+ rs = load_refactor_status(state_dir, rid)
214
+ status_data_map[rid] = rs
215
+
216
+ completed_set = {rid for rid, st in status_map.items() if st in TERMINAL_STATUSES}
217
+
218
+ # Check if all refactors are terminal
219
+ non_terminal = [
220
+ r for r in refactors
221
+ if isinstance(r, dict) and r.get("id")
222
+ and status_map.get(r["id"], "pending") not in TERMINAL_STATUSES
223
+ ]
224
+ if not non_terminal:
225
+ print("PIPELINE_COMPLETE")
226
+ return
227
+
228
+ # Filter to only those with met dependencies
229
+ eligible = [r for r in non_terminal if _dependencies_met(r, completed_set)]
230
+ if not eligible:
231
+ print("PIPELINE_BLOCKED")
232
+ return
233
+
234
+ # Separate in_progress from pending
235
+ in_progress_refactors = []
236
+ pending_refactors = []
237
+ for r in eligible:
238
+ rid = r.get("id")
239
+ rstatus = status_map.get(rid, "pending")
240
+ if rstatus == "in_progress":
241
+ in_progress_refactors.append(r)
242
+ elif rstatus == "pending":
243
+ pending_refactors.append(r)
244
+
245
+ def sort_key(r):
246
+ unmet = _count_unmet_deps(r, completed_set)
247
+ priority = PRIORITY_ORDER.get(r.get("priority", "medium"), 2)
248
+ complexity = COMPLEXITY_ORDER.get(r.get("complexity", "medium"), 1)
249
+ return (unmet, priority, complexity)
250
+
251
+ if in_progress_refactors:
252
+ candidates = sorted(in_progress_refactors, key=sort_key)
253
+ elif pending_refactors:
254
+ candidates = sorted(pending_refactors, key=sort_key)
255
+ else:
256
+ print("PIPELINE_BLOCKED")
257
+ return
258
+
259
+ chosen = candidates[0]
260
+ chosen_id = chosen["id"]
261
+ chosen_status_data = status_data_map.get(chosen_id, {})
262
+
263
+ result = {
264
+ "refactor_id": chosen_id,
265
+ "title": chosen.get("title", ""),
266
+ "type": chosen.get("type", "restructure"),
267
+ "priority": chosen.get("priority", "medium"),
268
+ "complexity": chosen.get("complexity", "medium"),
269
+ "retry_count": chosen_status_data.get("retry_count", 0),
270
+ "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
271
+ }
272
+ print(json.dumps(result, indent=2, ensure_ascii=False))
273
+
274
+
275
+ # ---------------------------------------------------------------------------
276
+ # Action: update
277
+ # ---------------------------------------------------------------------------
278
+
279
+ def action_update(args, refactor_list_path, state_dir):
280
+ refactor_id = args.refactor_id
281
+ session_status = args.session_status
282
+ session_id = args.session_id
283
+ max_retries = args.max_retries
284
+
285
+ if not refactor_id:
286
+ error_out("--refactor-id is required for 'update' action")
287
+ return
288
+ if not session_status:
289
+ error_out("--session-status is required for 'update' action")
290
+ return
291
+
292
+ rs = load_refactor_status(state_dir, refactor_id)
293
+
294
+ # Track what status we write to refactor-list.json
295
+ new_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
296
+
297
+ if session_status == "success":
298
+ new_status = "completed"
299
+ rs["resume_from_phase"] = None
300
+ err = update_refactor_in_list(refactor_list_path, refactor_id, "completed")
301
+ if err:
302
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
303
+ return
304
+ elif session_status in ("commit_missing", "docs_missing", "merge_conflict"):
305
+ rs["retry_count"] = rs.get("retry_count", 0) + 1
306
+
307
+ if rs["retry_count"] >= max_retries:
308
+ new_status = "failed"
309
+ else:
310
+ new_status = "pending"
311
+
312
+ rs["degraded_reason"] = session_status
313
+ rs["resume_from_phase"] = None
314
+ rs["sessions"] = []
315
+ rs["last_session_id"] = None
316
+
317
+ err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
318
+ if err:
319
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
320
+ return
321
+ else:
322
+ rs["retry_count"] = rs.get("retry_count", 0) + 1
323
+
324
+ cleaned = cleanup_refactor_artifacts(
325
+ state_dir=state_dir,
326
+ refactor_id=refactor_id,
327
+ project_root=args.project_root,
328
+ )
329
+
330
+ if rs["retry_count"] >= max_retries:
331
+ new_status = "failed"
332
+ else:
333
+ new_status = "pending"
334
+
335
+ rs["resume_from_phase"] = None
336
+ rs["sessions"] = []
337
+ rs["last_session_id"] = None
338
+
339
+ err = update_refactor_in_list(refactor_list_path, refactor_id, new_status)
340
+ if err:
341
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
342
+ return
343
+
344
+ if session_status == "success" and session_id:
345
+ sessions = rs.get("sessions", [])
346
+ if session_id not in sessions:
347
+ sessions.append(session_id)
348
+ rs["sessions"] = sessions
349
+ rs["last_session_id"] = session_id
350
+
351
+ rs["updated_at"] = now_iso()
352
+
353
+ err = save_refactor_status(state_dir, refactor_id, rs)
354
+ if err:
355
+ error_out("Failed to save refactor status: {}".format(err))
356
+ return
357
+
358
+ # Auto-skip downstream refactors when this refactor is marked as failed or skipped
359
+ auto_skipped_refactors = []
360
+ if new_status in ("failed", "skipped"):
361
+ auto_skipped_refactors = auto_skip_blocked_refactors(
362
+ refactor_list_path, state_dir, refactor_id
363
+ )
364
+
365
+ summary = {
366
+ "action": "update",
367
+ "refactor_id": refactor_id,
368
+ "session_status": session_status,
369
+ "new_status": new_status,
370
+ "retry_count": rs["retry_count"],
371
+ "resume_from_phase": rs.get("resume_from_phase"),
372
+ "updated_at": rs["updated_at"],
373
+ }
374
+ if auto_skipped_refactors:
375
+ summary["auto_skipped"] = [info["refactor_id"] for info in auto_skipped_refactors]
376
+ if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
377
+ summary["degraded_reason"] = session_status
378
+ summary["restart_policy"] = "finalization_retry"
379
+ elif session_status != "success":
380
+ summary["restart_policy"] = "full_restart"
381
+ summary["cleanup_performed"] = cleaned
382
+
383
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
384
+
385
+
386
+ def _default_project_root():
387
+ env = os.environ.get("PROJECT_ROOT")
388
+ if env:
389
+ return os.path.abspath(env)
390
+ script_dir = os.path.dirname(os.path.abspath(__file__))
391
+ pipeline_dir = os.path.dirname(script_dir)
392
+ pipeline_parent = os.path.dirname(pipeline_dir)
393
+ if os.path.basename(pipeline_parent) == ".prizmkit":
394
+ return os.path.dirname(pipeline_parent)
395
+ return pipeline_parent
396
+
397
+
398
+ def cleanup_refactor_artifacts(state_dir, refactor_id, project_root=None):
399
+ """Delete intermediate artifacts for a failed refactor run."""
400
+ if not project_root:
401
+ project_root = _default_project_root()
402
+
403
+ cleaned = []
404
+
405
+ # 1) Remove all session history
406
+ sessions_dir = os.path.join(state_dir, "refactors", refactor_id, "sessions")
407
+ sessions_deleted = 0
408
+ if os.path.isdir(sessions_dir):
409
+ for entry in os.listdir(sessions_dir):
410
+ entry_path = os.path.join(sessions_dir, entry)
411
+ if os.path.isdir(entry_path):
412
+ shutil.rmtree(entry_path)
413
+ sessions_deleted += 1
414
+ cleaned.append("Deleted {} session(s) from {}".format(sessions_deleted, sessions_dir))
415
+
416
+ # 2) Remove transient files under refactor dir (keep status.json)
417
+ refactor_dir = os.path.join(state_dir, "refactors", refactor_id)
418
+ if os.path.isdir(refactor_dir):
419
+ for entry in os.listdir(refactor_dir):
420
+ if entry == "status.json" or entry == "sessions":
421
+ continue
422
+ entry_path = os.path.join(refactor_dir, entry)
423
+ if os.path.isdir(entry_path):
424
+ shutil.rmtree(entry_path)
425
+ cleaned.append("Deleted directory {}".format(entry_path))
426
+ elif os.path.isfile(entry_path):
427
+ os.remove(entry_path)
428
+ cleaned.append("Deleted file {}".format(entry_path))
429
+
430
+ # 3) Remove refactor artifacts
431
+ refactor_artifact_dir = os.path.join(project_root, REFACTOR_ARTIFACTS_REL, refactor_id)
432
+ if os.path.isdir(refactor_artifact_dir):
433
+ shutil.rmtree(refactor_artifact_dir)
434
+ cleaned.append("Deleted {}".format(refactor_artifact_dir))
435
+
436
+ # 4) Remove shared dev-team workspace
437
+ dev_team_dir = os.path.join(project_root, DEV_TEAM_DIR_NAME)
438
+ if os.path.isdir(dev_team_dir):
439
+ shutil.rmtree(dev_team_dir)
440
+ cleaned.append("Deleted {}".format(dev_team_dir))
441
+
442
+ return cleaned
443
+
444
+
445
+ def load_session_status(state_dir, refactor_id, session_id):
446
+ session_status_path = os.path.join(
447
+ state_dir, "refactors", refactor_id, "sessions",
448
+ session_id, "session-status.json"
449
+ )
450
+ data, err = load_json_file(session_status_path)
451
+ if err:
452
+ return None, err
453
+ return data, None
454
+
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # Auto-skip: cascade failure to blocked downstream refactors
458
+ # ---------------------------------------------------------------------------
459
+
460
+ def auto_skip_blocked_refactors(refactor_list_path, state_dir, failed_refactor_id):
461
+ """Recursively mark all downstream refactors blocked by a failed refactor as auto_skipped.
462
+
463
+ When a refactor is marked as failed, any refactor whose dependency chain includes
464
+ the failed refactor can never be executed. This function propagates the failure
465
+ by marking those blocked refactors as auto_skipped, allowing the pipeline to
466
+ continue processing unblocked refactors and eventually reach PIPELINE_COMPLETE.
467
+
468
+ Re-reads .prizmkit/plans/refactor-list.json from disk to get the latest state (including the
469
+ just-written failed status from update_refactor_in_list).
470
+
471
+ NOTE: This function performs a read-modify-write on .prizmkit/plans/refactor-list.json without
472
+ file locking. The caller (action_update) also writes to .prizmkit/plans/refactor-list.json
473
+ immediately before calling this. Safe for single-pipeline execution, but if
474
+ multiple pipeline instances share the same .prizmkit/plans/refactor-list.json concurrently,
475
+ a race condition may cause lost writes. Add file locking if parallel pipelines
476
+ are introduced.
477
+ """
478
+ data, err = load_json_file(refactor_list_path)
479
+ if err:
480
+ return []
481
+ refactors = data.get("refactors", [])
482
+
483
+ # Build current status map
484
+ status_map = {}
485
+ for r in refactors:
486
+ if isinstance(r, dict) and r.get("id"):
487
+ status_map[r["id"]] = r.get("status", "pending")
488
+
489
+ # Collect all refactors to auto-skip (recursive propagation)
490
+ to_skip = set()
491
+ changed = True
492
+ while changed:
493
+ changed = False
494
+ for r in refactors:
495
+ if not isinstance(r, dict):
496
+ continue
497
+ rid = r.get("id")
498
+ if not rid or rid in to_skip:
499
+ continue
500
+ current = status_map.get(rid, "pending")
501
+ if current in TERMINAL_STATUSES:
502
+ continue
503
+ deps = r.get("dependencies", [])
504
+ for dep_id in deps:
505
+ dep_status = status_map.get(dep_id, "pending")
506
+ if dep_status in ("failed", "skipped", "auto_skipped") or dep_id in to_skip:
507
+ to_skip.add(rid)
508
+ status_map[rid] = "auto_skipped"
509
+ changed = True
510
+ break
511
+
512
+ if not to_skip:
513
+ return []
514
+
515
+ # Batch-write to .prizmkit/plans/refactor-list.json
516
+ for r in refactors:
517
+ if isinstance(r, dict) and r.get("id") in to_skip:
518
+ r["status"] = "auto_skipped"
519
+ write_json_file(refactor_list_path, data)
520
+
521
+ # Update timestamps in status.json for each auto-skipped refactor
522
+ for rid in to_skip:
523
+ rs = load_refactor_status(state_dir, rid)
524
+ rs["updated_at"] = now_iso()
525
+ save_refactor_status(state_dir, rid, rs)
526
+
527
+ # Build blocking reason map for logging
528
+ skipped_info = []
529
+ for r in refactors:
530
+ if not isinstance(r, dict):
531
+ continue
532
+ rid = r.get("id")
533
+ if rid not in to_skip:
534
+ continue
535
+ deps = r.get("dependencies", [])
536
+ blockers = [
537
+ d for d in deps
538
+ if d == failed_refactor_id or d in to_skip
539
+ ]
540
+ skipped_info.append({
541
+ "refactor_id": rid,
542
+ "title": r.get("title", ""),
543
+ "blocked_by": blockers,
544
+ })
545
+
546
+ print(
547
+ "[auto-skip] {} refactor(s) auto-skipped due to failed {}:".format(
548
+ len(skipped_info), failed_refactor_id
549
+ ),
550
+ file=sys.stderr,
551
+ )
552
+ for info in skipped_info:
553
+ print(
554
+ " {} ({}) — blocked by {}".format(
555
+ info["refactor_id"],
556
+ info["title"],
557
+ ", ".join(info["blocked_by"]),
558
+ ),
559
+ file=sys.stderr,
560
+ )
561
+
562
+ return skipped_info
563
+
564
+
565
+ # ---------------------------------------------------------------------------
566
+ # Action: status
567
+ # ---------------------------------------------------------------------------
568
+
569
+ COLOR_GREEN = "\033[92m"
570
+ COLOR_YELLOW = "\033[93m"
571
+ COLOR_RED = "\033[91m"
572
+ COLOR_GRAY = "\033[90m"
573
+ COLOR_MAGENTA = "\033[95m"
574
+ COLOR_CYAN = "\033[96m"
575
+ COLOR_BOLD = "\033[1m"
576
+ COLOR_RESET = "\033[0m"
577
+
578
+ BOX_WIDTH = 72
579
+
580
+ TYPE_ICONS = {
581
+ "extract": "📦",
582
+ "rename": "🏷️",
583
+ "restructure": "🏗️",
584
+ "simplify": "✂️",
585
+ "decouple": "🔗",
586
+ "migrate": "🚀",
587
+ }
588
+
589
+ COMPLEXITY_BADGES = {
590
+ "low": COLOR_GREEN + "[LOW]" + COLOR_RESET,
591
+ "medium": COLOR_YELLOW + "[MED]" + COLOR_RESET,
592
+ "high": COLOR_RED + "[HI]" + COLOR_RESET,
593
+ }
594
+
595
+
596
+ def action_status(refactor_list_data, state_dir):
597
+ refactors = refactor_list_data.get("refactors", [])
598
+ project_name = refactor_list_data.get("project_name", "Unknown")
599
+
600
+ counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "skipped": 0, "auto_skipped": 0}
601
+ refactor_lines = []
602
+
603
+ for r in refactors:
604
+ if not isinstance(r, dict):
605
+ continue
606
+ rid = r.get("id")
607
+ title = r.get("title", "Untitled")
608
+ rtype = r.get("type", "restructure")
609
+ complexity = r.get("complexity", "medium")
610
+ if not rid:
611
+ continue
612
+
613
+ rstatus = r.get("status", "pending")
614
+ rs = load_refactor_status(state_dir, rid)
615
+ retry_count = rs.get("retry_count", 0)
616
+ max_retries_val = rs.get("max_retries", 3)
617
+ resume_phase = rs.get("resume_from_phase")
618
+
619
+ if rstatus in counts:
620
+ counts[rstatus] += 1
621
+ else:
622
+ counts["pending"] += 1
623
+
624
+ # Status icon
625
+ if rstatus == "completed":
626
+ icon = COLOR_GREEN + "[✓]" + COLOR_RESET
627
+ elif rstatus == "in_progress":
628
+ icon = COLOR_YELLOW + "[→]" + COLOR_RESET
629
+ elif rstatus == "failed":
630
+ icon = COLOR_RED + "[✗]" + COLOR_RESET
631
+ elif rstatus == "skipped":
632
+ icon = COLOR_GRAY + "[—]" + COLOR_RESET
633
+ elif rstatus == "auto_skipped":
634
+ icon = COLOR_GRAY + "[⊘]" + COLOR_RESET
635
+ else:
636
+ icon = COLOR_GRAY + "[ ]" + COLOR_RESET
637
+
638
+ # Type badge
639
+ type_icon = TYPE_ICONS.get(rtype, "🔧")
640
+ type_badge = "[{}]".format(rtype[:6].upper())
641
+
642
+ # Complexity badge
643
+ cmplx_badge = COMPLEXITY_BADGES.get(complexity, "[MED]")
644
+
645
+ # Detail
646
+ detail = ""
647
+ if rstatus == "in_progress":
648
+ parts = []
649
+ if retry_count > 0:
650
+ parts.append("retry {}/{}".format(retry_count, max_retries_val))
651
+ if resume_phase is not None:
652
+ parts.append("CP-RF-{}".format(resume_phase))
653
+ if parts:
654
+ detail = " ({})".format(", ".join(parts))
655
+ elif rstatus == "failed":
656
+ detail = " (failed after {} retries)".format(retry_count)
657
+
658
+ # Colorize title based on status
659
+ if rstatus == "completed":
660
+ colored_title = COLOR_GREEN + title + COLOR_RESET
661
+ elif rstatus == "in_progress":
662
+ colored_title = COLOR_YELLOW + title + COLOR_RESET
663
+ elif rstatus == "failed":
664
+ colored_title = COLOR_RED + title + COLOR_RESET
665
+ else:
666
+ colored_title = COLOR_GRAY + title + COLOR_RESET
667
+
668
+ line_content = "{} {} {} {} {} {} {}{}".format(
669
+ rid, icon, type_badge, cmplx_badge, type_icon, colored_title, "", detail
670
+ )
671
+
672
+ refactor_lines.append(line_content)
673
+
674
+ total = len(refactors)
675
+ completed = counts["completed"]
676
+ percent = round(completed / total * 100, 1) if total > 0 else 0.0
677
+ progress_bar = _build_progress_bar(percent, width=24)
678
+
679
+ summary_line = "Total: {} refactors | Completed: {} | In Progress: {}".format(
680
+ total, completed, counts["in_progress"]
681
+ )
682
+ summary_line2 = "Failed: {} | Pending: {} | Skipped: {} | Auto-skipped: {}".format(
683
+ counts["failed"], counts["pending"], counts["skipped"], counts["auto_skipped"]
684
+ )
685
+
686
+ inner = BOX_WIDTH - 2
687
+ print("╔" + "═" * BOX_WIDTH + "╗")
688
+ print("║" + pad_right(COLOR_BOLD + " Refactor Pipeline Status" + COLOR_RESET, inner) + " ║")
689
+ print("╠" + "═" * BOX_WIDTH + "╣")
690
+ print("║" + pad_right(" Project: {}".format(project_name), inner) + " ║")
691
+ print("║" + pad_right(" {}".format(summary_line), inner) + " ║")
692
+ print("║" + pad_right(" {}".format(summary_line2), inner) + " ║")
693
+ print("╠" + "─" * BOX_WIDTH + "╣")
694
+ print("║" + pad_right(" Progress: {}".format(progress_bar), inner) + " ║")
695
+ print("╠" + "═" * BOX_WIDTH + "╣")
696
+ for line in refactor_lines:
697
+ print("║" + pad_right(" {}".format(line), inner) + " ║")
698
+ print("╚" + "═" * BOX_WIDTH + "╝")
699
+
700
+
701
+ # ---------------------------------------------------------------------------
702
+ # Action: reset
703
+ # ---------------------------------------------------------------------------
704
+
705
+ def action_reset(args, refactor_list_path, state_dir):
706
+ refactor_id = args.refactor_id
707
+ if not refactor_id:
708
+ error_out("--refactor-id is required for 'reset' action")
709
+ return
710
+
711
+ rs = load_refactor_status(state_dir, refactor_id)
712
+ old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
713
+ old_retry = rs.get("retry_count", 0)
714
+
715
+ rs["retry_count"] = 0
716
+ rs["sessions"] = []
717
+ rs["last_session_id"] = None
718
+ rs["resume_from_phase"] = None
719
+ rs["updated_at"] = now_iso()
720
+
721
+ err = save_refactor_status(state_dir, refactor_id, rs)
722
+ if err:
723
+ error_out("Failed to save refactor status: {}".format(err))
724
+ return
725
+
726
+ err = update_refactor_in_list(refactor_list_path, refactor_id, "pending")
727
+ if err:
728
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
729
+ return
730
+
731
+ result = {
732
+ "action": "reset",
733
+ "refactor_id": refactor_id,
734
+ "old_status": old_status,
735
+ "old_retry_count": old_retry,
736
+ "new_status": "pending",
737
+ }
738
+ print(json.dumps(result, indent=2, ensure_ascii=False))
739
+
740
+
741
+ # ---------------------------------------------------------------------------
742
+ # Action: clean
743
+ # ---------------------------------------------------------------------------
744
+
745
+ def action_clean(args, refactor_list_path, state_dir):
746
+ refactor_id = args.refactor_id
747
+ project_root = args.project_root
748
+
749
+ if not refactor_id:
750
+ error_out("--refactor-id is required for 'clean' action")
751
+ return
752
+ if not project_root:
753
+ error_out("--project-root is required for 'clean' action")
754
+ return
755
+
756
+ cleaned = []
757
+
758
+ # 1. Delete session history
759
+ sessions_dir = os.path.join(state_dir, "refactors", refactor_id, "sessions")
760
+ sessions_deleted = 0
761
+ if os.path.isdir(sessions_dir):
762
+ for entry in os.listdir(sessions_dir):
763
+ entry_path = os.path.join(sessions_dir, entry)
764
+ if os.path.isdir(entry_path):
765
+ shutil.rmtree(entry_path)
766
+ sessions_deleted += 1
767
+ cleaned.append("Deleted {} session(s) from {}".format(sessions_deleted, sessions_dir))
768
+
769
+ # 2. Delete refactor artifacts for this refactor
770
+ refactor_artifact_dir = os.path.join(project_root, REFACTOR_ARTIFACTS_REL, refactor_id)
771
+ if os.path.isdir(refactor_artifact_dir):
772
+ shutil.rmtree(refactor_artifact_dir)
773
+ cleaned.append("Deleted {}".format(refactor_artifact_dir))
774
+
775
+ # 3. Delete shared dev-team workspace
776
+ dev_team_dir = os.path.join(project_root, DEV_TEAM_DIR_NAME)
777
+ if os.path.isdir(dev_team_dir):
778
+ shutil.rmtree(dev_team_dir)
779
+ cleaned.append("Deleted {}".format(dev_team_dir))
780
+
781
+ # 4. Reset status
782
+ rs = load_refactor_status(state_dir, refactor_id)
783
+ old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
784
+ old_retry = rs.get("retry_count", 0)
785
+
786
+ rs["retry_count"] = 0
787
+ rs["sessions"] = []
788
+ rs["last_session_id"] = None
789
+ rs["resume_from_phase"] = None
790
+ rs["updated_at"] = now_iso()
791
+
792
+ err = save_refactor_status(state_dir, refactor_id, rs)
793
+ if err:
794
+ error_out("Failed to save refactor status: {}".format(err))
795
+ return
796
+
797
+ err = update_refactor_in_list(refactor_list_path, refactor_id, "pending")
798
+ if err:
799
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
800
+ return
801
+
802
+ result = {
803
+ "action": "clean",
804
+ "refactor_id": refactor_id,
805
+ "old_status": old_status,
806
+ "old_retry_count": old_retry,
807
+ "new_status": "pending",
808
+ "sessions_deleted": sessions_deleted,
809
+ "cleaned": cleaned,
810
+ }
811
+ print(json.dumps(result, indent=2, ensure_ascii=False))
812
+
813
+
814
+ # ---------------------------------------------------------------------------
815
+ # Action: pause
816
+ # ---------------------------------------------------------------------------
817
+
818
+ def action_pause(state_dir):
819
+ pipeline_path = os.path.join(state_dir, "pipeline.json")
820
+ data, err = load_json_file(pipeline_path)
821
+ if err:
822
+ data = {"status": "paused", "paused_at": now_iso()}
823
+ else:
824
+ data["status"] = "paused"
825
+ data["paused_at"] = now_iso()
826
+
827
+ err = write_json_file(pipeline_path, data)
828
+ if err:
829
+ error_out("Failed to write pipeline.json: {}".format(err))
830
+ return
831
+
832
+ result = {
833
+ "action": "pause",
834
+ "status": "paused",
835
+ "paused_at": data["paused_at"],
836
+ }
837
+ print(json.dumps(result, indent=2, ensure_ascii=False))
838
+
839
+
840
+ # ---------------------------------------------------------------------------
841
+ # Action: start
842
+ # ---------------------------------------------------------------------------
843
+
844
+ def action_start(args, refactor_list_path, state_dir):
845
+ """Mark a refactor as in_progress when a session starts.
846
+
847
+ This keeps refactor-list.json/state status in sync during execution,
848
+ instead of only updating after session end.
849
+ """
850
+ refactor_id = args.refactor_id
851
+ if not refactor_id:
852
+ error_out("--refactor-id is required for 'start' action")
853
+ return
854
+
855
+ rs = load_refactor_status(state_dir, refactor_id)
856
+ old_status = get_refactor_status_from_list(refactor_list_path, refactor_id)
857
+
858
+ rs["updated_at"] = now_iso()
859
+
860
+ err = save_refactor_status(state_dir, refactor_id, rs)
861
+ if err:
862
+ error_out("Failed to save refactor status: {}".format(err))
863
+ return
864
+
865
+ err = update_refactor_in_list(refactor_list_path, refactor_id, "in_progress")
866
+ if err:
867
+ error_out("Failed to update .prizmkit/plans/refactor-list.json: {}".format(err))
868
+ return
869
+
870
+ result = {
871
+ "action": "start",
872
+ "refactor_id": refactor_id,
873
+ "old_status": old_status,
874
+ "new_status": "in_progress",
875
+ "updated_at": rs["updated_at"],
876
+ }
877
+ print(json.dumps(result, indent=2, ensure_ascii=False))
878
+
879
+
880
+ # ---------------------------------------------------------------------------
881
+ # Action: unskip
882
+ # ---------------------------------------------------------------------------
883
+
884
+ def action_unskip(args, refactor_list_path, state_dir):
885
+ """Recover skipped/auto_skipped/failed refactors by resetting them and their failed upstream.
886
+
887
+ Two modes:
888
+ - --refactor-id R-001: Reset the specified failed/skipped/auto_skipped refactor + all
889
+ downstream auto_skipped refactors whose dependency chain includes it.
890
+ If the target is auto_skipped, also walk upstream to find and reset the
891
+ failed/skipped ancestor that caused the cascade.
892
+ - No --refactor-id: Reset ALL failed, skipped, and auto_skipped refactors to pending.
893
+ """
894
+ refactor_id = args.refactor_id
895
+
896
+ data, err = load_json_file(refactor_list_path)
897
+ if err:
898
+ error_out("Cannot load refactor list: {}".format(err))
899
+ return
900
+ refactors = data.get("refactors", [])
901
+
902
+ to_reset = set()
903
+
904
+ if refactor_id:
905
+ # Find the target refactor
906
+ target = None
907
+ for r in refactors:
908
+ if isinstance(r, dict) and r.get("id") == refactor_id:
909
+ target = r
910
+ break
911
+ if not target:
912
+ error_out("Refactor '{}' not found in .prizmkit/plans/refactor-list.json".format(refactor_id))
913
+ return
914
+ if target.get("status") not in ("failed", "skipped", "auto_skipped"):
915
+ error_out(
916
+ "Refactor '{}' has status '{}', expected 'failed', 'skipped', or 'auto_skipped'".format(
917
+ refactor_id, target.get("status", "unknown")
918
+ )
919
+ )
920
+ return
921
+
922
+ # If target is failed or skipped, reset it and find all auto_skipped descendants
923
+ if target.get("status") in ("failed", "skipped"):
924
+ to_reset.add(refactor_id)
925
+ # Find all auto_skipped refactors that depend (transitively) on this one
926
+ changed = True
927
+ while changed:
928
+ changed = False
929
+ for r in refactors:
930
+ if not isinstance(r, dict):
931
+ continue
932
+ rid = r.get("id")
933
+ if not rid or rid in to_reset:
934
+ continue
935
+ if r.get("status") != "auto_skipped":
936
+ continue
937
+ deps = r.get("dependencies", [])
938
+ if any(d in to_reset for d in deps):
939
+ to_reset.add(rid)
940
+ changed = True
941
+
942
+ # If target is auto_skipped, reset it and its failed upstream + siblings
943
+ elif target.get("status") == "auto_skipped":
944
+ to_reset.add(refactor_id)
945
+ # Transitively walk upstream to find ALL failed/auto_skipped ancestors
946
+ # (e.g., R-001 failed → R-002 auto_skipped → R-003 auto_skipped;
947
+ # unskip R-003 must also find and reset R-001)
948
+ upstream_changed = True
949
+ while upstream_changed:
950
+ upstream_changed = False
951
+ for r in refactors:
952
+ if not isinstance(r, dict):
953
+ continue
954
+ rid = r.get("id")
955
+ if not rid or rid not in to_reset:
956
+ continue
957
+ for dep_id in r.get("dependencies", []):
958
+ if dep_id in to_reset:
959
+ continue
960
+ for dep_r in refactors:
961
+ if isinstance(dep_r, dict) and dep_r.get("id") == dep_id:
962
+ if dep_r.get("status") in ("failed", "skipped", "auto_skipped"):
963
+ to_reset.add(dep_id)
964
+ upstream_changed = True
965
+ # Also reset downstream auto_skipped refactors blocked by the same upstreams
966
+ changed = True
967
+ while changed:
968
+ changed = False
969
+ for r in refactors:
970
+ if not isinstance(r, dict):
971
+ continue
972
+ rid = r.get("id")
973
+ if not rid or rid in to_reset:
974
+ continue
975
+ if r.get("status") != "auto_skipped":
976
+ continue
977
+ rdeps = r.get("dependencies", [])
978
+ if any(d in to_reset for d in rdeps):
979
+ to_reset.add(rid)
980
+ changed = True
981
+ else:
982
+ # No refactor-id: reset ALL failed + skipped + auto_skipped
983
+ for r in refactors:
984
+ if isinstance(r, dict) and r.get("id"):
985
+ if r.get("status") in ("failed", "skipped", "auto_skipped"):
986
+ to_reset.add(r["id"])
987
+
988
+ if not to_reset:
989
+ error_out("No refactors to unskip")
990
+ return
991
+
992
+ # Reset all collected refactors in refactor-list.json
993
+ reset_details = []
994
+ for r in refactors:
995
+ if isinstance(r, dict) and r.get("id") in to_reset:
996
+ old_status = r.get("status", "unknown")
997
+ r["status"] = "pending"
998
+ reset_details.append({
999
+ "refactor_id": r["id"],
1000
+ "title": r.get("title", ""),
1001
+ "old_status": old_status,
1002
+ })
1003
+
1004
+ err = write_json_file(refactor_list_path, data)
1005
+ if err:
1006
+ error_out("Failed to write .prizmkit/plans/refactor-list.json: {}".format(err))
1007
+ return
1008
+
1009
+ # Reset runtime fields in status.json for each refactor
1010
+ for rid in to_reset:
1011
+ rs = load_refactor_status(state_dir, rid)
1012
+ rs["retry_count"] = 0
1013
+ rs["sessions"] = []
1014
+ rs["last_session_id"] = None
1015
+ rs["resume_from_phase"] = None
1016
+ rs["updated_at"] = now_iso()
1017
+ save_refactor_status(state_dir, rid, rs)
1018
+
1019
+ result = {
1020
+ "action": "unskip",
1021
+ "reset_count": len(to_reset),
1022
+ "refactors": reset_details,
1023
+ }
1024
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1025
+
1026
+
1027
+ # ---------------------------------------------------------------------------
1028
+ # Main
1029
+ # ---------------------------------------------------------------------------
1030
+
1031
+ def main():
1032
+ args = parse_args()
1033
+
1034
+ if args.action == "update":
1035
+ if not args.refactor_id:
1036
+ error_out("--refactor-id is required for 'update' action")
1037
+ if not args.session_status:
1038
+ error_out("--session-status is required for 'update' action")
1039
+ if args.action in ("start", "reset", "clean", "complete"):
1040
+ if not args.refactor_id:
1041
+ error_out("--refactor-id is required for '{}' action".format(args.action))
1042
+ if args.action == "clean":
1043
+ if not args.project_root:
1044
+ error_out("--project-root is required for 'clean' action")
1045
+
1046
+ refactor_list_data, err = load_json_file(args.refactor_list)
1047
+ if err:
1048
+ error_out("Cannot load refactor list: {}".format(err))
1049
+
1050
+ if args.action == "get_next":
1051
+ action_get_next(refactor_list_data, args.state_dir)
1052
+ elif args.action == "update":
1053
+ action_update(args, args.refactor_list, args.state_dir)
1054
+ elif args.action == "status":
1055
+ action_status(refactor_list_data, args.state_dir)
1056
+ elif args.action == "reset":
1057
+ action_reset(args, args.refactor_list, args.state_dir)
1058
+ elif args.action == "clean":
1059
+ action_clean(args, args.refactor_list, args.state_dir)
1060
+ elif args.action == "start":
1061
+ action_start(args, args.refactor_list, args.state_dir)
1062
+ elif args.action == "pause":
1063
+ action_pause(args.state_dir)
1064
+ elif args.action == "unskip":
1065
+ action_unskip(args, args.refactor_list, args.state_dir)
1066
+ elif args.action == "complete":
1067
+ # Shortcut: 'complete' is equivalent to 'update --session-status success'
1068
+ args.session_status = "success"
1069
+ action_update(args, args.refactor_list, args.state_dir)
1070
+
1071
+
1072
+ if __name__ == "__main__":
1073
+ main()