prizmkit 1.1.149 → 1.1.151
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/README.md +20 -17
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +77 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +234 -39
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +57 -30
- package/bundled/dev-pipeline/prizmkit_runtime/reset_preserve.py +822 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +28 -11
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +3 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +71 -28
- package/bundled/dev-pipeline/prizmkit_runtime/task_checkout.py +18 -0
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +20 -2
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +86 -13
- package/bundled/dev-pipeline/tests/test_reset_preserve.py +959 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +362 -5
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +21 -9
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +21 -9
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +21 -9
- package/bundled/templates/project-memory-template.md +38 -0
- package/package.json +1 -1
|
@@ -13,11 +13,11 @@ from dataclasses import dataclass
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
from .gitops import (
|
|
16
|
-
HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
17
16
|
UNTRACKED_TRANSIENT_EXCLUDES,
|
|
18
17
|
GitCommandResult,
|
|
19
|
-
|
|
18
|
+
addable_task_paths,
|
|
20
19
|
run_git_command,
|
|
20
|
+
visible_task_paths,
|
|
21
21
|
)
|
|
22
22
|
from .runner_models import RunnerFamily
|
|
23
23
|
|
|
@@ -203,7 +203,7 @@ def _remaining_preflight_changes(project_root: Path) -> GitCommandResult:
|
|
|
203
203
|
|
|
204
204
|
|
|
205
205
|
def _visible_status_result(project_root: Path) -> GitCommandResult:
|
|
206
|
-
return run_git_command(project_root, ("status", "--porcelain", "--", "."
|
|
206
|
+
return run_git_command(project_root, ("status", "--porcelain", "--", "."))
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def _paths_from_status(stdout: str) -> tuple[str, ...]:
|
|
@@ -294,12 +294,21 @@ def commit_task_branch_changes(
|
|
|
294
294
|
message = f"chore({item_id}): save {family.kind} {status} state"
|
|
295
295
|
if status == "completed":
|
|
296
296
|
message = f"chore({item_id}): complete {family.kind} task"
|
|
297
|
-
|
|
297
|
+
paths, status_result = visible_task_paths(project_root)
|
|
298
|
+
if status_result.return_code != 0:
|
|
299
|
+
return BookkeepingResult(attempted=True, committed=False, result=status_result)
|
|
300
|
+
if not paths:
|
|
298
301
|
return BookkeepingResult(attempted=False, committed=False)
|
|
299
|
-
|
|
302
|
+
add_paths, indexed_result = addable_task_paths(project_root, paths)
|
|
303
|
+
if indexed_result.return_code != 0:
|
|
304
|
+
return BookkeepingResult(attempted=True, committed=False, result=indexed_result)
|
|
305
|
+
if add_paths:
|
|
306
|
+
add_result = run_git_command(project_root, ("add", "-A", "--", *_literal_pathspecs(add_paths)))
|
|
307
|
+
if add_result.return_code != 0:
|
|
308
|
+
return BookkeepingResult(attempted=True, committed=False, result=add_result)
|
|
300
309
|
result = run_git_command(
|
|
301
310
|
project_root,
|
|
302
|
-
("commit", "--no-verify", "-m", message, "--",
|
|
311
|
+
("commit", "--no-verify", "-m", message, "--", *_literal_pathspecs(paths)),
|
|
303
312
|
)
|
|
304
313
|
return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
|
|
305
314
|
|
|
@@ -310,10 +319,19 @@ def commit_wip_changes(
|
|
|
310
319
|
item_id: str,
|
|
311
320
|
status: str,
|
|
312
321
|
) -> BookkeepingResult:
|
|
313
|
-
"""Commit all visible task-branch changes as preserved WIP."""
|
|
314
|
-
|
|
322
|
+
"""Commit all Git-visible task-branch changes as preserved WIP."""
|
|
323
|
+
paths, status_result = visible_task_paths(project_root)
|
|
324
|
+
if status_result.return_code != 0:
|
|
325
|
+
return BookkeepingResult(attempted=True, committed=False, result=status_result)
|
|
326
|
+
if not paths:
|
|
315
327
|
return BookkeepingResult(attempted=False, committed=False)
|
|
316
|
-
|
|
328
|
+
add_paths, indexed_result = addable_task_paths(project_root, paths)
|
|
329
|
+
if indexed_result.return_code != 0:
|
|
330
|
+
return BookkeepingResult(attempted=True, committed=False, result=indexed_result)
|
|
331
|
+
if add_paths:
|
|
332
|
+
add_result = run_git_command(project_root, ("add", "-A", "--", *_literal_pathspecs(add_paths)))
|
|
333
|
+
if add_result.return_code != 0:
|
|
334
|
+
return BookkeepingResult(attempted=True, committed=False, result=add_result)
|
|
317
335
|
result = run_git_command(
|
|
318
336
|
project_root,
|
|
319
337
|
(
|
|
@@ -324,8 +342,7 @@ def commit_wip_changes(
|
|
|
324
342
|
"-m",
|
|
325
343
|
"Pipeline preserved task branch work without merging it to the base branch.",
|
|
326
344
|
"--",
|
|
327
|
-
|
|
328
|
-
*HIDDEN_TOOL_WORKTREE_EXCLUDES,
|
|
345
|
+
*_literal_pathspecs(paths),
|
|
329
346
|
),
|
|
330
347
|
)
|
|
331
348
|
return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
|
|
@@ -168,6 +168,7 @@ def _is_terminal_success(result: AISessionResult) -> bool:
|
|
|
168
168
|
and result.termination_reason not in {"timeout", "stale_session"}
|
|
169
169
|
and not result.fatal_error_code
|
|
170
170
|
and not result.progress_summary.fatal_error_code
|
|
171
|
+
and result.progress_summary.stop_reason.strip().lower() != "length"
|
|
171
172
|
and result.progress_summary.result_subtype == "success"
|
|
172
173
|
)
|
|
173
174
|
|
|
@@ -228,6 +229,8 @@ def _next_no_progress_count(previous: object | None, current: object, count: int
|
|
|
228
229
|
|
|
229
230
|
|
|
230
231
|
def _is_context_overflow(result: AISessionResult) -> bool:
|
|
232
|
+
if result.progress_summary.stop_reason.strip().lower() == "length":
|
|
233
|
+
return True
|
|
231
234
|
if (result.fatal_error_code or "").lower() == CONTEXT_OVERFLOW:
|
|
232
235
|
return True
|
|
233
236
|
text = "\n".join(
|
|
@@ -19,6 +19,7 @@ from .gitops import (
|
|
|
19
19
|
branch_create_plan,
|
|
20
20
|
branch_ensure_return,
|
|
21
21
|
branch_merge_plan,
|
|
22
|
+
branch_save_wip,
|
|
22
23
|
current_branch,
|
|
23
24
|
default_branch,
|
|
24
25
|
ensure_linked_worktree,
|
|
@@ -796,10 +797,9 @@ def _process_item_locked(
|
|
|
796
797
|
_cleanup_interrupted_task(
|
|
797
798
|
item_id=item_id,
|
|
798
799
|
paths=paths,
|
|
800
|
+
execution_root=execution_root,
|
|
799
801
|
base_branch=base_branch,
|
|
800
802
|
branch_name=branch_name,
|
|
801
|
-
use_worktree=use_worktree,
|
|
802
|
-
worktree_runtime=worktree_runtime,
|
|
803
803
|
)
|
|
804
804
|
raise
|
|
805
805
|
|
|
@@ -808,41 +808,53 @@ def _cleanup_interrupted_task(
|
|
|
808
808
|
*,
|
|
809
809
|
item_id: str,
|
|
810
810
|
paths,
|
|
811
|
+
execution_root: Path,
|
|
811
812
|
base_branch: str,
|
|
812
813
|
branch_name: str,
|
|
813
|
-
use_worktree: bool,
|
|
814
|
-
worktree_runtime,
|
|
815
814
|
) -> None:
|
|
816
|
-
"""
|
|
817
|
-
_emit_warning(f"Interrupted while processing {item_id}; preserving
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
)
|
|
825
|
-
return
|
|
815
|
+
"""Commit interrupted task WIP, then leave the caller checkout on its base branch."""
|
|
816
|
+
_emit_warning(f"Interrupted while processing {item_id}; preserving task WIP before base return")
|
|
817
|
+
execution_branch = current_branch(execution_root)
|
|
818
|
+
if execution_root.resolve() == paths.project_root.resolve() and execution_branch == base_branch:
|
|
819
|
+
preserved = None
|
|
820
|
+
else:
|
|
821
|
+
preserved = branch_save_wip(execution_root, branch_name)
|
|
822
|
+
if not getattr(preserved, "ok", True):
|
|
826
823
|
_emit_interruption_cleanup_failure(
|
|
827
824
|
preserved,
|
|
828
|
-
f"Could not
|
|
829
|
-
f"
|
|
825
|
+
f"Could not commit interrupted WIP for {item_id}; no base return was attempted",
|
|
826
|
+
f"Task checkout remains active on {branch_name}; resolve the reported Git failure, then resume the recorded task",
|
|
830
827
|
)
|
|
831
828
|
return
|
|
832
829
|
|
|
833
830
|
returned = branch_ensure_return(paths.project_root, base_branch, branch_name)
|
|
834
|
-
if getattr(returned, "ok", True)
|
|
835
|
-
|
|
831
|
+
if not getattr(returned, "ok", True) or current_branch(paths.project_root) != base_branch:
|
|
832
|
+
_emit_interruption_cleanup_failure(
|
|
833
|
+
returned,
|
|
834
|
+
f"Interrupted WIP for {item_id} is safe, but the caller checkout could not return to {base_branch}",
|
|
835
|
+
f"Task checkout remains recorded on {branch_name}; resolve the reported checkout failure, then resume the task",
|
|
836
|
+
)
|
|
836
837
|
return
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
838
|
+
|
|
839
|
+
committed = any(
|
|
840
|
+
getattr(command_result, "args", ())[:1] == ("commit",)
|
|
841
|
+
and getattr(command_result, "return_code", 1) == 0
|
|
842
|
+
for command_result in tuple(getattr(preserved, "results", ()))
|
|
841
843
|
)
|
|
844
|
+
if committed:
|
|
845
|
+
_emit_warning(
|
|
846
|
+
f"Interrupted task {item_id}: WIP committed on {branch_name}; caller checkout returned to {base_branch}; "
|
|
847
|
+
"no merge or push performed"
|
|
848
|
+
)
|
|
849
|
+
else:
|
|
850
|
+
_emit_warning(
|
|
851
|
+
f"Interrupted task {item_id}: no WIP commit needed; caller checkout returned to {base_branch}; "
|
|
852
|
+
"no merge or push performed"
|
|
853
|
+
)
|
|
842
854
|
|
|
843
855
|
|
|
844
856
|
def _emit_interruption_cleanup_failure(result, message: str, recovery: str) -> None:
|
|
845
|
-
"""Emit
|
|
857
|
+
"""Emit the failed Git step, complete Git diagnostics, and a non-destructive recovery action."""
|
|
846
858
|
_emit_warning(message)
|
|
847
859
|
commands = tuple(getattr(getattr(result, "plan", None), "commands", ()))
|
|
848
860
|
results = tuple(getattr(result, "results", ()))
|
|
@@ -853,9 +865,10 @@ def _emit_interruption_cleanup_failure(result, message: str, recovery: str) -> N
|
|
|
853
865
|
if command is not None and getattr(command, "allow_failure", False):
|
|
854
866
|
continue
|
|
855
867
|
args = " ".join(map(str, getattr(command_result, "args", ())))
|
|
868
|
+
_emit_warning(f"git {args} failed")
|
|
856
869
|
detail = str(getattr(command_result, "stderr", "") or getattr(command_result, "stdout", "")).strip()
|
|
857
|
-
|
|
858
|
-
|
|
870
|
+
for line in detail.splitlines():
|
|
871
|
+
_emit_warning(line)
|
|
859
872
|
break
|
|
860
873
|
_emit_warning(recovery)
|
|
861
874
|
|
|
@@ -946,8 +959,38 @@ def _complete_success_merge(
|
|
|
946
959
|
task_commit = commit_task_branch_changes(execution_root, family, item_id, new_status)
|
|
947
960
|
finalization_committed = getattr(final_bookkeeping, "ok", True) and getattr(task_commit, "ok", True)
|
|
948
961
|
if not finalization_committed:
|
|
949
|
-
_emit_warning(f"Task finalization commit failed for {item_id};
|
|
950
|
-
|
|
962
|
+
_emit_warning(f"Task finalization commit failed for {item_id}; merge was not attempted and the task checkout remains active")
|
|
963
|
+
_write_session_status(
|
|
964
|
+
session_paths,
|
|
965
|
+
status="commit_missing",
|
|
966
|
+
session_id=session_id,
|
|
967
|
+
task_id=item_id,
|
|
968
|
+
task_type=family.task_type,
|
|
969
|
+
base_branch=base_branch,
|
|
970
|
+
active_dev_branch=branch_name,
|
|
971
|
+
worktree_path=worktree_runtime.worktree_path if worktree_runtime is not None else None,
|
|
972
|
+
reason="commit_missing",
|
|
973
|
+
fatal_error_code=classification_fatal_error_code,
|
|
974
|
+
)
|
|
975
|
+
update = update_item(
|
|
976
|
+
status_family,
|
|
977
|
+
status_invocation,
|
|
978
|
+
item_id,
|
|
979
|
+
"commit_missing",
|
|
980
|
+
session_id,
|
|
981
|
+
status_root,
|
|
982
|
+
active_dev_branch=branch_name,
|
|
983
|
+
base_branch=base_branch,
|
|
984
|
+
last_fatal_error_code=classification_fatal_error_code,
|
|
985
|
+
)
|
|
986
|
+
if not update.ok:
|
|
987
|
+
raise RuntimeError(update.text)
|
|
988
|
+
_emit_update_summary(family, item_id, update)
|
|
989
|
+
commit_missing_status = _status_from_update(update)
|
|
990
|
+
commit_final_bookkeeping(status_root, status_family, item_id, commit_missing_status)
|
|
991
|
+
_save_failure_wip(execution_root, family, item_id, commit_missing_status, worktree_runtime)
|
|
992
|
+
return commit_missing_status
|
|
993
|
+
if worktree_runtime is not None:
|
|
951
994
|
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
952
995
|
merge = merge_linked_worktree(worktree_runtime, auto_push=env.auto_push)
|
|
953
996
|
_emit_git_plan_output(merge)
|
|
@@ -959,7 +1002,7 @@ def _complete_success_merge(
|
|
|
959
1002
|
guarded_worktree_remove(worktree_runtime, delete_branch=True)
|
|
960
1003
|
_emit_success(f"Merged {branch_name} into {base_branch}")
|
|
961
1004
|
return new_status
|
|
962
|
-
|
|
1005
|
+
else:
|
|
963
1006
|
_emit_info(f"Merging {branch_name} into {base_branch}...")
|
|
964
1007
|
merge = run_git_plan(paths.project_root, branch_merge_plan(branch_context, auto_push=env.auto_push))
|
|
965
1008
|
_emit_git_plan_output(merge)
|
|
@@ -262,6 +262,24 @@ def complete_task_checkout_locked(state_dir: Path | str, checkout: TaskCheckout)
|
|
|
262
262
|
return persist_task_checkout(state_dir, checkout.completed())
|
|
263
263
|
|
|
264
264
|
|
|
265
|
+
def replace_active_task_checkout_locked(
|
|
266
|
+
state_dir: Path | str,
|
|
267
|
+
expected: TaskCheckout,
|
|
268
|
+
replacement: TaskCheckout,
|
|
269
|
+
) -> Path:
|
|
270
|
+
"""Replace one active checkout identity after guarded recovery preflight."""
|
|
271
|
+
if not expected.is_active or not replacement.is_active:
|
|
272
|
+
raise TaskCheckoutError("Checkout replacement requires active identities")
|
|
273
|
+
if (expected.task_type, expected.task_id) != (replacement.task_type, replacement.task_id):
|
|
274
|
+
raise TaskCheckoutError("Checkout replacement cannot change task identity")
|
|
275
|
+
if expected.base_branch != replacement.base_branch:
|
|
276
|
+
raise TaskCheckoutError("Checkout replacement cannot change the recorded base branch")
|
|
277
|
+
current = load_task_checkout(state_dir, expected.task_type, expected.task_id)
|
|
278
|
+
if current != expected:
|
|
279
|
+
raise TaskCheckoutError(f"Task {expected.task_id} checkout changed before recovery replacement")
|
|
280
|
+
return persist_task_checkout(state_dir, replacement)
|
|
281
|
+
|
|
282
|
+
|
|
265
283
|
def mark_task_checkout_reset_locked(state_dir: Path | str, task_type: str, task_id: str) -> Path:
|
|
266
284
|
"""Publish an explicit reset boundary while the caller holds the task guard."""
|
|
267
285
|
return persist_task_checkout(state_dir, TaskCheckout.reset(task_type, task_id))
|
|
@@ -277,6 +277,7 @@ class SemanticLogNormalizer:
|
|
|
277
277
|
|
|
278
278
|
_SUCCESS_STOPS = {"stop", "end_turn", "completed", "success"}
|
|
279
279
|
_ERROR_STOPS = {"error", "aborted", "failed", "cancelled", "canceled"}
|
|
280
|
+
_CONTEXT_STOPS = {"length"}
|
|
280
281
|
|
|
281
282
|
def __init__(self):
|
|
282
283
|
self.renderer = SemanticLogRenderer()
|
|
@@ -617,7 +618,10 @@ class SemanticLogNormalizer:
|
|
|
617
618
|
will_retry = bool(event.get("willRetry"))
|
|
618
619
|
if will_retry:
|
|
619
620
|
return ""
|
|
620
|
-
|
|
621
|
+
if stop_reason in self._CONTEXT_STOPS:
|
|
622
|
+
status = CONTEXT_OVERFLOW_CODE
|
|
623
|
+
else:
|
|
624
|
+
status = "error" if stop_reason in self._ERROR_STOPS else "success"
|
|
621
625
|
details = f"status={status}"
|
|
622
626
|
if stop_reason:
|
|
623
627
|
details += f" stop_reason={stop_reason}"
|
|
@@ -768,6 +772,16 @@ class ProgressTracker:
|
|
|
768
772
|
self._current_tool_id = ""
|
|
769
773
|
self._pi_retrying_terminal_error = False
|
|
770
774
|
|
|
775
|
+
def _record_pi_length_stop(self):
|
|
776
|
+
"""Normalize Pi's token-limit stop into the existing context continuation path."""
|
|
777
|
+
self.result_subtype = "error"
|
|
778
|
+
self.stop_reason = "length"
|
|
779
|
+
self.last_result_is_error = True
|
|
780
|
+
self.api_error_code = CONTEXT_OVERFLOW_CODE
|
|
781
|
+
self.fatal_error_code = CONTEXT_OVERFLOW_CODE
|
|
782
|
+
self.terminal_result_text = "stop_reason=length"
|
|
783
|
+
self.terminal_success_at = ""
|
|
784
|
+
|
|
771
785
|
def process_event(self, event):
|
|
772
786
|
"""Process a single stream-json event and update state.
|
|
773
787
|
|
|
@@ -846,7 +860,9 @@ class ProgressTracker:
|
|
|
846
860
|
stop_reason = str(message.get("stopReason") or "")
|
|
847
861
|
if stop_reason:
|
|
848
862
|
self.stop_reason = stop_reason
|
|
849
|
-
if stop_reason
|
|
863
|
+
if stop_reason == "length":
|
|
864
|
+
self._record_pi_length_stop()
|
|
865
|
+
elif stop_reason in {"error", "aborted"}:
|
|
850
866
|
error_text = str(message.get("errorMessage") or stop_reason)
|
|
851
867
|
self.result_subtype = "error"
|
|
852
868
|
self.last_result_is_error = True
|
|
@@ -880,6 +896,8 @@ class ProgressTracker:
|
|
|
880
896
|
messages = event.get("messages") if isinstance(event.get("messages"), list) else []
|
|
881
897
|
final_message = messages[-1] if messages and isinstance(messages[-1], dict) else {}
|
|
882
898
|
final_stop_reason = str(final_message.get("stopReason") or self.stop_reason or "")
|
|
899
|
+
if final_stop_reason == "length":
|
|
900
|
+
self._record_pi_length_stop()
|
|
883
901
|
will_retry = bool(event.get("willRetry"))
|
|
884
902
|
if will_retry:
|
|
885
903
|
self._pi_retrying_terminal_error = bool(
|
|
@@ -1105,6 +1105,7 @@ def _install_runner_session_fakes(monkeypatch, runners, *, statuses=("success",)
|
|
|
1105
1105
|
monkeypatch.setattr(runners, "commit_preflight_ready_changes", lambda *args, **kwargs: SimpleNamespace(attempted=False, committed=False, ok=True, result=None))
|
|
1106
1106
|
monkeypatch.setattr(runners, "commit_pre_branch_bookkeeping", lambda *args, **kwargs: None)
|
|
1107
1107
|
monkeypatch.setattr(runners, "commit_final_bookkeeping", lambda *args, **kwargs: None)
|
|
1108
|
+
monkeypatch.setattr(runners, "commit_task_branch_changes", lambda *args, **kwargs: None)
|
|
1108
1109
|
monkeypatch.setattr(runners, "current_branch", lambda _root: "main")
|
|
1109
1110
|
monkeypatch.setattr(runners, "_session_id", lambda item_id: f"{item_id}-session-{len(prompt_calls) + 1}")
|
|
1110
1111
|
monkeypatch.setattr(
|
|
@@ -1722,7 +1723,7 @@ def test_success_final_commit_failure_preserves_checkout_without_merge(monkeypat
|
|
|
1722
1723
|
checkout = load_task_checkout(family.state_dir, family.task_type, "F-001")
|
|
1723
1724
|
assert checkout is not None and checkout.is_active
|
|
1724
1725
|
session_status = paths.feature_state_dir / "F-001" / "sessions" / "F-001-session-1" / "session-status.json"
|
|
1725
|
-
assert json.loads(session_status.read_text(encoding="utf-8"))["status"] == "
|
|
1726
|
+
assert json.loads(session_status.read_text(encoding="utf-8"))["status"] == "commit_missing"
|
|
1726
1727
|
|
|
1727
1728
|
|
|
1728
1729
|
def test_wip_commit_failure_skips_base_return(monkeypatch, tmp_path):
|
|
@@ -1970,7 +1971,7 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
|
|
|
1970
1971
|
family = family_for("feature", paths)
|
|
1971
1972
|
invocation = parse_invocation(family, "run", ())
|
|
1972
1973
|
invocation = invocation.__class__(**{**invocation.__dict__, "list_path": paths.feature_plan})
|
|
1973
|
-
_install_runner_session_fakes(monkeypatch, runners)
|
|
1974
|
+
observed = _install_runner_session_fakes(monkeypatch, runners)
|
|
1974
1975
|
monkeypatch.setattr(runners, "current_branch", real_current_branch)
|
|
1975
1976
|
monkeypatch.setattr(runners, "run_git_command", real_run_git_command)
|
|
1976
1977
|
|
|
@@ -2001,7 +2002,10 @@ def test_no_worktree_interrupt_saves_wip_and_returns_to_base(monkeypatch, tmp_pa
|
|
|
2001
2002
|
stdout=subprocess.PIPE,
|
|
2002
2003
|
text=True,
|
|
2003
2004
|
).stdout == "preserved\n"
|
|
2004
|
-
assert "Interrupted task F-001:
|
|
2005
|
+
assert "Interrupted task F-001: WIP committed on dev/F-001-interrupt" in captured.err
|
|
2006
|
+
assert "caller checkout returned to main; no merge or push performed" in captured.err
|
|
2007
|
+
assert observed.merge_calls == []
|
|
2008
|
+
assert observed.cleanup_calls == []
|
|
2005
2009
|
|
|
2006
2010
|
|
|
2007
2011
|
def test_interrupt_return_failure_is_reported_without_forcing_checkout(monkeypatch, tmp_path, capsys):
|
|
@@ -2034,10 +2038,11 @@ def test_interrupt_return_failure_is_reported_without_forcing_checkout(monkeypat
|
|
|
2034
2038
|
args=("checkout", "main"),
|
|
2035
2039
|
return_code=1,
|
|
2036
2040
|
stdout="",
|
|
2037
|
-
stderr="untracked files would be overwritten",
|
|
2041
|
+
stderr="untracked files would be overwritten\nconflicting path: local.txt",
|
|
2038
2042
|
),
|
|
2039
2043
|
),
|
|
2040
2044
|
)
|
|
2045
|
+
monkeypatch.setattr(runners, "branch_save_wip", lambda project_root, branch: SimpleNamespace(ok=True, results=()))
|
|
2041
2046
|
monkeypatch.setattr(runners, "branch_ensure_return", lambda project_root, base, branch: failure)
|
|
2042
2047
|
|
|
2043
2048
|
class InterruptingLauncher:
|
|
@@ -2053,12 +2058,14 @@ def test_interrupt_return_failure_is_reported_without_forcing_checkout(monkeypat
|
|
|
2053
2058
|
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
2054
2059
|
|
|
2055
2060
|
captured = capsys.readouterr()
|
|
2056
|
-
assert "
|
|
2057
|
-
assert "git checkout main failed
|
|
2058
|
-
assert "
|
|
2061
|
+
assert "Interrupted WIP for F-001 is safe, but the caller checkout could not return to main" in captured.err
|
|
2062
|
+
assert "git checkout main failed" in captured.err
|
|
2063
|
+
assert "untracked files would be overwritten" in captured.err
|
|
2064
|
+
assert "conflicting path: local.txt" in captured.err
|
|
2065
|
+
assert "After resolving the Git blocker" not in captured.err
|
|
2059
2066
|
|
|
2060
2067
|
|
|
2061
|
-
def
|
|
2068
|
+
def test_worktree_interrupt_uses_same_wip_and_base_return_contract(monkeypatch, tmp_path, capsys):
|
|
2062
2069
|
from prizmkit_runtime import runners
|
|
2063
2070
|
from prizmkit_runtime.runner_models import family_for, parse_invocation
|
|
2064
2071
|
|
|
@@ -2074,11 +2081,17 @@ def test_worktree_interrupt_preserves_task_worktree_without_returning_caller_che
|
|
|
2074
2081
|
"ensure_linked_worktree",
|
|
2075
2082
|
lambda runtime: SimpleNamespace(ok=True, runtime=runtime, reason=""),
|
|
2076
2083
|
)
|
|
2084
|
+
preserve_calls = []
|
|
2077
2085
|
return_calls = []
|
|
2086
|
+
monkeypatch.setattr(
|
|
2087
|
+
runners,
|
|
2088
|
+
"branch_save_wip",
|
|
2089
|
+
lambda root, branch: preserve_calls.append((root, branch)) or SimpleNamespace(ok=True, results=()),
|
|
2090
|
+
)
|
|
2078
2091
|
monkeypatch.setattr(
|
|
2079
2092
|
runners,
|
|
2080
2093
|
"branch_ensure_return",
|
|
2081
|
-
lambda *args: return_calls.append(args),
|
|
2094
|
+
lambda *args: return_calls.append(args) or SimpleNamespace(ok=True, results=()),
|
|
2082
2095
|
)
|
|
2083
2096
|
|
|
2084
2097
|
class InterruptingLauncher:
|
|
@@ -2094,10 +2107,12 @@ def test_worktree_interrupt_preserves_task_worktree_without_returning_caller_che
|
|
|
2094
2107
|
runners._process_item(family, invocation, "F-001", paths, initial_metadata={})
|
|
2095
2108
|
|
|
2096
2109
|
captured = capsys.readouterr()
|
|
2097
|
-
assert
|
|
2098
|
-
assert return_calls == []
|
|
2099
|
-
assert "Interrupted task F-001:
|
|
2100
|
-
assert "
|
|
2110
|
+
assert preserve_calls and preserve_calls[0][0] != paths.project_root
|
|
2111
|
+
assert return_calls == [(paths.project_root, "main", "dev/F-001-interrupt")]
|
|
2112
|
+
assert "Interrupted task F-001: no WIP commit needed" in captured.err
|
|
2113
|
+
assert "caller checkout returned to main; no merge or push performed" in captured.err
|
|
2114
|
+
assert observed.merge_calls == []
|
|
2115
|
+
assert observed.cleanup_calls == []
|
|
2101
2116
|
|
|
2102
2117
|
|
|
2103
2118
|
def test_run_pipeline_command_bridges_sigterm_and_restores_handler(monkeypatch, tmp_path):
|
|
@@ -2383,6 +2398,45 @@ def test_tracked_python_cache_remains_visible_to_task_commit(tmp_path):
|
|
|
2383
2398
|
).stdout == ""
|
|
2384
2399
|
|
|
2385
2400
|
|
|
2401
|
+
@pytest.mark.parametrize("helper_name", ["commit_task_branch_changes", "commit_wip_changes"])
|
|
2402
|
+
def test_task_commit_helpers_propagate_staging_failure_without_commit(monkeypatch, tmp_path, helper_name):
|
|
2403
|
+
from prizmkit_runtime import runner_bookkeeping
|
|
2404
|
+
from prizmkit_runtime.gitops import GitCommandResult
|
|
2405
|
+
from prizmkit_runtime.runner_models import family_for
|
|
2406
|
+
|
|
2407
|
+
paths = _make_paths(tmp_path)
|
|
2408
|
+
family = family_for("bugfix", paths)
|
|
2409
|
+
status_result = GitCommandResult(("status",), 0, " M app.txt\0", "")
|
|
2410
|
+
indexed_result = GitCommandResult(("ls-files",), 0, "app.txt\0", "")
|
|
2411
|
+
monkeypatch.setattr(
|
|
2412
|
+
runner_bookkeeping,
|
|
2413
|
+
"visible_task_paths",
|
|
2414
|
+
lambda _root: (("app.txt",), status_result),
|
|
2415
|
+
)
|
|
2416
|
+
monkeypatch.setattr(
|
|
2417
|
+
runner_bookkeeping,
|
|
2418
|
+
"addable_task_paths",
|
|
2419
|
+
lambda _root, _paths: (("app.txt",), indexed_result),
|
|
2420
|
+
)
|
|
2421
|
+
calls = []
|
|
2422
|
+
|
|
2423
|
+
def fail_add(_root, args):
|
|
2424
|
+
args = tuple(args)
|
|
2425
|
+
calls.append(args)
|
|
2426
|
+
assert args[0] != "commit"
|
|
2427
|
+
return GitCommandResult(args, 1, "", "staging failed")
|
|
2428
|
+
|
|
2429
|
+
monkeypatch.setattr(runner_bookkeeping, "run_git_command", fail_add)
|
|
2430
|
+
helper = getattr(runner_bookkeeping, helper_name)
|
|
2431
|
+
|
|
2432
|
+
result = helper(paths.project_root, family, "B-001", "failed")
|
|
2433
|
+
|
|
2434
|
+
assert result.attempted is True
|
|
2435
|
+
assert result.committed is False
|
|
2436
|
+
assert result.result is not None and result.result.stderr == "staging failed"
|
|
2437
|
+
assert calls and all(args[0] != "commit" for args in calls)
|
|
2438
|
+
|
|
2439
|
+
|
|
2386
2440
|
def test_preflight_ready_commit_includes_tracked_state_but_excludes_untracked_state(tmp_path):
|
|
2387
2441
|
from prizmkit_runtime.runner_bookkeeping import commit_preflight_ready_changes
|
|
2388
2442
|
|
|
@@ -3741,6 +3795,25 @@ def test_terminal_success_without_semantic_completion_is_skipped(tmp_path):
|
|
|
3741
3795
|
assert classification.fatal_error_code == ""
|
|
3742
3796
|
|
|
3743
3797
|
|
|
3798
|
+
def test_pi_length_stop_uses_existing_context_overflow_continuation(tmp_path):
|
|
3799
|
+
from dataclasses import replace
|
|
3800
|
+
|
|
3801
|
+
from prizmkit_runtime.runner_classification import classify_session
|
|
3802
|
+
from prizmkit_runtime.status import ProgressSummary
|
|
3803
|
+
|
|
3804
|
+
base_head = _init_git_repo_for_invalid_checkpoint_test(tmp_path)
|
|
3805
|
+
raw = replace(
|
|
3806
|
+
_terminal_success_result(tmp_path),
|
|
3807
|
+
progress_summary=ProgressSummary(result_subtype="success", stop_reason="length"),
|
|
3808
|
+
)
|
|
3809
|
+
|
|
3810
|
+
classification = classify_session(raw, project_root=tmp_path, base_head=base_head)
|
|
3811
|
+
|
|
3812
|
+
assert classification.session_status == "context_overflow"
|
|
3813
|
+
assert classification.reason == "context_overflow"
|
|
3814
|
+
assert classification.fatal_error_code == "context_overflow"
|
|
3815
|
+
|
|
3816
|
+
|
|
3744
3817
|
def test_terminal_success_text_does_not_hide_provider_failure(tmp_path):
|
|
3745
3818
|
from prizmkit_runtime.runner_classification import classify_session
|
|
3746
3819
|
from prizmkit_runtime.runner_models import PromptGenerationResult
|