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
@@ -0,0 +1,935 @@
1
+ """Focused tests for explicit reset modes and causal state recovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from utils import causal_reset_selection
13
+
14
+ REPO_ROOT = Path(__file__).resolve().parents[2]
15
+ PIPELINE_ROOT = REPO_ROOT / "dev-pipeline"
16
+
17
+
18
+ def _item(item_id, status="pending", dependencies=None, **extra):
19
+ return {
20
+ "id": item_id,
21
+ "title": item_id,
22
+ "status": status,
23
+ "dependencies": list(dependencies or []),
24
+ **extra,
25
+ }
26
+
27
+
28
+ @pytest.mark.parametrize(
29
+ ("prefix", "root_statuses"),
30
+ (
31
+ ("F-", {"failed", "skipped"}),
32
+ ("B-", {"failed", "skipped", "needs_info"}),
33
+ ("R-", {"failed", "skipped"}),
34
+ ),
35
+ )
36
+ def test_causal_selection_single_root_recovers_only_complete_chain(prefix, root_statuses):
37
+ items = [
38
+ _item(f"{prefix}001", "failed"),
39
+ _item(f"{prefix}002", "auto_skipped", [f"{prefix}001"]),
40
+ _item(f"{prefix}003", "auto_skipped", [f"{prefix}002"]),
41
+ _item(f"{prefix}004", "completed", [f"{prefix}001"], marker="preserve"),
42
+ _item(f"{prefix}005", "failed"),
43
+ _item(f"{prefix}006", "auto_skipped", [f"{prefix}005"]),
44
+ ]
45
+
46
+ selected = causal_reset_selection(
47
+ items,
48
+ selected_id=f"{prefix}001",
49
+ blocking_root_statuses=root_statuses,
50
+ item_label="Task",
51
+ )
52
+
53
+ assert selected == (f"{prefix}001", f"{prefix}002", f"{prefix}003")
54
+ assert items[3]["status"] == "completed"
55
+ assert items[3]["marker"] == "preserve"
56
+
57
+
58
+ @pytest.mark.parametrize("prefix", ("F-", "B-", "R-"))
59
+ def test_causal_selection_keeps_multi_root_descendant_blocked(prefix):
60
+ root_statuses = {"failed", "skipped", "needs_info"} if prefix == "B-" else {"failed", "skipped"}
61
+ items = [
62
+ _item(f"{prefix}001", "failed"),
63
+ _item(f"{prefix}002", "skipped"),
64
+ _item(f"{prefix}003", "auto_skipped", [f"{prefix}001"]),
65
+ _item(f"{prefix}004", "auto_skipped", [f"{prefix}003", f"{prefix}002"]),
66
+ _item(f"{prefix}005", "auto_skipped", [f"{prefix}002"]),
67
+ ]
68
+
69
+ selected = causal_reset_selection(
70
+ items,
71
+ selected_id=f"{prefix}001",
72
+ blocking_root_statuses=root_statuses,
73
+ )
74
+
75
+ assert selected == (f"{prefix}001", f"{prefix}003")
76
+
77
+
78
+ def test_causal_selection_does_not_cross_an_orphan_auto_skipped_blocker():
79
+ items = [
80
+ _item("F-001", "failed"),
81
+ _item("F-002", "auto_skipped"),
82
+ _item("F-003", "auto_skipped", ["F-001", "F-002"]),
83
+ ]
84
+
85
+ selected = causal_reset_selection(
86
+ items,
87
+ selected_id="F-001",
88
+ blocking_root_statuses={"failed", "skipped"},
89
+ )
90
+
91
+ assert selected == ("F-001",)
92
+
93
+
94
+ @pytest.mark.parametrize("prefix", ("F-", "B-", "R-"))
95
+ def test_selecting_auto_skipped_leaf_includes_every_required_root(prefix):
96
+ root_statuses = {"failed", "skipped", "needs_info"} if prefix == "B-" else {"failed", "skipped"}
97
+ second_root_status = "needs_info" if prefix == "B-" else "skipped"
98
+ items = [
99
+ _item(f"{prefix}001", "failed"),
100
+ _item(f"{prefix}002", second_root_status),
101
+ _item(f"{prefix}003", "auto_skipped", [f"{prefix}001"]),
102
+ _item(f"{prefix}004", "auto_skipped", [f"{prefix}002"]),
103
+ _item(f"{prefix}005", "auto_skipped", [f"{prefix}003", f"{prefix}004"]),
104
+ _item(f"{prefix}006", "completed"),
105
+ ]
106
+
107
+ selected = causal_reset_selection(
108
+ items,
109
+ selected_id=f"{prefix}005",
110
+ blocking_root_statuses=root_statuses,
111
+ )
112
+
113
+ assert selected == (
114
+ f"{prefix}001",
115
+ f"{prefix}002",
116
+ f"{prefix}003",
117
+ f"{prefix}004",
118
+ f"{prefix}005",
119
+ )
120
+
121
+
122
+ @pytest.mark.parametrize(
123
+ ("prefix", "root_statuses", "root_status"),
124
+ (
125
+ ("F-", {"failed", "skipped"}, "skipped"),
126
+ ("B-", {"failed", "skipped", "needs_info"}, "needs_info"),
127
+ ("R-", {"failed", "skipped"}, "skipped"),
128
+ ),
129
+ )
130
+ def test_causal_selection_all_recovers_every_supported_chain(prefix, root_statuses, root_status):
131
+ items = [
132
+ _item(f"{prefix}001", "failed"),
133
+ _item(f"{prefix}002", root_status),
134
+ _item(f"{prefix}003", "auto_skipped", [f"{prefix}001", f"{prefix}002"]),
135
+ _item(f"{prefix}004", "completed"),
136
+ _item(f"{prefix}005", "pending"),
137
+ ]
138
+
139
+ selected = causal_reset_selection(
140
+ items,
141
+ selected_id=None,
142
+ blocking_root_statuses=root_statuses,
143
+ )
144
+
145
+ assert selected == (f"{prefix}001", f"{prefix}002", f"{prefix}003")
146
+
147
+
148
+ def test_causal_selection_rejects_missing_invalid_and_orphan_targets():
149
+ items = [
150
+ _item("F-001", "failed"),
151
+ _item("F-002", "pending"),
152
+ _item("F-003", "auto_skipped"),
153
+ ]
154
+
155
+ with pytest.raises(ValueError, match="not found"):
156
+ causal_reset_selection(items, selected_id="F-999", blocking_root_statuses={"failed", "skipped"})
157
+ with pytest.raises(ValueError, match="not recoverable"):
158
+ causal_reset_selection(items, selected_id="F-002", blocking_root_statuses={"failed", "skipped"})
159
+ with pytest.raises(ValueError, match="unresolved auto_skipped blocker"):
160
+ causal_reset_selection(items, selected_id="F-003", blocking_root_statuses={"failed", "skipped"})
161
+
162
+
163
+ def test_causal_selection_rejects_invalid_dependency_graph_before_selection():
164
+ items = [
165
+ _item("F-001", "failed", ["F-002"]),
166
+ _item("F-002", "auto_skipped", ["F-001"]),
167
+ ]
168
+
169
+ with pytest.raises(ValueError, match="Dependency cycle"):
170
+ causal_reset_selection(items, selected_id="F-001", blocking_root_statuses={"failed", "skipped"})
171
+
172
+
173
+ FAMILY_CONTRACTS = {
174
+ "feature": {
175
+ "prefix": "F-",
176
+ "list_key": "features",
177
+ "list_arg": "--feature-list",
178
+ "id_arg": "--feature-id",
179
+ "schema": "dev-pipeline-feature-list-v1",
180
+ "script": "update-feature-status.py",
181
+ "branch_prefix": "dev",
182
+ "artifact_root": "specs",
183
+ },
184
+ "bugfix": {
185
+ "prefix": "B-",
186
+ "list_key": "bugs",
187
+ "list_arg": "--bug-list",
188
+ "id_arg": "--bug-id",
189
+ "schema": "dev-pipeline-bug-fix-list-v1",
190
+ "script": "update-bug-status.py",
191
+ "branch_prefix": "bugfix",
192
+ "artifact_root": "bugfix",
193
+ },
194
+ "refactor": {
195
+ "prefix": "R-",
196
+ "list_key": "refactors",
197
+ "list_arg": "--refactor-list",
198
+ "id_arg": "--refactor-id",
199
+ "schema": "dev-pipeline-refactor-list-v1",
200
+ "script": "update-refactor-status.py",
201
+ "branch_prefix": "refactor",
202
+ "artifact_root": "refactor",
203
+ },
204
+ }
205
+
206
+
207
+ def _make_paths(tmp_path):
208
+ from prizmkit_runtime.paths import resolve_runtime_paths
209
+
210
+ project_root = tmp_path / "project"
211
+ (project_root / ".prizmkit" / "plans").mkdir(parents=True)
212
+ return resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=project_root)
213
+
214
+
215
+ def _family_and_plan(paths, kind, items):
216
+ from prizmkit_runtime.runner_models import family_for
217
+
218
+ family = family_for(kind, paths)
219
+ contract = FAMILY_CONTRACTS[kind]
220
+ family.plan_path.write_text(
221
+ json.dumps(
222
+ {
223
+ "$schema": contract["schema"],
224
+ "project_name": "Reset Test",
225
+ contract["list_key"]: items,
226
+ },
227
+ indent=2,
228
+ )
229
+ + "\n",
230
+ encoding="utf-8",
231
+ )
232
+ return family, family.plan_path
233
+
234
+
235
+ def _write_runtime_status(family, item_id, **overrides):
236
+ status_dir = family.state_dir / item_id
237
+ status_dir.mkdir(parents=True, exist_ok=True)
238
+ status = {
239
+ f"{family.kind if family.kind != 'bugfix' else 'bug'}_id": item_id,
240
+ "retry_count": 4,
241
+ "infra_error_count": 3,
242
+ "last_infra_error_session_id": "old-session",
243
+ "sessions": ["old-session"],
244
+ "last_session_id": "old-session",
245
+ "last_failed_session_id": "old-session",
246
+ "resume_from_phase": 4,
247
+ "continuation_pending": True,
248
+ "continuation_reason": "context_overflow",
249
+ "continuation_count": 2,
250
+ "context_overflow_count": 2,
251
+ "previous_session_id": "old-session",
252
+ "custom_diagnostic": {"keep": True},
253
+ **overrides,
254
+ }
255
+ status_path = status_dir / "status.json"
256
+ status_path.write_text(json.dumps(status, indent=2) + "\n", encoding="utf-8")
257
+ return status_path, status
258
+
259
+
260
+ def _tree_snapshot(root):
261
+ if not root.exists():
262
+ return {}
263
+ return {
264
+ str(path.relative_to(root)): path.read_bytes()
265
+ for path in sorted(root.rglob("*"))
266
+ if path.is_file()
267
+ }
268
+
269
+
270
+ def _git(project_root, *args):
271
+ return subprocess.run(
272
+ ["git", *args],
273
+ cwd=project_root,
274
+ text=True,
275
+ stdout=subprocess.PIPE,
276
+ stderr=subprocess.PIPE,
277
+ check=True,
278
+ ).stdout.strip()
279
+
280
+
281
+ def _init_git_repository(project_root):
282
+ subprocess.run(
283
+ ["git", "init", "-b", "main"],
284
+ cwd=project_root,
285
+ stdout=subprocess.DEVNULL,
286
+ check=True,
287
+ )
288
+ _git(project_root, "config", "user.email", "reset@example.com")
289
+ _git(project_root, "config", "user.name", "Reset Test")
290
+ source = project_root / "source.txt"
291
+ source.write_text("committed\n", encoding="utf-8")
292
+ _git(project_root, "add", "source.txt")
293
+ _git(project_root, "commit", "-m", "base")
294
+ source.write_text("committed\nlocal work\n", encoding="utf-8")
295
+ return source
296
+
297
+
298
+ @pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
299
+ def test_state_only_resets_exact_causal_records_and_preserves_checkout_git_and_artifacts(tmp_path, kind):
300
+ from prizmkit_runtime.reset import run_reset_command
301
+ from prizmkit_runtime.task_checkout import (
302
+ CHECKOUT_MODE_WORKTREE,
303
+ TaskCheckout,
304
+ persist_task_checkout,
305
+ )
306
+
307
+ paths = _make_paths(tmp_path / kind)
308
+ contract = FAMILY_CONTRACTS[kind]
309
+ prefix = contract["prefix"]
310
+ root_id = f"{prefix}001"
311
+ child_id = f"{prefix}002"
312
+ other_root_id = f"{prefix}003"
313
+ shared_id = f"{prefix}004"
314
+ completed_id = f"{prefix}005"
315
+ unrelated_child_id = f"{prefix}006"
316
+ items = [
317
+ _item(root_id, "failed", marker="root-field"),
318
+ _item(child_id, "auto_skipped", [root_id], marker="child-field"),
319
+ _item(other_root_id, "failed", marker="other-root-field"),
320
+ _item(shared_id, "auto_skipped", [child_id, other_root_id], marker="shared-field"),
321
+ _item(completed_id, "completed", marker="completed-field"),
322
+ _item(unrelated_child_id, "auto_skipped", [other_root_id], marker="unrelated-field"),
323
+ ]
324
+ family, plan_path = _family_and_plan(paths, kind, items)
325
+ source_path = _init_git_repository(paths.project_root)
326
+ branch_name = f"{contract['branch_prefix']}/{root_id}-preserved"
327
+ _git(paths.project_root, "branch", branch_name, "main")
328
+
329
+ worktree_path = paths.project_root / ".task-worktrees" / root_id
330
+ worktree_path.mkdir(parents=True)
331
+ worktree_marker = worktree_path / "keep.txt"
332
+ worktree_marker.write_text("worktree remains\n", encoding="utf-8")
333
+ checkout = TaskCheckout.active(
334
+ kind,
335
+ root_id,
336
+ branch_name,
337
+ "main",
338
+ checkout_mode=CHECKOUT_MODE_WORKTREE,
339
+ worktree_path=str(worktree_path),
340
+ )
341
+ persist_task_checkout(family.state_dir, checkout)
342
+ checkout_path = family.state_dir / root_id / "checkout.json"
343
+
344
+ root_status_path, _root_before = _write_runtime_status(
345
+ family,
346
+ root_id,
347
+ active_dev_branch=branch_name,
348
+ base_branch="main",
349
+ )
350
+ child_status_path, _child_before = _write_runtime_status(family, child_id)
351
+ other_status_path, _other_before = _write_runtime_status(family, other_root_id)
352
+ completed_status_path, _completed_before = _write_runtime_status(family, completed_id)
353
+
354
+ session_log = family.state_dir / root_id / "sessions" / "old-session" / "logs" / "session.log"
355
+ session_log.parent.mkdir(parents=True)
356
+ session_log.write_text("session history\n", encoding="utf-8")
357
+ artifact_name = "001-root" if kind == "feature" else root_id
358
+ artifact_dir = paths.project_root / ".prizmkit" / contract["artifact_root"] / artifact_name
359
+ artifact_dir.mkdir(parents=True)
360
+ checkpoint = artifact_dir / "workflow-checkpoint.json"
361
+ checkpoint.write_text('{"sentinel":"checkpoint"}\n', encoding="utf-8")
362
+ artifact = artifact_dir / "spec.md"
363
+ artifact.write_text("artifact remains\n", encoding="utf-8")
364
+
365
+ protected_bytes = {
366
+ "checkout": checkout_path.read_bytes(),
367
+ "session": session_log.read_bytes(),
368
+ "checkpoint": checkpoint.read_bytes(),
369
+ "artifact": artifact.read_bytes(),
370
+ "worktree": worktree_marker.read_bytes(),
371
+ "other_status": other_status_path.read_bytes(),
372
+ "completed_status": completed_status_path.read_bytes(),
373
+ "source": source_path.read_bytes(),
374
+ }
375
+ branch_refs_before = _git(paths.project_root, "show-ref", "--heads")
376
+ branch_before = _git(paths.project_root, "branch", "--show-current")
377
+ source_diff_before = _git(paths.project_root, "diff", "--", "source.txt")
378
+
379
+ result = run_reset_command(
380
+ kind,
381
+ (root_id, "--state-only", str(plan_path)),
382
+ paths,
383
+ )
384
+
385
+ assert result.exit_code == 0, result.render()
386
+ payload = json.loads(result.render())
387
+ assert payload["action"] == "reset_state"
388
+ assert payload["reset_count"] == 2
389
+ updated = json.loads(plan_path.read_text(encoding="utf-8"))[contract["list_key"]]
390
+ statuses = {item["id"]: item["status"] for item in updated}
391
+ assert statuses == {
392
+ root_id: "pending",
393
+ child_id: "pending",
394
+ other_root_id: "failed",
395
+ shared_id: "auto_skipped",
396
+ completed_id: "completed",
397
+ unrelated_child_id: "auto_skipped",
398
+ }
399
+ assert {item["id"]: item["marker"] for item in updated} == {
400
+ root_id: "root-field",
401
+ child_id: "child-field",
402
+ other_root_id: "other-root-field",
403
+ shared_id: "shared-field",
404
+ completed_id: "completed-field",
405
+ unrelated_child_id: "unrelated-field",
406
+ }
407
+
408
+ for selected_status_path in (root_status_path, child_status_path):
409
+ selected_status = json.loads(selected_status_path.read_text(encoding="utf-8"))
410
+ assert selected_status["retry_count"] == 0
411
+ assert selected_status["infra_error_count"] == 0
412
+ assert selected_status["last_infra_error_session_id"] is None
413
+ assert selected_status["sessions"] == []
414
+ assert selected_status["last_session_id"] is None
415
+ assert selected_status["last_failed_session_id"] is None
416
+ assert selected_status["resume_from_phase"] is None
417
+ assert selected_status["custom_diagnostic"] == {"keep": True}
418
+ for continuation_key in (
419
+ "continuation_pending",
420
+ "continuation_reason",
421
+ "continuation_count",
422
+ "context_overflow_count",
423
+ "previous_session_id",
424
+ ):
425
+ assert continuation_key not in selected_status
426
+
427
+ root_status = json.loads(root_status_path.read_text(encoding="utf-8"))
428
+ assert root_status["active_dev_branch"] == branch_name
429
+ assert root_status["base_branch"] == "main"
430
+ assert checkout_path.read_bytes() == protected_bytes["checkout"]
431
+ assert session_log.read_bytes() == protected_bytes["session"]
432
+ assert checkpoint.read_bytes() == protected_bytes["checkpoint"]
433
+ assert artifact.read_bytes() == protected_bytes["artifact"]
434
+ assert worktree_marker.read_bytes() == protected_bytes["worktree"]
435
+ assert other_status_path.read_bytes() == protected_bytes["other_status"]
436
+ assert completed_status_path.read_bytes() == protected_bytes["completed_status"]
437
+ assert source_path.read_bytes() == protected_bytes["source"]
438
+ assert _git(paths.project_root, "show-ref", "--heads") == branch_refs_before
439
+ assert _git(paths.project_root, "branch", "--show-current") == branch_before
440
+ assert _git(paths.project_root, "diff", "--", "source.txt") == source_diff_before
441
+ assert not (family.state_dir / "pipeline.json").exists()
442
+
443
+
444
+ @pytest.mark.parametrize(
445
+ ("kind", "second_root_status"),
446
+ (("feature", "skipped"), ("bugfix", "needs_info"), ("refactor", "skipped")),
447
+ )
448
+ def test_state_only_all_recovers_all_supported_chains_without_pipeline_state(tmp_path, kind, second_root_status):
449
+ from prizmkit_runtime.reset import run_reset_command
450
+
451
+ paths = _make_paths(tmp_path / kind)
452
+ contract = FAMILY_CONTRACTS[kind]
453
+ prefix = contract["prefix"]
454
+ items = [
455
+ _item(f"{prefix}001", "failed"),
456
+ _item(f"{prefix}002", second_root_status),
457
+ _item(f"{prefix}003", "auto_skipped", [f"{prefix}001", f"{prefix}002"]),
458
+ _item(f"{prefix}004", "completed", marker="done"),
459
+ _item(f"{prefix}005", "pending", marker="unrelated"),
460
+ ]
461
+ family, plan_path = _family_and_plan(paths, kind, items)
462
+ completed_status_path, _ = _write_runtime_status(family, f"{prefix}004")
463
+ completed_before = completed_status_path.read_bytes()
464
+
465
+ result = run_reset_command(
466
+ kind,
467
+ ("--state-only", "--all", str(plan_path)),
468
+ paths,
469
+ )
470
+
471
+ assert result.exit_code == 0, result.render()
472
+ payload = json.loads(result.render())
473
+ assert payload["action"] == "reset_state"
474
+ assert payload["reset_count"] == 3
475
+ updated = json.loads(plan_path.read_text(encoding="utf-8"))[contract["list_key"]]
476
+ assert [item["status"] for item in updated] == [
477
+ "pending",
478
+ "pending",
479
+ "pending",
480
+ "completed",
481
+ "pending",
482
+ ]
483
+ assert completed_status_path.read_bytes() == completed_before
484
+ assert not (family.state_dir / "pipeline.json").exists()
485
+
486
+
487
+ @pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
488
+ def test_reset_without_mode_fails_before_any_mutation(tmp_path, kind):
489
+ from prizmkit_runtime.reset import run_reset_command
490
+
491
+ paths = _make_paths(tmp_path / kind)
492
+ prefix = FAMILY_CONTRACTS[kind]["prefix"]
493
+ family, plan_path = _family_and_plan(paths, kind, [_item(f"{prefix}001", "failed")])
494
+ marker = family.state_dir / "sentinel.bin"
495
+ marker.parent.mkdir(parents=True)
496
+ marker.write_bytes(b"runtime-sentinel")
497
+ before_plan = plan_path.read_bytes()
498
+ before_state = _tree_snapshot(family.state_dir)
499
+
500
+ result = run_reset_command(kind, (f"{prefix}001", str(plan_path)), paths)
501
+
502
+ assert result.exit_code != 0
503
+ assert "Exactly one reset mode is required" in result.render()
504
+ assert "--state-only" in result.render()
505
+ assert plan_path.read_bytes() == before_plan
506
+ assert _tree_snapshot(family.state_dir) == before_state
507
+
508
+
509
+ @pytest.mark.parametrize(
510
+ "mode_pair",
511
+ (
512
+ ("--state-only", "--fresh-checkout"),
513
+ ("--state-only", "--clean"),
514
+ ("--state-only", "--preserve-runtime"),
515
+ ("--fresh-checkout", "--clean"),
516
+ ("--fresh-checkout", "--preserve-runtime"),
517
+ ("--clean", "--preserve-runtime"),
518
+ ),
519
+ )
520
+ def test_conflicting_reset_modes_fail_before_mutation(tmp_path, mode_pair):
521
+ from prizmkit_runtime.reset import run_reset_command
522
+
523
+ paths = _make_paths(tmp_path)
524
+ family, plan_path = _family_and_plan(paths, "feature", [_item("F-001", "failed")])
525
+ marker = family.state_dir / "sentinel.bin"
526
+ marker.parent.mkdir(parents=True)
527
+ marker.write_bytes(b"runtime-sentinel")
528
+ before_plan = plan_path.read_bytes()
529
+ before_state = _tree_snapshot(family.state_dir)
530
+
531
+ result = run_reset_command(
532
+ "feature",
533
+ ("F-001", *mode_pair, "--failed", str(plan_path)),
534
+ paths,
535
+ )
536
+
537
+ assert result.exit_code != 0
538
+ assert "Exactly one reset mode is required" in result.render()
539
+ assert plan_path.read_bytes() == before_plan
540
+ assert _tree_snapshot(family.state_dir) == before_state
541
+
542
+
543
+ @pytest.mark.parametrize(
544
+ ("args", "message"),
545
+ (
546
+ (("F-001", "--standard"), "Unsupported reset arguments"),
547
+ (("--state-only",), "requires exactly one item ID or explicit --all"),
548
+ (("F-001", "--state-only", "--all"), "requires exactly one item ID or explicit --all"),
549
+ (("F-001:F-002", "--state-only"), "ranges and status filters are unsupported"),
550
+ (("--failed", "--state-only"), "ranges and status filters are unsupported"),
551
+ (("--fresh-checkout",), "requires exactly one item ID, range, or supported status filter"),
552
+ (("--fresh-checkout", "--all"), "--all is supported only with --state-only"),
553
+ (("--clean",), "requires exactly one item ID, range, or supported status filter"),
554
+ (("--clean", "--all"), "--all is supported only with --state-only"),
555
+ (("--preserve-runtime",), "requires --failed"),
556
+ (("F-001", "--failed", "--preserve-runtime"), "cannot target an ID, range, or --all"),
557
+ (("--failed", "--preserve-runtime", "--all"), "cannot target an ID, range, or --all"),
558
+ ),
559
+ )
560
+ def test_reset_selector_matrix_rejects_unsupported_forms_before_mutation(tmp_path, args, message):
561
+ from prizmkit_runtime.reset import run_reset_command
562
+
563
+ paths = _make_paths(tmp_path)
564
+ family, plan_path = _family_and_plan(
565
+ paths,
566
+ "feature",
567
+ [_item("F-001", "failed"), _item("F-002", "auto_skipped", ["F-001"])],
568
+ )
569
+ marker = family.state_dir / "sentinel.bin"
570
+ marker.parent.mkdir(parents=True)
571
+ marker.write_bytes(b"runtime-sentinel")
572
+ before_plan = plan_path.read_bytes()
573
+ before_state = _tree_snapshot(family.state_dir)
574
+
575
+ result = run_reset_command("feature", (*args, str(plan_path)), paths)
576
+
577
+ assert result.exit_code != 0
578
+ assert message in result.render()
579
+ assert plan_path.read_bytes() == before_plan
580
+ assert _tree_snapshot(family.state_dir) == before_state
581
+
582
+
583
+ @pytest.mark.parametrize(
584
+ ("items", "args", "message"),
585
+ (
586
+ ([_item("F-001", "failed")], ("F-999", "--state-only"), "not found"),
587
+ ([_item("F-001", "pending")], ("F-001", "--state-only"), "not recoverable"),
588
+ ([_item("F-001", "auto_skipped")], ("F-001", "--state-only"), "unresolved auto_skipped blocker"),
589
+ ([_item("F-001", "completed")], ("--state-only", "--all"), "No recoverable blocking roots"),
590
+ (
591
+ [_item("F-001", "failed", ["F-002"]), _item("F-002", "auto_skipped", ["F-001"])],
592
+ ("F-001", "--state-only"),
593
+ "Dependency cycle",
594
+ ),
595
+ ),
596
+ )
597
+ def test_state_only_invalid_target_or_graph_is_fail_closed(tmp_path, items, args, message):
598
+ from prizmkit_runtime.reset import run_reset_command
599
+
600
+ paths = _make_paths(tmp_path)
601
+ family, plan_path = _family_and_plan(paths, "feature", items)
602
+ marker = family.state_dir / "sentinel.bin"
603
+ marker.parent.mkdir(parents=True)
604
+ marker.write_bytes(b"runtime-sentinel")
605
+ before_plan = plan_path.read_bytes()
606
+ before_state = _tree_snapshot(family.state_dir)
607
+
608
+ result = run_reset_command("feature", (*args, str(plan_path)), paths)
609
+
610
+ assert result.exit_code != 0
611
+ assert message in result.render()
612
+ assert plan_path.read_bytes() == before_plan
613
+ assert _tree_snapshot(family.state_dir) == before_state
614
+
615
+
616
+ @pytest.mark.parametrize("kind", ("feature", "bugfix", "refactor"))
617
+ @pytest.mark.parametrize("mode", ("--fresh-checkout", "--clean"))
618
+ @pytest.mark.parametrize(
619
+ ("selector", "expected_numbers"),
620
+ (
621
+ (("X-003",), (3,)),
622
+ (("X-001:X-002",), (1, 2)),
623
+ (("--failed",), (1,)),
624
+ ),
625
+ )
626
+ def test_fresh_checkout_and_clean_keep_id_range_and_filter_selection(
627
+ monkeypatch,
628
+ tmp_path,
629
+ kind,
630
+ mode,
631
+ selector,
632
+ expected_numbers,
633
+ ):
634
+ from prizmkit_runtime import reset as reset_runtime
635
+ from prizmkit_runtime.reset import run_reset_command
636
+
637
+ paths = _make_paths(tmp_path)
638
+ prefix = FAMILY_CONTRACTS[kind]["prefix"]
639
+ normalized_selector = tuple(value.replace("X-", prefix) for value in selector)
640
+ family, plan_path = _family_and_plan(
641
+ paths,
642
+ kind,
643
+ [
644
+ _item(f"{prefix}001", "failed"),
645
+ _item(f"{prefix}002", "auto_skipped", [f"{prefix}001"]),
646
+ _item(f"{prefix}003", "pending"),
647
+ ],
648
+ )
649
+ family.state_dir.mkdir(parents=True, exist_ok=True)
650
+ (family.state_dir / "pipeline.json").write_text("{}\n", encoding="utf-8")
651
+ captured = []
652
+
653
+ def fake_reset_one(received_family, item, options, project_root, return_branch, lines):
654
+ captured.append((received_family.kind, item.item_id, options.fresh_checkout, options.clean))
655
+ return True
656
+
657
+ monkeypatch.setattr(reset_runtime, "_reset_one", fake_reset_one)
658
+ monkeypatch.setattr(reset_runtime, "default_branch", lambda _root: "main")
659
+
660
+ result = run_reset_command(
661
+ kind,
662
+ (*normalized_selector, mode, str(plan_path)),
663
+ paths,
664
+ )
665
+
666
+ assert result.exit_code == 0, result.render()
667
+ expected_ids = [f"{prefix}{number:03d}" for number in expected_numbers]
668
+ assert [entry[1] for entry in captured] == expected_ids
669
+ assert all(entry[2] is (mode == "--fresh-checkout") for entry in captured)
670
+ assert all(entry[3] is (mode == "--clean") for entry in captured)
671
+
672
+
673
+ def _valid_bug(bug_id, status="pending", dependencies=None):
674
+ return {
675
+ "id": bug_id,
676
+ "title": bug_id,
677
+ "description": "Bug description",
678
+ "severity": "high",
679
+ "error_source": {"type": "failed_test", "content": "failure"},
680
+ "verification_type": "automated",
681
+ "acceptance_criteria": ["Regression is fixed"],
682
+ "status": status,
683
+ "dependencies": list(dependencies or []),
684
+ }
685
+
686
+
687
+ def _run_bug_updater(plan_path, state_dir, *args):
688
+ return subprocess.run(
689
+ [
690
+ sys.executable,
691
+ str(PIPELINE_ROOT / "scripts" / "update-bug-status.py"),
692
+ "--bug-list",
693
+ str(plan_path),
694
+ "--state-dir",
695
+ str(state_dir),
696
+ "--project-root",
697
+ str(plan_path.parent),
698
+ *args,
699
+ ],
700
+ cwd=REPO_ROOT,
701
+ text=True,
702
+ stdout=subprocess.PIPE,
703
+ stderr=subprocess.PIPE,
704
+ check=False,
705
+ )
706
+
707
+
708
+ @pytest.mark.parametrize(
709
+ ("session_status", "extra_args", "expected_root_status"),
710
+ (
711
+ ("failed", ("--max-retries", "1"), "failed"),
712
+ ("workflow_skipped", (), "skipped"),
713
+ ("workflow_blocked", (), "needs_info"),
714
+ ),
715
+ )
716
+ def test_bugfix_terminal_roots_auto_skip_pending_descendants(
717
+ tmp_path,
718
+ session_status,
719
+ extra_args,
720
+ expected_root_status,
721
+ ):
722
+ plan_path = tmp_path / "bug-fix-list.json"
723
+ state_dir = tmp_path / "state"
724
+ completed_status = state_dir / "B-004" / "status.json"
725
+ completed_status.parent.mkdir(parents=True)
726
+ completed_status.write_text('{"custom":"unchanged"}\n', encoding="utf-8")
727
+ completed_before = completed_status.read_bytes()
728
+ plan_path.write_text(
729
+ json.dumps(
730
+ {
731
+ "$schema": "dev-pipeline-bug-fix-list-v1",
732
+ "project_name": "Bug Cascade",
733
+ "bugs": [
734
+ _valid_bug("B-001", "in_progress"),
735
+ _valid_bug("B-002", "pending", ["B-001"]),
736
+ _valid_bug("B-003", "pending", ["B-002"]),
737
+ _valid_bug("B-004", "completed"),
738
+ ],
739
+ }
740
+ ),
741
+ encoding="utf-8",
742
+ )
743
+
744
+ result = _run_bug_updater(
745
+ plan_path,
746
+ state_dir,
747
+ "--action",
748
+ "update",
749
+ "--bug-id",
750
+ "B-001",
751
+ "--session-status",
752
+ session_status,
753
+ *extra_args,
754
+ )
755
+
756
+ assert result.returncode == 0, (result.stdout, result.stderr)
757
+ summary = json.loads(result.stdout)
758
+ assert summary["auto_skipped"] == ["B-002", "B-003"]
759
+ statuses = {
760
+ bug["id"]: bug["status"]
761
+ for bug in json.loads(plan_path.read_text(encoding="utf-8"))["bugs"]
762
+ }
763
+ assert statuses == {
764
+ "B-001": expected_root_status,
765
+ "B-002": "auto_skipped",
766
+ "B-003": "auto_skipped",
767
+ "B-004": "completed",
768
+ }
769
+ assert completed_status.read_bytes() == completed_before
770
+
771
+
772
+ def test_existing_bugfix_list_lazily_normalizes_blocked_pending_descendants(tmp_path):
773
+ plan_path = tmp_path / "bug-fix-list.json"
774
+ state_dir = tmp_path / "state"
775
+ data = {
776
+ "$schema": "dev-pipeline-bug-fix-list-v1",
777
+ "project_name": "Legacy Bug List",
778
+ "bugs": [
779
+ _valid_bug("B-001", "failed"),
780
+ _valid_bug("B-002", "pending", ["B-001"]),
781
+ _valid_bug("B-003", "pending", ["B-002"]),
782
+ _valid_bug("B-004", "completed"),
783
+ ],
784
+ }
785
+ plan_path.write_text(json.dumps(data), encoding="utf-8")
786
+
787
+ result = _run_bug_updater(plan_path, state_dir, "--action", "get_next")
788
+
789
+ assert result.returncode == 0, (result.stdout, result.stderr)
790
+ assert result.stdout.strip() == "PIPELINE_COMPLETE"
791
+ updated = json.loads(plan_path.read_text(encoding="utf-8"))
792
+ statuses = {bug["id"]: bug["status"] for bug in updated["bugs"]}
793
+ assert statuses == {
794
+ "B-001": "failed",
795
+ "B-002": "auto_skipped",
796
+ "B-003": "auto_skipped",
797
+ "B-004": "completed",
798
+ }
799
+
800
+ status_result = _run_bug_updater(plan_path, state_dir, "--action", "status")
801
+ assert status_result.returncode == 0
802
+ assert "Auto-skipped: 2" in status_result.stdout
803
+ assert "[⊘]" in status_result.stdout
804
+
805
+
806
+ def test_bugfix_auto_skip_write_failure_is_nonzero_without_partial_state(monkeypatch, tmp_path, capsys):
807
+ plan_path = tmp_path / "bug-fix-list.json"
808
+ state_dir = tmp_path / "state"
809
+ data = {
810
+ "$schema": "dev-pipeline-bug-fix-list-v1",
811
+ "project_name": "Write Failure",
812
+ "bugs": [
813
+ _valid_bug("B-001", "failed"),
814
+ _valid_bug("B-002", "pending", ["B-001"]),
815
+ ],
816
+ }
817
+ plan_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
818
+ before = plan_path.read_bytes()
819
+ module = _load_source_module(
820
+ "f060_update_bug_status_write_failure",
821
+ PIPELINE_ROOT / "scripts" / "update-bug-status.py",
822
+ )
823
+ monkeypatch.setattr(module, "write_json_file", lambda *_args, **_kwargs: "permission denied")
824
+
825
+ with pytest.raises(SystemExit):
826
+ module.auto_skip_blocked_bugs(str(plan_path), str(state_dir), "B-001")
827
+
828
+ output = capsys.readouterr().out
829
+ assert "Failed to write auto-skipped bugs" in output
830
+ assert plan_path.read_bytes() == before
831
+ assert _tree_snapshot(state_dir) == {}
832
+
833
+
834
+ def _load_source_module(name, path):
835
+ import importlib.util
836
+
837
+ spec = importlib.util.spec_from_file_location(name, path)
838
+ module = importlib.util.module_from_spec(spec)
839
+ assert spec.loader is not None
840
+ spec.loader.exec_module(module)
841
+ return module
842
+
843
+
844
+ def test_bugfix_auto_skipped_schema_validators_and_aggregation_are_consistent():
845
+ from prizmkit_runtime.daemon import PROGRESS_COUNTS_BY_KIND
846
+ from prizmkit_runtime.runner_models import family_for
847
+ from prizmkit_runtime.runners import _completion_compatible_statuses
848
+ from prizmkit_runtime.paths import resolve_runtime_paths
849
+
850
+ schema = json.loads(
851
+ (PIPELINE_ROOT / "templates" / "bug-fix-list-schema.json").read_text(encoding="utf-8")
852
+ )
853
+ status_enum = schema["properties"]["bugs"]["items"]["properties"]["status"]["enum"]
854
+ assert "auto_skipped" in status_enum
855
+
856
+ init_module = _load_source_module(
857
+ "f060_init_bugfix",
858
+ PIPELINE_ROOT / "scripts" / "init-bugfix-pipeline.py",
859
+ )
860
+ planner_module = _load_source_module(
861
+ "f060_validate_bug_list",
862
+ REPO_ROOT
863
+ / "core"
864
+ / "skills"
865
+ / "orchestration-skill"
866
+ / "pipelines"
867
+ / "bug-planner"
868
+ / "scripts"
869
+ / "validate-bug-list.py",
870
+ )
871
+ assert "auto_skipped" in init_module.VALID_STATUSES
872
+ assert "auto_skipped" in init_module.TERMINAL_STATUSES
873
+ assert "auto_skipped" in planner_module.VALID_STATUSES
874
+
875
+ bugs = [
876
+ _valid_bug("B-001", "failed"),
877
+ _valid_bug("B-002", "pending", ["B-001"]),
878
+ _valid_bug("B-003", "auto_skipped", ["B-001"]),
879
+ ]
880
+ init_errors, _seen = init_module.validate_bugs(bugs)
881
+ assert init_errors == []
882
+ planner_result = planner_module.validate(
883
+ {
884
+ "$schema": "dev-pipeline-bug-fix-list-v1",
885
+ "project_name": "Validator Test",
886
+ "bugs": bugs,
887
+ }
888
+ )
889
+ assert planner_result["valid"] is True, planner_result["errors"]
890
+
891
+ assert "auto_skipped" in PROGRESS_COUNTS_BY_KIND["bugfix"]
892
+ paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT, project_root=REPO_ROOT)
893
+ bug_family = family_for("bugfix", paths)
894
+ assert "auto_skipped" in _completion_compatible_statuses(bug_family)
895
+
896
+
897
+ def test_all_updaters_expose_only_reset_state_internal_recovery_action():
898
+ retired_action = "un" + "skip"
899
+ for contract in FAMILY_CONTRACTS.values():
900
+ source = (PIPELINE_ROOT / "scripts" / contract["script"]).read_text(encoding="utf-8")
901
+ assert retired_action not in source
902
+ assert '"reset_state"' in source
903
+ assert '"action": "reset_state"' in source
904
+
905
+
906
+ def test_canonical_reset_guidance_documents_every_explicit_mode_without_generated_outputs():
907
+ launcher_root = (
908
+ REPO_ROOT
909
+ / "core"
910
+ / "skills"
911
+ / "orchestration-skill"
912
+ / "pipelines"
913
+ )
914
+ launchers = {
915
+ "feature-pipeline-launcher": ("feature", "F-001"),
916
+ "bugfix-pipeline-launcher": ("bugfix", "B-001"),
917
+ "refactor-pipeline-launcher": ("refactor", "R-001"),
918
+ }
919
+ for launcher, (family, item_id) in launchers.items():
920
+ source = (launcher_root / launcher / "SKILL.md").read_text(encoding="utf-8")
921
+ assert f"reset {family} {item_id} --state-only" in source
922
+ assert f"reset {family} {item_id} --fresh-checkout" in source
923
+ assert f"reset {family} {item_id} --clean" in source
924
+ assert f"reset {family} --failed --preserve-runtime" in source
925
+ assert "exactly one mode" in source.lower()
926
+
927
+ memory_source = (REPO_ROOT / "core" / "templates" / "project-memory-template.md").read_text(
928
+ encoding="utf-8"
929
+ )
930
+ assert "reset <family> <task-id> --state-only" in memory_source
931
+ assert "reset <family> --state-only --all" in memory_source
932
+ assert "reset <family> <task-id-or-range> --fresh-checkout" in memory_source
933
+ assert "reset <family> <task-id-or-range> --clean" in memory_source
934
+ assert "reset <family> --failed --preserve-runtime" in memory_source
935
+ assert "<family> " + "un" + "skip" not in memory_source