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.
- package/bundled/VERSION.json +3 -3
- package/bundled/agents/prizm-dev-team-reviewer.md +12 -1
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +1 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +19 -73
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +283 -421
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +259 -481
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +34 -0
- package/bundled/dev-pipeline/scripts/init-pipeline.py +12 -0
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +24 -1
- package/bundled/dev-pipeline/scripts/prompt_framework.py +312 -0
- package/bundled/dev-pipeline/scripts/update-bug-status.py +87 -20
- package/bundled/dev-pipeline/scripts/update-feature-status.py +82 -16
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +68 -28
- package/bundled/dev-pipeline/scripts/utils.py +171 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +10 -18
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +14 -25
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +9 -1
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +11 -8
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +122 -182
- package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +16 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +12 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-mission.md +9 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +31 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +22 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +23 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +20 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-init.md +9 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +15 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +29 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +11 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +7 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +31 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-task-contract.md +40 -0
- package/bundled/dev-pipeline/templates/sections/failure-capture.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +17 -0
- package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +13 -0
- package/bundled/dev-pipeline/templates/sections/refactor-mission.md +9 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +37 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +22 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +16 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-init.md +9 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +22 -0
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +19 -0
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +11 -0
- package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +7 -0
- package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +28 -0
- package/bundled/dev-pipeline/templates/sections/refactor-task-contract.md +52 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +17 -5
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +197 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +6 -2
- package/bundled/dev-pipeline/tests/test_utils.py +66 -1
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/app-planner/SKILL.md +8 -7
- package/bundled/skills/app-planner/references/project-brief-guide.md +2 -2
- package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +1 -1
- package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +1 -1
- package/bundled/skills/app-planner/references/rules-configuration.md +53 -26
- package/bundled/skills/bug-fix-workflow/SKILL.md +191 -336
- package/bundled/skills/bug-planner/SKILL.md +6 -5
- package/bundled/skills/bug-planner/references/critic-and-verification.md +3 -3
- package/bundled/skills/bug-planner/references/schema-validation.md +2 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +30 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +16 -16
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +2 -2
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +19 -11
- package/bundled/skills/feature-planner/SKILL.md +9 -11
- package/bundled/skills/feature-planner/assets/planning-guide.md +1 -1
- package/bundled/skills/feature-planner/references/decomposition-patterns.md +1 -1
- package/bundled/skills/feature-planner/references/error-recovery.md +2 -1
- package/bundled/skills/feature-planner/references/incremental-feature-planning.md +2 -2
- package/bundled/skills/feature-planner/references/new-project-planning.md +35 -39
- package/bundled/skills/feature-planner/scripts/validate-and-generate.py +1 -1
- package/bundled/skills/feature-workflow/SKILL.md +169 -298
- package/bundled/skills/prizmkit/SKILL.md +103 -57
- package/bundled/skills/prizmkit-code-review/SKILL.md +97 -116
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -0
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +12 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +59 -47
- package/bundled/skills/prizmkit-deploy/SKILL.md +162 -381
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +220 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +51 -40
- package/bundled/skills/prizmkit-init/SKILL.md +4 -3
- package/bundled/skills/prizmkit-plan/SKILL.md +85 -70
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +94 -73
- package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +20 -18
- package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +18 -14
- package/bundled/skills/prizmkit-retrospective/SKILL.md +56 -48
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +79 -27
- package/bundled/skills/prizmkit-test/SKILL.md +138 -141
- package/bundled/skills/prizmkit-test/references/examples.md +10 -8
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +2 -2
- package/bundled/skills/recovery-workflow/SKILL.md +195 -256
- package/bundled/skills/recovery-workflow/evals/evals.json +21 -13
- package/bundled/skills/recovery-workflow/references/detection.md +48 -39
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +258 -322
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -11
- package/bundled/skills/refactor-planner/SKILL.md +2 -2
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +24 -19
- package/bundled/skills/refactor-planner/references/fast-path.md +2 -4
- package/bundled/skills/refactor-planner/references/planning-phases.md +2 -2
- package/bundled/skills/refactor-workflow/SKILL.md +173 -307
- package/package.json +1 -1
- package/src/scaffold.js +5 -0
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +0 -6
- package/bundled/skills/feature-workflow/references/brainstorm-guide.md +0 -137
- 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
|
-
|
|
785
|
+
elif fstatus == "pending":
|
|
744
786
|
pending_features.append(feature)
|
|
745
787
|
|
|
746
|
-
#
|
|
747
|
-
|
|
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:
|
|
794
|
+
key=lambda f: topo_rank.get(f.get("id"), len(features))
|
|
754
795
|
)
|
|
755
|
-
|
|
796
|
+
elif pending_features:
|
|
756
797
|
candidates = sorted(
|
|
757
798
|
pending_features,
|
|
758
|
-
key=lambda f:
|
|
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":
|
|
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
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
|
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
|
|
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
|
|
346
|
+
2. Only consider refactors whose dependencies are completed
|
|
328
347
|
3. Prefer in_progress refactors (interrupted session resume) over pending
|
|
329
|
-
4.
|
|
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
|
|
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(
|
|
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(
|
|
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":
|
|
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
|
-
|
|
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
|
|
39
|
+
7. **Commit**: Run `/prizmkit-committer` to commit all changes. Do NOT push.
|
|
40
40
|
|
|
41
41
|
### Critical Rules
|
|
42
42
|
|
|
@@ -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
|
|
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:
|
|
163
|
+
### Phase 3: Plan Self-Check — Orchestrator Direct
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Do not spawn a top-level Reviewer subagent for planning analysis. Perform a bounded self-check before implementation:
|
|
166
166
|
|
|
167
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -555,7 +548,6 @@ Rules for writing completion notes:
|
|
|
555
548
|
|----------|------|
|
|
556
549
|
| Feature Artifacts Dir | `.prizmkit/specs/{{FEATURE_SLUG}}/` |
|
|
557
550
|
| Context Snapshot | `.prizmkit/specs/{{FEATURE_SLUG}}/context-snapshot.md` |
|
|
558
|
-
| Reviewer Agent Def | {{REVIEWER_SUBAGENT_PATH}} |
|
|
559
551
|
{{IF_CRITIC_ENABLED}}
|
|
560
552
|
| Critic Agent Def | {{CRITIC_SUBAGENT_PATH}} |
|
|
561
553
|
{{END_IF_CRITIC_ENABLED}}
|
|
@@ -580,11 +572,11 @@ If you encounter an unrecoverable error, context overflow, or are about to exit
|
|
|
580
572
|
|
|
581
573
|
## Reminders
|
|
582
574
|
|
|
583
|
-
- Tier 2: orchestrator builds context+plan and implements directly; Critic challenges plan when enabled;
|
|
575
|
+
- Tier 2: orchestrator builds context+plan and implements directly; Critic challenges plan when enabled; `/prizmkit-code-review` handles review later
|
|
584
576
|
- Build context-snapshot.md FIRST; use it throughout instead of re-reading source files
|
|
585
577
|
- context-snapshot.md is append-only: orchestrator writes Sections 1-4 + Implementation Log, Reviewer appends Review Notes
|
|
586
578
|
- Gate checks enforce Implementation Log and Review Notes are written before proceeding
|
|
587
|
-
- Do NOT use `run_in_background=true` when spawning Critic
|
|
579
|
+
- Do NOT use `run_in_background=true` when spawning Critic agents
|
|
588
580
|
- **NEVER delete, modify, or touch any file under `.prizmkit/state/`** — those are pipeline runtime files managed by the runner
|
|
589
581
|
- NEVER run `rm -rf`, `git clean`, or any destructive filesystem operations
|
|
590
582
|
- `/prizmkit-committer` is mandatory, and must not be replaced with manual git commit commands
|