prizmkit 1.1.111 → 1.1.112

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 (110) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/agents/prizm-dev-team-reviewer.md +12 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
  4. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +19 -73
  5. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +283 -421
  6. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
  7. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +259 -481
  8. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +34 -0
  9. package/bundled/dev-pipeline/scripts/init-pipeline.py +12 -0
  10. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +24 -1
  11. package/bundled/dev-pipeline/scripts/prompt_framework.py +312 -0
  12. package/bundled/dev-pipeline/scripts/update-bug-status.py +87 -20
  13. package/bundled/dev-pipeline/scripts/update-feature-status.py +82 -16
  14. package/bundled/dev-pipeline/scripts/update-refactor-status.py +68 -28
  15. package/bundled/dev-pipeline/scripts/utils.py +171 -0
  16. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  17. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +10 -18
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +14 -25
  19. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +9 -1
  20. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +11 -8
  21. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +122 -182
  22. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +16 -0
  23. package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +12 -0
  24. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +9 -0
  25. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +31 -0
  26. package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +22 -0
  27. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +23 -0
  28. package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +20 -0
  29. package/bundled/dev-pipeline/templates/sections/bugfix-phase-init.md +9 -0
  30. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +15 -0
  31. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +29 -0
  32. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +11 -0
  33. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +7 -0
  34. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +31 -0
  35. package/bundled/dev-pipeline/templates/sections/bugfix-task-contract.md +40 -0
  36. package/bundled/dev-pipeline/templates/sections/failure-capture.md +2 -2
  37. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +1 -1
  38. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +17 -0
  39. package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +13 -0
  40. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +9 -0
  41. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +37 -0
  42. package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +22 -0
  43. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +16 -0
  44. package/bundled/dev-pipeline/templates/sections/refactor-phase-init.md +9 -0
  45. package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +22 -0
  46. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +19 -0
  47. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +11 -0
  48. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +7 -0
  49. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +28 -0
  50. package/bundled/dev-pipeline/templates/sections/refactor-task-contract.md +52 -0
  51. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +17 -5
  52. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +197 -0
  53. package/bundled/dev-pipeline/tests/test_unified_cli.py +6 -2
  54. package/bundled/dev-pipeline/tests/test_utils.py +66 -1
  55. package/bundled/skills/_metadata.json +1 -1
  56. package/bundled/skills/app-planner/SKILL.md +8 -7
  57. package/bundled/skills/app-planner/references/project-brief-guide.md +2 -2
  58. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +1 -1
  59. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +1 -1
  60. package/bundled/skills/app-planner/references/rules-configuration.md +53 -26
  61. package/bundled/skills/bug-fix-workflow/SKILL.md +191 -336
  62. package/bundled/skills/bug-planner/SKILL.md +6 -5
  63. package/bundled/skills/bug-planner/references/critic-and-verification.md +3 -3
  64. package/bundled/skills/bug-planner/references/schema-validation.md +2 -1
  65. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +30 -1
  66. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +16 -16
  67. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +2 -2
  68. package/bundled/skills/feature-pipeline-launcher/SKILL.md +19 -11
  69. package/bundled/skills/feature-planner/SKILL.md +9 -11
  70. package/bundled/skills/feature-planner/assets/planning-guide.md +1 -1
  71. package/bundled/skills/feature-planner/references/decomposition-patterns.md +1 -1
  72. package/bundled/skills/feature-planner/references/error-recovery.md +2 -1
  73. package/bundled/skills/feature-planner/references/incremental-feature-planning.md +2 -2
  74. package/bundled/skills/feature-planner/references/new-project-planning.md +35 -39
  75. package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -1
  76. package/bundled/skills/feature-workflow/SKILL.md +169 -298
  77. package/bundled/skills/prizmkit/SKILL.md +103 -57
  78. package/bundled/skills/prizmkit-code-review/SKILL.md +97 -116
  79. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -0
  80. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +12 -1
  81. package/bundled/skills/prizmkit-committer/SKILL.md +59 -47
  82. package/bundled/skills/prizmkit-deploy/SKILL.md +162 -381
  83. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +220 -0
  84. package/bundled/skills/prizmkit-implement/SKILL.md +51 -40
  85. package/bundled/skills/prizmkit-init/SKILL.md +4 -3
  86. package/bundled/skills/prizmkit-plan/SKILL.md +85 -70
  87. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +94 -73
  88. package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +20 -18
  89. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +18 -14
  90. package/bundled/skills/prizmkit-retrospective/SKILL.md +56 -48
  91. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +79 -27
  92. package/bundled/skills/prizmkit-test/SKILL.md +138 -141
  93. package/bundled/skills/prizmkit-test/references/examples.md +10 -8
  94. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +1 -1
  95. package/bundled/skills/prizmkit-test/references/test-report-template.md +2 -2
  96. package/bundled/skills/recovery-workflow/SKILL.md +195 -256
  97. package/bundled/skills/recovery-workflow/evals/evals.json +21 -13
  98. package/bundled/skills/recovery-workflow/references/detection.md +48 -39
  99. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +258 -322
  100. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -11
  101. package/bundled/skills/refactor-planner/SKILL.md +2 -2
  102. package/bundled/skills/refactor-planner/references/behavior-preservation.md +24 -19
  103. package/bundled/skills/refactor-planner/references/fast-path.md +2 -4
  104. package/bundled/skills/refactor-planner/references/planning-phases.md +2 -2
  105. package/bundled/skills/refactor-workflow/SKILL.md +173 -307
  106. package/package.json +1 -1
  107. package/src/scaffold.js +5 -0
  108. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +0 -6
  109. package/bundled/skills/feature-workflow/references/brainstorm-guide.md +0 -137
  110. package/bundled/skills/refactor-workflow/references/brainstorm-guide.md +0 -116
@@ -15,6 +15,8 @@ import re
15
15
  import sys
16
16
  from datetime import datetime, timezone
17
17
 
18
+ from utils import stable_topological_order
19
+
18
20
 
19
21
  EXPECTED_SCHEMA = "dev-pipeline-bug-fix-list-v1"
20
22
  BUG_ID_PATTERN = re.compile(r"^B-\d{3}$")
@@ -198,6 +200,38 @@ def validate_bugs(bugs):
198
200
  )
199
201
  )
200
202
 
203
+ # Validate dependencies
204
+ deps = bug.get("dependencies")
205
+ if deps is not None and not isinstance(deps, list):
206
+ errors.append(
207
+ "Bug '{}' dependencies must be an array".format(
208
+ bid or "index {}".format(i)
209
+ )
210
+ )
211
+ elif isinstance(deps, list):
212
+ for dep in deps:
213
+ if not isinstance(dep, str) or not BUG_ID_PATTERN.match(dep):
214
+ errors.append(
215
+ "Bug '{}' has invalid dependency '{}' "
216
+ "(must match B-NNN pattern)".format(
217
+ bid or "index {}".format(i), dep
218
+ )
219
+ )
220
+ elif dep == bid:
221
+ errors.append(
222
+ "Bug '{}' cannot depend on itself".format(
223
+ bid or "index {}".format(i)
224
+ )
225
+ )
226
+
227
+ topo_errors = stable_topological_order(
228
+ bugs,
229
+ id_key="id",
230
+ dependencies_key="dependencies",
231
+ item_label="Bug",
232
+ )[1]
233
+ errors.extend(topo_errors)
234
+
201
235
  return errors, seen_ids
202
236
 
203
237
 
@@ -16,6 +16,8 @@ import sys
16
16
  from collections import deque
17
17
  from datetime import datetime, timezone
18
18
 
19
+ from utils import stable_topological_order
20
+
19
21
 
20
22
  EXPECTED_SCHEMA = "dev-pipeline-feature-list-v1"
21
23
  FEATURE_ID_PATTERN = re.compile(r"^F-\d{3}(-[A-Z])?$")
@@ -168,6 +170,16 @@ def validate_features(features):
168
170
  errors.append(
169
171
  "Feature '{}' depends on unknown feature '{}'".format(fid, dep)
170
172
  )
173
+ elif dep == fid:
174
+ errors.append("Feature '{}' cannot depend on itself".format(fid))
175
+
176
+ topo_errors = stable_topological_order(
177
+ features,
178
+ id_key="id",
179
+ dependencies_key="dependencies",
180
+ item_label="Feature",
181
+ )[1]
182
+ errors.extend(topo_errors)
171
183
 
172
184
  return errors, feature_ids
173
185
 
@@ -16,7 +16,7 @@ import re
16
16
  import sys
17
17
  from datetime import datetime, timezone
18
18
 
19
- from utils import load_json_file
19
+ from utils import load_json_file, stable_topological_order
20
20
 
21
21
 
22
22
  EXPECTED_SCHEMA = "dev-pipeline-refactor-list-v1"
@@ -215,6 +215,29 @@ def validate_refactors(refactors):
215
215
  rid or "index {}".format(i)
216
216
  )
217
217
  )
218
+ elif isinstance(deps, list):
219
+ for dep in deps:
220
+ if not isinstance(dep, str) or not REFACTOR_ID_PATTERN.match(dep):
221
+ errors.append(
222
+ "Refactor '{}' has invalid dependency '{}' "
223
+ "(must match R-NNN pattern)".format(
224
+ rid or "index {}".format(i), dep
225
+ )
226
+ )
227
+ elif dep == rid:
228
+ errors.append(
229
+ "Refactor '{}' cannot depend on itself".format(
230
+ rid or "index {}".format(i)
231
+ )
232
+ )
233
+
234
+ topo_errors = stable_topological_order(
235
+ refactors,
236
+ id_key="id",
237
+ dependencies_key="dependencies",
238
+ item_label="Refactor",
239
+ )[1]
240
+ errors.extend(topo_errors)
218
241
 
219
242
  return errors, seen_ids
220
243
 
@@ -0,0 +1,312 @@
1
+ """Shared prompt rendering and checkpoint helpers for dev-pipeline generators.
2
+
3
+ This module intentionally stays small: prompt phase ordering remains explicit in
4
+ family-specific generators, while common rendering/checkpoint mechanics live here
5
+ so feature, bugfix, and refactor prompts do not drift.
6
+ """
7
+
8
+ import glob
9
+ import os
10
+ import re
11
+
12
+
13
+ def read_text_file(path):
14
+ """Read and return the text content of a file, plus an error string."""
15
+ abs_path = os.path.abspath(path)
16
+ if not os.path.isfile(abs_path):
17
+ return None, "File not found: {}".format(abs_path)
18
+ try:
19
+ with open(abs_path, "r", encoding="utf-8") as f:
20
+ return f.read(), None
21
+ except IOError as exc:
22
+ return None, "Cannot read file: {}".format(str(exc))
23
+
24
+
25
+ def load_section(sections_dir, name):
26
+ """Load a section file from the sections directory."""
27
+ path = os.path.join(sections_dir, name)
28
+ if not os.path.isfile(path):
29
+ raise FileNotFoundError("Section file not found: {}".format(path))
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ return f.read()
32
+
33
+
34
+ def render_from_sections(sections, replacements):
35
+ """Join assembled sections and replace placeholders with two passes."""
36
+ content = "\n".join(text for _, text in sections if text is not None)
37
+ for _pass in range(2):
38
+ for placeholder, value in replacements.items():
39
+ content = content.replace(placeholder, str(value))
40
+ return content
41
+
42
+
43
+ def replace_placeholders(content, replacements):
44
+ """Replace placeholders in arbitrary content with two passes."""
45
+ for _pass in range(2):
46
+ for placeholder, value in replacements.items():
47
+ content = content.replace(placeholder, str(value))
48
+ return content
49
+
50
+
51
+ def validate_rendered(content, required_markers=None, logger=None):
52
+ """Validate rendered prompt content for placeholder and section drift."""
53
+ warnings = []
54
+ errors = []
55
+
56
+ unreplaced = re.findall(r"\{\{[A-Z][A-Z_0-9]+\}\}", content)
57
+ if unreplaced:
58
+ warnings.append(
59
+ "Unreplaced placeholders: {}".format(", ".join(sorted(set(unreplaced))))
60
+ )
61
+
62
+ unclosed_if = re.findall(r"\{\{(?:IF|END_IF)_[A-Z_]+\}\}", content)
63
+ if unclosed_if:
64
+ errors.append(
65
+ "Unclosed conditional blocks: {}".format(", ".join(sorted(set(unclosed_if))))
66
+ )
67
+
68
+ for marker, label in required_markers or []:
69
+ if marker not in content:
70
+ errors.append("Missing required section: {}".format(label))
71
+
72
+ if logger is not None:
73
+ for warning in warnings:
74
+ logger.warning("VALIDATE: %s", warning)
75
+ for error in errors:
76
+ logger.error("VALIDATE: %s", error)
77
+
78
+ return len(errors) == 0, warnings, errors
79
+
80
+
81
+ def write_output(output_path, content):
82
+ """Write rendered prompt content to output_path."""
83
+ abs_path = os.path.abspath(output_path)
84
+ output_dir = os.path.dirname(abs_path)
85
+ if output_dir and not os.path.isdir(output_dir):
86
+ try:
87
+ os.makedirs(output_dir, exist_ok=True)
88
+ except OSError as exc:
89
+ return "Cannot create output directory: {}".format(str(exc))
90
+ try:
91
+ with open(abs_path, "w", encoding="utf-8") as f:
92
+ f.write(content)
93
+ except IOError as exc:
94
+ return "Cannot write output file: {}".format(str(exc))
95
+ return None
96
+
97
+
98
+ def resolve_artifacts(artifact_templates, slug):
99
+ """Replace {slug} placeholders in checkpoint artifact templates."""
100
+ return [artifact.replace("{slug}", slug) for artifact in artifact_templates]
101
+
102
+
103
+ def artifact_exists(project_root, artifact):
104
+ """Return whether an artifact path or glob exists under project_root."""
105
+ full_path = os.path.join(project_root, artifact)
106
+ if glob.has_magic(full_path):
107
+ return bool(glob.glob(full_path))
108
+ return os.path.exists(full_path)
109
+
110
+
111
+ def completed_step_artifacts_valid(project_root, artifacts):
112
+ """Return whether generic completed-step artifacts are present."""
113
+ return all(artifact_exists(project_root, artifact) for artifact in artifacts)
114
+
115
+
116
+ def generate_checkpoint_from_sections(
117
+ sections,
118
+ step_mapping,
119
+ pipeline_mode,
120
+ workflow_type,
121
+ item_id,
122
+ item_slug,
123
+ session_id,
124
+ *,
125
+ init_done=False,
126
+ split_steps=None,
127
+ ):
128
+ """Generate a linear checkpoint definition from assembled prompt sections."""
129
+ steps = []
130
+ step_counter = 1
131
+ prev_step_id = None
132
+ split_steps = split_steps or {}
133
+
134
+ for section_name, _content in sections:
135
+ if section_name not in step_mapping:
136
+ continue
137
+
138
+ mapping = step_mapping[section_name]
139
+ if mapping is None:
140
+ for skill, name, artifacts in split_steps.get(section_name, []):
141
+ step_id = "S{:02d}".format(step_counter)
142
+ steps.append({
143
+ "id": step_id,
144
+ "skill": skill,
145
+ "name": name,
146
+ "status": "pending",
147
+ "required_artifacts": resolve_artifacts(artifacts, item_slug),
148
+ "depends_on": prev_step_id,
149
+ })
150
+ prev_step_id = step_id
151
+ step_counter += 1
152
+ continue
153
+
154
+ skill, name, artifacts = mapping
155
+ step_id = "S{:02d}".format(step_counter)
156
+ status = "pending"
157
+ if init_done and section_name in ("phase0-init", "phase0-test-baseline"):
158
+ status = "skipped"
159
+
160
+ steps.append({
161
+ "id": step_id,
162
+ "skill": skill,
163
+ "name": name,
164
+ "status": status,
165
+ "required_artifacts": resolve_artifacts(artifacts, item_slug),
166
+ "depends_on": prev_step_id,
167
+ })
168
+ prev_step_id = step_id
169
+ step_counter += 1
170
+
171
+ return {
172
+ "version": 1,
173
+ "workflow_type": workflow_type,
174
+ "pipeline_mode": pipeline_mode,
175
+ "item_id": item_id,
176
+ "item_slug": item_slug,
177
+ "session_id": session_id,
178
+ "steps": steps,
179
+ }
180
+
181
+
182
+ def merge_checkpoint_state(existing, fresh, project_root, validator=None, logger=None):
183
+ """Merge existing checkpoint state into a freshly generated definition."""
184
+ validator = validator or (lambda _skill, artifacts: completed_step_artifacts_valid(project_root, artifacts))
185
+ existing_status = {}
186
+ existing_artifacts = {}
187
+ fresh_artifacts = {
188
+ step["skill"]: step.get("required_artifacts", [])
189
+ for step in fresh.get("steps", [])
190
+ }
191
+
192
+ for step in existing.get("steps", []):
193
+ existing_status[step["skill"]] = step.get("status", "pending")
194
+ existing_artifacts[step["skill"]] = step.get("required_artifacts", [])
195
+
196
+ valid_completed = set()
197
+ for skill_key, status in existing_status.items():
198
+ if status == "completed":
199
+ artifacts = fresh_artifacts.get(skill_key, existing_artifacts.get(skill_key, []))
200
+ if validator(skill_key, artifacts):
201
+ valid_completed.add(skill_key)
202
+ elif logger is not None:
203
+ logger.warning(
204
+ "Step '%s' was completed but artifacts missing — resetting to pending",
205
+ skill_key,
206
+ )
207
+ elif status == "skipped":
208
+ valid_completed.add(skill_key)
209
+
210
+ chain_broken = False
211
+ for step in fresh.get("steps", []):
212
+ if chain_broken:
213
+ step["status"] = "pending"
214
+ continue
215
+ previous = existing_status.get(step["skill"])
216
+ if step["skill"] in valid_completed:
217
+ step["status"] = previous
218
+ else:
219
+ chain_broken = True
220
+ step["status"] = "pending"
221
+
222
+ return fresh
223
+
224
+
225
+ def determine_mode_from_complexity(value):
226
+ """Map a coarse complexity/severity value to lite/standard/full."""
227
+ mapping = {
228
+ "low": "lite",
229
+ "medium": "lite",
230
+ "minor": "lite",
231
+ "high": "standard",
232
+ "major": "standard",
233
+ "critical": "full",
234
+ }
235
+ return mapping.get(str(value or "").lower(), "lite")
236
+
237
+
238
+ def normalize_mode(value, default="lite"):
239
+ """Normalize a mode string to lite/standard/full."""
240
+ value = str(value or "").lower()
241
+ return value if value in {"lite", "standard", "full"} else default
242
+
243
+
244
+ def mode_agent_count(pipeline_mode, critic_enabled=False, critic_count=1):
245
+ """Return the prompt metadata agent count for a mode."""
246
+ base = {"lite": 1, "standard": 3, "full": 3}.get(pipeline_mode, 1)
247
+ return base + (int(critic_count or 1) if critic_enabled else 0)
248
+
249
+
250
+ def detect_dev_server_port(project_root):
251
+ """Return a detected dev-server port string, or an unknown placeholder."""
252
+ unknown = "<UNKNOWN_DETECT_FROM_PROJECT_CONFIG>"
253
+ pkg_path = os.path.join(project_root, "package.json")
254
+ try:
255
+ if os.path.isfile(pkg_path):
256
+ import json
257
+ with open(pkg_path, "r", encoding="utf-8") as f:
258
+ pkg = json.load(f)
259
+ scripts = pkg.get("scripts", {}) if isinstance(pkg, dict) else {}
260
+ dev_script = scripts.get("dev", "")
261
+ port_match = re.search(r"(?:-p|--port)\s+([0-9]+)", dev_script)
262
+ if port_match:
263
+ return port_match.group(1)
264
+ for env_file in [".env.local", ".env"]:
265
+ env_path = os.path.join(project_root, env_file)
266
+ if not os.path.isfile(env_path):
267
+ continue
268
+ with open(env_path, "r", encoding="utf-8") as env:
269
+ for line in env:
270
+ match = re.match(r"[A-Z0-9_]*(?:URL|ORIGIN|HOST)\s*=\s*.*?:([0-9]+)", line.strip())
271
+ if match:
272
+ return match.group(1)
273
+ except Exception:
274
+ return unknown
275
+ return unknown
276
+
277
+
278
+ def browser_enabled_from_item(item):
279
+ """Return browser enabled/tool/steps with mandatory-attempt defaults."""
280
+ browser_interaction = item.get("browser_interaction") if isinstance(item, dict) else None
281
+ env_value = os.environ.get("BROWSER_VERIFY", "").lower()
282
+ if env_value == "false":
283
+ return False, "auto", "", False
284
+
285
+ enabled = True
286
+ tool = "auto"
287
+ blocking = False
288
+ steps = []
289
+
290
+ if isinstance(browser_interaction, bool):
291
+ enabled = browser_interaction
292
+ elif isinstance(browser_interaction, dict):
293
+ enabled = browser_interaction.get("enabled", True)
294
+ tool = browser_interaction.get("tool", "auto")
295
+ blocking = bool(browser_interaction.get("blocking", False))
296
+ steps = browser_interaction.get("verify_steps", []) or []
297
+
298
+ if tool not in ("playwright-cli", "opencli", "auto"):
299
+ tool = "auto"
300
+
301
+ if steps:
302
+ rendered_steps = "\n".join(
303
+ " # Goal {}: {}".format(index + 1, step)
304
+ for index, step in enumerate(steps)
305
+ )
306
+ else:
307
+ rendered_steps = (
308
+ " # (mandatory attempt: discover whether this project exposes "
309
+ "a browser-verifiable app; verify relevant behavior if applicable)"
310
+ )
311
+
312
+ return enabled, tool, rendered_steps, blocking
@@ -3,7 +3,7 @@
3
3
 
4
4
  Handles eight actions:
5
5
  - get_next: Find the next bug to process based on priority and severity
6
- - start: Mark a bug as fixing when a session starts
6
+ - start: Mark a bug as in_progress when a session starts
7
7
  - update: Update a bug's status based on session outcome
8
8
  - status: Print a formatted overview of all bugs
9
9
  - pause: Save pipeline state for graceful shutdown
@@ -31,6 +31,8 @@ from utils import (
31
31
  error_out,
32
32
  pad_right,
33
33
  _build_progress_bar,
34
+ read_checkpoint_cursor,
35
+ dependency_rank_map,
34
36
  )
35
37
 
36
38
 
@@ -215,6 +217,36 @@ def reset_continuation_metadata(status_data):
215
217
  STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
216
218
 
217
219
 
220
+ def _read_bugfix_checkpoint_phase(project_root, bug_id):
221
+ """Read the phase index from the bugfix checkpoint file.
222
+
223
+ Returns an integer step index (e.g., 3 for S03) or None if the checkpoint
224
+ is not available. The value is consumed by the terminal status display as
225
+ ``CP-BF-<n>``.
226
+ """
227
+ checkpoint_path = os.path.join(
228
+ os.path.abspath(project_root) if project_root else _default_project_root(),
229
+ ".prizmkit", "bugfix", bug_id, "workflow-checkpoint.json",
230
+ )
231
+ return read_checkpoint_cursor(checkpoint_path)
232
+
233
+
234
+ def _checkpoint_phase_or_cached(project_root, bug_id, status_data):
235
+ """Return live checkpoint phase, falling back to cached status data."""
236
+ checkpoint_phase = _read_bugfix_checkpoint_phase(project_root, bug_id)
237
+ if checkpoint_phase is not None:
238
+ return checkpoint_phase
239
+ return status_data.get("resume_from_phase")
240
+
241
+
242
+ def _sync_checkpoint_phase(project_root, bug_id, status_data):
243
+ """Cache live checkpoint phase in status.json runtime data."""
244
+ status_data["resume_from_phase"] = _read_bugfix_checkpoint_phase(
245
+ project_root, bug_id,
246
+ )
247
+ return status_data.get("resume_from_phase")
248
+
249
+
218
250
  def _load_progress_fingerprint(raw):
219
251
  if not raw:
220
252
  return None
@@ -298,13 +330,14 @@ def continuation_metadata_summary(status_data):
298
330
  # Action: get_next
299
331
  # ---------------------------------------------------------------------------
300
332
 
301
- def action_get_next(bug_list_data, state_dir):
333
+ def action_get_next(bug_list_data, state_dir, project_root=None):
302
334
  """Find the next bug to process.
303
335
 
304
336
  Priority logic:
305
337
  1. Skip terminal statuses (completed, failed, skipped, needs_info)
306
- 2. Prefer in_progress bugs (interrupted session resume) over pending
307
- 3. Sort by: severity (critical > high > medium > low), then by priority (high > medium > low)
338
+ 2. Only consider bugs whose dependencies are all completed
339
+ 3. Prefer in_progress bugs (interrupted session resume) over pending
340
+ 4. Within each status class, use stable topological list order
308
341
  """
309
342
  bugs = bug_list_data.get("bugs", [])
310
343
  if not bugs:
@@ -324,6 +357,16 @@ def action_get_next(bug_list_data, state_dir):
324
357
  bs = load_bug_status(state_dir, bid)
325
358
  status_data_map[bid] = bs
326
359
 
360
+ topo_rank, topo_errors = dependency_rank_map(
361
+ bugs,
362
+ id_key="id",
363
+ dependencies_key="dependencies",
364
+ item_label="Bug",
365
+ )
366
+ if topo_errors:
367
+ print("PIPELINE_BLOCKED")
368
+ return
369
+
327
370
  # Check if all bugs are terminal
328
371
  non_terminal = [
329
372
  b for b in bugs
@@ -334,10 +377,22 @@ def action_get_next(bug_list_data, state_dir):
334
377
  print("PIPELINE_COMPLETE")
335
378
  return
336
379
 
380
+ eligible = []
381
+ for bug in non_terminal:
382
+ deps = bug.get("dependencies", [])
383
+ if not isinstance(deps, list):
384
+ continue
385
+ if all(status_map.get(dep_id, "pending") == "completed" for dep_id in deps):
386
+ eligible.append(bug)
387
+
388
+ if not eligible:
389
+ print("PIPELINE_BLOCKED")
390
+ return
391
+
337
392
  # Separate in_progress from pending
338
393
  in_progress_bugs = []
339
394
  pending_bugs = []
340
- for bug in non_terminal:
395
+ for bug in eligible:
341
396
  bid = bug.get("id")
342
397
  bstatus = status_map.get(bid, "pending")
343
398
  if bstatus == "in_progress":
@@ -345,18 +400,16 @@ def action_get_next(bug_list_data, state_dir):
345
400
  elif bstatus == "pending":
346
401
  pending_bugs.append(bug)
347
402
 
348
- _PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
349
-
350
- def sort_key(b):
351
- severity = b.get("severity", "medium")
352
- sev_order = SEVERITY_PRIORITY.get(severity, 2)
353
- priority = _PRIORITY_ORDER.get(b.get("priority", "low"), 2)
354
- return (sev_order, priority)
355
-
356
403
  if in_progress_bugs:
357
- candidates = sorted(in_progress_bugs, key=sort_key)
404
+ candidates = sorted(
405
+ in_progress_bugs,
406
+ key=lambda b: topo_rank.get(b.get("id"), len(bugs))
407
+ )
358
408
  elif pending_bugs:
359
- candidates = sorted(pending_bugs, key=sort_key)
409
+ candidates = sorted(
410
+ pending_bugs,
411
+ key=lambda b: topo_rank.get(b.get("id"), len(bugs))
412
+ )
360
413
  else:
361
414
  # All remaining bugs are in non-terminal but also non-pending/in_progress states
362
415
  print("PIPELINE_BLOCKED")
@@ -365,6 +418,10 @@ def action_get_next(bug_list_data, state_dir):
365
418
  chosen = candidates[0]
366
419
  chosen_id = chosen["id"]
367
420
  chosen_status_data = status_data_map.get(chosen_id, {})
421
+ project_root = os.path.abspath(project_root) if project_root else _default_project_root()
422
+ resume_phase = _checkpoint_phase_or_cached(
423
+ project_root, chosen_id, chosen_status_data,
424
+ )
368
425
 
369
426
  result = {
370
427
  "bug_id": chosen_id,
@@ -372,7 +429,7 @@ def action_get_next(bug_list_data, state_dir):
372
429
  "severity": chosen.get("severity", "medium"),
373
430
  "retry_count": chosen_status_data.get("retry_count", 0),
374
431
  "infra_error_count": chosen_status_data.get("infra_error_count", 0),
375
- "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
432
+ "resume_from_phase": resume_phase,
376
433
  }
377
434
  result.update(continuation_metadata_summary(chosen_status_data))
378
435
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -502,6 +559,10 @@ def action_update(args, bug_list_path, state_dir):
502
559
  bs["last_session_id"] = session_id
503
560
 
504
561
  bs["updated_at"] = now_iso()
562
+ # Sync resume_from_phase from checkpoint (single source of truth)
563
+ _sync_checkpoint_phase(
564
+ args.project_root or _default_project_root(), bug_id, bs,
565
+ )
505
566
 
506
567
  err = save_bug_status(state_dir, bug_id, bs)
507
568
  if err:
@@ -676,9 +737,10 @@ SEVERITY_ICONS = {
676
737
  }
677
738
 
678
739
 
679
- def action_status(bug_list_data, state_dir):
740
+ def action_status(bug_list_data, state_dir, project_root=None):
680
741
  bugs = bug_list_data.get("bugs", [])
681
742
  project_name = bug_list_data.get("project_name", "Unknown")
743
+ project_root = os.path.abspath(project_root) if project_root else _default_project_root()
682
744
 
683
745
  counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "needs_info": 0, "skipped": 0}
684
746
  bug_lines = []
@@ -696,7 +758,12 @@ def action_status(bug_list_data, state_dir):
696
758
  bs = load_bug_status(state_dir, bid)
697
759
  retry_count = bs.get("retry_count", 0)
698
760
  max_retries_val = bs.get("max_retries", 3)
699
- resume_phase = bs.get("resume_from_phase")
761
+ # Read phase cursor from checkpoint (single source of truth). The
762
+ # status.json field is only a cache/fallback for missing checkpoints.
763
+ resume_phase = _checkpoint_phase_or_cached(project_root, bid, bs)
764
+ if resume_phase != bs.get("resume_from_phase"):
765
+ bs["resume_from_phase"] = resume_phase
766
+ save_bug_status(state_dir, bid, bs)
700
767
 
701
768
  if bstatus in counts:
702
769
  counts[bstatus] += 1
@@ -1096,13 +1163,13 @@ def main():
1096
1163
  error_out("Cannot load bug fix list: {}".format(err))
1097
1164
 
1098
1165
  if args.action == "get_next":
1099
- action_get_next(bug_list_data, args.state_dir)
1166
+ action_get_next(bug_list_data, args.state_dir, args.project_root)
1100
1167
  elif args.action == "start":
1101
1168
  action_start(args, args.bug_list, args.state_dir)
1102
1169
  elif args.action == "update":
1103
1170
  action_update(args, args.bug_list, args.state_dir)
1104
1171
  elif args.action == "status":
1105
- action_status(bug_list_data, args.state_dir)
1172
+ action_status(bug_list_data, args.state_dir, args.project_root)
1106
1173
  elif args.action == "reset":
1107
1174
  action_reset(args, args.bug_list, args.state_dir)
1108
1175
  elif args.action == "clean":