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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/README.md +100 -87
- package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +14 -0
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +192 -110
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -5
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +19 -52
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +10 -12
- package/bundled/dev-pipeline/scripts/update-bug-status.py +196 -67
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +16 -3
- package/bundled/dev-pipeline/scripts/update-feature-status.py +45 -117
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +45 -119
- package/bundled/dev-pipeline/scripts/utils.py +119 -0
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
- package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +148 -12
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +0 -19
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +113 -188
- package/bundled/dev-pipeline/tests/test_recovery_workflow.py +211 -0
- package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -5
- package/bundled/dev-pipeline/tests/test_unified_cli.py +544 -181
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
- package/bundled/skills/recovery-workflow/SKILL.md +7 -5
- package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
- package/bundled/skills/recovery-workflow/references/detection.md +3 -3
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +1 -1
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
- package/bundled/templates/project-memory-template.md +19 -11
- package/package.json +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for auto-skip and
|
|
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
|
|
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", "
|
|
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 == "
|
|
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
|
-
|
|
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", "
|
|
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
|
-
#
|
|
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
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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
|
|
1367
|
-
"""
|
|
1445
|
+
class TestResetStateByFeatureId:
|
|
1446
|
+
"""ResetState with --feature-id targets a specific failed feature + downstream."""
|
|
1368
1447
|
|
|
1369
|
-
def
|
|
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 =
|
|
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
|
|
1385
|
-
"""C2 fix:
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
1419
|
-
"""
|
|
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 =
|
|
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
|
|
1436
|
-
"""
|
|
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 =
|
|
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 →
|
|
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 →
|
|
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.
|
|
1481
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
299
|
-
assert
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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), "
|
|
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:
|