prizmkit 1.1.152 → 1.1.154

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 (44) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +100 -87
  3. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
  4. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +14 -0
  5. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +192 -110
  6. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
  7. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -5
  8. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
  9. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
  10. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
  11. package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
  12. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -1
  13. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +19 -52
  14. package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
  15. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
  16. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +10 -12
  17. package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
  18. package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
  19. package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
  20. package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
  21. package/bundled/dev-pipeline/scripts/utils.py +119 -0
  22. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
  23. package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
  24. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
  25. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
  26. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
  27. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
  28. package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
  29. package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
  30. package/bundled/dev-pipeline/tests/test_unified_cli.py +544 -181
  31. package/bundled/skills/_metadata.json +1 -1
  32. package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
  33. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
  34. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
  35. package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
  36. package/bundled/skills/recovery-workflow/SKILL.md +7 -5
  37. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  38. package/bundled/skills/recovery-workflow/references/detection.md +3 -3
  39. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
  40. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
  41. package/bundled/templates/project-memory-template.md +19 -11
  42. package/package.json +1 -1
  43. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
  44. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
@@ -1,4 +1,4 @@
1
- """Tests for auto-skip and unskip functionality in update-feature-status.py."""
1
+ """Tests for auto-skip and reset_state functionality in update-feature-status.py."""
2
2
 
3
3
  import json
4
4
  import os
@@ -221,6 +221,76 @@ class TestContextOverflowRetrySemantics:
221
221
  ]:
222
222
  assert key not in status
223
223
 
224
+ def test_start_and_update_persist_effective_retry_budgets_without_charging_workflow_skip(self, tmp_path):
225
+ scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
226
+
227
+ for case in self._script_cases(scripts_dir):
228
+ case_dir = tmp_path / f"budgets-{case['id']}"
229
+ state_dir = case_dir / "state"
230
+ list_path = case_dir / case["list_name"]
231
+ payload = json.loads(json.dumps(case["list_payload"]))
232
+ payload[case["collection"]][0]["status"] = "pending"
233
+ write_json_file(str(list_path), payload)
234
+
235
+ start = subprocess.run(
236
+ [
237
+ "python3",
238
+ str(case["script"]),
239
+ case["list_option"],
240
+ str(list_path),
241
+ "--state-dir",
242
+ str(state_dir),
243
+ case["id_option"],
244
+ case["id"],
245
+ "--action",
246
+ "start",
247
+ "--max-retries",
248
+ "7",
249
+ "--max-infra-retries",
250
+ "4",
251
+ ],
252
+ text=True,
253
+ capture_output=True,
254
+ check=True,
255
+ )
256
+ assert json.loads(start.stdout)["new_status"] == "in_progress"
257
+ started = self._runtime_status(state_dir, case["id"])
258
+ assert started["max_retries"] == 7
259
+ assert started["max_infra_retries"] == 4
260
+
261
+ skipped = subprocess.run(
262
+ [
263
+ "python3",
264
+ str(case["script"]),
265
+ case["list_option"],
266
+ str(list_path),
267
+ "--state-dir",
268
+ str(state_dir),
269
+ case["id_option"],
270
+ case["id"],
271
+ "--session-id",
272
+ "sess-skip",
273
+ "--session-status",
274
+ "workflow_skipped",
275
+ "--action",
276
+ "update",
277
+ "--max-retries",
278
+ "5",
279
+ "--max-infra-retries",
280
+ "2",
281
+ ],
282
+ text=True,
283
+ capture_output=True,
284
+ check=True,
285
+ )
286
+ summary = json.loads(skipped.stdout)
287
+ runtime = self._runtime_status(state_dir, case["id"])
288
+ assert summary["new_status"] == "skipped"
289
+ assert runtime["max_retries"] == 5
290
+ assert runtime["max_infra_retries"] == 2
291
+ assert runtime["retry_count"] == 0
292
+ assert runtime["infra_error_count"] == 0
293
+
224
294
  def test_context_overflow_consumes_retry_and_records_bounded_continuation_metadata(self, tmp_path):
225
295
  scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
226
296
 
@@ -441,10 +511,10 @@ class TestContextOverflowRetrySemantics:
441
511
  assert runtime["context_overflow_count"] == 2
442
512
  assert runtime["continuation_count"] == 2
443
513
 
444
- def test_manual_reset_clean_and_unskip_clear_all_continuation_state(self, tmp_path):
514
+ def test_manual_reset_clean_and_reset_state_clear_all_continuation_state(self, tmp_path):
445
515
  scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
446
516
 
447
- for action in ["reset", "clean", "unskip"]:
517
+ for action in ["reset", "clean", "reset_state"]:
448
518
  for case in self._script_cases(scripts_dir):
449
519
  case_dir = tmp_path / f"{action}-{case['id']}"
450
520
  state_dir = case_dir / "state"
@@ -503,7 +573,7 @@ class TestContextOverflowRetrySemantics:
503
573
  ]
504
574
  if action in ("reset", "clean"):
505
575
  cmd.extend([case["id_option"], case["id"]])
506
- elif action == "unskip":
576
+ elif action == "reset_state":
507
577
  cmd.extend([case["id_option"], case["id"]])
508
578
  if action == "clean":
509
579
  cmd.extend(["--project-root", str(case_dir)])
@@ -515,7 +585,15 @@ class TestContextOverflowRetrySemantics:
515
585
  for continuation_file in continuation_files:
516
586
  assert not continuation_file.exists()
517
587
  runtime = self._runtime_status(state_dir, case["id"])
518
- self._assert_no_continuation_runtime_state(runtime)
588
+ if action == "reset_state":
589
+ assert runtime["active_dev_branch"] == "dev/existing-branch"
590
+ assert runtime["base_branch"] == "main"
591
+ continuation_only = dict(runtime)
592
+ continuation_only.pop("active_dev_branch")
593
+ continuation_only.pop("base_branch")
594
+ self._assert_no_continuation_runtime_state(continuation_only)
595
+ else:
596
+ self._assert_no_continuation_runtime_state(runtime)
519
597
  assert runtime["retry_count"] == 0
520
598
  assert runtime["infra_error_count"] == 0
521
599
  assert runtime["last_infra_error_session_id"] is None
@@ -601,6 +679,7 @@ class TestContextOverflowRetrySemantics:
601
679
  "reset",
602
680
  case["target"],
603
681
  case["item_id"],
682
+ "--fresh-checkout",
604
683
  str(prizmkit_dir / "plans" / case["list_name"]),
605
684
  ],
606
685
  cwd=case_dir,
@@ -758,7 +837,7 @@ def _run_status_snapshot_action(case, list_path, state_dir, action, session_stat
758
837
  "--action",
759
838
  action,
760
839
  ]
761
- if action in {"update", "reset", "clean", "unskip"}:
840
+ if action in {"update", "reset", "clean", "reset_state"}:
762
841
  cmd.extend([case["id_option"], case["id"]])
763
842
  if action == "update":
764
843
  cmd.extend([
@@ -1200,7 +1279,7 @@ class TestAutoSkipEdgeCases:
1200
1279
 
1201
1280
 
1202
1281
  # ---------------------------------------------------------------------------
1203
- # action_unskip — via subprocess (tests the full CLI interface)
1282
+ # action_reset_state — via subprocess (tests the full CLI interface)
1204
1283
  # ---------------------------------------------------------------------------
1205
1284
 
1206
1285
  import subprocess
@@ -1210,12 +1289,12 @@ _SCRIPT = os.path.join(
1210
1289
  )
1211
1290
 
1212
1291
 
1213
- def _run_unskip(fl_path, state_dir, feature_id=None):
1292
+ def _run_reset_state(fl_path, state_dir, feature_id=None):
1214
1293
  cmd = [
1215
1294
  "python3", _SCRIPT,
1216
1295
  "--feature-list", fl_path,
1217
1296
  "--state-dir", state_dir,
1218
- "--action", "unskip",
1297
+ "--action", "reset_state",
1219
1298
  ]
1220
1299
  if feature_id:
1221
1300
  cmd += ["--feature-id", feature_id]
@@ -1344,7 +1423,7 @@ class TestInfraErrorUpdate:
1344
1423
  assert fs["last_infra_error_session_id"] is None
1345
1424
  assert _read_statuses(fl_path)["F-001"] == "pending"
1346
1425
 
1347
- def test_unskip_clears_infra_retry_counters(self, tmp_path):
1426
+ def test_reset_state_clears_infra_retry_counters(self, tmp_path):
1348
1427
  features = [_make_feature("F-001", "Root", status="failed")]
1349
1428
  fl_path = _write_fl(tmp_path, features)
1350
1429
  state_dir = _init_state(tmp_path, ["F-001"])
@@ -1355,7 +1434,7 @@ class TestInfraErrorUpdate:
1355
1434
  fs["last_infra_error_session_id"] = "session-infra-final"
1356
1435
  write_json_file(status_path, fs)
1357
1436
 
1358
- _run_unskip(fl_path, state_dir, "F-001")
1437
+ _run_reset_state(fl_path, state_dir, "F-001")
1359
1438
 
1360
1439
  fs = load_feature_status(state_dir, "F-001")
1361
1440
  assert fs["retry_count"] == 0
@@ -1363,10 +1442,10 @@ class TestInfraErrorUpdate:
1363
1442
  assert fs["last_infra_error_session_id"] is None
1364
1443
  assert _read_statuses(fl_path)["F-001"] == "pending"
1365
1444
 
1366
- class TestUnskipByFeatureId:
1367
- """Unskip with --feature-id targets a specific failed feature + downstream."""
1445
+ class TestResetStateByFeatureId:
1446
+ """ResetState with --feature-id targets a specific failed feature + downstream."""
1368
1447
 
1369
- def test_unskip_failed_root_resets_all(self, tmp_path):
1448
+ def test_reset_state_failed_root_resets_all(self, tmp_path):
1370
1449
  features = [
1371
1450
  _make_feature("F-001", "Root", status="failed"),
1372
1451
  _make_feature("F-002", "Mid", deps=["F-001"], status="auto_skipped"),
@@ -1375,14 +1454,14 @@ class TestUnskipByFeatureId:
1375
1454
  fl_path = _write_fl(tmp_path, features)
1376
1455
  state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
1377
1456
 
1378
- result = _run_unskip(fl_path, state_dir, "F-001")
1457
+ result = _run_reset_state(fl_path, state_dir, "F-001")
1379
1458
 
1380
1459
  assert result["reset_count"] == 3
1381
1460
  statuses = _read_statuses(fl_path)
1382
1461
  assert all(s == "pending" for s in statuses.values())
1383
1462
 
1384
- def test_unskip_auto_skipped_leaf_resets_upstream(self, tmp_path):
1385
- """C2 fix: unskip F-003 must also find and reset F-001 (failed root)."""
1463
+ def test_reset_state_auto_skipped_leaf_resets_upstream(self, tmp_path):
1464
+ """C2 fix: reset_state F-003 must also find and reset F-001 (failed root)."""
1386
1465
  features = [
1387
1466
  _make_feature("F-001", "Root", status="failed"),
1388
1467
  _make_feature("F-002", "Mid", deps=["F-001"], status="auto_skipped"),
@@ -1391,14 +1470,14 @@ class TestUnskipByFeatureId:
1391
1470
  fl_path = _write_fl(tmp_path, features)
1392
1471
  state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
1393
1472
 
1394
- result = _run_unskip(fl_path, state_dir, "F-003")
1473
+ result = _run_reset_state(fl_path, state_dir, "F-003")
1395
1474
 
1396
1475
  assert result["reset_count"] == 3
1397
1476
  reset_ids = {f["feature_id"] for f in result["features"]}
1398
1477
  assert "F-001" in reset_ids # transitive upstream reset
1399
1478
  assert all(s == "pending" for s in _read_statuses(fl_path).values())
1400
1479
 
1401
- def test_unskip_preserves_completed_features(self, tmp_path):
1480
+ def test_reset_state_preserves_completed_features(self, tmp_path):
1402
1481
  features = [
1403
1482
  _make_feature("F-001", "Done", status="completed"),
1404
1483
  _make_feature("F-002", "Failed", deps=["F-001"], status="failed"),
@@ -1407,7 +1486,7 @@ class TestUnskipByFeatureId:
1407
1486
  fl_path = _write_fl(tmp_path, features)
1408
1487
  state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003"])
1409
1488
 
1410
- result = _run_unskip(fl_path, state_dir, "F-002")
1489
+ result = _run_reset_state(fl_path, state_dir, "F-002")
1411
1490
 
1412
1491
  assert result["reset_count"] == 2
1413
1492
  statuses = _read_statuses(fl_path)
@@ -1415,8 +1494,8 @@ class TestUnskipByFeatureId:
1415
1494
  assert statuses["F-002"] == "pending"
1416
1495
  assert statuses["F-003"] == "pending"
1417
1496
 
1418
- def test_unskip_skipped_feature_resets_downstream(self, tmp_path):
1419
- """Unskip a manually-skipped feature should also reset its auto_skipped downstream."""
1497
+ def test_reset_state_skipped_feature_resets_downstream(self, tmp_path):
1498
+ """ResetState a manually-skipped feature should also reset its auto_skipped downstream."""
1420
1499
  features = [
1421
1500
  _make_feature("F-001", "Skipped", status="skipped"),
1422
1501
  _make_feature("F-002", "Child", deps=["F-001"], status="auto_skipped"),
@@ -1424,7 +1503,7 @@ class TestUnskipByFeatureId:
1424
1503
  fl_path = _write_fl(tmp_path, features)
1425
1504
  state_dir = _init_state(tmp_path, ["F-001", "F-002"])
1426
1505
 
1427
- result = _run_unskip(fl_path, state_dir, "F-001")
1506
+ result = _run_reset_state(fl_path, state_dir, "F-001")
1428
1507
 
1429
1508
  assert result["reset_count"] == 2
1430
1509
  statuses = _read_statuses(fl_path)
@@ -1432,8 +1511,8 @@ class TestUnskipByFeatureId:
1432
1511
  assert statuses["F-002"] == "pending"
1433
1512
 
1434
1513
 
1435
- class TestUnskipAll:
1436
- """Unskip without --feature-id resets all failed + auto_skipped."""
1514
+ class TestResetStateAll:
1515
+ """ResetState without --feature-id resets all failed + auto_skipped."""
1437
1516
 
1438
1517
  def test_resets_all_failed_and_auto_skipped(self, tmp_path):
1439
1518
  features = [
@@ -1445,7 +1524,7 @@ class TestUnskipAll:
1445
1524
  fl_path = _write_fl(tmp_path, features)
1446
1525
  state_dir = _init_state(tmp_path, ["F-001", "F-002", "F-003", "F-004"])
1447
1526
 
1448
- result = _run_unskip(fl_path, state_dir)
1527
+ result = _run_reset_state(fl_path, state_dir)
1449
1528
 
1450
1529
  assert result["reset_count"] == 2
1451
1530
  statuses = _read_statuses(fl_path)
@@ -1456,11 +1535,11 @@ class TestUnskipAll:
1456
1535
 
1457
1536
 
1458
1537
  # ---------------------------------------------------------------------------
1459
- # Integration: auto-skip → get_next → unskip → get_next
1538
+ # Integration: auto-skip → get_next → reset_state → get_next
1460
1539
  # ---------------------------------------------------------------------------
1461
1540
 
1462
1541
  class TestAutoSkipIntegration:
1463
- """Full lifecycle: fail → auto-skip → pipeline complete → unskip → resume."""
1542
+ """Full lifecycle: fail → auto-skip → pipeline complete → reset_state → resume."""
1464
1543
 
1465
1544
  def test_full_lifecycle(self, tmp_path):
1466
1545
  features = [
@@ -1477,8 +1556,8 @@ class TestAutoSkipIntegration:
1477
1556
  # 2. get_next should return PIPELINE_COMPLETE
1478
1557
  assert _run_get_next(fl_path, state_dir) == "PIPELINE_COMPLETE"
1479
1558
 
1480
- # 3. Unskip recovers everything
1481
- _run_unskip(fl_path, state_dir, "F-001")
1559
+ # 3. ResetState recovers everything
1560
+ _run_reset_state(fl_path, state_dir, "F-001")
1482
1561
  statuses = _read_statuses(fl_path)
1483
1562
  assert statuses["F-001"] == "pending"
1484
1563
  assert statuses["F-002"] == "pending"
@@ -1488,7 +1567,7 @@ class TestAutoSkipIntegration:
1488
1567
  data = json.loads(output)
1489
1568
  assert data["feature_id"] == "F-001"
1490
1569
 
1491
- def test_retry_count_reset_after_unskip(self, tmp_path):
1570
+ def test_retry_count_reset_after_reset_state(self, tmp_path):
1492
1571
  features = [
1493
1572
  _make_feature("F-001", "Root", status="failed"),
1494
1573
  ]
@@ -1502,7 +1581,7 @@ class TestAutoSkipIntegration:
1502
1581
  with open(fs_path, "w") as f:
1503
1582
  json.dump(fs, f)
1504
1583
 
1505
- _run_unskip(fl_path, state_dir, "F-001")
1584
+ _run_reset_state(fl_path, state_dir, "F-001")
1506
1585
 
1507
1586
  with open(fs_path) as f:
1508
1587
  fs = json.load(f)
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import subprocess
7
+ import sys
6
8
 
7
9
  from prizmkit_runtime.checkpoint_state import (
8
10
  VALID_CHECKPOINT_STATUSES,
@@ -231,6 +233,120 @@ def test_semantic_metadata_rejects_legacy_or_mismatched_status(tmp_path):
231
233
  assert state.semantic.error_code == error_code
232
234
 
233
235
 
236
+ def test_semantic_blocker_metadata_requires_blocked_terminal_state(tmp_path):
237
+ cases = [
238
+ ({"blocked_reason": ""}, "invalid_blocked_reason"),
239
+ ({"blocked_reason": " "}, "invalid_blocked_reason"),
240
+ ({"blocked_reason": "unexpected blocker"}, "blocked_reason_without_blocked_terminal"),
241
+ (
242
+ {"blocked_reason": "unexpected blocker", "terminal_status": "WORKFLOW_COMPLETED"},
243
+ "completed_workflow_has_blocked_reason",
244
+ ),
245
+ ({"terminal_status": "WORKFLOW_BLOCKED"}, "missing_blocked_reason"),
246
+ ]
247
+
248
+ for index, (semantic, expected_error) in enumerate(cases):
249
+ payload = _family_checkpoint(tmp_path, semantic=semantic)
250
+ state = validate_checkpoint_data(
251
+ payload,
252
+ path=tmp_path / f"invalid-blocker-{index}.json",
253
+ project_root=tmp_path,
254
+ )
255
+
256
+ assert state.valid is True
257
+ assert state.semantic.error_code == expected_error
258
+ assert state.semantic_complete is False
259
+
260
+
261
+ def test_checkpoint_updater_rejects_invalid_blocker_metadata_without_mutation(tmp_path):
262
+ cases = [
263
+ ({"blocked_reason": "", "terminal_status": "WORKFLOW_COMPLETED"}, True),
264
+ ({"blocked_reason": " ", "terminal_status": "WORKFLOW_COMPLETED"}, True),
265
+ ({"blocked_reason": "unexpected blocker", "terminal_status": "WORKFLOW_COMPLETED"}, True),
266
+ ({"blocked_reason": "unexpected blocker"}, False),
267
+ ({"terminal_status": "WORKFLOW_BLOCKED"}, False),
268
+ ]
269
+
270
+ for index, (semantic_update, completed) in enumerate(cases):
271
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
272
+ if not completed:
273
+ payload["steps"][0]["status"] = "in_progress"
274
+ for step in payload["steps"][1:]:
275
+ step["status"] = "pending"
276
+ step.pop("stage_result", None)
277
+ checkpoint = _write(tmp_path / f"atomic-blocker-{index}.json", payload)
278
+ before = checkpoint.read_bytes()
279
+
280
+ result = update_checkpoint(
281
+ str(checkpoint),
282
+ "S06" if completed else "S01",
283
+ "completed" if completed else "in_progress",
284
+ semantic_update=semantic_update,
285
+ )
286
+
287
+ assert result["ok"] is False
288
+ assert checkpoint.read_bytes() == before
289
+
290
+
291
+ def test_checkpoint_updater_rejects_completed_terminal_for_blocking_stage_result(tmp_path):
292
+ for index, blocked_reason in enumerate((None, "safe environment unavailable")):
293
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
294
+ payload["steps"][2]["status"] = "in_progress"
295
+ payload["steps"][2].pop("stage_result", None)
296
+ for step in payload["steps"][3:]:
297
+ step["status"] = "pending"
298
+ step.pop("stage_result", None)
299
+ checkpoint = _write(tmp_path / f"conflicting-test-terminal-{index}.json", payload)
300
+ before = checkpoint.read_bytes()
301
+ semantic_update = {
302
+ "stage_result": "TEST_BLOCKED",
303
+ "terminal_status": "WORKFLOW_COMPLETED",
304
+ }
305
+ if blocked_reason is not None:
306
+ semantic_update["blocked_reason"] = blocked_reason
307
+
308
+ result = update_checkpoint(
309
+ str(checkpoint),
310
+ "prizmkit-test",
311
+ "failed",
312
+ semantic_update=semantic_update,
313
+ )
314
+
315
+ assert result["ok"] is False
316
+ assert checkpoint.read_bytes() == before
317
+
318
+
319
+ def test_checkpoint_updater_cli_returns_nonzero_without_mutating_invalid_blocker_input(tmp_path):
320
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
321
+ checkpoint = _write(tmp_path / "cli-invalid-blocker.json", payload)
322
+ before = checkpoint.read_bytes()
323
+ script = __import__("update_checkpoint").__file__
324
+
325
+ result = subprocess.run(
326
+ [
327
+ sys.executable,
328
+ script,
329
+ "--checkpoint-path",
330
+ str(checkpoint),
331
+ "--step",
332
+ "S06",
333
+ "--status",
334
+ "completed",
335
+ "--terminal-status",
336
+ "WORKFLOW_COMPLETED",
337
+ "--blocked-reason",
338
+ "",
339
+ ],
340
+ text=True,
341
+ capture_output=True,
342
+ check=False,
343
+ )
344
+
345
+ assert result.returncode == 1
346
+ assert json.loads(result.stdout)["ok"] is False
347
+ assert checkpoint.read_bytes() == before
348
+
349
+
234
350
  def test_update_checkpoint_reenters_failed_stage_without_stale_result(tmp_path):
235
351
  payload = _family_checkpoint(
236
352
  tmp_path,
@@ -280,11 +396,23 @@ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
280
396
 
281
397
  assert {stage_result for _skill, stage_result, _status in cases} <= VALID_STAGE_RESULTS
282
398
  for index, (skill, stage_result, status) in enumerate(cases):
283
- step = _step("S01", skill, skill, "pending")
284
- checkpoint = _write(
285
- tmp_path / f"owned-result-{index}.json",
286
- _checkpoint([step]),
287
- )
399
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
400
+ if skill == "prizmkit-plan":
401
+ for offset, step in enumerate(payload["steps"], start=2):
402
+ step["id"] = f"S{offset:02d}"
403
+ step["depends_on"] = f"S{offset - 1:02d}"
404
+ payload["steps"].insert(0, _step("S01", skill, skill, "pending"))
405
+ target_index = 0
406
+ else:
407
+ target_index = next(
408
+ position
409
+ for position, step in enumerate(payload["steps"])
410
+ if step["skill"] == skill
411
+ )
412
+ for step in payload["steps"][target_index:]:
413
+ step["status"] = "pending"
414
+ step.pop("stage_result", None)
415
+ checkpoint = _write(tmp_path / f"owned-result-{index}.json", payload)
288
416
 
289
417
  result = update_checkpoint(
290
418
  str(checkpoint),
@@ -295,8 +423,9 @@ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
295
423
 
296
424
  assert result["ok"] is True
297
425
  persisted = json.loads(checkpoint.read_text(encoding="utf-8"))
298
- assert persisted["steps"][0]["status"] == status
299
- assert persisted["steps"][0]["stage_result"] == stage_result
426
+ target = next(step for step in persisted["steps"] if step["skill"] == skill)
427
+ assert target["status"] == status
428
+ assert target["stage_result"] == stage_result
300
429
  assert persisted["feature_state"]["stage"] == {
301
430
  "prizmkit-plan": "plan",
302
431
  "prizmkit-implement": "implement",
@@ -310,12 +439,19 @@ def test_checkpoint_updater_supports_every_owned_stage_result(tmp_path):
310
439
 
311
440
 
312
441
  def test_duplicate_skills_require_exact_step_id(tmp_path):
313
- path = _write(tmp_path / "workflow-checkpoint.json", _checkpoint([
314
- _step("S01", "prizmkit-committer", "First", "completed"),
315
- _step("S02", "prizmkit-committer", "Second", "pending", "S01"),
316
- ]))
442
+ payload = _family_checkpoint(tmp_path, semantic={}, terminal=False)
443
+ for offset, step in enumerate(payload["steps"], start=3):
444
+ step["id"] = f"S{offset:02d}"
445
+ step["depends_on"] = f"S{offset - 1:02d}"
446
+ step["status"] = "pending"
447
+ step.pop("stage_result", None)
448
+ payload["steps"][:0] = [
449
+ _step("S01", "custom-stage", "First", "completed"),
450
+ _step("S02", "custom-stage", "Second", "pending", "S01"),
451
+ ]
452
+ path = _write(tmp_path / "workflow-checkpoint.json", payload)
317
453
 
318
- rejected = update_checkpoint(str(path), "prizmkit-committer", "completed")
454
+ rejected = update_checkpoint(str(path), "custom-stage", "completed")
319
455
  accepted = update_checkpoint(str(path), "S02", "completed")
320
456
 
321
457
  assert rejected["ok"] is False
@@ -1,7 +1,6 @@
1
1
  """Tests for generate-bootstrap-prompt.py core functions."""
2
2
 
3
3
  import json
4
- import importlib.util
5
4
  import os
6
5
  import re
7
6
  from argparse import Namespace
@@ -87,14 +86,6 @@ def assert_complete_prompt_rendering(prompt):
87
86
  assert re.findall(r"\{\{[A-Z][A-Z_0-9]+\}\}", prompt) == []
88
87
 
89
88
 
90
- def _load_recovery_prompt_module():
91
- path = Path("dev-pipeline/scripts/generate-recovery-prompt.py")
92
- spec = importlib.util.spec_from_file_location("generate_recovery_prompt", path)
93
- module = importlib.util.module_from_spec(spec)
94
- spec.loader.exec_module(module)
95
- return module
96
-
97
-
98
89
  def test_rendered_prompt_validation_rejects_unresolved_control_placeholders():
99
90
  content = (
100
91
  "## Your Mission\n"
@@ -698,7 +689,6 @@ class TestDirectOrchestratorImplementationPrompts:
698
689
  assert "Reviewer 3" not in rendered
699
690
 
700
691
  def test_all_rendered_pipeline_prompts_have_one_main_agent_review_model(self, tmp_path):
701
- recovery = _load_recovery_prompt_module()
702
692
  prompts = [
703
693
  _render_feature_prompt(tmp_path / "feature-lite", mode="lite"),
704
694
  _render_feature_prompt(tmp_path / "feature-standard", mode="standard"),
@@ -710,15 +700,6 @@ class TestDirectOrchestratorImplementationPrompts:
710
700
  ),
711
701
  _render_bugfix_prompt(tmp_path / "bugfix"),
712
702
  _render_refactor_prompt(tmp_path / "refactor"),
713
- recovery.build_bugfix_prompt(
714
- {
715
- "workflow_type": "bug-fix-workflow",
716
- "phase": 5,
717
- "phase_name": "Review",
718
- "workflow_data": {"bug_id": "B-001"},
719
- },
720
- str(tmp_path),
721
- ),
722
703
  ]
723
704
 
724
705
  for prompt in prompts: