prizmkit 1.1.130 → 1.1.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/command-adapter.js +0 -2
  3. package/bundled/dev-pipeline/.env.example +1 -1
  4. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +11 -6
  5. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
  6. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -9
  7. package/bundled/dev-pipeline/prizmkit_runtime/processes.py +21 -1
  8. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
  9. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
  10. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
  11. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
  12. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +119 -18
  13. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +3 -2
  14. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +411 -17
  15. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +57 -14
  16. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +5 -4
  17. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +55 -2
  18. package/bundled/dev-pipeline/scripts/prompt_framework.py +152 -1
  19. package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
  20. package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
  21. package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
  22. package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
  23. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +5 -5
  24. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +28 -22
  25. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +24 -10
  26. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +20 -2
  27. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +2 -2
  28. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +5 -10
  30. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +9 -19
  31. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
  32. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +1 -1
  33. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
  34. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  35. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +15 -0
  36. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +2 -2
  37. package/bundled/dev-pipeline/templates/sections/phase-commit.md +2 -2
  38. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
  39. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +5 -5
  40. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +15 -11
  41. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +1 -1
  42. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +6 -6
  43. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
  44. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
  45. package/bundled/dev-pipeline/templates/sections/subagent-timeout-recovery.md +1 -1
  46. package/bundled/dev-pipeline/templates/sections/task-contract.md +4 -0
  47. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
  48. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +349 -48
  49. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +129 -0
  50. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
  51. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +567 -23
  52. package/bundled/dev-pipeline/tests/test_unified_cli.py +37 -10
  53. package/bundled/skills/_metadata.json +63 -26
  54. package/bundled/skills/app-planner/SKILL.md +4 -3
  55. package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
  56. package/bundled/skills/bug-planner/SKILL.md +12 -27
  57. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
  58. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
  59. package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
  60. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
  61. package/bundled/skills/feature-planner/SKILL.md +27 -22
  62. package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
  63. package/bundled/skills/feature-workflow/SKILL.md +94 -121
  64. package/bundled/skills/prizmkit/SKILL.md +130 -94
  65. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
  66. package/bundled/skills/prizmkit-code-review/SKILL.md +132 -86
  67. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +243 -0
  68. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +42 -1
  69. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
  70. package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +93 -1
  71. package/bundled/skills/prizmkit-committer/SKILL.md +125 -48
  72. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
  73. package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
  74. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
  75. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
  76. package/bundled/skills/prizmkit-init/SKILL.md +5 -5
  77. package/bundled/skills/prizmkit-plan/SKILL.md +146 -105
  78. package/bundled/skills/prizmkit-plan/assets/plan-template.md +18 -0
  79. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +242 -0
  80. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +4 -4
  81. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
  82. package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
  83. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
  84. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
  85. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
  86. package/bundled/skills/prizmkit-test/SKILL.md +52 -6
  87. package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +63 -12
  88. package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +1 -1
  89. package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +13 -0
  90. package/bundled/skills/prizmkit-test/references/evidence-protocol.md +6 -1
  91. package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +8 -1
  92. package/bundled/skills/prizmkit-test/references/examples.md +1 -1
  93. package/bundled/skills/prizmkit-test/references/test-generation-steps.md +4 -1
  94. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  95. package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +15 -0
  96. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
  97. package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +744 -23
  98. package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +194 -15
  99. package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
  100. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
  101. package/bundled/skills/recovery-workflow/SKILL.md +62 -241
  102. package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
  103. package/bundled/skills/recovery-workflow/references/detection.md +28 -52
  104. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
  105. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
  106. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
  107. package/bundled/skills/refactor-planner/SKILL.md +12 -57
  108. package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
  109. package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
  110. package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
  111. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
  112. package/bundled/skills/refactor-workflow/SKILL.md +98 -139
  113. package/bundled/templates/project-memory-template.md +4 -4
  114. package/package.json +1 -1
  115. package/src/index.js +4 -0
  116. package/src/prompts.js +10 -3
  117. package/src/scaffold.js +1 -1
  118. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
  119. package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
@@ -3,6 +3,7 @@
3
3
  import json
4
4
  import os
5
5
  import shutil
6
+ import signal
6
7
  import subprocess
7
8
  import sys
8
9
  from pathlib import Path
@@ -90,6 +91,93 @@ def _item_id_from_next_result(kind, result):
90
91
  return result.data[key]
91
92
 
92
93
 
94
+ def _write_semantic_completion_checkpoint(project_root, artifact_dir, workflow_type="feature-pipeline"):
95
+ validator = (
96
+ project_root / ".claude" / "command-assets" / "prizmkit-test"
97
+ / "scripts" / "validate_test_evidence.py"
98
+ )
99
+ validator.parent.mkdir(parents=True, exist_ok=True)
100
+ validator.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
101
+ contracts = {
102
+ "feature-pipeline": (
103
+ "feature_state",
104
+ [
105
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
106
+ "prizmkit-retrospective", "prizmkit-committer", "completion-summary",
107
+ ],
108
+ ),
109
+ "bugfix-pipeline": (
110
+ "bugfix_state",
111
+ [
112
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
113
+ "prizmkit-retrospective", "prizmkit-committer", "bug-report",
114
+ ],
115
+ ),
116
+ "refactor-pipeline": (
117
+ "refactor_state",
118
+ [
119
+ "prizmkit-implement", "prizmkit-code-review", "prizmkit-test",
120
+ "prizmkit-retrospective", "prizmkit-committer", "refactor-report",
121
+ "completion-summary",
122
+ ],
123
+ ),
124
+ }
125
+ state_key, skills = contracts[workflow_type]
126
+ evidence_id = "semantic-evidence"
127
+ evidence_dir = project_root / ".prizmkit" / "test" / "evidence" / evidence_id
128
+ evidence_dir.mkdir(parents=True, exist_ok=True)
129
+ evidence = {
130
+ "manifest": evidence_dir / "manifest.json",
131
+ "verdict": evidence_dir / "verdict.json",
132
+ "validation": evidence_dir / "validation.json",
133
+ }
134
+ evidence["manifest"].write_text(
135
+ json.dumps({"evidence_id": evidence_id, "final_verdict": "TEST_PASS"}),
136
+ encoding="utf-8",
137
+ )
138
+ evidence["verdict"].write_text(json.dumps({"verdict": "TEST_PASS"}), encoding="utf-8")
139
+ evidence["validation"].write_text(
140
+ json.dumps({"verdict": "TEST_PASS", "result": "passed"}), encoding="utf-8"
141
+ )
142
+ relative_evidence = {
143
+ key: str(path.relative_to(project_root)) for key, path in evidence.items()
144
+ }
145
+ steps = []
146
+ previous = None
147
+ for index, skill in enumerate(skills, start=1):
148
+ step = {
149
+ "id": f"S{index:02d}",
150
+ "skill": skill,
151
+ "name": skill,
152
+ "status": "completed",
153
+ "required_artifacts": [],
154
+ "depends_on": previous,
155
+ }
156
+ if skill == "prizmkit-code-review":
157
+ step["stage_result"] = "REVIEW_PASS"
158
+ if skill == "prizmkit-test":
159
+ step["stage_result"] = "TEST_PASS"
160
+ step["authoritative_evidence_paths"] = relative_evidence
161
+ steps.append(step)
162
+ previous = step["id"]
163
+ artifact_dir.mkdir(parents=True, exist_ok=True)
164
+ checkpoint = artifact_dir / "workflow-checkpoint.json"
165
+ checkpoint.write_text(
166
+ json.dumps({
167
+ "version": 1,
168
+ "workflow_type": workflow_type,
169
+ "steps": steps,
170
+ state_key: {
171
+ "repair_round": 0,
172
+ "authoritative_evidence_paths": relative_evidence,
173
+ "terminal_status": "WORKFLOW_COMPLETED",
174
+ },
175
+ }),
176
+ encoding="utf-8",
177
+ )
178
+ return checkpoint
179
+
180
+
93
181
  def test_retired_shell_runtime_files_are_absent_from_source():
94
182
  retired = [
95
183
  "run-feature.sh",
@@ -346,6 +434,7 @@ def test_runner_models_parse_legacy_args_and_environment(tmp_path):
346
434
  assert env.stale_kill_threshold_seconds == 9
347
435
  assert env.max_retries == 9
348
436
  assert env.strict_behavior_check is False
437
+ assert not hasattr(env, "enable_deploy")
349
438
  assert not hasattr(env, "max_" + "log_size")
350
439
 
351
440
 
@@ -1443,8 +1532,7 @@ def test_has_branch_completion_evidence_requires_task_branch_ahead_of_base(tmp_p
1443
1532
  subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
1444
1533
  subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
1445
1534
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
1446
- artifact_dir.mkdir(parents=True)
1447
- (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
1535
+ _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
1448
1536
  (tmp_path / "base.txt").write_text("base", encoding="utf-8")
1449
1537
  subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
1450
1538
  subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
@@ -1505,11 +1593,7 @@ def test_no_worktree_recovers_completed_pending_branch_before_new_session(monkey
1505
1593
  session_dir = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-old"
1506
1594
  session_dir.mkdir(parents=True)
1507
1595
  artifact_dir = paths.project_root / ".prizmkit" / "specs" / "001-low"
1508
- artifact_dir.mkdir(parents=True)
1509
- (artifact_dir / "workflow-checkpoint.json").write_text(
1510
- json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}),
1511
- encoding="utf-8",
1512
- )
1596
+ _write_semantic_completion_checkpoint(paths.project_root, artifact_dir)
1513
1597
  updates = []
1514
1598
  events = []
1515
1599
 
@@ -1876,26 +1960,29 @@ def test_completed_checkout_blocks_rerun_until_explicit_reset(monkeypatch, tmp_p
1876
1960
  runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
1877
1961
 
1878
1962
 
1879
- def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_path):
1963
+ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_path, capsys):
1880
1964
  from prizmkit_runtime import runners
1965
+ from prizmkit_runtime.gitops import current_branch as real_current_branch
1966
+ from prizmkit_runtime.gitops import run_git_command as real_run_git_command
1881
1967
  from prizmkit_runtime.runner_models import family_for, parse_invocation
1882
1968
  from prizmkit_runtime.task_checkout import load_task_checkout
1883
1969
 
1884
1970
  monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
1885
1971
  paths = _make_paths(tmp_path)
1972
+ _init_bookkeeping_repo(paths.project_root)
1886
1973
  family = family_for("feature", paths)
1887
1974
  invocation = parse_invocation(family, "run", ())
1888
1975
  invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
1889
1976
  _install_runner_session_fakes(monkeypatch, runners)
1890
- ensure_calls = []
1891
- monkeypatch.setattr(runners, "run_git_plan", lambda project_root, plan: SimpleNamespace(ok=True, plan=plan, results=()))
1892
- monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, branch: ensure_calls.append((base, branch)))
1977
+ monkeypatch.setattr(runners, "current_branch", real_current_branch)
1978
+ monkeypatch.setattr(runners, "run_git_command", real_run_git_command)
1893
1979
 
1894
1980
  class InterruptingLauncher:
1895
1981
  def __init__(self, config):
1896
1982
  self.config = config
1897
1983
 
1898
1984
  def run(self):
1985
+ (self.config.cwd / "interrupted.txt").write_text("preserved\n", encoding="utf-8")
1899
1986
  raise KeyboardInterrupt
1900
1987
 
1901
1988
  monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
@@ -1904,11 +1991,158 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
1904
1991
  runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
1905
1992
 
1906
1993
  checkout = load_task_checkout(family.state_dir, family.task_type, "F-001")
1994
+ captured = capsys.readouterr()
1907
1995
  assert checkout is not None
1908
1996
  assert checkout.is_active
1909
1997
  assert checkout.active_dev_branch == "dev/F-001-interrupt"
1910
1998
  assert checkout.base_branch == "main"
1911
- assert ensure_calls == [("main", "dev/F-001-interrupt")]
1999
+ assert real_current_branch(paths.project_root) == "main"
2000
+ assert subprocess.run(
2001
+ ["git", "show", "dev/F-001-interrupt:interrupted.txt"],
2002
+ cwd=paths.project_root,
2003
+ check=True,
2004
+ stdout=subprocess.PIPE,
2005
+ text=True,
2006
+ ).stdout == "preserved\n"
2007
+ assert "Interrupted task F-001: work preserved and returned to main" in captured.err
2008
+
2009
+
2010
+ def test_interrupt_return_failure_is_reported_without_forcing_checkout(monkeypatch, tmp_path, capsys):
2011
+ from prizmkit_runtime import runners
2012
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
2013
+
2014
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
2015
+ paths = _make_paths(tmp_path)
2016
+ family = family_for("feature", paths)
2017
+ invocation = parse_invocation(family, "run", ())
2018
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
2019
+ _install_runner_session_fakes(monkeypatch, runners)
2020
+ monkeypatch.setattr(runners, "run_git_plan", lambda project_root, plan: SimpleNamespace(ok=True, plan=plan, results=()))
2021
+ failure = SimpleNamespace(
2022
+ ok=False,
2023
+ plan=SimpleNamespace(
2024
+ commands=(
2025
+ SimpleNamespace(allow_failure=True),
2026
+ SimpleNamespace(allow_failure=False),
2027
+ )
2028
+ ),
2029
+ results=(
2030
+ SimpleNamespace(
2031
+ args=("commit", "--no-verify", "-m", "wip"),
2032
+ return_code=1,
2033
+ stdout="nothing to commit",
2034
+ stderr="",
2035
+ ),
2036
+ SimpleNamespace(
2037
+ args=("checkout", "main"),
2038
+ return_code=1,
2039
+ stdout="",
2040
+ stderr="untracked files would be overwritten",
2041
+ ),
2042
+ ),
2043
+ )
2044
+ monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, branch: failure)
2045
+
2046
+ class InterruptingLauncher:
2047
+ def __init__(self, config):
2048
+ self.config = config
2049
+
2050
+ def run(self):
2051
+ raise KeyboardInterrupt
2052
+
2053
+ monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
2054
+
2055
+ with pytest.raises(KeyboardInterrupt):
2056
+ runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
2057
+
2058
+ captured = capsys.readouterr()
2059
+ assert "Could not safely return to main; leaving dev/F-001-interrupt active" in captured.err
2060
+ assert "git checkout main failed: untracked files would be overwritten" in captured.err
2061
+ assert "After resolving the Git blocker, run: git checkout main" in captured.err
2062
+
2063
+
2064
+ def test_worktree_interrupt_preserves_task_worktree_without_returning_caller_checkout(monkeypatch, tmp_path, capsys):
2065
+ from prizmkit_runtime import runners
2066
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
2067
+
2068
+ monkeypatch.setenv("USE_WORKTREE", "1")
2069
+ monkeypatch.setenv("DEV_BRANCH", "dev/F-001-interrupt")
2070
+ paths = _make_paths(tmp_path)
2071
+ family = family_for("feature", paths)
2072
+ invocation = parse_invocation(family, "run", ())
2073
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
2074
+ observed = _install_runner_session_fakes(monkeypatch, runners)
2075
+ monkeypatch.setattr(
2076
+ runners,
2077
+ "ensure_linked_worktree",
2078
+ lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""),
2079
+ )
2080
+ return_calls = []
2081
+ monkeypatch.setattr(
2082
+ runners,
2083
+ "branch_ensure_return",
2084
+ lambda *args: return_calls.append(args),
2085
+ )
2086
+
2087
+ class InterruptingLauncher:
2088
+ def __init__(self, config):
2089
+ self.config = config
2090
+
2091
+ def run(self):
2092
+ raise KeyboardInterrupt
2093
+
2094
+ monkeypatch.setattr(runners, "AISessionLauncher", InterruptingLauncher)
2095
+
2096
+ with pytest.raises(KeyboardInterrupt):
2097
+ runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
2098
+
2099
+ captured = capsys.readouterr()
2100
+ assert observed.wip_calls
2101
+ assert return_calls == []
2102
+ assert "Interrupted task F-001: work preserved in" in captured.err
2103
+ assert "task worktree remains active" in captured.err
2104
+
2105
+
2106
+ def test_run_pipeline_command_bridges_sigterm_and_restores_handler(monkeypatch, tmp_path):
2107
+ from prizmkit_runtime import runners
2108
+ from prizmkit_runtime.runner_models import PipelineRunResult
2109
+
2110
+ paths = _make_paths(tmp_path)
2111
+ paths.feature_plan.write_text("{}", encoding="utf-8")
2112
+ previous = signal.getsignal(signal.SIGTERM)
2113
+ observed = []
2114
+
2115
+ def interrupted_pipeline(*args, **kwargs):
2116
+ installed = signal.getsignal(signal.SIGTERM)
2117
+ observed.append(installed)
2118
+ try:
2119
+ installed(signal.SIGTERM, None)
2120
+ except KeyboardInterrupt:
2121
+ return PipelineRunResult(False, 1, "interrupted", "interrupted")
2122
+ raise AssertionError("SIGTERM handler did not interrupt the pipeline")
2123
+
2124
+ monkeypatch.setattr(runners, "_run_pipeline", interrupted_pipeline)
2125
+
2126
+ result = runners.run_pipeline_command("feature", "run", (), paths)
2127
+
2128
+ assert result.exit_code == 1
2129
+ assert result.message == "processed=1 completed=False stopped_reason=interrupted"
2130
+ assert observed and observed[0] not in {previous, signal.SIG_DFL, signal.SIG_IGN}
2131
+ assert signal.getsignal(signal.SIGTERM) is previous
2132
+
2133
+
2134
+ def test_interruption_cleanup_shield_ignores_and_restores_signals():
2135
+ from prizmkit_runtime.processes import shield_interruption_signals
2136
+
2137
+ previous_sigint = signal.getsignal(signal.SIGINT)
2138
+ previous_sigterm = signal.getsignal(signal.SIGTERM)
2139
+
2140
+ with shield_interruption_signals():
2141
+ assert signal.getsignal(signal.SIGINT) is signal.SIG_IGN
2142
+ assert signal.getsignal(signal.SIGTERM) is signal.SIG_IGN
2143
+
2144
+ assert signal.getsignal(signal.SIGINT) is previous_sigint
2145
+ assert signal.getsignal(signal.SIGTERM) is previous_sigterm
1912
2146
 
1913
2147
 
1914
2148
  @pytest.mark.parametrize(
@@ -2555,9 +2789,7 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
2555
2789
  subprocess.run(["git", "add", "done.txt"], cwd=tmp_path, check=True)
2556
2790
  subprocess.run(["git", "commit", "-m", "done"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2557
2791
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
2558
- artifact_dir.mkdir(parents=True)
2559
- checkpoint = artifact_dir / "workflow-checkpoint.json"
2560
- checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
2792
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
2561
2793
  log = tmp_path / "session.log"
2562
2794
  log.write_text("model quota exceeded; API error after commit", encoding="utf-8")
2563
2795
  raw = AISessionResult(
@@ -2579,10 +2811,10 @@ def test_classification_accepts_late_infra_error_with_completed_checkpoint_and_c
2579
2811
  classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
2580
2812
 
2581
2813
  assert classification.session_status == "success"
2582
- assert classification.reason == "completed_checkpoint_with_late_runtime_error"
2814
+ assert classification.reason == "semantic_checkpoint_with_late_runtime_error"
2583
2815
 
2584
2816
 
2585
- def test_classification_accepts_late_infra_with_completion_artifact_without_new_commit(tmp_path):
2817
+ def test_classification_rejects_late_infra_with_completion_summary_only(tmp_path):
2586
2818
  from prizmkit_runtime.runner_classification import classify_session
2587
2819
  from prizmkit_runtime.runner_models import PromptGenerationResult
2588
2820
  from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
@@ -2627,8 +2859,8 @@ def test_classification_accepts_late_infra_with_completion_artifact_without_new_
2627
2859
 
2628
2860
  classification = classify_session(raw, project_root=tmp_path, base_head=side_head, prompt=prompt)
2629
2861
 
2630
- assert classification.session_status == "success"
2631
- assert classification.reason == "completed_checkpoint_with_late_runtime_error"
2862
+ assert classification.session_status == "infra_error"
2863
+ assert classification.reason == "ai_runtime_or_provider_error"
2632
2864
 
2633
2865
 
2634
2866
  def test_classification_keeps_pre_completion_provider_error_as_infra_error(tmp_path):
@@ -2681,9 +2913,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
2681
2913
  subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
2682
2914
  base_head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, stdout=subprocess.PIPE, text=True).stdout.strip()
2683
2915
  artifact_dir = tmp_path / ".prizmkit" / "specs" / "001-done"
2684
- artifact_dir.mkdir(parents=True)
2685
- checkpoint = artifact_dir / "workflow-checkpoint.json"
2686
- checkpoint.write_text(json.dumps({"steps": [{"id": "S01", "skill": "done", "name": "Done", "status": "completed"}]}), encoding="utf-8")
2916
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact_dir)
2687
2917
  (artifact_dir / "completion-summary.json").write_text(json.dumps({"completion_notes": ["Completed durable workflow"]}), encoding="utf-8")
2688
2918
  log = tmp_path / "session.log"
2689
2919
  log.write_text("terminal success", encoding="utf-8")
@@ -2709,7 +2939,7 @@ def test_classification_accepts_terminal_success_with_completed_checkpoint_witho
2709
2939
 
2710
2940
  classification = classify_session(raw, project_root=tmp_path, base_head=base_head, prompt=prompt)
2711
2941
  assert classification.session_status == "success"
2712
- assert classification.reason == "terminal_success_with_completed_checkpoint"
2942
+ assert classification.reason == "terminal_success_with_semantic_checkpoint"
2713
2943
 
2714
2944
 
2715
2945
  def test_classification_preserves_success_before_context_overflow(tmp_path):
@@ -3475,3 +3705,317 @@ def test_timeout_and_stale_termination_override_provider_error_markers(tmp_path,
3475
3705
 
3476
3706
  assert classification.session_status == expected_status
3477
3707
  assert classification.reason == expected_reason
3708
+
3709
+
3710
+
3711
+ @pytest.mark.parametrize("workflow_type", ["feature-pipeline", "bugfix-pipeline", "refactor-pipeline"])
3712
+ def test_semantic_completion_classification_has_cross_family_parity(tmp_path, workflow_type):
3713
+ from prizmkit_runtime.runner_classification import classify_session
3714
+ from prizmkit_runtime.runner_models import PromptGenerationResult
3715
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
3716
+ from prizmkit_runtime.status import ProgressSummary
3717
+
3718
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3719
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
3720
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
3721
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
3722
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
3723
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3724
+ artifact = tmp_path / ".prizmkit" / "artifacts" / workflow_type
3725
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact, workflow_type)
3726
+ log = tmp_path / f"{workflow_type}.log"
3727
+ log.write_text("success", encoding="utf-8")
3728
+ raw = AISessionResult(
3729
+ command=AISessionCommand(("fake",), None, "claude", "stdin"),
3730
+ exit_code=0,
3731
+ timed_out=False,
3732
+ termination_reason=None,
3733
+ stale_marker=None,
3734
+ fatal_error_code="",
3735
+ progress_summary=ProgressSummary(result_subtype="success"),
3736
+ session_log=log,
3737
+ backup_log=tmp_path / "backup.log",
3738
+ log_recovery=None,
3739
+ started_epoch=0,
3740
+ ended_epoch=1,
3741
+ )
3742
+
3743
+ classification = classify_session(
3744
+ raw,
3745
+ project_root=tmp_path,
3746
+ base_head="",
3747
+ prompt=PromptGenerationResult(
3748
+ output_path=tmp_path / "prompt.md",
3749
+ checkpoint_path=checkpoint,
3750
+ artifact_path=artifact,
3751
+ ),
3752
+ )
3753
+
3754
+ assert classification.session_status == "success"
3755
+ assert classification.checkpoint_state["terminal_status"] == "WORKFLOW_COMPLETED"
3756
+
3757
+
3758
+ def test_semantic_blocked_classification_is_terminal_and_retry_neutral(tmp_path):
3759
+ from prizmkit_runtime.runner_classification import classify_session
3760
+ from prizmkit_runtime.runner_models import PromptGenerationResult
3761
+ from prizmkit_runtime.sessions import AISessionCommand, AISessionResult
3762
+ from prizmkit_runtime.status import ProgressSummary
3763
+
3764
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3765
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
3766
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
3767
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
3768
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
3769
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3770
+ artifact = tmp_path / ".prizmkit" / "specs" / "blocked"
3771
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
3772
+ data = json.loads(checkpoint.read_text(encoding="utf-8"))
3773
+ data["feature_state"].update(
3774
+ terminal_status="WORKFLOW_BLOCKED",
3775
+ blocked_reason="repair_round_limit",
3776
+ repair_round=3,
3777
+ )
3778
+ checkpoint.write_text(json.dumps(data), encoding="utf-8")
3779
+ log = tmp_path / "blocked.log"
3780
+ log.write_text("ambiguous failure", encoding="utf-8")
3781
+ raw = AISessionResult(
3782
+ command=AISessionCommand(("fake",), None, "codebuddy", "stdin"),
3783
+ exit_code=1,
3784
+ timed_out=False,
3785
+ termination_reason=None,
3786
+ stale_marker=None,
3787
+ fatal_error_code="",
3788
+ progress_summary=ProgressSummary(terminal_result_text="ambiguous failure"),
3789
+ session_log=log,
3790
+ backup_log=tmp_path / "backup.log",
3791
+ log_recovery=None,
3792
+ started_epoch=0,
3793
+ ended_epoch=1,
3794
+ )
3795
+
3796
+ classification = classify_session(
3797
+ raw,
3798
+ project_root=tmp_path,
3799
+ base_head="",
3800
+ prompt=PromptGenerationResult(
3801
+ output_path=tmp_path / "prompt.md",
3802
+ checkpoint_path=checkpoint,
3803
+ artifact_path=artifact,
3804
+ ),
3805
+ )
3806
+
3807
+ assert classification.session_status == "workflow_blocked"
3808
+ assert classification.reason == "repair_round_limit"
3809
+ assert classification.checkpoint_state["repair_round"] == 3
3810
+
3811
+
3812
+ def test_recovery_outcome_preserves_dirty_partial_work_and_uses_semantic_checkpoint(tmp_path):
3813
+ from prizmkit_runtime.runner_recovery import _recovery_outcome
3814
+
3815
+ subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3816
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True)
3817
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True)
3818
+ (tmp_path / "base.txt").write_text("base", encoding="utf-8")
3819
+ subprocess.run(["git", "add", "base.txt"], cwd=tmp_path, check=True)
3820
+ subprocess.run(["git", "commit", "-m", "base"], cwd=tmp_path, check=True, stdout=subprocess.DEVNULL)
3821
+ (tmp_path / "partial.txt").write_text("preserve me", encoding="utf-8")
3822
+ artifact = tmp_path / ".prizmkit" / "specs" / "done"
3823
+ checkpoint = _write_semantic_completion_checkpoint(tmp_path, artifact)
3824
+
3825
+ outcome, semantic = _recovery_outcome(tmp_path, 1, checkpoint_path=checkpoint)
3826
+
3827
+ assert outcome == "success"
3828
+ assert semantic["terminal_status"] == "WORKFLOW_COMPLETED"
3829
+ assert (tmp_path / "partial.txt").read_text(encoding="utf-8") == "preserve me"
3830
+ assert subprocess.run(
3831
+ ["git", "status", "--porcelain", "--", "partial.txt"],
3832
+ cwd=tmp_path,
3833
+ check=True,
3834
+ stdout=subprocess.PIPE,
3835
+ text=True,
3836
+ ).stdout.startswith("??")
3837
+
3838
+
3839
+ def test_runtime_semantic_paths_do_not_invoke_composite_workflow():
3840
+ sources = [
3841
+ PIPELINE_ROOT / "prizmkit_runtime" / "checkpoint_state.py",
3842
+ PIPELINE_ROOT / "prizmkit_runtime" / "runner_classification.py",
3843
+ PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py",
3844
+ PIPELINE_ROOT / "prizmkit_runtime" / "runners.py",
3845
+ ]
3846
+
3847
+ for source in sources:
3848
+ assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
3849
+
3850
+
3851
+
3852
+ @pytest.mark.parametrize(
3853
+ ("kind", "item_id", "blocked_status"),
3854
+ [("feature", "F-001", "failed"), ("bugfix", "B-001", "needs_info"), ("refactor", "R-001", "failed")],
3855
+ )
3856
+ def test_family_status_persists_identical_semantic_blocked_snapshot_without_retry_spend(
3857
+ tmp_path, kind, item_id, blocked_status
3858
+ ):
3859
+ from prizmkit_runtime.runner_models import family_for, parse_invocation
3860
+ from prizmkit_runtime.runner_status import start_item, update_item
3861
+
3862
+ paths = _make_paths(tmp_path)
3863
+ item = {
3864
+ "id": item_id,
3865
+ "title": "Semantic blocked",
3866
+ "status": "pending",
3867
+ "priority": "high",
3868
+ "dependencies": [],
3869
+ }
3870
+ if kind == "bugfix":
3871
+ item.update(severity="high", description="blocked", reproduction_steps=["run"])
3872
+ elif kind == "refactor":
3873
+ item.update(description="blocked", targets=["src"], goals=["preserve"])
3874
+ _write_family_plan(paths, kind, [item])
3875
+ family = family_for(kind, paths)
3876
+ invocation = parse_invocation(family, "run", ())
3877
+ invocation = invocation.__class__(**{**invocation.__dict__, "list_path": family.plan_path})
3878
+ assert start_item(family, invocation, item_id, paths.project_root).ok is True
3879
+ semantic = {
3880
+ "current_stage": None,
3881
+ "next_stage": None,
3882
+ "stage_result": "TEST_BLOCKED",
3883
+ "repair_scope": "production",
3884
+ "repair_round": 2,
3885
+ "authoritative_evidence_paths": {
3886
+ "manifest": ".prizmkit/test/evidence/id/manifest.json",
3887
+ "verdict": ".prizmkit/test/evidence/id/verdict.json",
3888
+ "validation": ".prizmkit/test/evidence/id/validation.json",
3889
+ },
3890
+ "blocked_reason": "test_blocked",
3891
+ "terminal_status": "WORKFLOW_BLOCKED",
3892
+ }
3893
+
3894
+ result = update_item(
3895
+ family,
3896
+ invocation,
3897
+ item_id,
3898
+ "workflow_blocked",
3899
+ "session-1",
3900
+ paths.project_root,
3901
+ checkpoint_state=semantic,
3902
+ )
3903
+ status = json.loads((family.state_dir / item_id / "status.json").read_text(encoding="utf-8"))
3904
+
3905
+ assert result.ok is True
3906
+ assert result.data["new_status"] == blocked_status
3907
+ assert result.data["checkpoint_state"] == semantic
3908
+ assert result.data["restart_policy"] == "terminal_blocked"
3909
+ assert status["retry_count"] == 0
3910
+ assert status["infra_error_count"] == 0
3911
+ for key, value in semantic.items():
3912
+ assert status[key] == value
3913
+
3914
+
3915
+
3916
+ def test_recovery_detector_returns_no_candidate_for_empty_project(tmp_path):
3917
+ import importlib.util
3918
+
3919
+ detector_path = (
3920
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3921
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3922
+ )
3923
+ spec = importlib.util.spec_from_file_location("recovery_detector_empty", detector_path)
3924
+ module = importlib.util.module_from_spec(spec)
3925
+ spec.loader.exec_module(module)
3926
+
3927
+ report = module.build_report(tmp_path)
3928
+
3929
+ assert report["detected"] is False
3930
+ assert report["candidates"] == []
3931
+
3932
+
3933
+ def test_recovery_detector_returns_active_checkout_command(tmp_path, monkeypatch):
3934
+ import importlib.util
3935
+
3936
+ detector_path = (
3937
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3938
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3939
+ )
3940
+ spec = importlib.util.spec_from_file_location("recovery_detector_active", detector_path)
3941
+ module = importlib.util.module_from_spec(spec)
3942
+ spec.loader.exec_module(module)
3943
+
3944
+ checkout_path = tmp_path / ".prizmkit" / "state" / "features" / "F-001" / "checkout.json"
3945
+ checkout_path.parent.mkdir(parents=True)
3946
+ checkout_path.write_text(
3947
+ json.dumps({
3948
+ "task_type": "feature",
3949
+ "task_id": "F-001",
3950
+ "state": "active",
3951
+ "active_dev_branch": "dev/F-001-resume",
3952
+ "base_branch": "main",
3953
+ "checkout_mode": "branch",
3954
+ }),
3955
+ encoding="utf-8",
3956
+ )
3957
+ monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
3958
+
3959
+ report = module.build_report(tmp_path)
3960
+
3961
+ assert report["detected"] is True
3962
+ assert report["selection_required"] is False
3963
+ assert report["candidates"][0]["continue_command"] == (
3964
+ "python3 ./.prizmkit/dev-pipeline/cli.py feature run F-001"
3965
+ )
3966
+
3967
+
3968
+ def test_recovery_detector_ignores_completed_checkout(tmp_path, monkeypatch):
3969
+ import importlib.util
3970
+
3971
+ detector_path = (
3972
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
3973
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
3974
+ )
3975
+ spec = importlib.util.spec_from_file_location("recovery_detector_completed", detector_path)
3976
+ module = importlib.util.module_from_spec(spec)
3977
+ spec.loader.exec_module(module)
3978
+
3979
+ checkout_path = tmp_path / ".prizmkit" / "state" / "bugfix" / "B-001" / "checkout.json"
3980
+ checkout_path.parent.mkdir(parents=True)
3981
+ checkout_path.write_text(
3982
+ json.dumps({
3983
+ "task_type": "bugfix",
3984
+ "task_id": "B-001",
3985
+ "state": "completed",
3986
+ "active_dev_branch": "bugfix/B-001-done",
3987
+ "base_branch": "main",
3988
+ }),
3989
+ encoding="utf-8",
3990
+ )
3991
+ monkeypatch.setattr(module, "branch_exists", lambda *_args: True)
3992
+
3993
+ report = module.build_report(tmp_path)
3994
+
3995
+ assert report["detected"] is False
3996
+
3997
+
3998
+ def test_l4_runtime_and_recovery_sources_do_not_depend_on_direct_l1_composite():
3999
+ runtime_root = REPO_ROOT / "dev-pipeline"
4000
+ source_files = list(runtime_root.rglob("*.py"))
4001
+ for source in source_files:
4002
+ if "__pycache__" in source.parts or "tests" in source.parts or not source.is_file():
4003
+ continue
4004
+ assert "prizmkit-workflow" not in source.read_text(encoding="utf-8")
4005
+
4006
+
4007
+ def test_recovery_detector_has_no_direct_lifecycle_fallback(tmp_path):
4008
+ import importlib.util
4009
+
4010
+ detector_path = (
4011
+ REPO_ROOT / "core" / "skills" / "orchestration-skill" / "workflows"
4012
+ / "recovery-workflow" / "scripts" / "detect-recovery-state.py"
4013
+ )
4014
+ spec = importlib.util.spec_from_file_location("recovery_detector_contract", detector_path)
4015
+ module = importlib.util.module_from_spec(spec)
4016
+ spec.loader.exec_module(module)
4017
+
4018
+ report = module.build_report(tmp_path)
4019
+
4020
+ assert "continue_command" not in report
4021
+ assert report["detected"] is False