prizmkit 1.1.153 → 1.1.155

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 (178) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codex/skill-adapter.js +1 -1
  3. package/bundled/adapters/pi/skill-adapter.js +2 -1
  4. package/bundled/dev-pipeline/.env.example +2 -6
  5. package/bundled/dev-pipeline/README.md +120 -113
  6. package/bundled/dev-pipeline/assets/skill-subagent-integration.md +1 -1
  7. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +355 -15
  8. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +189 -110
  9. package/bundled/dev-pipeline/prizmkit_runtime/commands.py +146 -111
  10. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +4 -20
  11. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +48 -14
  12. package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +0 -1
  13. package/bundled/dev-pipeline/prizmkit_runtime/paths.py +0 -3
  14. package/bundled/dev-pipeline/prizmkit_runtime/reset.py +122 -15
  15. package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +1 -1
  16. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +50 -2
  17. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +1 -10
  18. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +3 -3
  19. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +139 -74
  20. package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +482 -0
  21. package/bundled/dev-pipeline/prizmkit_runtime/status.py +106 -1
  22. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +64 -11
  23. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +145 -367
  24. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +91 -62
  25. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +105 -69
  26. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +2 -2
  27. package/bundled/dev-pipeline/scripts/prompt_framework.py +36 -41
  28. package/bundled/dev-pipeline/scripts/update-bug-status.py +197 -71
  29. package/bundled/dev-pipeline/scripts/update-checkpoint.py +77 -16
  30. package/bundled/dev-pipeline/scripts/update-feature-status.py +46 -121
  31. package/bundled/dev-pipeline/scripts/update-refactor-status.py +46 -123
  32. package/bundled/dev-pipeline/scripts/utils.py +119 -0
  33. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +22 -27
  34. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +71 -104
  35. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +3 -2
  36. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +59 -111
  37. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +37 -72
  38. package/bundled/dev-pipeline/templates/sections/artifact-manifest.md +39 -0
  39. package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +3 -1
  40. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +8 -25
  41. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +3 -3
  42. package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +2 -2
  43. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
  44. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +2 -2
  45. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +4 -4
  46. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +0 -1
  47. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +7 -4
  48. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +14 -27
  49. package/bundled/dev-pipeline/templates/sections/feature-completion-summary.md +20 -0
  50. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -71
  51. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -7
  52. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +3 -3
  53. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
  54. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +5 -17
  55. package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +3 -1
  56. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +10 -23
  57. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
  58. package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +2 -2
  59. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
  60. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +2 -2
  61. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +7 -7
  62. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +0 -1
  63. package/bundled/dev-pipeline/templates/sections/retrospective.md +12 -0
  64. package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +35 -0
  65. package/bundled/dev-pipeline/templates/session-status-schema.json +45 -62
  66. package/bundled/dev-pipeline/tests/test_auto_skip.py +111 -32
  67. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +617 -30
  68. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +262 -194
  69. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +62 -23
  70. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +76 -28
  71. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +281 -206
  72. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +322 -0
  73. package/bundled/dev-pipeline/tests/test_reset_modes.py +935 -0
  74. package/bundled/dev-pipeline/tests/test_reset_preserve.py +5 -7
  75. package/bundled/dev-pipeline/tests/test_runtime_commit.py +608 -0
  76. package/bundled/dev-pipeline/tests/test_unified_cli.py +617 -188
  77. package/bundled/skills/_metadata.json +9 -9
  78. package/bundled/skills/app-planner/SKILL.md +3 -3
  79. package/bundled/skills/app-planner/references/generated-plan-review.md +2 -1
  80. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +56 -64
  81. package/bundled/skills/app-planner/references/rules/backend/fixed-rules.md +85 -240
  82. package/bundled/skills/app-planner/references/rules/backend/question-bank.md +25 -25
  83. package/bundled/skills/app-planner/references/rules/backend/question-manifest.json +16 -7
  84. package/bundled/skills/app-planner/references/rules/backend/template.md +74 -92
  85. package/bundled/skills/app-planner/references/rules/database/derivation-rules.md +93 -93
  86. package/bundled/skills/app-planner/references/rules/database/fixed-rules.md +66 -173
  87. package/bundled/skills/app-planner/references/rules/database/question-bank.md +31 -26
  88. package/bundled/skills/app-planner/references/rules/database/question-manifest.json +13 -4
  89. package/bundled/skills/app-planner/references/rules/database/template.md +76 -98
  90. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +68 -95
  91. package/bundled/skills/app-planner/references/rules/frontend/fixed-rules.md +93 -166
  92. package/bundled/skills/app-planner/references/rules/frontend/question-bank.md +46 -36
  93. package/bundled/skills/app-planner/references/rules/frontend/question-manifest.json +11 -2
  94. package/bundled/skills/app-planner/references/rules/frontend/template.md +79 -257
  95. package/bundled/skills/app-planner/references/rules/mobile/derivation-rules.md +91 -99
  96. package/bundled/skills/app-planner/references/rules/mobile/fixed-rules.md +73 -246
  97. package/bundled/skills/app-planner/references/rules/mobile/question-bank.md +17 -17
  98. package/bundled/skills/app-planner/references/rules/mobile/question-manifest.json +17 -2
  99. package/bundled/skills/app-planner/references/rules/mobile/template.md +73 -85
  100. package/bundled/skills/app-planner/references/rules-configuration.md +133 -65
  101. package/bundled/skills/app-planner/scripts/validate-rules-configuration.py +291 -0
  102. package/bundled/skills/bug-planner/references/schema-validation.md +1 -1
  103. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +1 -1
  104. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -13
  105. package/bundled/skills/feature-pipeline-launcher/SKILL.md +22 -14
  106. package/bundled/skills/prizmkit/SKILL.md +17 -18
  107. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +22 -18
  108. package/bundled/skills/prizmkit-code-review/SKILL.md +22 -58
  109. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +6 -6
  110. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +2 -2
  111. package/bundled/skills/prizmkit-committer/SKILL.md +82 -132
  112. package/bundled/skills/prizmkit-deploy/SKILL.md +27 -11
  113. package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +89 -0
  114. package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +44 -14
  115. package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +19 -3
  116. package/bundled/skills/prizmkit-deploy/references/database-setup.md +22 -12
  117. package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +3 -1
  118. package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +22 -6
  119. package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +49 -0
  120. package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +1 -1
  121. package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +164 -0
  122. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +9 -8
  123. package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +6 -6
  124. package/bundled/skills/prizmkit-implement/SKILL.md +30 -66
  125. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +4 -4
  126. package/bundled/skills/prizmkit-init/SKILL.md +4 -0
  127. package/bundled/skills/prizmkit-plan/SKILL.md +21 -64
  128. package/bundled/skills/prizmkit-plan/references/artifact-identity.md +38 -0
  129. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +4 -4
  130. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +5 -5
  131. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +11 -1
  132. package/bundled/skills/prizmkit-retrospective/SKILL.md +63 -77
  133. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -3
  134. package/bundled/skills/prizmkit-retrospective/references/retrospective-result-schema.json +138 -0
  135. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +17 -23
  136. package/bundled/skills/prizmkit-test/SKILL.md +64 -51
  137. package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +206 -0
  138. package/bundled/skills/prizmkit-test/references/independent-test-review.md +17 -9
  139. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +45 -21
  140. package/bundled/skills/prizmkit-test/references/test-report-template.md +20 -13
  141. package/bundled/skills/prizmkit-workflow/SKILL.md +35 -37
  142. package/bundled/skills/prizmkit-workflow/references/artifact-identity.md +34 -0
  143. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +22 -18
  144. package/bundled/skills/recovery-workflow/SKILL.md +9 -6
  145. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  146. package/bundled/skills/recovery-workflow/references/detection.md +7 -6
  147. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +16 -6
  148. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -13
  149. package/bundled/templates/project-memory-template.md +19 -11
  150. package/package.json +1 -1
  151. package/src/scaffold.js +33 -22
  152. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +0 -228
  153. package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +0 -767
  154. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +0 -16
  155. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +0 -31
  156. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -7
  157. package/bundled/dev-pipeline/templates/sections/critical-paths-lite.md +0 -7
  158. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +0 -9
  159. package/bundled/dev-pipeline/templates/sections/directory-convention-lite.md +0 -6
  160. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +0 -15
  161. package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -61
  162. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -24
  163. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -12
  164. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -44
  165. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -25
  166. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +0 -20
  167. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +0 -20
  168. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -20
  169. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +0 -17
  170. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +0 -28
  171. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +0 -52
  172. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +0 -52
  173. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +0 -174
  174. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +0 -174
  175. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +0 -174
  176. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +0 -174
  177. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +0 -174
  178. package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +0 -119
@@ -54,23 +54,16 @@ def _load_progress_parser_module():
54
54
  return module
55
55
 
56
56
 
57
- def test_cli_help_exposes_python_runtime_command_groups():
57
+ def test_cli_help_exposes_only_operation_first_public_namespaces():
58
58
  result = run_cli("--help")
59
59
 
60
60
  assert result.returncode == 0
61
61
  assert "canonical Python runtime CLI" in result.stdout
62
- assert "without shell wrappers" in result.stdout
63
- for group in [
64
- "feature",
65
- "bugfix",
66
- "refactor",
67
- "recovery",
68
- "reset",
69
- "daemon",
70
- "status",
71
- "diagnostics",
72
- ]:
62
+ assert "operation-first" in result.stdout
63
+ for group in ["run", "status", "reset", "daemon", "diagnostics", "helper"]:
73
64
  assert group in result.stdout
65
+ for removed in ["feature", "bugfix", "refactor", "recovery"]:
66
+ assert removed not in result.stdout.lower()
74
67
  assert "Traceback" not in result.stderr
75
68
 
76
69
 
@@ -79,7 +72,7 @@ def test_cli_help_preserves_runtime_identity_at_narrow_terminal_width():
79
72
 
80
73
  assert result.returncode == 0
81
74
  assert "canonical Python runtime CLI" in result.stdout
82
- assert "without shell wrappers" in result.stdout
75
+ assert "operation-first" in result.stdout
83
76
 
84
77
 
85
78
  def test_module_entrypoint_help_exposes_same_cli():
@@ -99,6 +92,45 @@ def test_module_entrypoint_help_exposes_same_cli():
99
92
  assert "diagnostics" in result.stdout
100
93
 
101
94
 
95
+ @pytest.mark.parametrize(
96
+ "args",
97
+ [
98
+ ("--help",),
99
+ ("run", "--help"),
100
+ ("run", "feature", "--help"),
101
+ ("run", "bugfix", "--help"),
102
+ ("run", "refactor", "--help"),
103
+ ("status", "--help"),
104
+ ("status", "feature", "--help"),
105
+ ("status", "all", "--help"),
106
+ ("reset", "--help"),
107
+ ("reset", "feature", "--help"),
108
+ ("daemon", "--help"),
109
+ ("daemon", "feature", "--help"),
110
+ ("daemon", "feature", "start", "--help"),
111
+ ("daemon", "bugfix", "stop", "--help"),
112
+ ("daemon", "refactor", "status", "--help"),
113
+ ("daemon", "feature", "logs", "--help"),
114
+ ("daemon", "feature", "restart", "--help"),
115
+ ("diagnostics", "--help"),
116
+ ("diagnostics", "ai-cli", "--help"),
117
+ ("helper", "--help"),
118
+ ],
119
+ )
120
+ def test_help_at_every_namespace_depth_is_parser_owned_and_never_dispatches(monkeypatch, args):
121
+ from prizmkit_runtime import cli as runtime_cli
122
+
123
+ dispatched = []
124
+ monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
125
+ monkeypatch.setattr(runtime_cli, "_dispatch", lambda parsed: dispatched.append(parsed))
126
+
127
+ with pytest.raises(SystemExit) as exc_info:
128
+ runtime_cli.main(list(args))
129
+
130
+ assert exc_info.value.code == 0
131
+ assert dispatched == []
132
+
133
+
102
134
  def test_invalid_arguments_report_usage_without_traceback():
103
135
  result = run_cli("unknown-command")
104
136
 
@@ -108,6 +140,80 @@ def test_invalid_arguments_report_usage_without_traceback():
108
140
  assert "Traceback" not in result.stderr
109
141
 
110
142
 
143
+ @pytest.mark.parametrize(
144
+ "args",
145
+ [
146
+ ("recovery",),
147
+ ("recovery", "detect"),
148
+ ("recovery", "run"),
149
+ ("recovery", "describe"),
150
+ ("recovery", "run", "--dry-run"),
151
+ ],
152
+ )
153
+ def test_former_recovery_invocations_are_unknown_commands(args):
154
+ result = run_cli(*args)
155
+
156
+ assert result.returncode == 2
157
+ assert "invalid choice: 'recovery'" in result.stderr
158
+ assert "Traceback" not in result.stderr
159
+
160
+
161
+ def test_former_recovery_invocation_never_reaches_dispatch(monkeypatch):
162
+ from prizmkit_runtime import cli as runtime_cli
163
+
164
+ dispatched = []
165
+ monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
166
+ monkeypatch.setattr(runtime_cli, "_dispatch", lambda args: dispatched.append(args))
167
+
168
+ with pytest.raises(SystemExit) as exc_info:
169
+ runtime_cli.main(["recovery", "run", "--dry-run"])
170
+
171
+ assert exc_info.value.code == 2
172
+ assert dispatched == []
173
+
174
+
175
+ @pytest.mark.parametrize(
176
+ "args",
177
+ [
178
+ ("feature", "run"),
179
+ ("feature", "status"),
180
+ ("feature", "reset"),
181
+ ("feature", "unskip"),
182
+ ("feature", "test-cli"),
183
+ ("feature", "describe"),
184
+ ("bugfix", "run"),
185
+ ("bugfix", "status"),
186
+ ("bugfix", "reset"),
187
+ ("bugfix", "unskip"),
188
+ ("bugfix", "describe"),
189
+ ("refactor", "run"),
190
+ ("refactor", "status"),
191
+ ("refactor", "reset"),
192
+ ("refactor", "unskip"),
193
+ ("refactor", "describe"),
194
+ ("reset", "describe"),
195
+ ("daemon", "feature", "describe"),
196
+ ("diagnostics", "describe"),
197
+ ("status", "all", "custom-plan.json"),
198
+ ("run", "feature", "--unknown-option"),
199
+ ("run", "feature", "F-001", "plan.json", "extra"),
200
+ ("daemon", "feature", "status", "extra"),
201
+ ],
202
+ )
203
+ def test_removed_or_invalid_forms_fail_in_parser_without_runtime_dispatch(monkeypatch, args):
204
+ from prizmkit_runtime import cli as runtime_cli
205
+
206
+ dispatched = []
207
+ monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
208
+ monkeypatch.setattr(runtime_cli, "_dispatch", lambda parsed: dispatched.append(parsed))
209
+
210
+ with pytest.raises(SystemExit) as exc_info:
211
+ runtime_cli.main(list(args))
212
+
213
+ assert exc_info.value.code == 2
214
+ assert dispatched == []
215
+
216
+
111
217
  def test_python_version_guard_mentions_required_version():
112
218
  from prizmkit_runtime.compat import UnsupportedPythonVersion, ensure_supported_python
113
219
 
@@ -136,6 +242,7 @@ def test_runtime_paths_support_framework_source_layout():
136
242
  assert paths.refactor_plan == paths.plans_dir / "refactor-list.json"
137
243
  assert paths.feature_state_dir == paths.state_dir / "features"
138
244
  assert paths.specs_dir == paths.prizmkit_dir / "specs"
245
+ assert not hasattr(paths, "recovery_state_dir")
139
246
  assert all(isinstance(value, Path) for value in paths.path_values())
140
247
 
141
248
 
@@ -154,6 +261,22 @@ def test_runtime_paths_support_installed_layout(tmp_path):
154
261
  assert paths.logs_dir == paths.state_dir / "logs"
155
262
 
156
263
 
264
+ def test_runtime_diagnostics_leave_historical_recovery_state_untouched(tmp_path):
265
+ project = tmp_path / "project"
266
+ historical = project / ".prizmkit" / "state" / "recovery" / "historical-session"
267
+ historical.mkdir(parents=True)
268
+ marker = historical / "marker.json"
269
+ marker.write_bytes(b'{"preserve": true}\n')
270
+ before = {path.relative_to(project).as_posix(): path.read_bytes() for path in historical.rglob("*") if path.is_file()}
271
+
272
+ result = run_cli("--project-root", str(project), "diagnostics", "paths")
273
+
274
+ after = {path.relative_to(project).as_posix(): path.read_bytes() for path in historical.rglob("*") if path.is_file()}
275
+ assert result.returncode == 0
276
+ assert before == after
277
+ assert marker.is_file()
278
+
279
+
157
280
  def test_runtime_config_models_environment_and_ai_resolution():
158
281
  from prizmkit_runtime.config import AIClientResolution, RuntimeEnvironment, load_runtime_config
159
282
  from prizmkit_runtime.paths import resolve_runtime_paths
@@ -298,59 +421,51 @@ def test_runtime_config_supplied_env_does_not_leak_ambient_path(tmp_path):
298
421
 
299
422
 
300
423
 
301
- from prizmkit_runtime.commands import list_runtime_commands
424
+ def test_runtime_command_registry_contains_only_executing_operation_first_entries():
425
+ from prizmkit_runtime.commands import find_runtime_command, list_runtime_commands
302
426
 
303
427
  command_keys = {command.key for command in list_runtime_commands()}
304
-
305
428
  expected = {
306
- ("feature", None, "run"),
307
- ("feature", None, "status"),
308
- ("feature", None, "unskip"),
309
- ("feature", None, "test-cli"),
310
- ("feature", None, "describe"),
311
- ("bugfix", None, "run"),
312
- ("bugfix", None, "status"),
313
- ("bugfix", None, "unskip"),
314
- ("bugfix", None, "describe"),
315
- ("refactor", None, "run"),
316
- ("refactor", None, "status"),
317
- ("refactor", None, "unskip"),
318
- ("refactor", None, "describe"),
319
- ("recovery", None, "run"),
320
- ("recovery", None, "detect"),
321
- ("recovery", None, "describe"),
322
- ("reset", "feature", "reset"),
323
- ("reset", "bugfix", "reset"),
324
- ("reset", "refactor", "reset"),
325
- ("reset", None, "describe"),
326
- ("status", "feature", "status"),
327
- ("status", "bugfix", "status"),
328
- ("status", "refactor", "status"),
329
- ("status", "all", "status"),
330
- ("diagnostics", None, "paths"),
331
- ("diagnostics", None, "entrypoints"),
332
- ("diagnostics", None, "version"),
429
+ *(('run', family, 'run') for family in ('feature', 'bugfix', 'refactor')),
430
+ *(('status', family, 'status') for family in ('feature', 'bugfix', 'refactor', 'all')),
431
+ *(('reset', family, 'reset') for family in ('feature', 'bugfix', 'refactor')),
432
+ *(('daemon', family, action)
433
+ for family in ('feature', 'bugfix', 'refactor')
434
+ for action in ('start', 'stop', 'status', 'logs', 'restart')),
435
+ *(('diagnostics', None, action)
436
+ for action in ('paths', 'entrypoints', 'version', 'ai-cli')),
437
+ ('helper', None, 'run'),
333
438
  }
334
- for target in ["feature", "bugfix", "refactor"]:
335
- for action in ["start", "stop", "status", "logs", "restart", "describe"]:
336
- expected.add(("daemon", target, action))
337
-
338
- assert expected.issubset(command_keys)
339
439
 
440
+ assert command_keys == expected
441
+ assert all(command.current_entrypoint for command in list_runtime_commands())
442
+ assert all(command.action != "describe" for command in list_runtime_commands())
443
+ assert all(command.group not in {"feature", "bugfix", "refactor", "recovery"} for command in list_runtime_commands())
444
+ for removed in [
445
+ ("feature", "run", None),
446
+ ("feature", "test-cli", None),
447
+ ("reset", "describe", None),
448
+ ("daemon", "describe", "feature"),
449
+ ("recovery", "detect", None),
450
+ ]:
451
+ with pytest.raises(KeyError, match="Unknown runtime command"):
452
+ find_runtime_command(removed[0], removed[1], removed[2])
340
453
 
341
- def test_python_runtime_command_output_describes_canonical_boundary():
342
- result = run_cli("feature", "describe")
343
454
 
344
- assert result.returncode == 0
345
- assert "Python runtime command contract" in result.stdout
346
- assert "canonical PrizmKit Python runtime" in result.stdout
347
- assert "current entrypoint" in result.stdout
348
- assert "python-runtime-canonical" in result.stdout
349
- assert "fully replaces" not in result.stdout.lower()
350
- assert "shell runtime remains canonical" not in result.stdout
455
+ def test_removed_describe_commands_are_rejected():
456
+ for args in [
457
+ ("feature", "describe"),
458
+ ("reset", "describe"),
459
+ ("daemon", "feature", "describe"),
460
+ ("diagnostics", "describe"),
461
+ ]:
462
+ result = run_cli(*args)
463
+ assert result.returncode == 2
464
+ assert "invalid choice" in result.stderr
465
+ assert "Python runtime command contract" not in result.stdout
351
466
 
352
467
 
353
- def test_option_first_runtime_arguments_are_forwarded(monkeypatch):
468
+ def test_operation_first_runtime_arguments_are_validated_then_forwarded_exactly(monkeypatch):
354
469
  from prizmkit_runtime import cli as runtime_cli
355
470
 
356
471
  captured = []
@@ -362,76 +477,131 @@ def test_option_first_runtime_arguments_are_forwarded(monkeypatch):
362
477
  monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
363
478
  monkeypatch.setattr(runtime_cli, "handle_runtime_command", fake_handle)
364
479
 
365
- assert runtime_cli.main(["recovery", "run", "--dry-run"]) == 0
366
- assert captured[-1] == ("recovery", "run", None, ("--dry-run",))
480
+ run_args = [
481
+ "run", "feature", "F-001", "custom.json", "--features", "F-001:F-005",
482
+ "--max-retries", "5", "--max-infra-retries", "4",
483
+ "--resume-phase", "6", "--dry-run",
484
+ ]
485
+ assert runtime_cli.main(run_args) == 0
486
+ assert captured[-1] == ("run", "run", "feature", tuple(run_args[2:]))
487
+
488
+ assert runtime_cli.main(["status", "bugfix", "bugs.json"]) == 0
489
+ assert captured[-1] == ("status", "status", "bugfix", ("bugs.json",))
367
490
 
368
- assert runtime_cli.main(["daemon", "feature", "start", "--mode", "standard"]) == 0
369
- assert captured[-1] == ("daemon", "start", "feature", ("--mode", "standard"))
491
+ assert runtime_cli.main(["daemon", "feature", "start"]) == 0
492
+ assert captured[-1] == ("daemon", "start", "feature", ())
370
493
 
371
494
  assert runtime_cli.main(["daemon", "feature", "logs", "--follow"]) == 0
372
495
  assert captured[-1] == ("daemon", "logs", "feature", ("--follow",))
373
496
 
374
- assert runtime_cli.main(["reset", "feature", "--failed"]) == 0
375
- assert captured[-1] == ("reset", "reset", "feature", ("--failed",))
497
+ assert runtime_cli.main(["reset", "feature", "--failed", "--preserve-runtime"]) == 0
498
+ assert captured[-1] == ("reset", "reset", "feature", ("--failed", "--preserve-runtime"))
376
499
 
377
- assert runtime_cli.main(["--project-root", "feature", "feature", "describe"]) == 0
378
- assert captured[-1] == ("feature", "describe", None, ())
500
+ interspersed_reset = ["reset", "feature", "F-001", "--fresh-checkout", "custom.json"]
501
+ assert runtime_cli.main(interspersed_reset) == 0
502
+ assert captured[-1] == (
503
+ "reset", "reset", "feature", ("F-001", "--fresh-checkout", "custom.json")
504
+ )
379
505
 
506
+ interspersed_daemon = ["daemon", "feature", "start", "custom.json"]
507
+ assert runtime_cli.main(interspersed_daemon) == 0
508
+ assert captured[-1] == (
509
+ "daemon", "start", "feature", ("custom.json",)
510
+ )
380
511
 
381
- def test_unskip_dispatches_status_updater_not_contract(monkeypatch):
382
- from prizmkit_runtime import runners
383
- from prizmkit_runtime.commands import handle_runtime_command
384
- from prizmkit_runtime.paths import resolve_runtime_paths
512
+ assert runtime_cli.main(["--project-root", "feature", "run", "feature", "--dry-run"]) == 0
513
+ assert captured[-1] == ("run", "run", "feature", ("--dry-run",))
514
+
515
+
516
+ def test_pipeline_mode_option_is_rejected():
517
+ foreground = run_cli("run", "feature", "--mode", "full")
518
+ daemon = run_cli("daemon", "feature", "start", "--mode", "full")
519
+
520
+ assert foreground.returncode == 2
521
+ assert daemon.returncode == 2
522
+ assert "unrecognized arguments" in foreground.stderr
523
+ assert "unrecognized arguments" in daemon.stderr
385
524
 
386
- class FakeStatus:
387
- return_code = 0
388
- stdout = "unskipped\n"
389
- stderr = ""
525
+
526
+ @pytest.mark.parametrize(
527
+ ("family", "task_id", "extra_args"),
528
+ [
529
+ ("feature", "F-007", ("--features", "F-007", "--dry-run")),
530
+ ("bugfix", "B-007", ("--max-retries", "4")),
531
+ ("refactor", "R-007", ("--resume-phase", "6", "--max-infra-retries", "5")),
532
+ ],
533
+ )
534
+ def test_run_family_dispatch_preserves_each_family_argument_tail(
535
+ monkeypatch, family, task_id, extra_args
536
+ ):
537
+ from prizmkit_runtime import cli as runtime_cli
390
538
 
391
539
  captured = []
540
+ monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
541
+ monkeypatch.setattr(
542
+ runtime_cli,
543
+ "handle_runtime_command",
544
+ lambda group, action, **kwargs: captured.append(
545
+ (group, action, kwargs["target"], kwargs["legacy_args"])
546
+ ) or runtime_cli.CommandResult(0, "ok", "ok"),
547
+ )
548
+ argv = ["run", family, task_id, *extra_args]
392
549
 
393
- def fake_run_status_action(family, action, invocation, *, item_id="", project_root=None, **kwargs):
394
- captured.append((family.kind, action, invocation.item_id, item_id, project_root))
395
- return FakeStatus()
550
+ assert runtime_cli.main(argv) == 0
551
+ assert captured == [("run", "run", family, (task_id, *extra_args))]
396
552
 
397
- monkeypatch.setattr(runners, "run_status_action", fake_run_status_action)
398
- paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
399
553
 
400
- result = handle_runtime_command("feature", "unskip", paths=paths, legacy_args=("F-005",))
554
+ @pytest.mark.parametrize("family", ("feature", "bugfix", "refactor"))
555
+ @pytest.mark.parametrize("legacy_action", ("reset", "un" + "skip"))
556
+ def test_family_level_reset_aliases_are_not_public_commands(family, legacy_action):
557
+ result = run_cli(family, legacy_action)
401
558
 
402
- assert result.exit_code == 0
403
- assert result.title == "Python feature unskip"
404
- assert "unskipped" in result.render()
405
- assert "Python runtime command contract" not in result.render()
406
- assert captured == [("feature", "unskip", "F-005", "F-005", REPO_ROOT.resolve())]
559
+ assert result.returncode == 2
560
+ assert "invalid choice" in result.stderr
407
561
 
408
562
 
409
- def test_test_cli_reports_ai_resolution_not_contract(monkeypatch):
410
- from prizmkit_runtime import runners
411
- from prizmkit_runtime.commands import handle_runtime_command
563
+ def test_diagnostics_ai_cli_reports_family_neutral_resolution(monkeypatch):
564
+ from prizmkit_runtime import config
565
+ from prizmkit_runtime.commands import handle_diagnostics
412
566
  from prizmkit_runtime.paths import resolve_runtime_paths
413
567
 
414
568
  class FakeAIClient:
415
569
  command = "claude"
416
570
  platform = "claude"
417
571
  source = "test"
418
- notes = ()
572
+ notes = ("configured globally",)
419
573
 
420
574
  class FakeConfig:
421
575
  ai_client = FakeAIClient()
422
576
  runtime_mode = "python-runtime-canonical"
423
577
  model = "claude-sonnet-4.6"
424
578
 
425
- monkeypatch.setattr(runners, "load_runtime_config", lambda paths: FakeConfig())
579
+ monkeypatch.setattr(config, "load_runtime_config", lambda paths: FakeConfig())
426
580
  paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
427
581
 
428
- result = handle_runtime_command("feature", "test-cli", paths=paths)
582
+ result = handle_diagnostics("ai-cli", paths)
429
583
  rendered = result.render()
430
584
 
431
585
  assert result.exit_code == 0
432
- assert result.title == "Python feature AI CLI test"
586
+ assert result.title == "Python runtime AI CLI diagnostics"
433
587
  assert "detected_cli: claude" in rendered
434
- assert "Python runtime command contract" not in rendered
588
+ assert "platform: claude" in rendered
589
+ assert "source: test" in rendered
590
+ assert "runtime_mode: python-runtime-canonical" in rendered
591
+ assert "model: claude-sonnet-4.6" in rendered
592
+ assert "note: configured globally" in rendered
593
+ assert "feature" not in result.title.lower()
594
+
595
+
596
+ def test_diagnostics_help_exposes_only_real_diagnostic_actions():
597
+ result = run_cli("diagnostics", "--help")
598
+
599
+ assert result.returncode == 0
600
+ for action in ("paths", "entrypoints", "version", "ai-cli"):
601
+ assert action in result.stdout
602
+ assert "describe" not in result.stdout
603
+ assert "commands" not in result.stdout
604
+ assert "test-cli" not in result.stdout
435
605
 
436
606
 
437
607
  def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
@@ -444,8 +614,203 @@ def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
444
614
  assert entrypoints_result.returncode == 0
445
615
  assert "Python runtime entrypoint inventory" in entrypoints_result.stdout
446
616
  assert "runtime:cli:" in entrypoints_result.stdout
617
+ assert "command:run:feature: python3 dev-pipeline/cli.py run feature" in entrypoints_result.stdout
618
+ assert "command:status:all: python3 dev-pipeline/cli.py status all" in entrypoints_result.stdout
619
+ assert "command:diagnostics:ai-cli:" in entrypoints_result.stdout
620
+ assert " describe" not in entrypoints_result.stdout
621
+ assert "test-cli" not in entrypoints_result.stdout
447
622
  assert "run-feature.sh" not in entrypoints_result.stdout
448
623
  assert "generate-bootstrap-prompt.py" in entrypoints_result.stdout
624
+ assert "generate-recovery-prompt.py" not in entrypoints_result.stdout
625
+
626
+
627
+ def _write_family_status_plan(paths, family, task_id):
628
+ plan_path, key = {
629
+ "feature": (paths.feature_plan, "features"),
630
+ "bugfix": (paths.bugfix_plan, "bugs"),
631
+ "refactor": (paths.refactor_plan, "refactors"),
632
+ }[family]
633
+ plan_path.parent.mkdir(parents=True, exist_ok=True)
634
+ plan_path.write_text(json.dumps({key: [{"id": task_id, "status": "pending"}]}), encoding="utf-8")
635
+ return plan_path
636
+
637
+
638
+ def _write_family_runtime_status(paths, family, task_id):
639
+ state_dir = {
640
+ "feature": paths.feature_state_dir,
641
+ "bugfix": paths.bugfix_state_dir,
642
+ "refactor": paths.refactor_state_dir,
643
+ }[family]
644
+ status_path = state_dir / task_id / "status.json"
645
+ status_path.parent.mkdir(parents=True, exist_ok=True)
646
+ status_path.write_text(
647
+ json.dumps(
648
+ {
649
+ {
650
+ "feature": "feature_id",
651
+ "bugfix": "bug_id",
652
+ "refactor": "refactor_id",
653
+ }[family]: task_id,
654
+ "retry_count": 0,
655
+ "sessions": [],
656
+ "resume_from_phase": 9,
657
+ }
658
+ ),
659
+ encoding="utf-8",
660
+ )
661
+ return status_path
662
+
663
+
664
+ def _file_bytes_under(root):
665
+ if not root.exists():
666
+ return {}
667
+ return {
668
+ path.relative_to(root).as_posix(): path.read_bytes()
669
+ for path in root.rglob("*")
670
+ if path.is_file()
671
+ }
672
+
673
+
674
+ def test_status_all_aggregates_configured_families_in_fixed_order_and_missing_is_nonfatal(
675
+ tmp_path, monkeypatch
676
+ ):
677
+ from prizmkit_runtime import runners
678
+ from prizmkit_runtime.commands import CommandResult, handle_runtime_command
679
+ from prizmkit_runtime.paths import resolve_runtime_paths
680
+
681
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
682
+ _write_family_status_plan(paths, "feature", "F-001")
683
+ _write_family_status_plan(paths, "refactor", "R-001")
684
+ before = _file_bytes_under(tmp_path)
685
+ calls = []
686
+
687
+ def fake_status(family, action, legacy_args, runtime_paths):
688
+ calls.append((family, action, legacy_args, runtime_paths))
689
+ return CommandResult(0, f"Python {family} status", f"normal {family} status")
690
+
691
+ monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
692
+ result = handle_runtime_command("status", "status", target="all", paths=paths)
693
+ rendered = result.render()
694
+
695
+ assert result.exit_code == 0
696
+ assert rendered.index("FEATURE\n=======") < rendered.index("BUGFIX\n======")
697
+ assert rendered.index("BUGFIX\n======") < rendered.index("REFACTOR\n========")
698
+ assert "normal feature status" in rendered
699
+ assert "normal refactor status" in rendered
700
+ assert "BUGFIX\n======\nnot configured: plan list not found" in rendered
701
+ assert [(family, action, args) for family, action, args, _paths in calls] == [
702
+ ("feature", "status", ()),
703
+ ("refactor", "status", ()),
704
+ ]
705
+ assert _file_bytes_under(tmp_path) == before
706
+ assert not paths.feature_state_dir.exists()
707
+ assert not paths.bugfix_state_dir.exists()
708
+ assert not paths.refactor_state_dir.exists()
709
+
710
+
711
+ def test_status_all_uses_real_family_status_handlers_without_filesystem_mutation(tmp_path):
712
+ from prizmkit_runtime.paths import resolve_runtime_paths
713
+
714
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
715
+ configured = {
716
+ "feature": "F-001",
717
+ "bugfix": "B-001",
718
+ "refactor": "R-001",
719
+ }
720
+ for family, task_id in configured.items():
721
+ _write_family_status_plan(paths, family, task_id)
722
+ _write_family_runtime_status(paths, family, task_id)
723
+ before = _file_bytes_under(tmp_path)
724
+
725
+ result = run_cli("--project-root", str(tmp_path), "status", "all")
726
+
727
+ assert result.returncode == 0, result.stderr
728
+ assert "FEATURE\n=======" in result.stdout
729
+ assert "Python feature status" in result.stdout
730
+ assert "BUGFIX\n======" in result.stdout
731
+ assert "Python bugfix status" in result.stdout
732
+ assert "REFACTOR\n========" in result.stdout
733
+ assert "Python refactor status" in result.stdout
734
+ assert _file_bytes_under(tmp_path) == before
735
+
736
+
737
+ def test_status_all_reports_plan_and_handler_errors_but_renders_unaffected_sections(
738
+ tmp_path, monkeypatch
739
+ ):
740
+ from prizmkit_runtime import runners
741
+ from prizmkit_runtime.commands import CommandResult, handle_runtime_command
742
+ from prizmkit_runtime.paths import resolve_runtime_paths
743
+
744
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
745
+ _write_family_status_plan(paths, "feature", "F-001")
746
+ paths.bugfix_plan.parent.mkdir(parents=True, exist_ok=True)
747
+ paths.bugfix_plan.write_text("{malformed", encoding="utf-8")
748
+ _write_family_status_plan(paths, "refactor", "R-001")
749
+
750
+ def fake_status(family, action, legacy_args, runtime_paths):
751
+ assert action == "status"
752
+ if family == "refactor":
753
+ raise RuntimeError("family handler exploded")
754
+ return CommandResult(0, f"Python {family} status", f"normal {family} status")
755
+
756
+ monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
757
+ result = handle_runtime_command("status", "status", target="all", paths=paths)
758
+ rendered = result.render()
759
+
760
+ assert result.exit_code != 0
761
+ assert "FEATURE\n=======\nPython feature status\nnormal feature status" in rendered
762
+ assert "BUGFIX\n======\nerror: malformed plan list:" in rendered
763
+ assert "REFACTOR\n========\nerror: family handler exploded" in rendered
764
+
765
+
766
+ def test_status_all_reports_malformed_runtime_unparseable_and_nonzero_results(
767
+ tmp_path, monkeypatch
768
+ ):
769
+ from prizmkit_runtime import runners
770
+ from prizmkit_runtime.commands import CommandResult, handle_runtime_command
771
+ from prizmkit_runtime.paths import resolve_runtime_paths
772
+
773
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
774
+ _write_family_status_plan(paths, "feature", "F-001")
775
+ _write_family_status_plan(paths, "bugfix", "B-001")
776
+ _write_family_status_plan(paths, "refactor", "R-001")
777
+ malformed_state = paths.feature_state_dir / "F-001" / "status.json"
778
+ malformed_state.parent.mkdir(parents=True, exist_ok=True)
779
+ malformed_state.write_text("not-json", encoding="utf-8")
780
+
781
+ def fake_status(family, action, legacy_args, runtime_paths):
782
+ if family == "bugfix":
783
+ return object()
784
+ return CommandResult(7 if family == "refactor" else 0, f"Python {family} status", "status output")
785
+
786
+ monkeypatch.setattr(runners, "run_pipeline_command", fake_status)
787
+ result = handle_runtime_command("status", "status", target="all", paths=paths)
788
+ rendered = result.render()
789
+
790
+ assert result.exit_code != 0
791
+ assert "FEATURE\n=======\nerror: malformed runtime state F-001/status.json" in rendered
792
+ assert "BUGFIX\n======\nerror: unparseable configured family result" in rendered
793
+ assert "REFACTOR\n========\nerror: Python refactor status\nstatus output" in rendered
794
+
795
+
796
+ def test_status_all_direct_api_rejects_family_specific_arguments_without_calling_handlers(
797
+ tmp_path, monkeypatch
798
+ ):
799
+ from prizmkit_runtime import runners
800
+ from prizmkit_runtime.commands import handle_runtime_command
801
+ from prizmkit_runtime.paths import resolve_runtime_paths
802
+
803
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=tmp_path)
804
+ called = []
805
+ monkeypatch.setattr(runners, "run_pipeline_command", lambda *args: called.append(args))
806
+
807
+ result = handle_runtime_command(
808
+ "status", "status", target="all", paths=paths, legacy_args=("custom.json",)
809
+ )
810
+
811
+ assert result.exit_code == 2
812
+ assert "does not accept a plan path" in result.render()
813
+ assert called == []
449
814
 
450
815
 
451
816
  def test_interoperability_preserves_existing_utility_mapping():
@@ -481,9 +846,55 @@ def test_interoperability_preserves_existing_utility_mapping():
481
846
  assert actual_scripts == mapped_scripts | explicitly_library_only
482
847
  assert explicitly_library_only == {"continuation.py", "prompt_framework.py", "utils.py"}
483
848
  assert "parse-stream-progress.py" in mapped_scripts
849
+ assert "generate-recovery-prompt" not in UTILITY_ENTRYPOINTS
484
850
  assert "prizmkit-test-gate.py" not in mapped_scripts
485
851
 
486
852
 
853
+ def test_obsolete_runtime_recovery_ai_session_sources_are_absent():
854
+ assert not (PIPELINE_ROOT / "prizmkit_runtime" / "runner_recovery.py").exists()
855
+ assert not (PIPELINE_ROOT / "scripts" / "generate-recovery-prompt.py").exists()
856
+
857
+ forbidden_by_source = {
858
+ "prizmkit_runtime/cli.py": ('"recovery"',),
859
+ "prizmkit_runtime/commands.py": ('"recovery": ("run"', 'group == "recovery"'),
860
+ "prizmkit_runtime/runners.py": ("runner_recovery", "run_recovery_command"),
861
+ "prizmkit_runtime/paths.py": ("recovery_state_dir",),
862
+ "prizmkit_runtime/interoperability.py": ("generate-recovery-prompt",),
863
+ }
864
+ for relative_path, forbidden_tokens in forbidden_by_source.items():
865
+ content = (PIPELINE_ROOT / relative_path).read_text(encoding="utf-8")
866
+ for token in forbidden_tokens:
867
+ assert token not in content, f"{relative_path} still contains {token}"
868
+
869
+
870
+ @pytest.mark.parametrize(
871
+ ("path", "ordinary_command"),
872
+ [
873
+ (REPO_ROOT / "README.md", "python3 ./.prizmkit/dev-pipeline/cli.py run feature F-003"),
874
+ (
875
+ REPO_ROOT / "core" / "templates" / "project-memory-template.md",
876
+ "python3 ./.prizmkit/dev-pipeline/cli.py run <family> <task-id>",
877
+ ),
878
+ (
879
+ REPO_ROOT
880
+ / "core"
881
+ / "skills"
882
+ / "orchestration-skill"
883
+ / "workflows"
884
+ / "recovery-workflow"
885
+ / "SKILL.md",
886
+ "python3 ./.prizmkit/dev-pipeline/cli.py run feature F-001",
887
+ ),
888
+ ],
889
+ )
890
+ def test_canonical_interrupted_task_guidance_uses_recovery_workflow(path, ordinary_command):
891
+ content = path.read_text(encoding="utf-8")
892
+
893
+ assert "recovery-workflow" in content
894
+ assert ordinary_command in content
895
+ assert "cli.py recovery" not in content
896
+
897
+
487
898
  def test_boundary_dataclasses_are_cohesive_and_frozen():
488
899
  from prizmkit_runtime.gitops import BranchContext, WorktreePolicy
489
900
  from prizmkit_runtime.heartbeat import HeartbeatConfig, HeartbeatState
@@ -876,7 +1287,7 @@ def test_session_preamble_writes_redacted_start_command_for_all_launch_profiles(
876
1287
  assert str(prompt) not in "\n".join(lines[:6])
877
1288
 
878
1289
 
879
- def test_headless_review_guidance_defers_to_skill_review_contract():
1290
+ def test_headless_review_guidance_maps_stage_local_skill_result():
880
1291
  headless_guidance_files = [
881
1292
  REPO_ROOT / "dev-pipeline" / "templates" / "sections" / "phase-review-full.md",
882
1293
  REPO_ROOT / "dev-pipeline" / "templates" / "bootstrap-tier3.md",
@@ -892,9 +1303,10 @@ def test_headless_review_guidance_defers_to_skill_review_contract():
892
1303
 
893
1304
  for path in headless_guidance_files:
894
1305
  text = path.read_text(encoding="utf-8")
895
- assert "current review contract" in text, path
896
- assert "second review path outside that contract" in text, path
1306
+ assert "returns only `PASS` or `NEEDS_FIXES`" in text, path
1307
+ assert "this injected session" in text, path
897
1308
  assert "text final-verdict" in text, path
1309
+ assert "follow the skill's current review contract" not in text, path
898
1310
  assert "low=0" not in text and "Reviewer 3" not in text, path
899
1311
  assert "forced-worktree changed-content fallback" not in text, path
900
1312
  assert "isolation:" not in text, path
@@ -2198,97 +2610,6 @@ def test_compact_live_heartbeat_stays_out_of_primary_and_backup_logs(tmp_path, c
2198
2610
  assert "stale:" not in backup_text.get("content", "")
2199
2611
 
2200
2612
 
2201
- def test_recovery_sessions_receive_live_heartbeat_configuration(tmp_path, monkeypatch):
2202
- import prizmkit_runtime.runner_recovery as runner_recovery
2203
-
2204
- project = tmp_path / "project"
2205
- pipeline = tmp_path / "pipeline"
2206
- recovery_state = tmp_path / "recovery"
2207
- project.mkdir()
2208
- pipeline.mkdir()
2209
- recovery_state.mkdir()
2210
- paths = type(
2211
- "Paths",
2212
- (),
2213
- {"project_root": project, "pipeline_root": pipeline, "recovery_state_dir": recovery_state},
2214
- )()
2215
- captured_config = {}
2216
-
2217
- monkeypatch.setattr(
2218
- runner_recovery,
2219
- "run_recovery_detect",
2220
- lambda _paths: (0, "{}", "", {"detected": True, "workflow_type": "feature"}),
2221
- )
2222
- monkeypatch.setattr(
2223
- runner_recovery,
2224
- "load_runtime_config",
2225
- lambda _paths: type(
2226
- "Config",
2227
- (),
2228
- {
2229
- "ai_client": type("Client", (), {"command": "fake-cli", "platform": "codebuddy"})(),
2230
- "model": "model",
2231
- "effort": "high",
2232
- },
2233
- )(),
2234
- )
2235
- monkeypatch.setattr(
2236
- runner_recovery,
2237
- "RunnerEnvironment",
2238
- type(
2239
- "EnvFactory",
2240
- (),
2241
- {
2242
- "from_env": staticmethod(
2243
- lambda: type(
2244
- "Env",
2245
- (),
2246
- {
2247
- "verbose": True,
2248
- "live_output": True,
2249
- "heartbeat_interval_seconds": 7,
2250
- "heartbeat_stale_threshold_seconds": 21,
2251
- "stale_kill_threshold_seconds": 42,
2252
- },
2253
- )()
2254
- )
2255
- },
2256
- ),
2257
- )
2258
- monkeypatch.setattr(runner_recovery, "detect_stream_json_support", lambda *_args: type("Support", (), {"enabled": True})())
2259
- monkeypatch.setattr(
2260
- runner_recovery.subprocess,
2261
- "run",
2262
- lambda *_args, **_kwargs: type("Completed", (), {"returncode": 0, "stdout": "0", "stderr": ""})(),
2263
- )
2264
- monkeypatch.setattr(runner_recovery, "_recovery_outcome", lambda *_args, **_kwargs: ("no_changes", {}))
2265
-
2266
- class FakeLauncher:
2267
- def __init__(self, config):
2268
- captured_config["config"] = config
2269
-
2270
- def run(self):
2271
- return type("Result", (), {"exit_code": 0})()
2272
-
2273
- monkeypatch.setattr(runner_recovery, "AISessionLauncher", FakeLauncher)
2274
-
2275
- code, stdout, stderr = runner_recovery.run_recovery(paths)
2276
- config = captured_config["config"]
2277
-
2278
- assert code == 0
2279
- assert stdout == "no_changes\n"
2280
- assert stderr == ""
2281
- assert config.live_output is True
2282
- assert config.verbose is True
2283
- assert config.use_stream_json is True
2284
- assert config.live_progress_interval_seconds == 7
2285
- assert config.heartbeat_stale_threshold_seconds == 21
2286
- assert config.stale_kill_threshold_seconds == 42
2287
- assert config.live_banner is False
2288
- assert config.suppress_stream_output is True
2289
-
2290
-
2291
-
2292
2613
  def test_session_summary_reports_normalized_subagents_and_child_liveness(tmp_path, capsys):
2293
2614
  import prizmkit_runtime.runners as runners
2294
2615
  from prizmkit_runtime.runner_models import SessionPaths
@@ -2773,6 +3094,60 @@ def test_branch_merge_preserves_preexisting_stash_when_workspace_is_clean(tmp_pa
2773
3094
  assert (tmp_path / "feature.txt").read_text(encoding="utf-8") == "done\n"
2774
3095
 
2775
3096
 
3097
+ def test_branch_merge_fast_forwards_with_dirty_prizmkit_internal_state(tmp_path):
3098
+ from prizmkit_runtime.gitops import (
3099
+ BranchContext,
3100
+ branch_fast_forward_possible,
3101
+ branch_merge_plan,
3102
+ run_git_plan,
3103
+ )
3104
+
3105
+ _init_merge_repo(tmp_path)
3106
+ context = BranchContext("main", "dev/example", tmp_path)
3107
+ subprocess.run(
3108
+ ["git", "checkout", "-b", context.working_branch],
3109
+ cwd=tmp_path,
3110
+ check=True,
3111
+ stdout=subprocess.DEVNULL,
3112
+ )
3113
+ (tmp_path / "feature.txt").write_text("done\n", encoding="utf-8")
3114
+ subprocess.run(["git", "add", "feature.txt"], cwd=tmp_path, check=True)
3115
+ subprocess.run(
3116
+ ["git", "commit", "-m", "feature"],
3117
+ cwd=tmp_path,
3118
+ check=True,
3119
+ stdout=subprocess.DEVNULL,
3120
+ )
3121
+ manifest = tmp_path / ".prizmkit" / "manifest.json"
3122
+ manifest.write_text('{"version":"session-local"}\n', encoding="utf-8")
3123
+
3124
+ assert branch_fast_forward_possible(tmp_path, context) is True
3125
+ result = run_git_plan(
3126
+ tmp_path,
3127
+ branch_merge_plan(context, rebase=False),
3128
+ )
3129
+
3130
+ assert result.ok is True
3131
+ assert not any(command.args and command.args[0] == "rebase" for command in result.plan.commands)
3132
+ assert subprocess.run(
3133
+ ["git", "branch", "--show-current"],
3134
+ cwd=tmp_path,
3135
+ check=True,
3136
+ stdout=subprocess.PIPE,
3137
+ text=True,
3138
+ ).stdout.strip() == "main"
3139
+ assert (tmp_path / "feature.txt").read_text(encoding="utf-8") == "done\n"
3140
+ assert json.loads(manifest.read_text(encoding="utf-8"))["version"] == "session-local"
3141
+ assert ".prizmkit/manifest.json" in subprocess.run(
3142
+ ["git", "status", "--short"],
3143
+ cwd=tmp_path,
3144
+ check=True,
3145
+ stdout=subprocess.PIPE,
3146
+ text=True,
3147
+ ).stdout
3148
+ assert _stash_oids(tmp_path) == ()
3149
+
3150
+
2776
3151
  def test_branch_merge_rejects_dirty_task_checkout_without_touching_stashes(tmp_path):
2777
3152
  from prizmkit_runtime.gitops import BranchContext, branch_merge_plan, run_git_plan
2778
3153
 
@@ -3507,6 +3882,60 @@ def test_materialize_worktree_support_assets_copies_ignored_assets_without_retir
3507
3882
  assert guarded_worktree_remove(runtime, delete_branch=False).ok
3508
3883
 
3509
3884
 
3885
+ def test_linked_worktree_fast_forwards_with_dirty_prizmkit_internal_state(tmp_path):
3886
+ from prizmkit_runtime.gitops import (
3887
+ BranchContext,
3888
+ WorktreePolicy,
3889
+ branch_fast_forward_possible,
3890
+ ensure_linked_worktree,
3891
+ guarded_worktree_remove,
3892
+ merge_linked_worktree,
3893
+ worktree_runtime_context,
3894
+ )
3895
+
3896
+ repo = tmp_path / "repo"
3897
+ _init_temp_repo(repo)
3898
+ config = repo / ".prizmkit" / "config.json"
3899
+ config.parent.mkdir(parents=True)
3900
+ config.write_text('{"mode":"base"}\n', encoding="utf-8")
3901
+ subprocess.run(["git", "add", ".prizmkit/config.json"], cwd=repo, check=True)
3902
+ subprocess.run(
3903
+ ["git", "commit", "-m", "track internal config"],
3904
+ cwd=repo,
3905
+ check=True,
3906
+ stdout=subprocess.DEVNULL,
3907
+ )
3908
+ runtime = worktree_runtime_context(
3909
+ BranchContext("main", "dev/internal-dirt", repo),
3910
+ WorktreePolicy(
3911
+ True,
3912
+ cleanup_on_success=True,
3913
+ preserve_on_failure=True,
3914
+ worktree_root=tmp_path / "worktrees",
3915
+ ),
3916
+ )
3917
+ assert ensure_linked_worktree(runtime).ok
3918
+ (runtime.worktree_path / "feature.txt").write_text("done\n", encoding="utf-8")
3919
+ subprocess.run(["git", "add", "feature.txt"], cwd=runtime.worktree_path, check=True)
3920
+ subprocess.run(
3921
+ ["git", "commit", "-m", "feature"],
3922
+ cwd=runtime.worktree_path,
3923
+ check=True,
3924
+ stdout=subprocess.DEVNULL,
3925
+ )
3926
+ worktree_config = runtime.worktree_path / ".prizmkit" / "config.json"
3927
+ worktree_config.write_text('{"mode":"session-local"}\n', encoding="utf-8")
3928
+
3929
+ assert branch_fast_forward_possible(repo, runtime.context) is True
3930
+ result = merge_linked_worktree(runtime)
3931
+
3932
+ assert result.ok is True
3933
+ assert not any(item.args and item.args[0] == "rebase" for item in result.results)
3934
+ assert (repo / "feature.txt").read_text(encoding="utf-8") == "done\n"
3935
+ assert json.loads(worktree_config.read_text(encoding="utf-8"))["mode"] == "session-local"
3936
+ assert guarded_worktree_remove(runtime, delete_branch=True).ok
3937
+
3938
+
3510
3939
  def test_worktree_terminal_failure_wip_commit_preserves_dirty_changes_before_cleanup(tmp_path):
3511
3940
  from prizmkit_runtime.gitops import (
3512
3941
  BranchContext,
@@ -3588,9 +4017,9 @@ def test_checkpoint_rewind_preserves_planning_and_resets_implementation_downstre
3588
4017
  "prizmkit-implement",
3589
4018
  "prizmkit-code-review",
3590
4019
  "prizmkit-test",
4020
+ "completion-summary",
3591
4021
  "prizmkit-retrospective",
3592
4022
  "prizmkit-committer",
3593
- "completion-summary",
3594
4023
  ]
3595
4024
  steps = []
3596
4025
  previous = None
@@ -3622,7 +4051,7 @@ def test_checkpoint_rewind_preserves_planning_and_resets_implementation_downstre
3622
4051
  "feature_state": {
3623
4052
  "schema_version": 1,
3624
4053
  "artifact_dir": ".prizmkit/specs/example",
3625
- "orchestrator": "prizmkit-l4",
4054
+ "orchestrator": "pipeline-runtime",
3626
4055
  "stage": "code-review",
3627
4056
  "current_stage": "prizmkit-code-review",
3628
4057
  "status": "failed",