prizmkit 1.1.154 → 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 (157) 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 +11 -1
  116. package/bundled/skills/prizmkit-retrospective/SKILL.md +63 -77
  117. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +3 -3
  118. package/bundled/skills/prizmkit-retrospective/references/retrospective-result-schema.json +138 -0
  119. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +17 -23
  120. package/bundled/skills/prizmkit-test/SKILL.md +64 -51
  121. package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +206 -0
  122. package/bundled/skills/prizmkit-test/references/independent-test-review.md +17 -9
  123. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +45 -21
  124. package/bundled/skills/prizmkit-test/references/test-report-template.md +20 -13
  125. package/bundled/skills/prizmkit-workflow/SKILL.md +35 -37
  126. package/bundled/skills/prizmkit-workflow/references/artifact-identity.md +34 -0
  127. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +22 -18
  128. package/bundled/skills/recovery-workflow/SKILL.md +2 -1
  129. package/bundled/skills/recovery-workflow/references/detection.md +4 -3
  130. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +15 -5
  131. package/package.json +1 -1
  132. package/src/scaffold.js +33 -22
  133. package/bundled/dev-pipeline/templates/sections/bugfix-artifacts.md +0 -16
  134. package/bundled/dev-pipeline/templates/sections/bugfix-session-status.md +0 -31
  135. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -7
  136. package/bundled/dev-pipeline/templates/sections/critical-paths-lite.md +0 -7
  137. package/bundled/dev-pipeline/templates/sections/directory-convention-agent.md +0 -9
  138. package/bundled/dev-pipeline/templates/sections/directory-convention-lite.md +0 -6
  139. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +0 -15
  140. package/bundled/dev-pipeline/templates/sections/phase-commit.md +0 -61
  141. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +0 -24
  142. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +0 -12
  143. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +0 -44
  144. package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +0 -25
  145. package/bundled/dev-pipeline/templates/sections/phase-plan-agent.md +0 -20
  146. package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +0 -20
  147. package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +0 -20
  148. package/bundled/dev-pipeline/templates/sections/refactor-artifacts.md +0 -17
  149. package/bundled/dev-pipeline/templates/sections/refactor-session-status.md +0 -28
  150. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +0 -52
  151. package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +0 -52
  152. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +0 -174
  153. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +0 -174
  154. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +0 -174
  155. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +0 -174
  156. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +0 -174
  157. 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