prizmkit 1.1.111 → 1.1.113

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 (119) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codex/skill-adapter.js +4 -0
  3. package/bundled/agents/prizm-dev-team-reviewer.md +12 -1
  4. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +10 -10
  5. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
  6. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +20 -73
  7. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +290 -421
  8. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
  9. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +259 -481
  10. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +34 -0
  11. package/bundled/dev-pipeline/scripts/init-pipeline.py +12 -0
  12. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +24 -1
  13. package/bundled/dev-pipeline/scripts/prompt_framework.py +312 -0
  14. package/bundled/dev-pipeline/scripts/update-bug-status.py +87 -20
  15. package/bundled/dev-pipeline/scripts/update-feature-status.py +82 -16
  16. package/bundled/dev-pipeline/scripts/update-refactor-status.py +68 -28
  17. package/bundled/dev-pipeline/scripts/utils.py +171 -0
  18. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  19. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +7 -1
  20. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +17 -19
  21. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +21 -26
  22. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +9 -1
  23. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +11 -8
  24. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +122 -182
  25. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +16 -0
  26. package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +12 -0
  27. package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +9 -0
  28. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +31 -0
  29. package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +22 -0
  30. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +23 -0
  31. package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +20 -0
  32. package/bundled/dev-pipeline/templates/sections/bugfix-phase-init.md +9 -0
  33. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +15 -0
  34. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +29 -0
  35. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +11 -0
  36. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +7 -0
  37. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +31 -0
  38. package/bundled/dev-pipeline/templates/sections/bugfix-task-contract.md +40 -0
  39. package/bundled/dev-pipeline/templates/sections/failure-capture.md +2 -2
  40. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +8 -2
  41. package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +7 -1
  42. package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +7 -1
  43. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +17 -0
  44. package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +13 -0
  45. package/bundled/dev-pipeline/templates/sections/refactor-mission.md +9 -0
  46. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +37 -0
  47. package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +22 -0
  48. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +16 -0
  49. package/bundled/dev-pipeline/templates/sections/refactor-phase-init.md +9 -0
  50. package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +22 -0
  51. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +19 -0
  52. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +11 -0
  53. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +7 -0
  54. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +28 -0
  55. package/bundled/dev-pipeline/templates/sections/refactor-task-contract.md +52 -0
  56. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +104 -26
  57. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +14 -0
  58. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +239 -0
  59. package/bundled/dev-pipeline/tests/test_unified_cli.py +6 -2
  60. package/bundled/dev-pipeline/tests/test_utils.py +66 -1
  61. package/bundled/skills/_metadata.json +1 -1
  62. package/bundled/skills/app-planner/SKILL.md +8 -7
  63. package/bundled/skills/app-planner/references/project-brief-guide.md +2 -2
  64. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +1 -1
  65. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +1 -1
  66. package/bundled/skills/app-planner/references/rules-configuration.md +53 -26
  67. package/bundled/skills/bug-fix-workflow/SKILL.md +191 -336
  68. package/bundled/skills/bug-planner/SKILL.md +6 -5
  69. package/bundled/skills/bug-planner/references/critic-and-verification.md +3 -3
  70. package/bundled/skills/bug-planner/references/schema-validation.md +2 -1
  71. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +70 -1
  72. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -21
  73. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +2 -2
  74. package/bundled/skills/feature-pipeline-launcher/SKILL.md +25 -26
  75. package/bundled/skills/feature-planner/SKILL.md +9 -11
  76. package/bundled/skills/feature-planner/assets/planning-guide.md +1 -1
  77. package/bundled/skills/feature-planner/references/decomposition-patterns.md +1 -1
  78. package/bundled/skills/feature-planner/references/error-recovery.md +2 -1
  79. package/bundled/skills/feature-planner/references/incremental-feature-planning.md +2 -2
  80. package/bundled/skills/feature-planner/references/new-project-planning.md +35 -39
  81. package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -1
  82. package/bundled/skills/feature-workflow/SKILL.md +170 -298
  83. package/bundled/skills/prizmkit/SKILL.md +103 -57
  84. package/bundled/skills/prizmkit-code-review/SKILL.md +97 -116
  85. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -0
  86. package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +14 -3
  87. package/bundled/skills/prizmkit-committer/SKILL.md +59 -47
  88. package/bundled/skills/prizmkit-deploy/SKILL.md +162 -381
  89. package/bundled/skills/prizmkit-deploy/references/database-setup.md +12 -3
  90. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +220 -0
  91. package/bundled/skills/prizmkit-implement/SKILL.md +51 -40
  92. package/bundled/skills/prizmkit-init/SKILL.md +4 -3
  93. package/bundled/skills/prizmkit-plan/SKILL.md +85 -70
  94. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +94 -73
  95. package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +20 -18
  96. package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +11 -13
  97. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +18 -14
  98. package/bundled/skills/prizmkit-retrospective/SKILL.md +56 -48
  99. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +79 -27
  100. package/bundled/skills/prizmkit-test/SKILL.md +138 -141
  101. package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +1 -1
  102. package/bundled/skills/prizmkit-test/references/examples.md +11 -9
  103. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +1 -1
  104. package/bundled/skills/prizmkit-test/references/test-report-template.md +2 -2
  105. package/bundled/skills/recovery-workflow/SKILL.md +195 -256
  106. package/bundled/skills/recovery-workflow/evals/evals.json +21 -13
  107. package/bundled/skills/recovery-workflow/references/detection.md +48 -39
  108. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +258 -322
  109. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +32 -13
  110. package/bundled/skills/refactor-planner/SKILL.md +2 -2
  111. package/bundled/skills/refactor-planner/references/behavior-preservation.md +24 -19
  112. package/bundled/skills/refactor-planner/references/fast-path.md +2 -4
  113. package/bundled/skills/refactor-planner/references/planning-phases.md +2 -2
  114. package/bundled/skills/refactor-workflow/SKILL.md +174 -307
  115. package/package.json +1 -1
  116. package/src/scaffold.js +5 -0
  117. package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +0 -6
  118. package/bundled/skills/feature-workflow/references/brainstorm-guide.md +0 -137
  119. package/bundled/skills/refactor-workflow/references/brainstorm-guide.md +0 -116
@@ -35,6 +35,8 @@ from utils import (
35
35
  error_out,
36
36
  pad_right,
37
37
  _build_progress_bar,
38
+ read_checkpoint_cursor,
39
+ dependency_rank_map,
38
40
  )
39
41
 
40
42
 
@@ -320,6 +322,36 @@ def reset_continuation_metadata(status_data):
320
322
  STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
321
323
 
322
324
 
325
+ def _read_feature_checkpoint_phase(project_root, feature_slug):
326
+ """Read the phase index from the feature checkpoint file.
327
+
328
+ Returns an integer step index (e.g., 3 for S03) or None if the checkpoint
329
+ is not available. The value is consumed by the terminal status display as
330
+ ``CP-F-<n>``.
331
+ """
332
+ checkpoint_path = os.path.join(
333
+ os.path.abspath(project_root) if project_root else _default_project_root(),
334
+ ".prizmkit", "specs", feature_slug, "workflow-checkpoint.json",
335
+ )
336
+ return read_checkpoint_cursor(checkpoint_path)
337
+
338
+
339
+ def _checkpoint_phase_or_cached(project_root, feature_slug, status_data):
340
+ """Return live checkpoint phase, falling back to cached status data."""
341
+ checkpoint_phase = _read_feature_checkpoint_phase(project_root, feature_slug)
342
+ if checkpoint_phase is not None:
343
+ return checkpoint_phase
344
+ return status_data.get("resume_from_phase")
345
+
346
+
347
+ def _sync_checkpoint_phase(project_root, feature_slug, status_data):
348
+ """Cache live checkpoint phase in status.json runtime data."""
349
+ status_data["resume_from_phase"] = _read_feature_checkpoint_phase(
350
+ project_root, feature_slug,
351
+ )
352
+ return status_data.get("resume_from_phase")
353
+
354
+
323
355
  def _load_progress_fingerprint(raw):
324
356
  if not raw:
325
357
  return None
@@ -656,7 +688,7 @@ def auto_skip_blocked_features(feature_list_path, state_dir, failed_feature_id):
656
688
  # Action: get_next
657
689
  # ---------------------------------------------------------------------------
658
690
 
659
- def action_get_next(feature_list_data, state_dir, feature_filter=None):
691
+ def action_get_next(feature_list_data, state_dir, feature_filter=None, project_root=None):
660
692
  """Find the next feature to process.
661
693
 
662
694
  Priority logic:
@@ -687,6 +719,16 @@ def action_get_next(feature_list_data, state_dir, feature_filter=None):
687
719
  fs = load_feature_status(state_dir, fid)
688
720
  status_data_map[fid] = fs
689
721
 
722
+ topo_rank, topo_errors = dependency_rank_map(
723
+ features,
724
+ id_key="id",
725
+ dependencies_key="dependencies",
726
+ item_label="Feature",
727
+ )
728
+ if topo_errors:
729
+ print("PIPELINE_BLOCKED")
730
+ return
731
+
690
732
  # Apply feature filter: only consider these features as candidates
691
733
  # for execution, but dependency checking still uses the full status_map
692
734
  if feature_filter is not None:
@@ -740,34 +782,41 @@ def action_get_next(feature_list_data, state_dir, feature_filter=None):
740
782
  fstatus = status_map.get(fid, "pending")
741
783
  if fstatus == "in_progress":
742
784
  in_progress_features.append(feature)
743
- else:
785
+ elif fstatus == "pending":
744
786
  pending_features.append(feature)
745
787
 
746
- # Priority mapping: string enum sort order (critical first)
747
- _PRIORITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
748
-
749
- # Prefer in_progress features, then pending; sort by priority (high > medium > low)
788
+ # Prefer in_progress features, then pending. Within each class use stable
789
+ # topological order so dependencies precede dependents and unrelated items
790
+ # keep list order.
750
791
  if in_progress_features:
751
792
  candidates = sorted(
752
793
  in_progress_features,
753
- key=lambda f: _PRIORITY_ORDER.get(f.get("priority", "low"), 3)
794
+ key=lambda f: topo_rank.get(f.get("id"), len(features))
754
795
  )
755
- else:
796
+ elif pending_features:
756
797
  candidates = sorted(
757
798
  pending_features,
758
- key=lambda f: _PRIORITY_ORDER.get(f.get("priority", "low"), 3)
799
+ key=lambda f: topo_rank.get(f.get("id"), len(features))
759
800
  )
801
+ else:
802
+ print("PIPELINE_BLOCKED")
803
+ return
760
804
 
761
805
  chosen = candidates[0]
762
806
  chosen_id = chosen["id"]
763
807
  chosen_status_data = status_data_map.get(chosen_id, {})
808
+ project_root = os.path.abspath(project_root) if project_root else _default_project_root()
809
+ feature_slug = _build_feature_slug(chosen_id, chosen.get("title", ""))
810
+ resume_phase = _checkpoint_phase_or_cached(
811
+ project_root, feature_slug, chosen_status_data,
812
+ )
764
813
 
765
814
  result = {
766
815
  "feature_id": chosen_id,
767
816
  "title": chosen.get("title", ""),
768
817
  "retry_count": chosen_status_data.get("retry_count", 0),
769
818
  "infra_error_count": chosen_status_data.get("infra_error_count", 0),
770
- "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
819
+ "resume_from_phase": resume_phase,
771
820
  }
772
821
  result.update(continuation_metadata_summary(chosen_status_data))
773
822
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -931,6 +980,12 @@ def action_update(args, feature_list_path, state_dir):
931
980
  fs["last_session_id"] = session_id
932
981
 
933
982
  fs["updated_at"] = now_iso()
983
+ # Sync resume_from_phase from checkpoint (single source of truth)
984
+ _sync_checkpoint_phase(
985
+ args.project_root or _default_project_root(),
986
+ _get_feature_slug(feature_list_path, feature_id) or feature_id,
987
+ fs,
988
+ )
934
989
 
935
990
  err = save_feature_status(state_dir, feature_id, fs)
936
991
  if err:
@@ -1145,15 +1200,16 @@ def _estimate_remaining_time(features, state_dir, counts, feature_list_data=None
1145
1200
  return remaining_seconds, confidence
1146
1201
 
1147
1202
 
1148
- def action_status(feature_list_data, state_dir, feature_filter=None):
1203
+ def action_status(feature_list_data, state_dir, feature_filter=None, project_root=None):
1149
1204
  """Print a formatted overview of all features and their status.
1150
1205
 
1151
- Status is read exclusively from .prizmkit/plans/feature-list.json (the single source of
1152
- truth). state_dir is only used for ETA estimation when session history
1153
- is available.
1206
+ Status is read from .prizmkit/plans/feature-list.json. Runtime metadata such
1207
+ as ETA, degraded reasons, and live checkpoint phase is read from status.json
1208
+ and workflow-checkpoint.json.
1154
1209
  """
1155
1210
  features = feature_list_data.get("features", [])
1156
1211
  app_name = feature_list_data.get("project_name", feature_list_data.get("app_name", "Unknown"))
1212
+ project_root = os.path.abspath(project_root) if project_root else _default_project_root()
1157
1213
 
1158
1214
  # Apply feature filter
1159
1215
  if feature_filter is not None:
@@ -1205,6 +1261,14 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
1205
1261
  continue
1206
1262
 
1207
1263
  fstatus = feature.get("status", "pending")
1264
+ fs = load_feature_status(state_dir, fid)
1265
+ feature_slug = _build_feature_slug(fid, title)
1266
+ # Read phase cursor from checkpoint (single source of truth). The
1267
+ # status.json field is only a cache/fallback for missing checkpoints.
1268
+ resume_phase = _checkpoint_phase_or_cached(project_root, feature_slug, fs)
1269
+ if resume_phase != fs.get("resume_from_phase"):
1270
+ fs["resume_from_phase"] = resume_phase
1271
+ save_feature_status(state_dir, fid, fs)
1208
1272
  degraded_reason = degraded_reason_map.get(fid)
1209
1273
 
1210
1274
  # Count statuses
@@ -1255,6 +1319,8 @@ def action_status(feature_list_data, state_dir, feature_filter=None):
1255
1319
  ]
1256
1320
  if blocking:
1257
1321
  detail = " (blocked by {})".format(", ".join(blocking))
1322
+ elif fstatus == "in_progress" and resume_phase is not None:
1323
+ detail = " (CP-F-{})".format(resume_phase)
1258
1324
  elif fstatus == "auto_skipped":
1259
1325
  deps = feature.get("dependencies", [])
1260
1326
  blockers = [
@@ -1764,13 +1830,13 @@ def main():
1764
1830
 
1765
1831
  # Dispatch action
1766
1832
  if args.action == "get_next":
1767
- action_get_next(feature_list_data, args.state_dir, feature_filter)
1833
+ action_get_next(feature_list_data, args.state_dir, feature_filter, args.project_root)
1768
1834
  elif args.action == "start":
1769
1835
  action_start(args, args.feature_list, args.state_dir)
1770
1836
  elif args.action == "update":
1771
1837
  action_update(args, args.feature_list, args.state_dir)
1772
1838
  elif args.action == "status":
1773
- action_status(feature_list_data, args.state_dir, feature_filter)
1839
+ action_status(feature_list_data, args.state_dir, feature_filter, args.project_root)
1774
1840
  elif args.action == "reset":
1775
1841
  action_reset(args, args.feature_list, args.state_dir)
1776
1842
  elif args.action == "clean":
@@ -32,6 +32,8 @@ from utils import (
32
32
  error_out,
33
33
  pad_right,
34
34
  _build_progress_bar,
35
+ read_checkpoint_cursor,
36
+ dependency_rank_map,
35
37
  )
36
38
 
37
39
 
@@ -219,6 +221,31 @@ def reset_continuation_metadata(status_data):
219
221
  STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
220
222
 
221
223
 
224
+ def _read_refactor_checkpoint_phase(project_root, refactor_id):
225
+ """Read the phase index from the refactor checkpoint file."""
226
+ checkpoint_path = os.path.join(
227
+ os.path.abspath(project_root) if project_root else _default_project_root(),
228
+ ".prizmkit", "refactor", refactor_id, "workflow-checkpoint.json",
229
+ )
230
+ return read_checkpoint_cursor(checkpoint_path)
231
+
232
+
233
+ def _checkpoint_phase_or_cached(project_root, refactor_id, status_data):
234
+ """Return live checkpoint phase, falling back to cached status data."""
235
+ checkpoint_phase = _read_refactor_checkpoint_phase(project_root, refactor_id)
236
+ if checkpoint_phase is not None:
237
+ return checkpoint_phase
238
+ return status_data.get("resume_from_phase")
239
+
240
+
241
+ def _sync_checkpoint_phase(project_root, refactor_id, status_data):
242
+ """Cache live checkpoint phase in status.json runtime data."""
243
+ status_data["resume_from_phase"] = _read_refactor_checkpoint_phase(
244
+ project_root, refactor_id,
245
+ )
246
+ return status_data.get("resume_from_phase")
247
+
248
+
222
249
  def _load_progress_fingerprint(raw):
223
250
  if not raw:
224
251
  return None
@@ -304,31 +331,21 @@ def continuation_metadata_summary(status_data):
304
331
 
305
332
 
306
333
  def _dependencies_met(refactor, completed_set):
307
- """Check if all dependencies for a refactor are in terminal (completed) status."""
334
+ """Check if all dependencies for a refactor are completed."""
308
335
  deps = refactor.get("dependencies", [])
309
336
  if not deps or not isinstance(deps, list):
310
337
  return True
311
338
  return all(dep in completed_set for dep in deps)
312
339
 
313
340
 
314
- def _count_unmet_deps(refactor, completed_set):
315
- """Count how many dependencies are not yet completed."""
316
- deps = refactor.get("dependencies", [])
317
- if not deps or not isinstance(deps, list):
318
- return 0
319
- return sum(1 for dep in deps if dep not in completed_set)
320
-
321
-
322
- def action_get_next(refactor_list_data, state_dir):
341
+ def action_get_next(refactor_list_data, state_dir, project_root=None):
323
342
  """Find the next refactor to process.
324
343
 
325
344
  Priority logic:
326
345
  1. Skip terminal statuses (completed, failed, skipped)
327
- 2. Only consider refactors whose dependencies are all completed
346
+ 2. Only consider refactors whose dependencies are completed
328
347
  3. Prefer in_progress refactors (interrupted session resume) over pending
329
- 4. Sort by: dependency order (no-dependency items first),
330
- then priority (critical > high > medium > low),
331
- then complexity (low first)
348
+ 4. Within each status class, use stable topological list order
332
349
  """
333
350
  refactors = refactor_list_data.get("refactors", [])
334
351
  if not refactors:
@@ -348,7 +365,16 @@ def action_get_next(refactor_list_data, state_dir):
348
365
  rs = load_refactor_status(state_dir, rid)
349
366
  status_data_map[rid] = rs
350
367
 
351
- completed_set = {rid for rid, st in status_map.items() if st in TERMINAL_STATUSES}
368
+ completed_set = {rid for rid, st in status_map.items() if st == "completed"}
369
+ topo_rank, topo_errors = dependency_rank_map(
370
+ refactors,
371
+ id_key="id",
372
+ dependencies_key="dependencies",
373
+ item_label="Refactor",
374
+ )
375
+ if topo_errors:
376
+ print("PIPELINE_BLOCKED")
377
+ return
352
378
 
353
379
  # Check if all refactors are terminal
354
380
  non_terminal = [
@@ -377,16 +403,16 @@ def action_get_next(refactor_list_data, state_dir):
377
403
  elif rstatus == "pending":
378
404
  pending_refactors.append(r)
379
405
 
380
- def sort_key(r):
381
- unmet = _count_unmet_deps(r, completed_set)
382
- priority = PRIORITY_ORDER.get(r.get("priority", "medium"), 2)
383
- complexity = COMPLEXITY_ORDER.get(r.get("complexity", "medium"), 1)
384
- return (unmet, priority, complexity)
385
-
386
406
  if in_progress_refactors:
387
- candidates = sorted(in_progress_refactors, key=sort_key)
407
+ candidates = sorted(
408
+ in_progress_refactors,
409
+ key=lambda r: topo_rank.get(r.get("id"), len(refactors))
410
+ )
388
411
  elif pending_refactors:
389
- candidates = sorted(pending_refactors, key=sort_key)
412
+ candidates = sorted(
413
+ pending_refactors,
414
+ key=lambda r: topo_rank.get(r.get("id"), len(refactors))
415
+ )
390
416
  else:
391
417
  print("PIPELINE_BLOCKED")
392
418
  return
@@ -394,6 +420,10 @@ def action_get_next(refactor_list_data, state_dir):
394
420
  chosen = candidates[0]
395
421
  chosen_id = chosen["id"]
396
422
  chosen_status_data = status_data_map.get(chosen_id, {})
423
+ project_root = os.path.abspath(project_root) if project_root else _default_project_root()
424
+ resume_phase = _checkpoint_phase_or_cached(
425
+ project_root, chosen_id, chosen_status_data,
426
+ )
397
427
 
398
428
  result = {
399
429
  "refactor_id": chosen_id,
@@ -403,7 +433,7 @@ def action_get_next(refactor_list_data, state_dir):
403
433
  "complexity": chosen.get("complexity", "medium"),
404
434
  "retry_count": chosen_status_data.get("retry_count", 0),
405
435
  "infra_error_count": chosen_status_data.get("infra_error_count", 0),
406
- "resume_from_phase": chosen_status_data.get("resume_from_phase", None),
436
+ "resume_from_phase": resume_phase,
407
437
  }
408
438
  result.update(continuation_metadata_summary(chosen_status_data))
409
439
  print(json.dumps(result, indent=2, ensure_ascii=False))
@@ -533,6 +563,10 @@ def action_update(args, refactor_list_path, state_dir):
533
563
  rs["last_session_id"] = session_id
534
564
 
535
565
  rs["updated_at"] = now_iso()
566
+ # Sync resume_from_phase from checkpoint (single source of truth)
567
+ _sync_checkpoint_phase(
568
+ args.project_root or _default_project_root(), refactor_id, rs,
569
+ )
536
570
 
537
571
  err = save_refactor_status(state_dir, refactor_id, rs)
538
572
  if err:
@@ -829,9 +863,10 @@ COMPLEXITY_BADGES = {
829
863
  }
830
864
 
831
865
 
832
- def action_status(refactor_list_data, state_dir):
866
+ def action_status(refactor_list_data, state_dir, project_root=None):
833
867
  refactors = refactor_list_data.get("refactors", [])
834
868
  project_name = refactor_list_data.get("project_name", "Unknown")
869
+ project_root = os.path.abspath(project_root) if project_root else _default_project_root()
835
870
 
836
871
  counts = {"completed": 0, "in_progress": 0, "failed": 0, "pending": 0, "skipped": 0, "auto_skipped": 0}
837
872
  refactor_lines = []
@@ -850,7 +885,12 @@ def action_status(refactor_list_data, state_dir):
850
885
  rs = load_refactor_status(state_dir, rid)
851
886
  retry_count = rs.get("retry_count", 0)
852
887
  max_retries_val = rs.get("max_retries", 3)
853
- resume_phase = rs.get("resume_from_phase")
888
+ # Read phase cursor from checkpoint (single source of truth). The
889
+ # status.json field is only a cache/fallback for missing checkpoints.
890
+ resume_phase = _checkpoint_phase_or_cached(project_root, rid, rs)
891
+ if resume_phase != rs.get("resume_from_phase"):
892
+ rs["resume_from_phase"] = resume_phase
893
+ save_refactor_status(state_dir, rid, rs)
854
894
 
855
895
  if rstatus in counts:
856
896
  counts[rstatus] += 1
@@ -1302,11 +1342,11 @@ def main():
1302
1342
  error_out("Cannot load refactor list: {}".format(err))
1303
1343
 
1304
1344
  if args.action == "get_next":
1305
- action_get_next(refactor_list_data, args.state_dir)
1345
+ action_get_next(refactor_list_data, args.state_dir, args.project_root)
1306
1346
  elif args.action == "update":
1307
1347
  action_update(args, args.refactor_list, args.state_dir)
1308
1348
  elif args.action == "status":
1309
- action_status(refactor_list_data, args.state_dir)
1349
+ action_status(refactor_list_data, args.state_dir, args.project_root)
1310
1350
  elif args.action == "reset":
1311
1351
  action_reset(args, args.refactor_list, args.state_dir)
1312
1352
  elif args.action == "clean":
@@ -93,6 +93,137 @@ def write_json_file(path, data):
93
93
  return None
94
94
 
95
95
 
96
+ def _find_dependency_cycle(dep_map, item_ids):
97
+ """Return one dependency cycle as a list of IDs, or an empty list."""
98
+ visiting = set()
99
+ visited = set()
100
+ stack = []
101
+
102
+ def visit(node):
103
+ visiting.add(node)
104
+ stack.append(node)
105
+ for dep in dep_map.get(node, []):
106
+ if dep not in item_ids:
107
+ continue
108
+ if dep in visiting:
109
+ start = stack.index(dep)
110
+ return stack[start:] + [dep]
111
+ if dep not in visited:
112
+ cycle = visit(dep)
113
+ if cycle:
114
+ return cycle
115
+ stack.pop()
116
+ visiting.remove(node)
117
+ visited.add(node)
118
+ return []
119
+
120
+ for item_id in sorted(item_ids):
121
+ if item_id not in visited:
122
+ cycle = visit(item_id)
123
+ if cycle:
124
+ return cycle
125
+ return []
126
+
127
+
128
+ def stable_topological_order(items, id_key="id", dependencies_key="dependencies", item_label="Item"):
129
+ """Validate dependencies and return items in stable topological order.
130
+
131
+ Dependency edges are interpreted as ``item depends on dependency``. The
132
+ returned order guarantees dependencies appear before dependents. Items with
133
+ no dependency relationship keep their original list order.
134
+
135
+ Returns ``(ordered_items, errors)``. Callers should treat non-empty errors
136
+ as blocking because missing dependencies, self-dependencies, and cycles make
137
+ autonomous selection ambiguous.
138
+ """
139
+ errors = []
140
+ indexed_items = []
141
+ item_by_id = {}
142
+ index_by_id = {}
143
+
144
+ for index, item in enumerate(items or []):
145
+ if not isinstance(item, dict):
146
+ continue
147
+ item_id = item.get(id_key)
148
+ if not item_id:
149
+ continue
150
+ if item_id in item_by_id:
151
+ errors.append("Duplicate {} id: {}".format(item_label.lower(), item_id))
152
+ continue
153
+ item_by_id[item_id] = item
154
+ index_by_id[item_id] = index
155
+ indexed_items.append((index, item_id, item))
156
+
157
+ adjacency = {item_id: [] for item_id in item_by_id}
158
+ in_degree = {item_id: 0 for item_id in item_by_id}
159
+ dep_map = {item_id: [] for item_id in item_by_id}
160
+
161
+ for _index, item_id, item in indexed_items:
162
+ deps = item.get(dependencies_key, [])
163
+ if deps is None:
164
+ deps = []
165
+ if not isinstance(deps, list):
166
+ errors.append("{} '{}' dependencies must be an array".format(item_label, item_id))
167
+ continue
168
+ for dep_id in deps:
169
+ if not isinstance(dep_id, str):
170
+ errors.append(
171
+ "{} '{}' has invalid dependency {!r}; dependency IDs must be strings".format(
172
+ item_label, item_id, dep_id
173
+ )
174
+ )
175
+ continue
176
+ if dep_id == item_id:
177
+ errors.append("{} '{}' cannot depend on itself".format(item_label, item_id))
178
+ continue
179
+ if dep_id not in item_by_id:
180
+ errors.append("{} '{}' depends on missing dependency '{}'".format(
181
+ item_label, item_id, dep_id
182
+ ))
183
+ continue
184
+ adjacency[dep_id].append(item_id)
185
+ in_degree[item_id] += 1
186
+ dep_map[item_id].append(dep_id)
187
+
188
+ queue = sorted(
189
+ [item_id for item_id, degree in in_degree.items() if degree == 0],
190
+ key=lambda value: index_by_id[value],
191
+ )
192
+ ordered_ids = []
193
+
194
+ while queue:
195
+ item_id = queue.pop(0)
196
+ ordered_ids.append(item_id)
197
+ for dependent_id in sorted(adjacency.get(item_id, []), key=lambda value: index_by_id[value]):
198
+ in_degree[dependent_id] -= 1
199
+ if in_degree[dependent_id] == 0:
200
+ queue.append(dependent_id)
201
+ queue.sort(key=lambda value: index_by_id[value])
202
+
203
+ if len(ordered_ids) != len(item_by_id):
204
+ remaining = {item_id for item_id, degree in in_degree.items() if degree > 0}
205
+ cycle = _find_dependency_cycle(dep_map, remaining)
206
+ if cycle:
207
+ errors.append("Dependency cycle detected: {}".format(" -> ".join(cycle)))
208
+ else:
209
+ errors.append("Dependency cycle detected involving: {}".format(
210
+ ", ".join(sorted(remaining, key=lambda value: index_by_id[value]))
211
+ ))
212
+
213
+ return [item_by_id[item_id] for item_id in ordered_ids], errors
214
+
215
+
216
+ def dependency_rank_map(items, id_key="id", dependencies_key="dependencies", item_label="Item"):
217
+ """Return ``(rank_map, errors)`` for stable dependency-aware ordering."""
218
+ ordered, errors = stable_topological_order(
219
+ items,
220
+ id_key=id_key,
221
+ dependencies_key=dependencies_key,
222
+ item_label=item_label,
223
+ )
224
+ return {item[id_key]: index for index, item in enumerate(ordered) if id_key in item}, errors
225
+
226
+
96
227
  def setup_logging(name="prizmkit.dev_pipeline", level=None):
97
228
  """Configure and return a standard logger for pipeline scripts.
98
229
 
@@ -634,3 +765,43 @@ def read_platform_conventions(project_root):
634
765
  return "`{}`".format(filename)
635
766
 
636
767
  return "(No project conventions file found — AGENTS.md, CLAUDE.md, or CODEBUDDY.md)"
768
+
769
+
770
+ def read_checkpoint_cursor(checkpoint_path):
771
+ """Read the phase cursor from a workflow-checkpoint.json file.
772
+
773
+ Returns the first non-completed/skipped step's step index (e.g., 3 for S03).
774
+ If every step is completed or skipped, returns the last completed/skipped
775
+ step index. Returns None if the file does not exist or is unreadable.
776
+
777
+ The cursor is what gets displayed as "CP-BF-<n>" in the terminal status.
778
+ """
779
+ if not os.path.isfile(checkpoint_path):
780
+ return None
781
+ try:
782
+ with open(checkpoint_path, "r", encoding="utf-8") as f:
783
+ data = json.load(f)
784
+ except (json.JSONDecodeError, IOError):
785
+ return None
786
+ if not isinstance(data, dict):
787
+ return None
788
+ steps = data.get("steps")
789
+ if not isinstance(steps, list):
790
+ return None
791
+ last_completed = 0
792
+ for step in steps:
793
+ if not isinstance(step, dict):
794
+ continue
795
+ status = step.get("status", "pending")
796
+ sid = step.get("id", "")
797
+ try:
798
+ step_num = int(sid.lstrip("S"))
799
+ except (ValueError, AttributeError):
800
+ continue
801
+ if status in ("completed", "skipped"):
802
+ last_completed = max(last_completed, step_num)
803
+ else:
804
+ # Return the first non-completed step's number
805
+ return step_num
806
+ # All steps are completed — return the last step number
807
+ return last_completed if last_completed > 0 else None
@@ -36,7 +36,7 @@ Infer what needs to be done from the feature context above and follow the standa
36
36
 
37
37
  6. **Retrospective**: Run `/prizmkit-retrospective` to sync `.prizmkit/prizm-docs/` with code changes.
38
38
 
39
- 7. **Commit**: Run `/prizmkit-committer --headless` to commit all changes. Do NOT push.
39
+ 7. **Commit**: Run `/prizmkit-committer` to commit all changes. Do NOT push.
40
40
 
41
41
  ### Critical Rules
42
42
 
@@ -338,7 +338,13 @@ Append results to `context-snapshot.md`:
338
338
  Browser cleanup: confirmed
339
339
  ```
340
340
 
341
- If verification fails, log the failure details but continue to commit. Failures do NOT block the commit, but you MUST attempt verification and MUST clean up the dev server.
341
+ Browser blocking policy: `{{BROWSER_BLOCKING}}`.
342
+
343
+ If verification fails:
344
+ - If `{{BROWSER_BLOCKING}}` is `true`: mark checkpoint step `browser-verification` as `failed`, write `failure-log.md` with browser evidence, clean up the dev server/browser, and stop before commit.
345
+ - If `{{BROWSER_BLOCKING}}` is `false`: log the failure details, mark checkpoint step `browser-verification` according to the evidence, clean up the dev server/browser, and continue.
346
+
347
+ You MUST attempt verification and MUST clean up the dev server in both cases.
342
348
  {{END_IF_BROWSER_INTERACTION}}
343
349
 
344
350
  ### Phase 4: Architecture Sync & Commit (SINGLE COMMIT)
@@ -12,7 +12,7 @@ You are the **session orchestrator**. Implement Feature {{FEATURE_ID}}: "{{FEATU
12
12
 
13
13
  **CRITICAL**: You MUST NOT exit until ALL work is complete and committed. When you spawn Critic or Reviewer agents, wait for each to finish (run_in_background=false).
14
14
 
15
- **Tier 2 — Orchestrator + Critic/Reviewer**: You handle context, planning, and implementation directly. Critic challenges the plan when enabled, Reviewer reviews. Do not spawn a top-level Dev subagent for implementation.
15
+ **Tier 2 — Orchestrator + Critic**: You handle context, planning, and implementation directly. Critic challenges the plan when enabled. Do not spawn top-level Dev or Reviewer subagents; code review runs later through `/prizmkit-code-review`.
16
16
 
17
17
  **Agent spawn failure policy (all Agent tool calls)**:
18
18
  - If spawning Reviewer or Critic fails with team/config/lock errors, retry at most once.
@@ -160,23 +160,16 @@ Before proceeding past CP-1, verify:
160
160
 
161
161
  **CP-1**: plan.md exists with Tasks section.
162
162
 
163
- ### Phase 3: AnalyzeReviewer Subagent
163
+ ### Phase 3: Plan Self-Check Orchestrator Direct
164
164
 
165
- Spawn Reviewer agent (Agent tool, subagent_type="prizm-dev-team-reviewer", mode="plan", run_in_background=false, isolation=default/no worktree). Do not request worktree isolation; this prevents AI tool-created `.claude/worktrees/...` subagent worktrees. PrizmKit pipeline-managed linked git worktrees under `.prizmkit/state/worktrees/...` are separate and controlled by `USE_WORKTREE`.
165
+ Do not spawn a top-level Reviewer subagent for planning analysis. Perform a bounded self-check before implementation:
166
166
 
167
- Spawn failure cap: for team/config/lock errors, retry at most once for this Reviewer spawn. If the second attempt fails, do not poll for report artifacts; fix/check the plan inline or write `failure-log.md` before stopping for recovery.
167
+ 1. Re-read only `context-snapshot.md` and `plan.md`.
168
+ 2. Cross-check plan.md Tasks against the feature description and acceptance criteria.
169
+ 3. Verify the plan references the File Manifest and avoids unrelated work.
170
+ 4. If you find CRITICAL issues, fix ONLY those issues in plan.md before continuing.
168
171
 
169
- Prompt:
170
- > "Read {{REVIEWER_SUBAGENT_PATH}}. For feature {{FEATURE_ID}} (slug: {{FEATURE_SLUG}}):
171
- > 1. Read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` FIRST — Section 3 has project context, Section 4 has file manifest.
172
- > 2. Cross-check `plan.md` (including Tasks section) against feature description and acceptance criteria for consistency.
173
- > 3. Before flagging CRITICAL or HIGH issues, read the relevant source files listed in the File Manifest to verify.
174
- > Report: CRITICAL, HIGH, MEDIUM issues found (or 'No issues found')."
175
-
176
- Wait for Reviewer to return.
177
- - If CRITICAL issues found: fix them yourself — read `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` for full project context. Fix ONLY the listed CRITICAL issues in plan.md. Then re-run analyze (max 1 round).
178
-
179
- **CP-2**: No CRITICAL issues.
172
+ **CP-2**: Plan self-check complete; no CRITICAL plan issues remain.
180
173
 
181
174
  {{IF_CRITIC_ENABLED}}
182
175
  ### Phase 3.5: Plan Challenge — Critic Agent(s)
@@ -495,7 +488,13 @@ Append results to `context-snapshot.md`:
495
488
  Browser cleanup: confirmed
496
489
  ```
497
490
 
498
- If verification fails, log the failure details but continue to commit. Failures do NOT block the commit, but you MUST attempt verification and MUST clean up the dev server.
491
+ Browser blocking policy: `{{BROWSER_BLOCKING}}`.
492
+
493
+ If verification fails:
494
+ - If `{{BROWSER_BLOCKING}}` is `true`: mark checkpoint step `browser-verification` as `failed`, write `failure-log.md` with browser evidence, clean up the dev server/browser, and stop before commit.
495
+ - If `{{BROWSER_BLOCKING}}` is `false`: log the failure details, mark checkpoint step `browser-verification` according to the evidence, clean up the dev server/browser, and continue.
496
+
497
+ You MUST attempt verification and MUST clean up the dev server in both cases.
499
498
  {{END_IF_BROWSER_INTERACTION}}
500
499
 
501
500
  ### Phase 6: Architecture Sync & Commit (SINGLE COMMIT)
@@ -555,7 +554,6 @@ Rules for writing completion notes:
555
554
  |----------|------|
556
555
  | Feature Artifacts Dir | `.prizmkit/specs/{{FEATURE_SLUG}}/` |
557
556
  | Context Snapshot | `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` |
558
- | Reviewer Agent Def | {{REVIEWER_SUBAGENT_PATH}} |
559
557
  {{IF_CRITIC_ENABLED}}
560
558
  | Critic Agent Def | {{CRITIC_SUBAGENT_PATH}} |
561
559
  {{END_IF_CRITIC_ENABLED}}
@@ -580,11 +578,11 @@ If you encounter an unrecoverable error, context overflow, or are about to exit
580
578
 
581
579
  ## Reminders
582
580
 
583
- - Tier 2: orchestrator builds context+plan and implements directly; Critic challenges plan when enabled; Reviewer reviews
581
+ - Tier 2: orchestrator builds context+plan and implements directly; Critic challenges plan when enabled; `/prizmkit-code-review` handles review later
584
582
  - Build context-snapshot.md FIRST; use it throughout instead of re-reading source files
585
583
  - context-snapshot.md is append-only: orchestrator writes Sections 1-4 + Implementation Log, Reviewer appends Review Notes
586
584
  - Gate checks enforce Implementation Log and Review Notes are written before proceeding
587
- - Do NOT use `run_in_background=true` when spawning Critic or Reviewer agents
585
+ - Do NOT use `run_in_background=true` when spawning Critic agents
588
586
  - **NEVER delete, modify, or touch any file under `.prizmkit/state/`** — those are pipeline runtime files managed by the runner
589
587
  - NEVER run `rm -rf`, `git clean`, or any destructive filesystem operations
590
588
  - `/prizmkit-committer` is mandatory, and must not be replaced with manual git commit commands