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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/.env.example +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +445 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +87 -66
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +53 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +55 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +4 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +15 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +396 -4
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +54 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +54 -1
- package/bundled/dev-pipeline/scripts/prompt_framework.py +152 -1
- package/bundled/dev-pipeline/scripts/update-bug-status.py +53 -0
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +294 -69
- package/bundled/dev-pipeline/scripts/update-feature-status.py +57 -0
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +53 -0
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +20 -14
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +21 -4
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +18 -0
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +5 -15
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +21 -0
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +10 -10
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +16 -0
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +26 -23
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +12 -8
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +21 -0
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +362 -3
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +310 -22
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +132 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +91 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +411 -18
- package/bundled/dev-pipeline/tests/test_unified_cli.py +3 -3
- package/bundled/skills/_metadata.json +62 -25
- package/bundled/skills/app-planner/SKILL.md +4 -3
- package/bundled/skills/bug-fix-workflow/SKILL.md +89 -180
- package/bundled/skills/bug-planner/SKILL.md +12 -27
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +131 -220
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +36 -90
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +129 -271
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +35 -76
- package/bundled/skills/feature-planner/SKILL.md +27 -22
- package/bundled/skills/feature-planner/assets/planning-guide.md +4 -4
- package/bundled/skills/feature-workflow/SKILL.md +94 -121
- package/bundled/skills/prizmkit/SKILL.md +130 -94
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-code-review/SKILL.md +114 -84
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +1 -1
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-committer/SKILL.md +131 -47
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +99 -73
- package/bundled/skills/prizmkit-implement/references/implementation-subagent-procedure.md +1 -1
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-init/SKILL.md +5 -5
- package/bundled/skills/prizmkit-plan/SKILL.md +132 -105
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-retrospective/SKILL.md +111 -53
- package/bundled/skills/prizmkit-retrospective/references/knowledge-injection-steps.md +7 -6
- package/bundled/skills/prizmkit-retrospective/references/structural-sync-steps.md +5 -4
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-test/SKILL.md +28 -6
- package/bundled/skills/prizmkit-test/references/examples.md +1 -1
- package/bundled/skills/prizmkit-test/references/test-report-template.md +1 -1
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/prizmkit-workflow/SKILL.md +266 -0
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +181 -0
- package/bundled/skills/recovery-workflow/SKILL.md +62 -241
- package/bundled/skills/recovery-workflow/evals/evals.json +17 -33
- package/bundled/skills/recovery-workflow/references/detection.md +28 -52
- package/bundled/skills/recovery-workflow/scripts/detect-recovery-state.py +105 -439
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +136 -275
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +53 -88
- package/bundled/skills/refactor-planner/SKILL.md +12 -57
- package/bundled/skills/refactor-planner/references/behavior-preservation.md +111 -230
- package/bundled/skills/refactor-planner/references/error-recovery.md +37 -0
- package/bundled/skills/refactor-planner/references/planning-phases.md +11 -17
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +1 -1
- package/bundled/skills/refactor-workflow/SKILL.md +98 -139
- package/bundled/templates/project-memory-template.md +4 -4
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/prompts.js +10 -3
- package/src/scaffold.js +1 -1
- package/bundled/dev-pipeline/tests/test-deploy-safety.sh +0 -223
- 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
|
|
49
|
+
"""Classify raw AI session signals using semantic checkpoint authority."""
|
|
50
50
|
fingerprint = progress_fingerprint(project_root, prompt)
|
|
51
|
-
|
|
52
|
-
if
|
|
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:{
|
|
55
|
+
reason=f"invalid_workflow_checkpoint:{checkpoint.error_code}",
|
|
56
56
|
progress_fingerprint=fingerprint,
|
|
57
|
+
checkpoint_state=checkpoint.semantic_snapshot,
|
|
57
58
|
)
|
|
58
|
-
if
|
|
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="
|
|
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="
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
98
|
-
|
|
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(
|
|
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
|
-
|
|
165
|
+
checkpoint: CheckpointState | None,
|
|
133
166
|
) -> bool:
|
|
134
|
-
if not
|
|
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
|
|
172
|
+
def _workflow_checkpoint(
|
|
173
|
+
prompt: PromptGenerationResult | None,
|
|
174
|
+
project_root: Path,
|
|
175
|
+
) -> CheckpointState | None:
|
|
140
176
|
if prompt is None:
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
147
|
-
|
|
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(
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
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
|
-
|
|
178
|
-
return
|
|
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(
|
|
217
|
-
|
|
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
|
-
|
|
254
|
-
digest.update(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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", "
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
|
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",
|