prizmkit 1.1.130 → 1.1.131

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 (88) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +1 -1
  3. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
  4. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
  5. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
  6. package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
  7. package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
  8. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
  9. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +396 -4
  10. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +54 -1
  11. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +54 -1
  12. package/bundled/dev-pipeline/scripts/prompt_framework.py +152 -1
  13. package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
  14. package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
  15. package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
  16. package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
  17. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
  18. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
  19. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
  20. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
  21. package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
  22. package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
  23. package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
  24. package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
  25. package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
  26. package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
  27. package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
  28. package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
  29. package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
  30. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
  31. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +310 -22
  32. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
  33. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
  34. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
  35. package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
  36. package/bundled/skills/_metadata.json +62 -25
  37. package/bundled/skills/app-planner/SKILL.md +4 -3
  38. package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
  39. package/bundled/skills/bug-planner/SKILL.md +12 -27
  40. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
  41. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
  42. package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
  43. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
  44. package/bundled/skills/feature-planner/SKILL.md +27 -22
  45. package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
  46. package/bundled/skills/feature-workflow/SKILL.md +94 -121
  47. package/bundled/skills/prizmkit/SKILL.md +130 -94
  48. package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
  49. package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
  50. package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
  51. package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
  52. package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
  53. package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
  54. package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
  55. package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
  56. package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
  57. package/bundled/skills/prizmkit-init/SKILL.md +5 -5
  58. package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
  59. package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
  60. package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
  61. package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
  62. package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
  63. package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
  64. package/bundled/skills/prizmkit-test/SKILL.md +28 -6
  65. package/bundled/skills/prizmkit-test/references/examples.md +1 -1
  66. package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
  67. package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
  68. package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
  69. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
  70. package/bundled/skills/recovery-workflow/SKILL.md +62 -241
  71. package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
  72. package/bundled/skills/recovery-workflow/references/detection.md +28 -52
  73. package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
  74. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
  75. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
  76. package/bundled/skills/refactor-planner/SKILL.md +12 -57
  77. package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
  78. package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
  79. package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
  80. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
  81. package/bundled/skills/refactor-workflow/SKILL.md +98 -139
  82. package/bundled/templates/project-memory-template.md +4 -4
  83. package/package.json +1 -1
  84. package/src/index.js +4 -0
  85. package/src/prompts.js +10 -3
  86. package/src/scaffold.js +1 -1
  87. package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
  88. package/bundled/skills/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
@@ -3,16 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
- import json
7
6
  import subprocess
8
7
  from pathlib import Path
9
8
 
10
- from .checkpoint_state import load_checkpoint_state
9
+ from .checkpoint_state import CheckpointState, load_checkpoint_state
11
10
  from .gitops import git_status_safe, run_git_command
12
11
  from .runner_models import PromptGenerationResult, SessionClassification
13
12
  from .sessions import AISessionResult
14
13
 
15
14
  CONTEXT_OVERFLOW = "context_overflow"
15
+ WORKFLOW_BLOCKED = "workflow_blocked"
16
16
  STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
17
17
  CONTEXT_MARKERS = (
18
18
  "context_overflow",
@@ -46,29 +46,39 @@ def classify_session(
46
46
  previous_fingerprint: object | None = None,
47
47
  previous_no_progress_count: int = 0,
48
48
  ) -> SessionClassification:
49
- """Classify raw AI session signals with durable success precedence."""
49
+ """Classify raw AI session signals using semantic checkpoint authority."""
50
50
  fingerprint = progress_fingerprint(project_root, prompt)
51
- invalid_checkpoint = _invalid_existing_workflow_checkpoint(prompt)
52
- if invalid_checkpoint is not None:
51
+ checkpoint = _workflow_checkpoint(prompt, project_root)
52
+ if checkpoint is not None and not checkpoint.valid:
53
53
  return SessionClassification(
54
54
  "crashed",
55
- reason=f"invalid_workflow_checkpoint:{invalid_checkpoint.error_code}",
55
+ reason=f"invalid_workflow_checkpoint:{checkpoint.error_code}",
56
56
  progress_fingerprint=fingerprint,
57
+ checkpoint_state=checkpoint.semantic_snapshot,
57
58
  )
58
- if _is_terminal_success(result) and _has_completed_workflow_artifact(prompt):
59
+ if checkpoint is not None and checkpoint.semantic_blocked:
60
+ return SessionClassification(
61
+ WORKFLOW_BLOCKED,
62
+ reason=checkpoint.semantic.blocked_reason or "workflow_blocked",
63
+ progress_fingerprint=fingerprint,
64
+ checkpoint_state=checkpoint.semantic_snapshot,
65
+ )
66
+ if _is_terminal_success(result) and checkpoint is not None and checkpoint.semantic_complete:
59
67
  return SessionClassification(
60
68
  "success",
61
- reason="terminal_success_with_completed_checkpoint",
69
+ reason="terminal_success_with_semantic_checkpoint",
62
70
  progress_fingerprint=fingerprint,
71
+ checkpoint_state=checkpoint.semantic_snapshot,
63
72
  )
64
-
65
- if _has_late_runtime_error_after_completion(result, prompt):
73
+ if _has_late_runtime_error_after_completion(result, checkpoint):
66
74
  return SessionClassification(
67
75
  "success",
68
- reason="completed_checkpoint_with_late_runtime_error",
76
+ reason="semantic_checkpoint_with_late_runtime_error",
69
77
  progress_fingerprint=fingerprint,
78
+ checkpoint_state=checkpoint.semantic_snapshot,
70
79
  )
71
80
 
81
+ semantic = checkpoint.semantic_snapshot if checkpoint is not None else {}
72
82
  if _is_context_overflow(result):
73
83
  no_progress_count = _next_no_progress_count(previous_fingerprint, fingerprint, previous_no_progress_count)
74
84
  if no_progress_count >= 2:
@@ -78,6 +88,7 @@ def classify_session(
78
88
  fatal_error_code=result.fatal_error_code or CONTEXT_OVERFLOW,
79
89
  progress_fingerprint=fingerprint,
80
90
  no_progress_count=no_progress_count,
91
+ checkpoint_state=semantic,
81
92
  )
82
93
  return SessionClassification(
83
94
  CONTEXT_OVERFLOW,
@@ -85,17 +96,37 @@ def classify_session(
85
96
  fatal_error_code=result.fatal_error_code or CONTEXT_OVERFLOW,
86
97
  progress_fingerprint=fingerprint,
87
98
  no_progress_count=no_progress_count,
99
+ checkpoint_state=semantic,
88
100
  )
89
101
 
90
102
  if result.termination_reason == "stale_session":
91
- return SessionClassification("crashed", reason="stale_session", progress_fingerprint=fingerprint)
103
+ return SessionClassification(
104
+ "crashed", reason="stale_session", progress_fingerprint=fingerprint, checkpoint_state=semantic
105
+ )
92
106
  if result.timed_out or result.termination_reason == "timeout":
93
- return SessionClassification("timed_out", reason="timeout", progress_fingerprint=fingerprint)
107
+ return SessionClassification(
108
+ "timed_out", reason="timeout", progress_fingerprint=fingerprint, checkpoint_state=semantic
109
+ )
94
110
  if _is_infra_error(result):
95
- return SessionClassification("infra_error", reason="ai_runtime_or_provider_error", progress_fingerprint=fingerprint)
111
+ return SessionClassification(
112
+ "infra_error",
113
+ reason="ai_runtime_or_provider_error",
114
+ progress_fingerprint=fingerprint,
115
+ checkpoint_state=semantic,
116
+ )
96
117
  if result.exit_code not in (0, None):
97
- return SessionClassification("crashed", reason=f"exit_code={result.exit_code}", progress_fingerprint=fingerprint)
98
- return SessionClassification("crashed", reason="no_durable_success", progress_fingerprint=fingerprint)
118
+ return SessionClassification(
119
+ "crashed",
120
+ reason=f"exit_code={result.exit_code}",
121
+ progress_fingerprint=fingerprint,
122
+ checkpoint_state=semantic,
123
+ )
124
+ reason = "no_semantic_completion"
125
+ if checkpoint is not None and checkpoint.semantic.error_code:
126
+ reason = f"{reason}:{checkpoint.semantic.error_code}"
127
+ return SessionClassification(
128
+ "crashed", reason=reason, progress_fingerprint=fingerprint, checkpoint_state=semantic
129
+ )
99
130
 
100
131
 
101
132
  def progress_fingerprint(project_root: Path, prompt: PromptGenerationResult | None = None) -> dict[str, object]:
@@ -107,7 +138,9 @@ def progress_fingerprint(project_root: Path, prompt: PromptGenerationResult | No
107
138
  if prompt and prompt.artifact_path and prompt.artifact_path.exists():
108
139
  artifact_hash = _hash_tree(prompt.artifact_path)
109
140
  if prompt and prompt.checkpoint_path and prompt.checkpoint_path.is_file():
110
- checkpoint_cursor = load_checkpoint_state(prompt.checkpoint_path).cursor_signature
141
+ checkpoint_cursor = load_checkpoint_state(
142
+ prompt.checkpoint_path, project_root=project_root
143
+ ).cursor_signature
111
144
  return {
112
145
  "git_diff_fingerprint": hashlib.sha256(status_text.encode()).hexdigest(),
113
146
  "checkpoint_cursor": checkpoint_cursor,
@@ -129,67 +162,51 @@ def _is_terminal_success(result: AISessionResult) -> bool:
129
162
 
130
163
  def _has_late_runtime_error_after_completion(
131
164
  result: AISessionResult,
132
- prompt: PromptGenerationResult | None,
165
+ checkpoint: CheckpointState | None,
133
166
  ) -> bool:
134
- if not _has_completed_workflow_artifact(prompt):
167
+ if checkpoint is None or not checkpoint.semantic_complete:
135
168
  return False
136
169
  return _is_context_overflow(result) or _is_infra_error(result)
137
170
 
138
171
 
139
- def _has_completed_workflow_artifact(prompt: PromptGenerationResult | None) -> bool:
172
+ def _workflow_checkpoint(
173
+ prompt: PromptGenerationResult | None,
174
+ project_root: Path,
175
+ ) -> CheckpointState | None:
140
176
  if prompt is None:
141
- return False
142
- if _invalid_existing_workflow_checkpoint(prompt) is not None:
143
- return False
144
- if prompt.checkpoint_path and prompt.checkpoint_path.is_file():
145
- state = load_checkpoint_state(prompt.checkpoint_path)
146
- return state.valid and state.all_completed
147
- if prompt.artifact_path and has_completed_artifact_path(prompt.artifact_path):
148
- return True
149
- return False
177
+ return None
178
+ checkpoint = prompt.checkpoint_path
179
+ if checkpoint is None and prompt.artifact_path is not None:
180
+ checkpoint = prompt.artifact_path / "workflow-checkpoint.json"
181
+ if checkpoint is None or not checkpoint.is_file():
182
+ return None
183
+ return load_checkpoint_state(checkpoint, project_root=project_root)
150
184
 
151
185
 
152
- def has_branch_completion_evidence(project_root: Path, base_branch: str, working_branch: str, artifact_path: Path | None) -> bool:
153
- """Return true when a recorded task branch already has completed work to merge."""
154
- if not base_branch or not working_branch or not has_completed_artifact_path(artifact_path):
186
+ def has_branch_completion_evidence(
187
+ project_root: Path,
188
+ base_branch: str,
189
+ working_branch: str,
190
+ artifact_path: Path | None,
191
+ ) -> bool:
192
+ """Return true only when a task branch is ahead and semantically complete."""
193
+ if not base_branch or not working_branch or not has_completed_artifact_path(artifact_path, project_root):
155
194
  return False
156
195
  return _has_commit_range(project_root, base_branch, working_branch)
157
196
 
158
197
 
159
- def has_completed_artifact_path(artifact_path: Path | None) -> bool:
198
+ def has_completed_artifact_path(
199
+ artifact_path: Path | None,
200
+ project_root: Path | None = None,
201
+ ) -> bool:
202
+ """Return whether an artifact directory contains semantic completion proof."""
160
203
  if artifact_path is None:
161
204
  return False
162
205
  checkpoint = artifact_path / "workflow-checkpoint.json"
163
- if checkpoint.is_file():
164
- state = load_checkpoint_state(checkpoint)
165
- return state.valid and state.all_completed
166
- return _has_valid_completion_summary(artifact_path / "completion-summary.json")
167
-
168
-
169
- def _has_valid_completion_summary(path: Path) -> bool:
170
- """Return whether a completion summary contains usable durable notes."""
171
- try:
172
- data = json.loads(path.read_text(encoding="utf-8"))
173
- except (OSError, UnicodeDecodeError, json.JSONDecodeError):
174
- return False
175
- if not isinstance(data, dict):
206
+ if not checkpoint.is_file():
176
207
  return False
177
- notes = data.get("completion_notes")
178
- return isinstance(notes, list) and bool(notes) and all(
179
- isinstance(note, str) and note.strip() for note in notes
180
- )
181
-
182
-
183
- def _invalid_existing_workflow_checkpoint(prompt: PromptGenerationResult | None):
184
- if prompt is None:
185
- return None
186
- checkpoint = prompt.checkpoint_path
187
- if checkpoint is None and prompt.artifact_path is not None:
188
- checkpoint = prompt.artifact_path / "workflow-checkpoint.json"
189
- if checkpoint is None or not checkpoint.is_file():
190
- return None
191
- state = load_checkpoint_state(checkpoint)
192
- return state if not state.valid else None
208
+ state = load_checkpoint_state(checkpoint, project_root=project_root)
209
+ return state.semantic_complete
193
210
 
194
211
 
195
212
  def _next_no_progress_count(previous: object | None, current: object, count: int) -> int:
@@ -213,8 +230,12 @@ def _is_context_overflow(result: AISessionResult) -> bool:
213
230
 
214
231
 
215
232
  def _is_infra_error(result: AISessionResult) -> bool:
216
- text = "\n".join([result.progress_summary.terminal_result_text, *result.progress_summary.errors, _tail(result.session_log)]).lower()
217
- return bool(result.fatal_error_code and result.fatal_error_code != CONTEXT_OVERFLOW) or any(marker in text for marker in INFRA_MARKERS)
233
+ text = "\n".join(
234
+ [result.progress_summary.terminal_result_text, *result.progress_summary.errors, _tail(result.session_log)]
235
+ ).lower()
236
+ return bool(result.fatal_error_code and result.fatal_error_code != CONTEXT_OVERFLOW) or any(
237
+ marker in text for marker in INFRA_MARKERS
238
+ )
218
239
 
219
240
 
220
241
  def _has_commit_range(project_root: Path, start_ref: str, end_ref: str = "HEAD") -> bool:
@@ -250,8 +271,8 @@ def _hash_tree(path: Path) -> str:
250
271
  for child in sorted(path.rglob("*")):
251
272
  if child.is_file():
252
273
  try:
253
- rel = child.relative_to(path).as_posix()
254
- digest.update(rel.encode())
274
+ relative = child.relative_to(path).as_posix()
275
+ digest.update(relative.encode())
255
276
  digest.update(child.read_bytes())
256
277
  except OSError:
257
278
  continue
@@ -39,7 +39,6 @@ class RunnerEnvironment:
39
39
  auto_push: bool = False
40
40
  use_worktree: bool = False
41
41
  dev_branch: str = ""
42
- enable_deploy: bool = False
43
42
  verbose: bool = False
44
43
  live_output: bool = True
45
44
  heartbeat_interval_seconds: int = 30
@@ -56,7 +55,6 @@ class RunnerEnvironment:
56
55
  auto_push=_truthy(values.get("AUTO_PUSH")),
57
56
  use_worktree=_truthy(values.get("USE_WORKTREE")),
58
57
  dev_branch=values.get("DEV_BRANCH", ""),
59
- enable_deploy=_truthy(values.get("ENABLE_DEPLOY")),
60
58
  verbose=_truthy(values.get("VERBOSE")),
61
59
  live_output=not _falsey(values.get("PRIZMKIT_LIVE_OUTPUT")),
62
60
  heartbeat_interval_seconds=_int_value(values, "HEARTBEAT_INTERVAL", 30),
@@ -131,6 +129,49 @@ class SessionPaths:
131
129
  )
132
130
 
133
131
 
132
+ @dataclass(frozen=True)
133
+ class SemanticCheckpointState:
134
+ """Cohesive provider-neutral checkpoint metadata persisted by L4."""
135
+
136
+ current_stage: str | None = None
137
+ next_stage: str | None = None
138
+ stage_result: str | None = None
139
+ repair_scope: str | None = None
140
+ repair_round: int = 0
141
+ authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
142
+ blocked_reason: str | None = None
143
+ terminal_status: str | None = None
144
+
145
+ @classmethod
146
+ def from_mapping(cls, value: Mapping[str, object] | None) -> "SemanticCheckpointState":
147
+ data = value or {}
148
+ evidence = data.get("authoritative_evidence_paths")
149
+ return cls(
150
+ current_stage=_optional_text(data.get("current_stage")),
151
+ next_stage=_optional_text(data.get("next_stage")),
152
+ stage_result=_optional_text(data.get("stage_result")),
153
+ repair_scope=_optional_text(data.get("repair_scope")),
154
+ repair_round=max(0, _safe_int(data.get("repair_round"), 0)),
155
+ authoritative_evidence_paths=(
156
+ dict(evidence) if isinstance(evidence, Mapping) else {}
157
+ ),
158
+ blocked_reason=_optional_text(data.get("blocked_reason")),
159
+ terminal_status=_optional_text(data.get("terminal_status")),
160
+ )
161
+
162
+ def as_dict(self) -> dict[str, object]:
163
+ return {
164
+ "current_stage": self.current_stage,
165
+ "next_stage": self.next_stage,
166
+ "stage_result": self.stage_result,
167
+ "repair_scope": self.repair_scope,
168
+ "repair_round": self.repair_round,
169
+ "authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
170
+ "blocked_reason": self.blocked_reason,
171
+ "terminal_status": self.terminal_status,
172
+ }
173
+
174
+
134
175
  @dataclass(frozen=True)
135
176
  class ContinuationState:
136
177
  """Provider-neutral durable handoff inputs for the next AI session."""
@@ -150,6 +191,7 @@ class ContinuationState:
150
191
  active_dev_branch: str = ""
151
192
  base_branch: str = ""
152
193
  fatal_error_code: str = ""
194
+ checkpoint_state: SemanticCheckpointState = field(default_factory=SemanticCheckpointState)
153
195
 
154
196
 
155
197
  @dataclass(frozen=True)
@@ -161,6 +203,7 @@ class SessionClassification:
161
203
  fatal_error_code: str = ""
162
204
  progress_fingerprint: Mapping[str, object] = field(default_factory=dict)
163
205
  no_progress_count: int = 0
206
+ checkpoint_state: Mapping[str, object] = field(default_factory=dict)
164
207
 
165
208
 
166
209
  @dataclass(frozen=True)
@@ -336,7 +379,14 @@ def _falsey(value: str | None) -> bool:
336
379
  return str(value or "").strip().lower() in {"0", "false", "no", "off"}
337
380
 
338
381
 
339
- def _safe_int(value: str | None, default: int) -> int:
382
+ def _optional_text(value: object) -> str | None:
383
+ if value is None:
384
+ return None
385
+ text = str(value).strip()
386
+ return text or None
387
+
388
+
389
+ def _safe_int(value: object, default: int) -> int:
340
390
  try:
341
391
  return int(str(value))
342
392
  except (TypeError, ValueError):
@@ -9,6 +9,7 @@ import time
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
 
12
+ from .checkpoint_state import load_checkpoint_state
12
13
  from .config import load_runtime_config
13
14
  from .paths import RuntimePaths
14
15
  from .runner_models import RunnerEnvironment, SessionPaths
@@ -148,12 +149,26 @@ def run_recovery(paths: RuntimePaths, legacy_args: tuple[str, ...] = ()) -> tupl
148
149
  )
149
150
  )
150
151
  result = launcher.run()
151
- outcome = _recovery_outcome(paths.project_root, result.exit_code)
152
+ checkpoint_path = _recovery_checkpoint_path(paths.project_root, detection)
153
+ outcome, semantic = _recovery_outcome(
154
+ paths.project_root,
155
+ result.exit_code,
156
+ checkpoint_path=checkpoint_path,
157
+ )
152
158
  session_paths.session_status_json.write_text(
153
- json.dumps({"session_id": session_id, "outcome": outcome}, indent=2) + "\n",
159
+ json.dumps(
160
+ {
161
+ "session_id": session_id,
162
+ "outcome": outcome,
163
+ "checkpoint_path": str(checkpoint_path) if checkpoint_path else None,
164
+ "checkpoint_state": semantic,
165
+ },
166
+ indent=2,
167
+ )
168
+ + "\n",
154
169
  encoding="utf-8",
155
170
  )
156
- return 0 if outcome in {"success", "success_wip", "no_changes"} else 1, outcome + "\n", ""
171
+ return 0 if outcome in {"success", "no_changes"} else 1, outcome + "\n", ""
157
172
 
158
173
 
159
174
  def _option_value(args: tuple[str, ...], flag: str) -> str | None:
@@ -165,16 +180,40 @@ def _option_value(args: tuple[str, ...], flag: str) -> str | None:
165
180
  return None
166
181
 
167
182
 
168
- def _recovery_outcome(project_root: Path, exit_code: int | None) -> str:
169
- commits = subprocess.run(
170
- ["git", "-C", str(project_root), "rev-list", "--count", "HEAD", "^main"],
171
- stdout=subprocess.PIPE,
172
- stderr=subprocess.DEVNULL,
173
- text=True,
174
- check=False,
175
- )
176
- if commits.returncode == 0 and commits.stdout.strip() not in {"", "0"}:
177
- return "success"
183
+ def _recovery_checkpoint_path(project_root: Path, detection: dict[str, object]) -> Path | None:
184
+ """Select the L4 checkpoint identified by recovery inspection."""
185
+ semantic = detection.get("semantic_checkpoint")
186
+ if isinstance(semantic, dict) and semantic.get("path"):
187
+ candidate = project_root / str(semantic["path"])
188
+ if candidate.is_file():
189
+ return candidate
190
+ execution_owner = detection.get("execution_owner")
191
+ checkpoints = execution_owner.get("l4_checkpoints") if isinstance(execution_owner, dict) else []
192
+ if isinstance(checkpoints, list) and len(checkpoints) == 1:
193
+ candidate = project_root / str(checkpoints[0])
194
+ if candidate.is_file():
195
+ return candidate
196
+ return None
197
+
198
+
199
+ def _recovery_outcome(
200
+ project_root: Path,
201
+ exit_code: int | None,
202
+ *,
203
+ checkpoint_path: Path | None = None,
204
+ ) -> tuple[str, dict[str, object]]:
205
+ """Classify recovery from semantic L4 state without mutating partial work."""
206
+ if checkpoint_path is not None:
207
+ state = load_checkpoint_state(checkpoint_path, project_root=project_root)
208
+ semantic = state.semantic_snapshot
209
+ if state.valid and state.semantic_complete:
210
+ return "success", semantic
211
+ if state.valid and state.semantic_blocked:
212
+ return "workflow_blocked", semantic
213
+ if state.exists and not state.valid:
214
+ return "blocked_invalid_checkpoint", semantic
215
+ return "partial" if exit_code == 0 else "failed", semantic
216
+
178
217
  status = subprocess.run(
179
218
  ["git", "-C", str(project_root), "status", "--porcelain"],
180
219
  stdout=subprocess.PIPE,
@@ -183,15 +222,6 @@ def _recovery_outcome(project_root: Path, exit_code: int | None) -> str:
183
222
  check=False,
184
223
  )
185
224
  has_changes = bool(status.stdout.strip())
186
- if exit_code == 0 and has_changes:
187
- subprocess.run(["git", "-C", str(project_root), "add", "-A"], check=False)
188
- subprocess.run(
189
- ["git", "-C", str(project_root), "commit", "--no-verify", "-m", "chore: save recovery work"],
190
- stdout=subprocess.DEVNULL,
191
- stderr=subprocess.DEVNULL,
192
- check=False,
193
- )
194
- return "success_wip"
195
- if exit_code == 0:
196
- return "no_changes"
197
- return "partial" if has_changes else "failed"
225
+ if exit_code == 0 and not has_changes:
226
+ return "no_changes", {}
227
+ return ("partial" if has_changes else "failed"), {}
@@ -51,6 +51,7 @@ def run_status_action(
51
51
  continuation_summary_path: Path | None = None,
52
52
  no_progress_count: int | None = None,
53
53
  progress_fingerprint: object | None = None,
54
+ checkpoint_state: object | None = None,
54
55
  project_root: Path | None = None,
55
56
  ) -> StatusCommandResult:
56
57
  max_retries = _resolved_max_retries(invocation) if max_retries is None else max(0, max_retries)
@@ -88,6 +89,8 @@ def run_status_action(
88
89
  command.extend(["--no-progress-count", str(no_progress_count)])
89
90
  if progress_fingerprint is not None:
90
91
  command.extend(["--progress-fingerprint", json.dumps(progress_fingerprint, sort_keys=True)])
92
+ if checkpoint_state is not None:
93
+ command.extend(["--checkpoint-state", json.dumps(checkpoint_state, sort_keys=True)])
91
94
  if project_root is not None:
92
95
  command.extend(["--project-root", str(project_root)])
93
96
 
@@ -146,6 +149,7 @@ def update_item(
146
149
  continuation_summary_path=metadata.get("continuation_summary_path") if isinstance(metadata.get("continuation_summary_path"), Path) else None,
147
150
  no_progress_count=metadata.get("no_progress_count") if isinstance(metadata.get("no_progress_count"), int) else None,
148
151
  progress_fingerprint=metadata.get("progress_fingerprint"),
152
+ checkpoint_state=metadata.get("checkpoint_state"),
149
153
  )
150
154
 
151
155
 
@@ -621,6 +621,15 @@ def _process_item_locked(
621
621
  "max_infra_retries": invocation.max_infra_retries,
622
622
  "no_progress_count": previous_no_progress_count,
623
623
  "progress_fingerprint": previous_fingerprint or {},
624
+ "checkpoint_state": {
625
+ key: metadata.get(key)
626
+ for key in (
627
+ "current_stage", "next_stage", "stage_result",
628
+ "repair_scope", "repair_round",
629
+ "authoritative_evidence_paths", "blocked_reason",
630
+ "terminal_status",
631
+ )
632
+ },
624
633
  "task_type": family.task_type,
625
634
  "active_dev_branch": branch_name,
626
635
  "base_branch": base_branch,
@@ -691,6 +700,7 @@ def _process_item_locked(
691
700
  worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
692
701
  reason=classification.reason,
693
702
  fatal_error_code=classification.fatal_error_code,
703
+ checkpoint_state=classification.checkpoint_state,
694
704
  )
695
705
 
696
706
  update = update_item(
@@ -706,6 +716,7 @@ def _process_item_locked(
706
716
  continuation_summary_path=summary_path,
707
717
  no_progress_count=classification.no_progress_count,
708
718
  progress_fingerprint=classification.progress_fingerprint,
719
+ checkpoint_state=classification.checkpoint_state,
709
720
  )
710
721
  if not update.ok:
711
722
  raise RuntimeError(update.text)
@@ -767,7 +778,7 @@ def _process_item_locked(
767
778
 
768
779
  def _is_retryable_ai_outcome(session_status: str, new_status: str) -> bool:
769
780
  """Return whether a persisted post-launch outcome should continue in-process."""
770
- if session_status == "success" or session_status == "stalled_context_continuation":
781
+ if session_status in {"success", "stalled_context_continuation", "workflow_blocked"}:
771
782
  return False
772
783
  return new_status in {"in_progress", "pending"} and session_status in {
773
784
  "context_overflow",
@@ -1079,6 +1090,7 @@ def _write_session_status(
1079
1090
  worktree_path: Path | None = None,
1080
1091
  reason: str = "",
1081
1092
  fatal_error_code: str = "",
1093
+ checkpoint_state: object | None = None,
1082
1094
  ) -> None:
1083
1095
  payload = {
1084
1096
  "status": status,
@@ -1094,6 +1106,8 @@ def _write_session_status(
1094
1106
  payload["reason"] = reason
1095
1107
  if fatal_error_code:
1096
1108
  payload["fatal_error_code"] = fatal_error_code
1109
+ if isinstance(checkpoint_state, dict):
1110
+ payload["checkpoint_state"] = checkpoint_state
1097
1111
  session_paths.session_status_json.parent.mkdir(parents=True, exist_ok=True)
1098
1112
  session_paths.session_status_json.write_text(
1099
1113
  json.dumps(payload, indent=2, sort_keys=True) + "\n",