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
@@ -110,8 +110,6 @@ def _write_semantic_completion_checkpoint(project_root, artifact_dir, workflow_t
110
110
  },
111
111
  "repair_rounds": 0,
112
112
  "production_changed": False,
113
- "review_required": False,
114
- "review_scope": None,
115
113
  "unresolved_items": [],
116
114
  }),
117
115
  encoding="utf-8",
@@ -306,9 +304,9 @@ def test_preserve_runtime_option_boundaries_and_removed_reset_run_do_not_mutate_
306
304
  invalid_cases = [
307
305
  (("F-001", "--run"), "no longer accepts --run"),
308
306
  (("--preserve-runtime",), "requires --failed"),
309
- (("--failed", "--preserve-runtime", "--clean"), "mutually exclusive"),
310
- (("F-001", "--failed", "--preserve-runtime"), "cannot target an ID or range"),
311
- (("F-001:F-003", "--failed", "--preserve-runtime"), "cannot target an ID or range"),
307
+ (("--failed", "--preserve-runtime", "--clean"), "Exactly one reset mode"),
308
+ (("F-001", "--failed", "--preserve-runtime"), "cannot target an ID, range, or --all"),
309
+ (("F-001:F-003", "--failed", "--preserve-runtime"), "cannot target an ID, range, or --all"),
312
310
  (("--failed", "--preserve-runtime", "--stalled"), "Unsupported reset arguments"),
313
311
  (("--failed", "--preserve-runtime", "--unknown"), "Unsupported reset arguments"),
314
312
  ]
@@ -441,7 +439,7 @@ def test_public_reset_cli_hard_rejects_run_for_every_family_without_state_change
441
439
  )
442
440
 
443
441
  assert completed.returncode == 2
444
- assert "Reset no longer accepts --run" in completed.stdout + completed.stderr
442
+ assert "unrecognized arguments: --run" in completed.stdout + completed.stderr
445
443
  assert not (project / ".prizmkit").exists()
446
444
 
447
445
 
@@ -503,7 +501,7 @@ def test_preserve_runtime_success_quotes_list_path_in_separate_run_instruction(t
503
501
  result = run_reset_command("feature", ("--failed", "--preserve-runtime"), paths)
504
502
 
505
503
  assert result.exit_code == 0
506
- assert f"feature run {shlex.quote(str(plan.resolve()))}" in result.render()
504
+ assert f"run feature {shlex.quote(str(plan.resolve()))}" in result.render()
507
505
 
508
506
 
509
507
  def test_preserve_runtime_requires_recorded_base_to_be_a_local_branch(tmp_path):
@@ -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