prizmkit 1.1.154 → 1.1.156

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 (164) 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 +21 -27
  6. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +341 -15
  7. package/bundled/dev-pipeline/prizmkit_runtime/cli.py +0 -3
  8. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +1 -15
  9. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +48 -14
  10. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +50 -2
  11. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -9
  12. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +3 -3
  13. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +120 -22
  14. package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +482 -0
  15. package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +64 -11
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +145 -367
  17. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +91 -62
  18. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +105 -69
  19. package/bundled/dev-pipeline/scripts/prompt_framework.py +36 -41
  20. package/bundled/dev-pipeline/scripts/update-bug-status.py +1 -4
  21. package/bundled/dev-pipeline/scripts/update-checkpoint.py +61 -13
  22. package/bundled/dev-pipeline/scripts/update-feature-status.py +1 -4
  23. package/bundled/dev-pipeline/scripts/update-refactor-status.py +1 -4
  24. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +22 -27
  25. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +71 -104
  26. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +59 -111
  27. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +37 -72
  28. package/bundled/dev-pipeline/templates/sections/artifact-manifest.md +39 -0
  29. package/bundled/dev-pipeline/templates/sections/bugfix-critical-paths.md +3 -1
  30. package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +8 -25
  31. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +3 -3
  32. package/bundled/dev-pipeline/templates/sections/bugfix-phase-implement.md +2 -2
  33. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
  34. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +2 -2
  35. package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +4 -4
  36. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +0 -1
  37. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +7 -4
  38. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +14 -27
  39. package/bundled/dev-pipeline/templates/sections/feature-completion-summary.md +20 -0
  40. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +8 -71
  41. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +7 -7
  42. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +3 -3
  43. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
  44. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +5 -17
  45. package/bundled/dev-pipeline/templates/sections/refactor-critical-paths.md +3 -1
  46. package/bundled/dev-pipeline/templates/sections/refactor-phase-commit-report.md +10 -23
  47. package/bundled/dev-pipeline/templates/sections/refactor-phase-implement.md +2 -2
  48. package/bundled/dev-pipeline/templates/sections/refactor-phase-plan.md +2 -2
  49. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
  50. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +2 -2
  51. package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +7 -7
  52. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +0 -1
  53. package/bundled/dev-pipeline/templates/sections/retrospective.md +12 -0
  54. package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +35 -0
  55. package/bundled/dev-pipeline/templates/session-status-schema.json +45 -62
  56. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +469 -18
  57. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +262 -175
  58. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +62 -23
  59. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +76 -28
  60. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +168 -18
  61. package/bundled/dev-pipeline/tests/test_recovery_workflow.py +111 -0
  62. package/bundled/dev-pipeline/tests/test_reset_preserve.py +0 -2
  63. package/bundled/dev-pipeline/tests/test_runtime_commit.py +608 -0
  64. package/bundled/dev-pipeline/tests/test_unified_cli.py +167 -25
  65. package/bundled/skills/_metadata.json +9 -9
  66. package/bundled/skills/app-planner/SKILL.md +3 -3
  67. package/bundled/skills/app-planner/references/generated-plan-review.md +2 -1
  68. package/bundled/skills/app-planner/references/rules/backend/derivation-rules.md +56 -64
  69. package/bundled/skills/app-planner/references/rules/backend/fixed-rules.md +85 -240
  70. package/bundled/skills/app-planner/references/rules/backend/question-bank.md +25 -25
  71. package/bundled/skills/app-planner/references/rules/backend/question-manifest.json +16 -7
  72. package/bundled/skills/app-planner/references/rules/backend/template.md +74 -92
  73. package/bundled/skills/app-planner/references/rules/database/derivation-rules.md +93 -93
  74. package/bundled/skills/app-planner/references/rules/database/fixed-rules.md +66 -173
  75. package/bundled/skills/app-planner/references/rules/database/question-bank.md +31 -26
  76. package/bundled/skills/app-planner/references/rules/database/question-manifest.json +13 -4
  77. package/bundled/skills/app-planner/references/rules/database/template.md +76 -98
  78. package/bundled/skills/app-planner/references/rules/frontend/derivation-rules.md +68 -95
  79. package/bundled/skills/app-planner/references/rules/frontend/fixed-rules.md +93 -166
  80. package/bundled/skills/app-planner/references/rules/frontend/question-bank.md +46 -36
  81. package/bundled/skills/app-planner/references/rules/frontend/question-manifest.json +11 -2
  82. package/bundled/skills/app-planner/references/rules/frontend/template.md +79 -257
  83. package/bundled/skills/app-planner/references/rules/mobile/derivation-rules.md +91 -99
  84. package/bundled/skills/app-planner/references/rules/mobile/fixed-rules.md +73 -246
  85. package/bundled/skills/app-planner/references/rules/mobile/question-bank.md +17 -17
  86. package/bundled/skills/app-planner/references/rules/mobile/question-manifest.json +17 -2
  87. package/bundled/skills/app-planner/references/rules/mobile/template.md +73 -85
  88. package/bundled/skills/app-planner/references/rules-configuration.md +133 -65
  89. package/bundled/skills/app-planner/scripts/validate-rules-configuration.py +291 -0
  90. package/bundled/skills/prizmkit/SKILL.md +17 -18
  91. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +22 -18
  92. package/bundled/skills/prizmkit-code-review/SKILL.md +22 -58
  93. package/bundled/skills/prizmkit-code-review/references/independent-code-review.md +6 -6
  94. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +2 -2
  95. package/bundled/skills/prizmkit-committer/SKILL.md +82 -132
  96. package/bundled/skills/prizmkit-deploy/SKILL.md +27 -11
  97. package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +89 -0
  98. package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +44 -14
  99. package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +19 -3
  100. package/bundled/skills/prizmkit-deploy/references/database-setup.md +22 -12
  101. package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +3 -1
  102. package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +22 -6
  103. package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +49 -0
  104. package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +1 -1
  105. package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +164 -0
  106. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +9 -8
  107. package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +6 -6
  108. package/bundled/skills/prizmkit-implement/SKILL.md +30 -66
  109. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +4 -4
  110. package/bundled/skills/prizmkit-init/SKILL.md +4 -0
  111. package/bundled/skills/prizmkit-plan/SKILL.md +21 -64
  112. package/bundled/skills/prizmkit-plan/references/artifact-identity.md +38 -0
  113. package/bundled/skills/prizmkit-plan/references/independent-plan-review.md +4 -4
  114. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +5 -5
  115. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +75 -11
  116. package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +191 -56
  117. package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +18 -18
  118. package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +14 -8
  119. package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +12 -5
  120. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +23 -12
  121. package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +13 -9
  122. package/bundled/skills/prizmkit-retrospective/SKILL.md +94 -97
  123. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +79 -37
  124. package/bundled/skills/prizmkit-retrospective/references/retrospective-result-schema.json +138 -0
  125. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +111 -54
  126. package/bundled/skills/prizmkit-test/SKILL.md +64 -51
  127. package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +206 -0
  128. package/bundled/skills/prizmkit-test/references/independent-test-review.md +17 -9
  129. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +45 -21
  130. package/bundled/skills/prizmkit-test/references/test-report-template.md +20 -13
  131. package/bundled/skills/prizmkit-workflow/SKILL.md +35 -37
  132. package/bundled/skills/prizmkit-workflow/references/artifact-identity.md +34 -0
  133. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +22 -18
  134. package/bundled/skills/recovery-workflow/SKILL.md +2 -1
  135. package/bundled/skills/recovery-workflow/references/detection.md +4 -3
  136. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +15 -5
  137. package/bundled/templates/hooks/validate-prizm-docs.py +124 -12
  138. package/package.json +1 -1
  139. package/src/scaffold.js +33 -22
  140. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +0 -16
  141. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +0 -31
  142. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -7
  143. package/bundled/dev-pipeline/templates/sections/critical-paths-lite.md +0 -7
  144. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +0 -9
  145. package/bundled/dev-pipeline/templates/sections/directory-convention-lite.md +0 -6
  146. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +0 -15
  147. package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -61
  148. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -24
  149. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -12
  150. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -44
  151. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -25
  152. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +0 -20
  153. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +0 -20
  154. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -20
  155. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +0 -17
  156. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +0 -28
  157. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +0 -52
  158. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +0 -52
  159. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +0 -174
  160. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +0 -174
  161. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +0 -174
  162. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +0 -174
  163. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +0 -174
  164. package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +0 -119
@@ -0,0 +1,608 @@
1
+ """Tests for runtime-owned local commit execution and finalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from prizmkit_runtime.checkpoint_state import load_checkpoint_state
12
+ from prizmkit_runtime.runtime_commit import finalize_runtime_commit
13
+
14
+
15
+ def _git(root: Path, *args: str) -> str:
16
+ completed = subprocess.run(
17
+ ["git", *args],
18
+ cwd=root,
19
+ check=True,
20
+ stdout=subprocess.PIPE,
21
+ stderr=subprocess.PIPE,
22
+ text=True,
23
+ )
24
+ return completed.stdout.strip()
25
+
26
+
27
+ def _init_repo(root: Path) -> str:
28
+ _git(root, "init")
29
+ _git(root, "config", "user.email", "runtime@example.invalid")
30
+ _git(root, "config", "user.name", "Runtime Test")
31
+ (root / "tracked.txt").write_text("before\n", encoding="utf-8")
32
+ (root / ".gitignore").write_text(".prizmkit/\n", encoding="utf-8")
33
+ _git(root, "add", "tracked.txt", ".gitignore")
34
+ _git(root, "commit", "-m", "initial")
35
+ return _git(root, "rev-parse", "HEAD")
36
+
37
+
38
+ def _write_test_artifacts(artifact_dir: Path) -> None:
39
+ artifact_dir.mkdir(parents=True, exist_ok=True)
40
+ for filename in ("context-snapshot.md", "spec.md", "plan.md"):
41
+ (artifact_dir / filename).write_text(f"# {filename}\n", encoding="utf-8")
42
+ (artifact_dir / "review-report.md").write_text(
43
+ "# Review Report\n\n## Final Result\n\n- Verdict: PASS\n",
44
+ encoding="utf-8",
45
+ )
46
+ (artifact_dir / "test-report.md").write_text(
47
+ "# Test Report\n\n## Final Result\n\n- Result: TEST_PASS\n",
48
+ encoding="utf-8",
49
+ )
50
+ (artifact_dir / "test-result.json").write_text(
51
+ json.dumps(
52
+ {
53
+ "schema_version": 1,
54
+ "result": "TEST_PASS",
55
+ "report": "test-report.md",
56
+ "main_review_rounds": 1,
57
+ "independent_review": {
58
+ "status": "downgraded",
59
+ "responses": 0,
60
+ "downgrade_reason": "no eligible reviewer",
61
+ "final_state_rechecked": False,
62
+ },
63
+ "repair_rounds": 0,
64
+ "production_changed": False,
65
+ "unresolved_items": [],
66
+ }
67
+ ),
68
+ encoding="utf-8",
69
+ )
70
+ (artifact_dir / "completion-summary.json").write_text(
71
+ '{"completion_notes":["Runtime commit fixture"]}\n', encoding="utf-8"
72
+ )
73
+ (artifact_dir / "retrospective-result.json").write_text(
74
+ json.dumps(
75
+ {
76
+ "schema_version": 1,
77
+ "outcome": "RETRO_COMPLETE",
78
+ "result": "NO_DOC_CHANGE",
79
+ "reason": "No durable documentation change in fixture",
80
+ "change_paths": ["tracked.txt"],
81
+ "documentation_paths": [],
82
+ "validation": [],
83
+ }
84
+ ),
85
+ encoding="utf-8",
86
+ )
87
+
88
+
89
+ def _write_commit_ready_checkpoint(root: Path) -> tuple[Path, Path]:
90
+ artifact_dir = root / ".prizmkit" / "specs" / "001-runtime-commit"
91
+ _write_test_artifacts(artifact_dir)
92
+ skills = [
93
+ "context-snapshot-and-plan",
94
+ "prizmkit-implement",
95
+ "prizmkit-code-review",
96
+ "prizmkit-test",
97
+ "completion-summary",
98
+ "prizmkit-retrospective",
99
+ "prizmkit-committer",
100
+ ]
101
+ steps = []
102
+ previous = None
103
+ for index, skill in enumerate(skills, start=1):
104
+ status = "in_progress" if skill == "prizmkit-committer" else "completed"
105
+ step = {
106
+ "id": f"S{index:02d}",
107
+ "skill": skill,
108
+ "name": skill,
109
+ "status": status,
110
+ "required_artifacts": [],
111
+ "depends_on": previous,
112
+ }
113
+ if skill == "context-snapshot-and-plan":
114
+ step["stage_result"] = "PLAN_READY"
115
+ step["required_artifacts"] = [
116
+ ".prizmkit/specs/001-runtime-commit/context-snapshot.md",
117
+ ".prizmkit/specs/001-runtime-commit/spec.md",
118
+ ".prizmkit/specs/001-runtime-commit/plan.md",
119
+ ]
120
+ elif skill == "prizmkit-implement":
121
+ step["stage_result"] = "IMPLEMENTED"
122
+ elif skill == "prizmkit-code-review":
123
+ step["stage_result"] = "REVIEW_PASS"
124
+ step["required_artifacts"] = [
125
+ ".prizmkit/specs/001-runtime-commit/review-report.md"
126
+ ]
127
+ elif skill == "prizmkit-test":
128
+ step["stage_result"] = "TEST_PASS"
129
+ step["required_artifacts"] = [
130
+ ".prizmkit/specs/001-runtime-commit/test-report.md",
131
+ ".prizmkit/specs/001-runtime-commit/test-result.json",
132
+ ]
133
+ elif skill == "completion-summary":
134
+ step["required_artifacts"] = [
135
+ ".prizmkit/specs/001-runtime-commit/completion-summary.json"
136
+ ]
137
+ elif skill == "prizmkit-retrospective":
138
+ step["stage_result"] = "RETRO_COMPLETE"
139
+ step["required_artifacts"] = [
140
+ ".prizmkit/specs/001-runtime-commit/retrospective-result.json"
141
+ ]
142
+ elif skill == "prizmkit-committer":
143
+ step["stage_result"] = "COMMIT_PENDING"
144
+ step["required_artifacts"] = [
145
+ ".prizmkit/specs/001-runtime-commit/runtime-commit-request.json"
146
+ ]
147
+ steps.append(step)
148
+ previous = step["id"]
149
+ checkpoint = {
150
+ "version": 1,
151
+ "workflow_type": "feature-pipeline",
152
+ "item_id": "F-001",
153
+ "item_slug": "001-runtime-commit",
154
+ "session_id": "session-1",
155
+ "steps": steps,
156
+ "feature_state": {
157
+ "schema_version": 1,
158
+ "artifact_dir": ".prizmkit/specs/001-runtime-commit",
159
+ "orchestrator": "pipeline-runtime",
160
+ "stage": "committer",
161
+ "current_stage": "prizmkit-committer",
162
+ "status": "in_progress",
163
+ "stage_result": "COMMIT_PENDING",
164
+ "repair_round": 0,
165
+ "next_stage": "prizmkit-committer",
166
+ "terminal_status": None,
167
+ },
168
+ }
169
+ checkpoint_path = artifact_dir / "workflow-checkpoint.json"
170
+ checkpoint_path.write_text(json.dumps(checkpoint), encoding="utf-8")
171
+ return artifact_dir, checkpoint_path
172
+
173
+
174
+ def _write_request(root: Path, artifact_dir: Path, base_head: str, paths: list[str]) -> Path:
175
+ request_path = artifact_dir / "runtime-commit-request.json"
176
+ request_path.write_text(
177
+ json.dumps(
178
+ {
179
+ "schema_version": 1,
180
+ "artifact_dir": artifact_dir.relative_to(root).as_posix(),
181
+ "base_head": base_head,
182
+ "commit_message": "fix(runtime): finalize prepared task",
183
+ "intended_paths": paths,
184
+ }
185
+ ),
186
+ encoding="utf-8",
187
+ )
188
+ return request_path
189
+
190
+
191
+ def test_runtime_commit_executes_exact_request_and_finalizes_only_checkpoint(tmp_path):
192
+ base_head = _init_repo(tmp_path)
193
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
194
+ caller_state = tmp_path / ".prizmkit" / "state" / "workflows" / "caller.json"
195
+ caller_state.parent.mkdir(parents=True, exist_ok=True)
196
+ caller_state.write_text('{"owner":"external-coordinator"}\n', encoding="utf-8")
197
+ expected_caller_state = caller_state.read_bytes()
198
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
199
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
200
+
201
+ result = finalize_runtime_commit(
202
+ tmp_path,
203
+ artifact_dir=artifact_dir,
204
+ checkpoint_path=checkpoint_path,
205
+ )
206
+
207
+ assert result.status == "committed"
208
+ assert result.commit_hash == _git(tmp_path, "rev-parse", "HEAD")
209
+ assert _git(tmp_path, "log", "-1", "--pretty=%s") == "fix(runtime): finalize prepared task"
210
+ checkpoint = json.loads(checkpoint_path.read_text(encoding="utf-8"))
211
+ committer = next(step for step in checkpoint["steps"] if step["skill"] == "prizmkit-committer")
212
+ assert committer["status"] == "completed"
213
+ assert committer["stage_result"] == "COMMITTED"
214
+ assert checkpoint["feature_state"]["terminal_status"] == "WORKFLOW_COMPLETED"
215
+ assert checkpoint["feature_state"]["runtime_base_head"] == base_head
216
+ assert checkpoint["feature_state"]["runtime_commit_hash"] == result.commit_hash
217
+ assert caller_state.read_bytes() == expected_caller_state
218
+ assert not (artifact_dir / "runtime-commit-request.json").exists()
219
+ assert load_checkpoint_state(
220
+ checkpoint_path,
221
+ project_root=tmp_path,
222
+ ).semantic_complete is True
223
+
224
+
225
+ @pytest.mark.parametrize(
226
+ "durable_path",
227
+ [
228
+ ".prizmkit/config.json",
229
+ ".prizmkit/prizm-docs/src/counter.prizm",
230
+ ],
231
+ )
232
+ def test_runtime_commit_excludes_tracked_prizmkit_internal_paths(tmp_path, durable_path):
233
+ _init_repo(tmp_path)
234
+ durable = tmp_path / durable_path
235
+ durable.parent.mkdir(parents=True, exist_ok=True)
236
+ durable.write_text("before\n", encoding="utf-8")
237
+ _git(tmp_path, "add", "-f", durable_path)
238
+ _git(tmp_path, "commit", "-m", "track durable PrizmKit project data")
239
+ base_head = _git(tmp_path, "rev-parse", "HEAD")
240
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
241
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
242
+ durable.write_text("after\n", encoding="utf-8")
243
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
244
+
245
+ result = finalize_runtime_commit(
246
+ tmp_path,
247
+ artifact_dir=artifact_dir,
248
+ checkpoint_path=checkpoint_path,
249
+ )
250
+
251
+ assert result.status == "committed", result
252
+ assert _git(
253
+ tmp_path,
254
+ "diff-tree",
255
+ "--no-commit-id",
256
+ "--name-only",
257
+ "-r",
258
+ "HEAD",
259
+ ) == "tracked.txt"
260
+ assert durable.read_text(encoding="utf-8") == "after\n"
261
+ assert durable_path in _git(
262
+ tmp_path,
263
+ "status",
264
+ "--short",
265
+ "--untracked-files=all",
266
+ )
267
+
268
+
269
+ def test_runtime_commit_ignores_runner_support_paths_but_leaves_them_unmodified(tmp_path):
270
+ base_head = _init_repo(tmp_path)
271
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
272
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
273
+ (tmp_path / "AGENTS.md").write_text("@./AGENTS.private.md\n", encoding="utf-8")
274
+ codex_config = tmp_path / ".codex" / "config.toml"
275
+ codex_config.parent.mkdir()
276
+ codex_config.write_text("[project]\ntrusted = true\n", encoding="utf-8")
277
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
278
+
279
+ result = finalize_runtime_commit(
280
+ tmp_path,
281
+ artifact_dir=artifact_dir,
282
+ checkpoint_path=checkpoint_path,
283
+ )
284
+
285
+ assert result.status == "committed"
286
+ assert _git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD") == "tracked.txt"
287
+ assert (tmp_path / "AGENTS.md").read_text(encoding="utf-8") == "@./AGENTS.private.md\n"
288
+ assert codex_config.read_text(encoding="utf-8") == "[project]\ntrusted = true\n"
289
+ untracked = set(_git(tmp_path, "ls-files", "--others", "--exclude-standard").splitlines())
290
+ assert {"AGENTS.md", ".codex/config.toml"}.issubset(untracked)
291
+
292
+
293
+ def test_runtime_commit_allows_task_path_adjacent_to_internal_namespace(tmp_path):
294
+ base_head = _init_repo(tmp_path)
295
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
296
+ adjacent_path = tmp_path / ".prizmkitx" / "task.txt"
297
+ adjacent_path.parent.mkdir()
298
+ adjacent_path.write_text("task-owned\n", encoding="utf-8")
299
+ _write_request(tmp_path, artifact_dir, base_head, [".prizmkitx/task.txt"])
300
+
301
+ result = finalize_runtime_commit(
302
+ tmp_path,
303
+ artifact_dir=artifact_dir,
304
+ checkpoint_path=checkpoint_path,
305
+ )
306
+
307
+ assert result.status == "committed"
308
+ assert _git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD") == ".prizmkitx/task.txt"
309
+
310
+
311
+ def test_runtime_commit_ignores_project_gitignored_transient_data(tmp_path):
312
+ _init_repo(tmp_path)
313
+ gitignore = tmp_path / ".gitignore"
314
+ gitignore.write_text(gitignore.read_text(encoding="utf-8") + "tmp/\n", encoding="utf-8")
315
+ _git(tmp_path, "add", ".gitignore")
316
+ _git(tmp_path, "commit", "-m", "ignore project transient data")
317
+ base_head = _git(tmp_path, "rev-parse", "HEAD")
318
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
319
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
320
+ transient = tmp_path / "tmp" / "tool-cache.json"
321
+ transient.parent.mkdir()
322
+ transient.write_text('{"cache":true}\n', encoding="utf-8")
323
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
324
+
325
+ result = finalize_runtime_commit(
326
+ tmp_path,
327
+ artifact_dir=artifact_dir,
328
+ checkpoint_path=checkpoint_path,
329
+ )
330
+
331
+ assert result.status == "committed"
332
+ assert _git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD") == "tracked.txt"
333
+ assert transient.read_text(encoding="utf-8") == '{"cache":true}\n'
334
+
335
+
336
+ def test_runtime_commit_rejects_unlisted_git_visible_change(tmp_path):
337
+ base_head = _init_repo(tmp_path)
338
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
339
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
340
+ (tmp_path / "unexpected.txt").write_text("unexpected\n", encoding="utf-8")
341
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
342
+
343
+ result = finalize_runtime_commit(
344
+ tmp_path,
345
+ artifact_dir=artifact_dir,
346
+ checkpoint_path=checkpoint_path,
347
+ )
348
+
349
+ assert result.status == "failed"
350
+ assert result.reason == "intended_paths_mismatch"
351
+ assert _git(tmp_path, "rev-parse", "HEAD") == base_head
352
+
353
+
354
+ def test_runtime_commit_recovers_commit_created_before_state_finalization(tmp_path):
355
+ base_head = _init_repo(tmp_path)
356
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
357
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
358
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
359
+ _git(tmp_path, "add", "tracked.txt")
360
+ _git(tmp_path, "commit", "-m", "fix(runtime): finalize prepared task")
361
+ committed_head = _git(tmp_path, "rev-parse", "HEAD")
362
+
363
+ result = finalize_runtime_commit(
364
+ tmp_path,
365
+ artifact_dir=artifact_dir,
366
+ checkpoint_path=checkpoint_path,
367
+ )
368
+
369
+ assert result.status == "committed"
370
+ assert result.commit_hash == committed_head
371
+ assert result.recovered is True
372
+ assert _git(tmp_path, "rev-list", "--count", f"{base_head}..HEAD") == "1"
373
+
374
+
375
+ def test_runtime_commit_recovers_after_checkpoint_finalization_failure(tmp_path, monkeypatch):
376
+ import prizmkit_runtime.runtime_commit as runtime_commit
377
+
378
+ base_head = _init_repo(tmp_path)
379
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
380
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
381
+ request_path = _write_request(
382
+ tmp_path, artifact_dir, base_head, ["tracked.txt"]
383
+ )
384
+
385
+ original_finalize = runtime_commit._finalize_checkpoint
386
+ monkeypatch.setattr(
387
+ runtime_commit,
388
+ "_finalize_checkpoint",
389
+ lambda _path, **_kwargs: "checkpoint_commit_finalization_failed",
390
+ )
391
+ first = finalize_runtime_commit(
392
+ tmp_path,
393
+ artifact_dir=artifact_dir,
394
+ checkpoint_path=checkpoint_path,
395
+ )
396
+
397
+ assert first.status == "failed"
398
+ assert first.reason == "checkpoint_commit_finalization_failed"
399
+ committed_head = _git(tmp_path, "rev-parse", "HEAD")
400
+ request = json.loads(request_path.read_text(encoding="utf-8"))
401
+ assert request["runtime_commit_hash"] == committed_head
402
+
403
+ monkeypatch.setattr(runtime_commit, "_finalize_checkpoint", original_finalize)
404
+ second = finalize_runtime_commit(
405
+ tmp_path,
406
+ artifact_dir=artifact_dir,
407
+ checkpoint_path=checkpoint_path,
408
+ )
409
+
410
+ assert second.status == "committed"
411
+ assert second.recovered is True
412
+ assert second.commit_hash == committed_head
413
+ assert _git(tmp_path, "rev-list", "--count", f"{base_head}..HEAD") == "1"
414
+
415
+
416
+ def test_runtime_commit_recovers_cleanup_after_completed_checkpoint(tmp_path, monkeypatch):
417
+ base_head = _init_repo(tmp_path)
418
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
419
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
420
+ request_path = _write_request(
421
+ tmp_path, artifact_dir, base_head, ["tracked.txt"]
422
+ )
423
+
424
+ original_unlink = Path.unlink
425
+
426
+ def fail_request_cleanup(path, *args, **kwargs):
427
+ if path == request_path:
428
+ raise OSError("simulated cleanup failure")
429
+ return original_unlink(path, *args, **kwargs)
430
+
431
+ monkeypatch.setattr(Path, "unlink", fail_request_cleanup)
432
+ first = finalize_runtime_commit(
433
+ tmp_path,
434
+ artifact_dir=artifact_dir,
435
+ checkpoint_path=checkpoint_path,
436
+ )
437
+
438
+ assert first.status == "failed"
439
+ assert first.reason == "commit_request_cleanup_failed"
440
+ assert request_path.is_file()
441
+
442
+ monkeypatch.setattr(Path, "unlink", original_unlink)
443
+ second = finalize_runtime_commit(
444
+ tmp_path,
445
+ artifact_dir=artifact_dir,
446
+ checkpoint_path=checkpoint_path,
447
+ )
448
+
449
+ assert second.status == "committed"
450
+ assert second.recovered is True
451
+ assert not request_path.exists()
452
+ assert _git(tmp_path, "rev-list", "--count", f"{base_head}..HEAD") == "1"
453
+
454
+
455
+ def test_runtime_commit_disables_repository_hooks(tmp_path):
456
+ base_head = _init_repo(tmp_path)
457
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
458
+ hook = tmp_path / ".git" / "hooks" / "pre-commit"
459
+ hook.write_text(
460
+ "#!/bin/sh\nprintf 'hook mutation\\n' > hook-output.txt\ngit add hook-output.txt\n",
461
+ encoding="utf-8",
462
+ )
463
+ hook.chmod(0o755)
464
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
465
+ _write_request(tmp_path, artifact_dir, base_head, ["tracked.txt"])
466
+
467
+ result = finalize_runtime_commit(
468
+ tmp_path,
469
+ artifact_dir=artifact_dir,
470
+ checkpoint_path=checkpoint_path,
471
+ )
472
+
473
+ assert result.status == "committed"
474
+ assert not (tmp_path / "hook-output.txt").exists()
475
+ assert _git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD") == "tracked.txt"
476
+
477
+
478
+ def test_runtime_commit_handles_rename_as_exact_delete_and_add(tmp_path):
479
+ _init_repo(tmp_path)
480
+ old_path = tmp_path / "old-name.txt"
481
+ old_path.write_text("rename me\n", encoding="utf-8")
482
+ _git(tmp_path, "add", "old-name.txt")
483
+ _git(tmp_path, "commit", "-m", "add rename source")
484
+ base_head = _git(tmp_path, "rev-parse", "HEAD")
485
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
486
+ old_path.rename(tmp_path / "new-name.txt")
487
+ _write_request(
488
+ tmp_path,
489
+ artifact_dir,
490
+ base_head,
491
+ ["old-name.txt", "new-name.txt"],
492
+ )
493
+
494
+ result = finalize_runtime_commit(
495
+ tmp_path,
496
+ artifact_dir=artifact_dir,
497
+ checkpoint_path=checkpoint_path,
498
+ )
499
+
500
+ assert result.status == "committed"
501
+ changed = _git(
502
+ tmp_path,
503
+ "diff-tree",
504
+ "--no-commit-id",
505
+ "--name-only",
506
+ "--no-renames",
507
+ "-r",
508
+ "HEAD",
509
+ ).splitlines()
510
+ assert set(changed) == {"old-name.txt", "new-name.txt"}
511
+
512
+
513
+ @pytest.mark.parametrize(
514
+ "unsafe_path",
515
+ [
516
+ "../escape.txt",
517
+ ".git/config",
518
+ ".env",
519
+ "AGENTS.md",
520
+ "skills-lock.json",
521
+ ".codex/config.toml",
522
+ ".CLAUDE/settings.local.json",
523
+ ".prizmkit/config.json",
524
+ ".prizmkit/prizm-docs/src/counter.prizm",
525
+ ".prizmkit/state/runtime.json",
526
+ ".prizmkit/dev-pipeline/cli.py",
527
+ ".prizmkit/scripts/helper.py",
528
+ ".prizmkit/manifest.json",
529
+ ".prizmkit/specs/001-runtime-commit/workflow-checkpoint.json",
530
+ ".prizmkit/specs/001-runtime-commit/runtime-commit-request.json",
531
+ ],
532
+ )
533
+ def test_runtime_commit_rejects_unsafe_intended_paths(tmp_path, unsafe_path):
534
+ base_head = _init_repo(tmp_path)
535
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
536
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
537
+ _write_request(tmp_path, artifact_dir, base_head, [unsafe_path])
538
+
539
+ result = finalize_runtime_commit(
540
+ tmp_path,
541
+ artifact_dir=artifact_dir,
542
+ checkpoint_path=checkpoint_path,
543
+ )
544
+
545
+ assert result.status == "failed"
546
+ assert result.reason == "commit_request_path_unsafe"
547
+ assert _git(tmp_path, "rev-parse", "HEAD") == base_head
548
+
549
+
550
+ def test_runtime_commit_rejects_caller_orchestration_fields(tmp_path):
551
+ base_head = _init_repo(tmp_path)
552
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
553
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
554
+ request_path = _write_request(
555
+ tmp_path, artifact_dir, base_head, ["tracked.txt"]
556
+ )
557
+ request = json.loads(request_path.read_text(encoding="utf-8"))
558
+ request["caller_state_path"] = ".runtime/caller-state.json"
559
+ request_path.write_text(json.dumps(request), encoding="utf-8")
560
+
561
+ result = finalize_runtime_commit(
562
+ tmp_path,
563
+ artifact_dir=artifact_dir,
564
+ checkpoint_path=checkpoint_path,
565
+ )
566
+
567
+ assert result.status == "failed"
568
+ assert result.reason == "commit_request_invalid_schema"
569
+ assert _git(tmp_path, "rev-parse", "HEAD") == base_head
570
+
571
+
572
+ def test_runtime_commit_rejects_ai_supplied_premature_runtime_hash(tmp_path):
573
+ base_head = _init_repo(tmp_path)
574
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
575
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
576
+ request_path = _write_request(
577
+ tmp_path, artifact_dir, base_head, ["tracked.txt"]
578
+ )
579
+ request = json.loads(request_path.read_text(encoding="utf-8"))
580
+ request["runtime_commit_hash"] = "a" * 40
581
+ request_path.write_text(json.dumps(request), encoding="utf-8")
582
+
583
+ result = finalize_runtime_commit(
584
+ tmp_path,
585
+ artifact_dir=artifact_dir,
586
+ checkpoint_path=checkpoint_path,
587
+ )
588
+
589
+ assert result.status == "failed"
590
+ assert result.reason == "premature_runtime_commit_hash"
591
+ assert _git(tmp_path, "rev-parse", "HEAD") == base_head
592
+
593
+
594
+ def test_runtime_commit_rejects_stale_base_head(tmp_path):
595
+ base_head = _init_repo(tmp_path)
596
+ artifact_dir, checkpoint_path = _write_commit_ready_checkpoint(tmp_path)
597
+ (tmp_path / "tracked.txt").write_text("after\n", encoding="utf-8")
598
+ _write_request(tmp_path, artifact_dir, "a" * 40, ["tracked.txt"])
599
+
600
+ result = finalize_runtime_commit(
601
+ tmp_path,
602
+ artifact_dir=artifact_dir,
603
+ checkpoint_path=checkpoint_path,
604
+ )
605
+
606
+ assert result.status == "failed"
607
+ assert result.reason == "base_head_mismatch"
608
+ assert _git(tmp_path, "rev-parse", "HEAD") == base_head