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,482 @@
1
+ """Runtime-owned local commit execution for prepared pipeline tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import tempfile
10
+ from dataclasses import dataclass
11
+ from pathlib import Path, PurePosixPath
12
+ from typing import Mapping, Sequence
13
+
14
+ from .checkpoint_state import (
15
+ load_checkpoint_state,
16
+ semantic_state_key,
17
+ validate_checkpoint_data,
18
+ )
19
+ from .gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES
20
+
21
+ REQUEST_FILENAME = "runtime-commit-request.json"
22
+ REQUEST_SCHEMA_VERSION = 1
23
+ _HEX_COMMIT = re.compile(r"^[0-9a-fA-F]{40,64}$")
24
+ _FORBIDDEN_EXACT_PATHS = {
25
+ ".claude/settings.local.json",
26
+ ".codebuddy/settings.local.json",
27
+ ".codex/settings.local.json",
28
+ ".agents/settings.local.json",
29
+ ".pi/settings.local.json",
30
+ }
31
+ _SENSITIVE_NAMES = {
32
+ ".env",
33
+ "id_rsa",
34
+ "id_dsa",
35
+ "id_ed25519",
36
+ "credentials.json",
37
+ "secrets.json",
38
+ }
39
+ _SENSITIVE_SUFFIXES = {".pem", ".p12", ".pfx"}
40
+ _SUPPORT_ROOT_FILES = {"agents.md", "skills-lock.json"}
41
+ _SUPPORT_ROOT_DIRS = {".claude", ".codebuddy", ".codex", ".agents", ".pi"}
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class RuntimeCommitRequest:
46
+ """Validated commit preparation emitted by the committer stage."""
47
+
48
+ artifact_dir: str
49
+ base_head: str
50
+ commit_message: str
51
+ intended_paths: tuple[str, ...]
52
+ runtime_commit_hash: str = ""
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class RuntimeCommitResult:
57
+ """Observable result of one runtime commit finalization attempt."""
58
+
59
+ status: str
60
+ reason: str = ""
61
+ commit_hash: str = ""
62
+ recovered: bool = False
63
+
64
+ @property
65
+ def committed(self) -> bool:
66
+ return self.status == "committed"
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class _GitResult:
71
+ return_code: int
72
+ stdout: str
73
+ stderr: str
74
+
75
+
76
+ def finalize_runtime_commit(
77
+ project_root: Path,
78
+ *,
79
+ artifact_dir: Path,
80
+ checkpoint_path: Path,
81
+ ) -> RuntimeCommitResult:
82
+ """Execute and finalize a valid COMMIT_PENDING runtime handoff.
83
+
84
+ A completed checkpoint is accepted only for idempotent cleanup recovery
85
+ when a request remains after commit finalization passed.
86
+ """
87
+ try:
88
+ root = project_root.resolve()
89
+ artifact = artifact_dir.resolve()
90
+ checkpoint = checkpoint_path.resolve()
91
+ except (OSError, RuntimeError):
92
+ return RuntimeCommitResult("failed", "unsafe_artifact_identity")
93
+ if not _within(artifact, root) or not _within(checkpoint, artifact):
94
+ return RuntimeCommitResult("failed", "unsafe_artifact_identity")
95
+
96
+ state = load_checkpoint_state(checkpoint, project_root=root)
97
+ if not state.valid:
98
+ return RuntimeCommitResult("failed", f"invalid_checkpoint:{state.error_code}")
99
+ if not state.semantic_commit_ready and not state.semantic_complete:
100
+ return RuntimeCommitResult("not_ready", "checkpoint_not_commit_ready")
101
+
102
+ request_path = artifact / REQUEST_FILENAME
103
+ request, error = _load_request(request_path, root, artifact)
104
+ if request is None:
105
+ if state.semantic_complete and error == "commit_request_missing":
106
+ return RuntimeCommitResult("not_ready", "runtime_commit_already_finalized")
107
+ return RuntimeCommitResult("failed", error or "invalid_commit_request")
108
+
109
+ if any(_is_tracked(root, path) for path in (request_path, checkpoint)):
110
+ return RuntimeCommitResult("failed", "runtime_metadata_must_be_untracked")
111
+
112
+ current_head = _git(root, "rev-parse", "HEAD")
113
+ if current_head.return_code != 0:
114
+ return RuntimeCommitResult("failed", "git_head_unavailable")
115
+ head = current_head.stdout.strip()
116
+ recovered = False
117
+
118
+ if head == request.base_head:
119
+ if request.runtime_commit_hash:
120
+ return RuntimeCommitResult("failed", "premature_runtime_commit_hash")
121
+ if state.semantic_complete:
122
+ return RuntimeCommitResult("failed", "completed_checkpoint_without_commit")
123
+ changed, status_error = _task_visible_changed_paths(
124
+ root,
125
+ artifact_dir=artifact,
126
+ checkpoint_path=checkpoint,
127
+ request_path=request_path,
128
+ )
129
+ if status_error:
130
+ return RuntimeCommitResult("failed", status_error)
131
+ if set(changed) != set(request.intended_paths):
132
+ return RuntimeCommitResult("failed", "intended_paths_mismatch")
133
+ staged = _stage_intended_paths(root, request.intended_paths)
134
+ if staged.return_code != 0:
135
+ return RuntimeCommitResult("failed", "git_stage_failed")
136
+ if _git(root, "diff", "--cached", "--quiet", "--exit-code").return_code == 0:
137
+ return RuntimeCommitResult("failed", "empty_staged_change")
138
+ committed = _git(
139
+ root,
140
+ "-c", "core.hooksPath=/dev/null",
141
+ "-c", "commit.gpgSign=false",
142
+ "commit", "-m", request.commit_message,
143
+ )
144
+ if committed.return_code != 0:
145
+ return RuntimeCommitResult("failed", "git_commit_failed")
146
+ head_result = _git(root, "rev-parse", "HEAD")
147
+ if head_result.return_code != 0:
148
+ return RuntimeCommitResult("failed", "commit_identity_unavailable")
149
+ head = head_result.stdout.strip()
150
+ if not _matches_prepared_commit(root, request, head):
151
+ return RuntimeCommitResult("failed", "git_commit_content_mismatch", head)
152
+ hash_error = _record_runtime_commit_hash(request_path, head)
153
+ if hash_error:
154
+ return RuntimeCommitResult("failed", hash_error, head)
155
+ elif _matches_prepared_commit(root, request, head):
156
+ if request.runtime_commit_hash and request.runtime_commit_hash.lower() != head.lower():
157
+ return RuntimeCommitResult("failed", "runtime_commit_hash_mismatch")
158
+ recovered = True
159
+ if not request.runtime_commit_hash:
160
+ hash_error = _record_runtime_commit_hash(request_path, head)
161
+ if hash_error:
162
+ return RuntimeCommitResult("failed", hash_error, head, recovered)
163
+ else:
164
+ return RuntimeCommitResult("failed", "base_head_mismatch")
165
+
166
+ checkpoint_error = _finalize_checkpoint(
167
+ checkpoint,
168
+ commit_hash=head,
169
+ base_head=request.base_head,
170
+ )
171
+ if checkpoint_error:
172
+ return RuntimeCommitResult("failed", checkpoint_error, head, recovered)
173
+
174
+ final_state = load_checkpoint_state(checkpoint, project_root=root)
175
+ if not final_state.semantic_complete:
176
+ return RuntimeCommitResult("failed", "checkpoint_not_completed_after_commit", head, recovered)
177
+
178
+ try:
179
+ request_path.unlink(missing_ok=True)
180
+ except OSError:
181
+ return RuntimeCommitResult("failed", "commit_request_cleanup_failed", head, recovered)
182
+ return RuntimeCommitResult("committed", "runtime_commit_completed", head, recovered)
183
+
184
+
185
+ def _load_request(
186
+ request_path: Path,
187
+ project_root: Path,
188
+ artifact_dir: Path,
189
+ ) -> tuple[RuntimeCommitRequest | None, str]:
190
+ try:
191
+ raw = json.loads(request_path.read_text(encoding="utf-8"))
192
+ except FileNotFoundError:
193
+ return None, "commit_request_missing"
194
+ except (OSError, UnicodeError, json.JSONDecodeError):
195
+ return None, "commit_request_invalid_json"
196
+ if not isinstance(raw, Mapping) or raw.get("schema_version") != REQUEST_SCHEMA_VERSION:
197
+ return None, "commit_request_invalid_schema"
198
+ required_keys = {
199
+ "schema_version", "artifact_dir", "base_head", "commit_message", "intended_paths",
200
+ }
201
+ allowed_keys = required_keys | {"runtime_commit_hash"}
202
+ if not required_keys.issubset(raw) or not set(raw).issubset(allowed_keys):
203
+ return None, "commit_request_invalid_schema"
204
+
205
+ expected_artifact = artifact_dir.relative_to(project_root).as_posix()
206
+ artifact_text = str(raw.get("artifact_dir") or "").strip().replace("\\", "/")
207
+ if artifact_text != expected_artifact:
208
+ return None, "commit_request_artifact_mismatch"
209
+
210
+ base_head = str(raw.get("base_head") or "").strip()
211
+ if not _HEX_COMMIT.fullmatch(base_head):
212
+ return None, "commit_request_base_head_invalid"
213
+
214
+ message = str(raw.get("commit_message") or "").strip()
215
+ if not message or "\n" in message or len(message) > 240:
216
+ return None, "commit_request_message_invalid"
217
+
218
+ raw_paths = raw.get("intended_paths")
219
+ if not isinstance(raw_paths, list) or not raw_paths:
220
+ return None, "commit_request_paths_invalid"
221
+ intended: list[str] = []
222
+ for value in raw_paths:
223
+ relative = _safe_relative_path(value)
224
+ if relative is None or _forbidden_commit_path(relative, expected_artifact):
225
+ return None, "commit_request_path_unsafe"
226
+ intended.append(relative)
227
+ if len(intended) != len(set(intended)):
228
+ return None, "commit_request_paths_duplicate"
229
+
230
+ runtime_commit_hash = str(raw.get("runtime_commit_hash") or "").strip()
231
+ if runtime_commit_hash and not _HEX_COMMIT.fullmatch(runtime_commit_hash):
232
+ return None, "runtime_commit_hash_invalid"
233
+
234
+ return RuntimeCommitRequest(
235
+ artifact_dir=expected_artifact,
236
+ base_head=base_head.lower(),
237
+ commit_message=message,
238
+ intended_paths=tuple(intended),
239
+ runtime_commit_hash=runtime_commit_hash.lower(),
240
+ ), ""
241
+
242
+
243
+ def _safe_relative_path(value: object) -> str | None:
244
+ text = str(value or "").strip().replace("\\", "/")
245
+ if not text or text.startswith("/") or re.match(r"^[A-Za-z]:/", text):
246
+ return None
247
+ path = PurePosixPath(text)
248
+ if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
249
+ return None
250
+ return path.as_posix()
251
+
252
+
253
+ def _forbidden_commit_path(path: str, artifact_dir: str) -> bool:
254
+ lower = path.lower()
255
+ name = PurePosixPath(lower).name
256
+ if lower in _FORBIDDEN_EXACT_PATHS:
257
+ return True
258
+ if lower == ".git" or lower.startswith(".git/"):
259
+ return True
260
+ if _known_support_path(lower):
261
+ return True
262
+ artifact_prefix = artifact_dir.lower().rstrip("/") + "/"
263
+ if lower in {
264
+ artifact_prefix + REQUEST_FILENAME,
265
+ artifact_prefix + "workflow-checkpoint.json",
266
+ }:
267
+ return True
268
+ if name in _SENSITIVE_NAMES or (name.startswith(".env.") and name != ".env.example"):
269
+ return True
270
+ return PurePosixPath(lower).suffix in _SENSITIVE_SUFFIXES
271
+
272
+
273
+ def _task_visible_changed_paths(
274
+ project_root: Path,
275
+ *,
276
+ artifact_dir: Path,
277
+ checkpoint_path: Path,
278
+ request_path: Path,
279
+ ) -> tuple[tuple[str, ...], str]:
280
+ result = _git(
281
+ project_root,
282
+ "status",
283
+ "--porcelain=v1",
284
+ "-z",
285
+ "--untracked-files=all",
286
+ "--",
287
+ ".",
288
+ *HIDDEN_TOOL_WORKTREE_EXCLUDES,
289
+ )
290
+ if result.return_code != 0:
291
+ return (), "git_status_failed"
292
+ local_paths = {
293
+ checkpoint_path.relative_to(project_root).as_posix(),
294
+ request_path.relative_to(project_root).as_posix(),
295
+ }
296
+ visible = []
297
+ for path in _status_record_paths(result.stdout):
298
+ if path in local_paths:
299
+ continue
300
+ if _known_support_path(path.lower()) or path.lower() in _FORBIDDEN_EXACT_PATHS:
301
+ continue
302
+ visible.append(path)
303
+ return tuple(dict.fromkeys(visible)), ""
304
+
305
+
306
+ def _known_support_path(lower_path: str) -> bool:
307
+ path = PurePosixPath(lower_path)
308
+ parts = path.parts
309
+ if lower_path in _SUPPORT_ROOT_FILES:
310
+ return True
311
+ if parts and parts[0] in _SUPPORT_ROOT_DIRS:
312
+ return True
313
+ if lower_path == ".prizmkit-worktree" or lower_path.startswith(
314
+ ".prizmkit-worktree/"
315
+ ):
316
+ return True
317
+ if (
318
+ len(parts) >= 2
319
+ and parts[0].startswith(".")
320
+ and parts[1] in {"worktree", "worktrees"}
321
+ ):
322
+ return True
323
+ return lower_path == ".prizmkit" or lower_path.startswith(".prizmkit/")
324
+
325
+
326
+ def _status_record_paths(stdout: str) -> tuple[str, ...]:
327
+ records = stdout.split("\0")
328
+ changed: list[str] = []
329
+ index = 0
330
+ while index < len(records):
331
+ record = records[index]
332
+ index += 1
333
+ if not record or len(record) < 4 or record[2] != " ":
334
+ continue
335
+ status = record[:2]
336
+ changed.append(record[3:])
337
+ if "R" in status or "C" in status:
338
+ if index < len(records) and records[index]:
339
+ changed.append(records[index])
340
+ index += 1
341
+ return tuple(dict.fromkeys(path for path in changed if path))
342
+
343
+
344
+ def _stage_intended_paths(project_root: Path, paths: Sequence[str]) -> _GitResult:
345
+ pathspecs = tuple(f":(top,literal){path}" for path in paths)
346
+ return _git(project_root, "add", "-A", "--", *pathspecs)
347
+
348
+
349
+ def _matches_prepared_commit(project_root: Path, request: RuntimeCommitRequest, head: str) -> bool:
350
+ parents = _git(project_root, "show", "-s", "--format=%P", head)
351
+ subject = _git(project_root, "show", "-s", "--format=%s", head)
352
+ paths = _git(
353
+ project_root,
354
+ "diff-tree", "--no-commit-id", "--name-only", "--no-renames", "-r", "-z", head,
355
+ )
356
+ if any(result.return_code != 0 for result in (parents, subject, paths)):
357
+ return False
358
+ parent_list = parents.stdout.strip().split()
359
+ changed = {path for path in paths.stdout.split("\0") if path}
360
+ return (
361
+ len(parent_list) == 1
362
+ and parent_list[0].lower() == request.base_head.lower()
363
+ and subject.stdout.strip() == request.commit_message
364
+ and bool(changed)
365
+ and changed == set(request.intended_paths)
366
+ )
367
+
368
+
369
+ def _record_runtime_commit_hash(path: Path, commit_hash: str) -> str:
370
+ try:
371
+ data = json.loads(path.read_text(encoding="utf-8"))
372
+ except (OSError, UnicodeError, json.JSONDecodeError):
373
+ return "commit_request_hash_record_failed"
374
+ if not isinstance(data, dict):
375
+ return "commit_request_hash_record_failed"
376
+ data["runtime_commit_hash"] = commit_hash
377
+ error = _write_json_atomic(path, data)
378
+ return "commit_request_hash_record_failed" if error else ""
379
+
380
+
381
+ def _is_tracked(project_root: Path, path: Path) -> bool:
382
+ try:
383
+ relative = path.relative_to(project_root).as_posix()
384
+ except ValueError:
385
+ return True
386
+ return _git(project_root, "ls-files", "--error-unmatch", "--", relative).return_code == 0
387
+
388
+
389
+ def _finalize_checkpoint(
390
+ path: Path,
391
+ *,
392
+ commit_hash: str,
393
+ base_head: str,
394
+ ) -> str:
395
+ """Finalize COMMITTED and persist the Runtime's verified commit receipt."""
396
+ state = load_checkpoint_state(path)
397
+ if not state.valid:
398
+ return "checkpoint_commit_finalization_rejected"
399
+ if state.semantic_complete:
400
+ return ""
401
+ if not state.semantic_commit_ready:
402
+ return "checkpoint_commit_finalization_rejected"
403
+ data = dict(state.raw_data)
404
+ raw_steps = data.get("steps")
405
+ if not isinstance(raw_steps, list):
406
+ return "checkpoint_commit_finalization_rejected"
407
+ committers = [
408
+ step for step in raw_steps
409
+ if isinstance(step, dict) and step.get("skill") == "prizmkit-committer"
410
+ ]
411
+ if len(committers) != 1:
412
+ return "checkpoint_commit_finalization_rejected"
413
+ committer = committers[0]
414
+ committer["status"] = "completed"
415
+ committer["stage_result"] = "COMMITTED"
416
+
417
+ state_key = semantic_state_key(str(data.get("workflow_type") or ""))
418
+ semantic = data.get(state_key)
419
+ if not isinstance(semantic, dict):
420
+ return "checkpoint_commit_finalization_rejected"
421
+ semantic.update(
422
+ {
423
+ "schema_version": 1,
424
+ "orchestrator": "pipeline-runtime",
425
+ "stage": "committer",
426
+ "status": "completed",
427
+ "stage_result": "COMMITTED",
428
+ "current_stage": None,
429
+ "next_stage": None,
430
+ "resume_from": None,
431
+ "blocked_reason": None,
432
+ "terminal_status": "WORKFLOW_COMPLETED",
433
+ "runtime_base_head": base_head,
434
+ "runtime_commit_hash": commit_hash,
435
+ }
436
+ )
437
+ candidate = validate_checkpoint_data(data, path=path, exists=True)
438
+ if not candidate.valid or not candidate.semantic_complete:
439
+ return "checkpoint_commit_finalization_rejected"
440
+ if _write_json_atomic(path, data):
441
+ return "checkpoint_commit_finalization_failed"
442
+ finalized = load_checkpoint_state(path)
443
+ return "" if finalized.semantic_complete else "checkpoint_commit_finalization_rejected"
444
+
445
+
446
+ def _write_json_atomic(path: Path, data: Mapping[str, object]) -> str:
447
+ temp_path: str | None = None
448
+ try:
449
+ path.parent.mkdir(parents=True, exist_ok=True)
450
+ fd, temp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
451
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
452
+ json.dump(data, handle, indent=2, ensure_ascii=False)
453
+ handle.write("\n")
454
+ os.replace(temp_path, path)
455
+ except OSError:
456
+ if temp_path:
457
+ try:
458
+ os.unlink(temp_path)
459
+ except OSError:
460
+ pass
461
+ return "atomic_json_write_failed"
462
+ return ""
463
+
464
+
465
+ def _git(project_root: Path, *args: str) -> _GitResult:
466
+ completed = subprocess.run(
467
+ ["git", *args],
468
+ cwd=project_root,
469
+ stdout=subprocess.PIPE,
470
+ stderr=subprocess.PIPE,
471
+ text=True,
472
+ check=False,
473
+ )
474
+ return _GitResult(completed.returncode, completed.stdout, completed.stderr)
475
+
476
+
477
+ def _within(path: Path, root: Path) -> bool:
478
+ try:
479
+ path.resolve().relative_to(root.resolve())
480
+ except ValueError:
481
+ return False
482
+ return True
@@ -23,8 +23,6 @@ _RESULT_KEYS = {
23
23
  "independent_review",
24
24
  "repair_rounds",
25
25
  "production_changed",
26
- "review_required",
27
- "review_scope",
28
26
  "unresolved_items",
29
27
  }
30
28
  _INDEPENDENT_REVIEW_KEYS = {
@@ -33,14 +31,40 @@ _INDEPENDENT_REVIEW_KEYS = {
33
31
  "downgrade_reason",
34
32
  "final_state_rechecked",
35
33
  }
34
+ _LEGACY_RESULT_KEYS = _RESULT_KEYS | {"review_required", "review_scope"}
36
35
 
37
36
 
38
37
  def load_test_result(path: str | Path) -> dict[str, Any] | None:
39
- """Load one valid terminal projection, or return None."""
38
+ """Load one strict route-free terminal projection, or return None."""
40
39
  try:
41
40
  value = json.loads(Path(path).read_text(encoding="utf-8"))
42
41
  except (OSError, ValueError, TypeError):
43
42
  return None
43
+ return _validate_test_result_value(value)
44
+
45
+
46
+ def load_legacy_test_result(path: str | Path) -> dict[str, Any] | None:
47
+ """Read a historical pass projection without permitting new legacy output."""
48
+ try:
49
+ value = json.loads(Path(path).read_text(encoding="utf-8"))
50
+ except (OSError, ValueError, TypeError):
51
+ return None
52
+ if not isinstance(value, dict):
53
+ return None
54
+ if set(value) == _RESULT_KEYS:
55
+ return _validate_test_result_value(value)
56
+ if set(value) != _LEGACY_RESULT_KEYS:
57
+ return None
58
+ if value.get("review_required") is not False or value.get("review_scope") is not None:
59
+ return None
60
+ normalized = {
61
+ key: item for key, item in value.items()
62
+ if key not in {"review_required", "review_scope"}
63
+ }
64
+ return _validate_test_result_value(normalized)
65
+
66
+
67
+ def _validate_test_result_value(value: object) -> dict[str, Any] | None:
44
68
  if not isinstance(value, dict):
45
69
  return None
46
70
  if set(value) != _RESULT_KEYS or value.get("schema_version") != 1:
@@ -79,11 +103,6 @@ def load_test_result(path: str | Path) -> dict[str, Any] | None:
79
103
 
80
104
  if not isinstance(value.get("production_changed"), bool):
81
105
  return None
82
- if not isinstance(value.get("review_required"), bool):
83
- return None
84
- expected_scope = "delta" if value["review_required"] else None
85
- if value.get("review_scope") != expected_scope:
86
- return None
87
106
  unresolved = value.get("unresolved_items")
88
107
  if not isinstance(unresolved, list) or any(
89
108
  not isinstance(item, str) or not item.strip() for item in unresolved
@@ -91,8 +110,10 @@ def load_test_result(path: str | Path) -> dict[str, Any] | None:
91
110
  return None
92
111
 
93
112
  if value["result"] == "TEST_PASS":
94
- if value["review_required"] or unresolved or rounds < 1:
113
+ if unresolved or rounds < 1:
95
114
  return None
115
+ elif not unresolved:
116
+ return None
96
117
  return value
97
118
 
98
119
 
@@ -110,14 +131,46 @@ def valid_test_artifact_pair(
110
131
  *,
111
132
  required_result: str | None = None,
112
133
  ) -> bool:
113
- """Validate one sibling report/result pair and their terminal result."""
134
+ """Validate one strict sibling report/result pair and terminal result."""
135
+ return _valid_test_artifact_pair(
136
+ report_path,
137
+ result_path,
138
+ required_result=required_result,
139
+ legacy=False,
140
+ )
141
+
142
+
143
+ def valid_legacy_test_artifact_pair(
144
+ report_path: str | Path,
145
+ result_path: str | Path,
146
+ *,
147
+ required_result: str | None = None,
148
+ ) -> bool:
149
+ """Validate a historical completed pair for read-only recovery."""
150
+ return _valid_test_artifact_pair(
151
+ report_path,
152
+ result_path,
153
+ required_result=required_result,
154
+ legacy=True,
155
+ )
156
+
157
+
158
+ def _valid_test_artifact_pair(
159
+ report_path: str | Path,
160
+ result_path: str | Path,
161
+ *,
162
+ required_result: str | None,
163
+ legacy: bool,
164
+ ) -> bool:
114
165
  report = Path(report_path)
115
166
  result = Path(result_path)
116
167
  if not report.is_file() or not result.is_file():
117
168
  return False
118
169
  if report.resolve().parent != result.resolve().parent:
119
170
  return False
120
- projection = load_test_result(result)
171
+ projection = (
172
+ load_legacy_test_result(result) if legacy else load_test_result(result)
173
+ )
121
174
  if projection is None:
122
175
  return False
123
176
  if required_result is not None and projection.get("result") != required_result: